Skip to content

Instantly share code, notes, and snippets.

@joshski
Last active September 2, 2016 09:41
Show Gist options
  • Save joshski/3c4deb823bc379f2a6559bc5066f9f8a to your computer and use it in GitHub Desktop.
Save joshski/3c4deb823bc379f2a6559bc5066f9f8a to your computer and use it in GitHub Desktop.
stateful-promise
/*
# 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