Last active
September 2, 2016 09:41
-
-
Save joshski/3c4deb823bc379f2a6559bc5066f9f8a to your computer and use it in GitHub Desktop.
stateful-promise
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
# stateful-promise | |
Encapsulates a repeatable asynchronous operation, and exposes the state of the | |
current invocation. | |
``` | |
model.documents = new StatefulPromise(() => | |
http.get('/documents').then(response => response.body); | |
); | |
// then... | |
model.documents.started // -> false | |
model.documents.load() // creates the underlying promise | |
model.documents.started // -> true | |
model.documents.inProgress // -> true | |
// time passes, then... | |
model.documents.result // -> { documents: [...] } | |
model.documents.error // -> [Error] | |
model.documents.started // -> true | |
model.documents.inProgress // -> false | |
model.documents.unload() // discards the underlying promise | |
model.documents.inProgress // -> false | |
## Why? | |
In a virtual-dom app, it's common to have an async "loading" operation | |
triggered when visiting a particular route. Often we want to show three | |
different UI variations depending on the progress and outcome: first we show a | |
loading indicator (e.g. spinner) then when the promise is resolved we render | |
the result, or if the promise is rejected, we render an error. But promises | |
don't expose this state. Some libraries like bluebird, support "synchronous | |
introspection", but still, it can be convenient to encapsulate a _repeatable_ | |
process that is configured in the model before it's actually used, then later | |
reset and re-triggered as a result of user interaction. | |
We can stitch this kind of thing together with `onarrival` in plastiq-router, | |
but that is quite a lot of boilerplate -- and you still often end up with a | |
subtle bug, where a slow running promise can overwrite the result of a | |
fast running promise... | |
*/ | |
var expect = require('chai').expect; | |
function StatefulPromise(promiseFactory) { | |
this.promiseFactory = promiseFactory; | |
this.currentPromise = null; | |
this.inProgress = false; | |
} | |
StatefulPromise.prototype.load = function() { | |
var self = this; | |
if (!this.started) { | |
this.started = true; | |
this.inProgress = true; | |
var promise = this.promiseFactory(); | |
self.currentPromise = promise; | |
promise | |
.then(function(result) { | |
if (promise === self.currentPromise) { | |
self.inProgress = false; | |
self.result = result; | |
} | |
}) | |
.catch(function(error) { | |
if (promise === self.currentPromise) { | |
self.inProgress = false; | |
self.error = error; | |
} | |
}); | |
} | |
}; | |
StatefulPromise.prototype.unload = function () { | |
this.started = false; | |
delete(this.error); | |
delete(this.result); | |
delete(this.currentPromise); | |
}; | |
describe('StatefulPromise', function() { | |
context('before loading', function() { | |
it('is not in progress', function() { | |
var statefulPromise = new StatefulPromise(function() { | |
return Promise.resolve(); | |
}); | |
expect(statefulPromise.inProgress).to.equal(false); | |
}) | |
}); | |
context('after .load() triggers a rejected promise', function() { | |
it('is not in progess', function(done) { | |
var statefulPromise = new StatefulPromise(function() { | |
var promise = { | |
then: function() { | |
return promise; | |
}, | |
catch: function(callback) { | |
expect(statefulPromise.inProgress).to.be.true; | |
callback(); | |
expect(statefulPromise.inProgress).to.be.false; | |
done(); | |
} | |
} | |
return promise; | |
}); | |
statefulPromise.load(); | |
}); | |
it('has no result', function() { | |
var statefulPromise = new StatefulPromise(function() { | |
return Promise.resolve(); | |
}); | |
expect(statefulPromise.result).to.be.undefined; | |
}); | |
it('has the error caught by the promise', function() { | |
var statefulPromise = new StatefulPromise(function() { | |
var promise = { | |
then: function() { | |
return this; | |
}, | |
catch: function(callback) { | |
callback(123); | |
return promise; | |
} | |
} | |
return promise; | |
}); | |
statefulPromise.load(); | |
expect(statefulPromise.error).to.equal(123); | |
}); | |
context('then calling unload()', function() { | |
it('has no error', function() { | |
var statefulPromise = new StatefulPromise(function() { | |
var promise = { | |
then: function() { | |
return this; | |
}, | |
catch: function(callback) { | |
callback(123); | |
return promise; | |
} | |
} | |
return promise; | |
}); | |
statefulPromise.load(); | |
statefulPromise.unload(); | |
expect(statefulPromise.error).to.be.undefined; | |
}); | |
it('has no result', function() { | |
var statefulPromise = new StatefulPromise(function() { | |
var promise = { | |
then: function() { | |
return this; | |
}, | |
catch: function(callback) { | |
callback(123); | |
return promise; | |
} | |
} | |
return promise; | |
}); | |
statefulPromise.load(); | |
statefulPromise.unload(); | |
expect(statefulPromise.result).to.be.undefined; | |
}); | |
}); | |
}); | |
context('after .load() triggers a resolved promise', function() { | |
it('is not in progess', function(done) { | |
var statefulPromise = new StatefulPromise(function() { | |
var promise = { | |
then: function(callback) { | |
expect(statefulPromise.inProgress).to.be.true; | |
callback(); | |
expect(statefulPromise.inProgress).to.be.false; | |
done(); | |
return promise; | |
}, | |
catch: function() { | |
return promise; | |
} | |
} | |
return promise; | |
}); | |
statefulPromise.load(); | |
}); | |
it('has the result resolved by the promise', function() { | |
var statefulPromise = new StatefulPromise(function() { | |
var promise = { | |
then: function(callback) { | |
callback(123); | |
return this; | |
}, | |
catch: function() { | |
return promise; | |
} | |
} | |
return promise; | |
}); | |
statefulPromise.load(); | |
expect(statefulPromise.result).to.equal(123); | |
}); | |
it('has no error', function() { | |
var statefulPromise = new StatefulPromise(function() { | |
return Promise.resolve(); | |
}); | |
expect(statefulPromise.error).to.be.undefined; | |
}); | |
context('then calling unload()', function() { | |
it('is not in progress', function() { | |
var statefulPromise = new StatefulPromise(function() { | |
var promise = { | |
then: function(callback) { | |
callback(123); | |
return this; | |
}, | |
catch: function() { | |
return promise; | |
} | |
} | |
return promise; | |
}); | |
statefulPromise.load(); | |
statefulPromise.unload(); | |
expect(statefulPromise.inProgress).to.be.false; | |
}); | |
it('has no error', function() { | |
var statefulPromise = new StatefulPromise(function() { | |
var promise = { | |
then: function(callback) { | |
callback(123); | |
return this; | |
}, | |
catch: function() { | |
return promise; | |
} | |
} | |
return promise; | |
}); | |
statefulPromise.load(); | |
statefulPromise.unload(); | |
expect(statefulPromise.error).to.be.undefined; | |
}); | |
it('has no result', function() { | |
var statefulPromise = new StatefulPromise(function() { | |
var promise = { | |
then: function(callback) { | |
callback(123); | |
return this; | |
}, | |
catch: function() { | |
return promise; | |
} | |
} | |
return promise; | |
}); | |
statefulPromise.load(); | |
statefulPromise.unload(); | |
expect(statefulPromise.result).to.be.undefined; | |
}); | |
}); | |
}); | |
describe('.load()', function() { | |
it('creates a new promise', function() { | |
var created = 0; | |
var statefulPromise = new StatefulPromise(function() { | |
created++; | |
return Promise.resolve(); | |
}); | |
statefulPromise.load(); | |
expect(created).to.equal(1); | |
}); | |
it('does not create two promises when called twice', function() { | |
var created = 0; | |
var statefulPromise = new StatefulPromise(function() { | |
created++; | |
return { | |
then: function(callback) { | |
callback(); | |
return this; | |
}, | |
catch: function() { | |
return this; | |
} | |
} | |
}); | |
statefulPromise.load(); | |
statefulPromise.load(); | |
expect(created).to.equal(1); | |
}); | |
}); | |
context('when .load() is called, then .unload() is called, then the promise is resolved', function() { | |
it('has no result', function(done) { | |
var _resolve; | |
var statefulPromise = new StatefulPromise(function() { | |
return new Promise(function(resolve) { | |
_resolve = resolve; | |
}); | |
}); | |
statefulPromise.load(); | |
statefulPromise.unload(); | |
_resolve(654); | |
setTimeout(function() { | |
expect(statefulPromise.result).to.be.undefined; | |
done(); | |
}, 1); | |
}); | |
}); | |
context('when .load() is called, then .unload() is called, then .load(), then the first promise is resolved', function() { | |
it('has no result', function(done) { | |
var resolves = []; | |
var statefulPromise = new StatefulPromise(function() { | |
return new Promise(function(resolve) { | |
resolves.push(resolve); | |
}); | |
}); | |
statefulPromise.load(); | |
statefulPromise.unload(); | |
statefulPromise.load(); | |
resolves[0](555); | |
setTimeout(function() { | |
expect(statefulPromise.result).to.be.undefined; | |
done(); | |
}, 1); | |
}); | |
}); | |
context('when .load() is called, then .unload() is called, then .load(), then the second promise is resolved', function() { | |
it("has the second promise's result", function(done) { | |
var resolves = []; | |
var statefulPromise = new StatefulPromise(function() { | |
return new Promise(function(resolve) { | |
resolves.push(resolve); | |
}); | |
}); | |
statefulPromise.load(); | |
statefulPromise.unload(); | |
statefulPromise.load(); | |
resolves[1](777); | |
setTimeout(function() { | |
expect(statefulPromise.result).to.equal(777); | |
done(); | |
}, 3); | |
}); | |
}); | |
context('when .load() is called, then .unload() is called, then the promise is rejected', function() { | |
it('has no error', function(done) { | |
var _reject; | |
var statefulPromise = new StatefulPromise(function() { | |
return new Promise(function(_, reject) { | |
_reject = reject; | |
}); | |
}); | |
statefulPromise.load(); | |
statefulPromise.unload(); | |
_reject(666); | |
setTimeout(function() { | |
expect(statefulPromise.error).to.be.undefined; | |
done(); | |
}, 1); | |
}); | |
}); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment