Skip to content

Commit

Permalink
Merge pull request #180 from jhollingworth/when-promise
Browse files Browse the repository at this point in the history
FetchResult#toPromise
  • Loading branch information
jhollingworth committed Mar 8, 2015
2 parents 9bbf717 + 3bd9a7d commit 2ef0896
Show file tree
Hide file tree
Showing 8 changed files with 188 additions and 26 deletions.
13 changes: 13 additions & 0 deletions docs/_api/stores/index.md
Expand Up @@ -513,6 +513,19 @@ var component = user.when({
});
{% endhighlight %}

<h4 id="fetch-result-toPromise">FetchResult#toPromise()</h4>

Converts a fetch result into a promise. Useful when you want to use a store outside of a React component.

{% highlight js %}
var getUser = UserStore.getUser().toPromise();

getUser
.then((user) => console.log(user))
.catch((error) => console.error(error));

{% endhighlight %}

<h2 id="fetch_pending">fetch.pending()</h2>

Returns a pending fetch result
Expand Down
46 changes: 30 additions & 16 deletions lib/store/fetch.js
Expand Up @@ -3,6 +3,7 @@ var _ = require('../utils/mindash');
var warnings = require('../warnings');
var Instances = require('../instances');
var fetchResult = require('./fetchResult');
var StoreConstants = require('./storeConstants');
var CompoundError = require('../../errors/compound');
var NotFoundError = require('../../errors/notFound');
var StatusConstants = require('../../constants/status');
Expand Down Expand Up @@ -42,7 +43,7 @@ function fetch(id, local, remote) {
error = instance.failedFetches[options.id];

if (error) {
return failed(error);
return fetchFailed(error);
}
}

Expand All @@ -65,18 +66,16 @@ function fetch(id, local, remote) {
}

if (_.isNull(result)) {
return notFound();
return fetchNotFound();
}

if (!remoteCalled) {
finished();
}

return fetchResult.done(result, options.id, store);
return fetchDone(result);
} catch (error) {
failed(error);

return fetchResult.failed(error, options.id, store);
return fetchFailed(error);
}
}

Expand All @@ -93,18 +92,18 @@ function fetch(id, local, remote) {
result = tryAndGetLocally(true);

if (result) {
finished();
fetchDone(result);
store.hasChanged();
} else {
notFound();
fetchNotFound();
store.hasChanged();
}
}).catch(function (error) {
failed(error);
fetchFailed(error);
store.hasChanged();
});

return fetchResult.pending(options.id, store);
return fetchPending();
} else {
instance.fetchHistory[options.id] = true;
result = tryAndGetLocally(true);
Expand All @@ -119,9 +118,9 @@ function fetch(id, local, remote) {
log.warn(promiseNotReturnedWarning());
}

return notFound();
return fetchNotFound();
} catch (error) {
return failed(error);
return fetchFailed(error);
}
}

Expand All @@ -146,18 +145,33 @@ function fetch(id, local, remote) {
}
}

function failed(error) {
function fetchPending() {
return fetchResult.pending(options.id, store);
}

function fetchDone(result) {
finished();

return fetchChanged(fetchResult.done(result, options.id, store));
}

function fetchFailed(error) {
if (cacheError) {
instance.failedFetches[options.id] = error;
}

finished();

return fetchResult.failed(error, options.id, store);
return fetchChanged(fetchResult.failed(error, options.id, store));
}

function fetchNotFound() {
return fetchFailed(new NotFoundError(), options.id, store);
}

function notFound() {
return failed(new NotFoundError(), options.id, store);
function fetchChanged(fetch) {
instance.emitter.emit(StoreConstants.FETCH_CHANGE_EVENT, fetch);
return fetch;
}
}

Expand Down
31 changes: 31 additions & 0 deletions lib/store/fetchResult.js
Expand Up @@ -40,11 +40,42 @@ function notFound(id, store) {

function fetchResult(result, store) {
result.when = when;
result.toPromise = toPromise;
result._isFetchResult = true;

if (store) {
result.store = store.displayName || store.id;
}

return result;

function toPromise() {
return new Promise(function (resolve, reject) {
var listener;

if (!tryResolveFetch(result)) {
listener = store.addFetchChangedListener(tryResolveFetch);
}

function tryResolveFetch(fetchResult) {
if (fetchResult.id !== result.id) {
return;
}

if (fetchResult.done) {
resolve(fetchResult.result);
} else if (fetchResult.failed) {
reject(fetchResult.error);
} else {
return false;
}

if (listener) {
listener.dispose();
}

return true;
}
});
}
}
27 changes: 22 additions & 5 deletions lib/store/store.js
@@ -1,4 +1,3 @@
var CHANGE_EVENT = 'changed';
var log = require('../logger');
var fetch = require('./fetch');
var _ = require('../utils/mindash');
Expand All @@ -8,6 +7,7 @@ var Instances = require('../instances');
var resolve = require('../utils/resolve');
var Environment = require('../environment');
var handleAction = require('./handleAction');
var StoreConstants = require('./storeConstants');
var EventEmitter = require('events').EventEmitter;
var validateHandlers = require('./validateHandlers');

Expand Down Expand Up @@ -112,7 +112,8 @@ class Store {
var emitter = instance.emitter;
var dispatchToken = this.dispatchToken;

emitter.removeAllListeners(CHANGE_EVENT);
emitter.removeAllListeners(StoreConstants.CHANGE_EVENT);
emitter.removeAllListeners(StoreConstants.FETCH_CHANGE_EVENT);
this.clear();

if (dispatchToken) {
Expand All @@ -129,7 +130,7 @@ class Store {
if (instance) {
var emitter = instance.emitter;

emitter.emit.call(emitter, CHANGE_EVENT, this.state, this, eventArgs);
emitter.emit.call(emitter, StoreConstants.CHANGE_EVENT, this.state, this, eventArgs);
}
}

Expand All @@ -148,15 +149,31 @@ class Store {
`The ${this.displayName} store (${this.id}) is adding a change listener`
);

emitter.on(CHANGE_EVENT, callback);
emitter.on(StoreConstants.CHANGE_EVENT, callback);

return {
dispose: () => {
log.trace(
`The ${this.displayName} store (${this.id}) is disposing of a change listener`
);

emitter.removeListener(CHANGE_EVENT, callback);
emitter.removeListener(StoreConstants.CHANGE_EVENT, callback);
}
};
}

addFetchChangedListener(callback, context) {
var emitter = getInstance(this).emitter;

if (context) {
callback = _.bind(callback, context);
}

emitter.on(StoreConstants.FETCH_CHANGE_EVENT, callback);

return {
dispose: () => {
emitter.removeListener(StoreConstants.FETCH_CHANGE_EVENT, callback);
}
};
}
Expand Down
4 changes: 4 additions & 0 deletions lib/store/storeConstants.js
@@ -0,0 +1,4 @@
module.exports = {
CHANGE_EVENT: 'changed',
FETCH_CHANGE_EVENT: 'fetch-changed'
};
2 changes: 1 addition & 1 deletion lib/storeObserver.js
Expand Up @@ -61,7 +61,7 @@ function tryGetState(component, store) {
handler.failed(e);
}

return {};
throw e;
} finally {
if (handler) {
handler.dispose();
Expand Down
5 changes: 4 additions & 1 deletion test/browser/stateMixinSpec.js
Expand Up @@ -76,7 +76,10 @@ describe('StateMixin', function () {
beforeEach(function () {
expectedError = new Error();
store.getState = sinon.stub().throws(expectedError);
getObserver(element).onStoreChanged(null, store, element);

try {
getObserver(element).onStoreChanged(null, store, element);
} catch (e) {}
});

it('should add a view to the handler', function () {
Expand Down
86 changes: 83 additions & 3 deletions test/browser/storeFetchSpec.js
@@ -1,5 +1,6 @@
var sinon = require('sinon');
var _ = require('lodash');
var sinon = require('sinon');
var fetch = require('../../fetch');
var Marty = require('../../marty');
var expect = require('chai').expect;
var warnings = require('../../lib/warnings');
Expand Down Expand Up @@ -93,6 +94,81 @@ describe('Store#fetch()', function () {
});
});

describe('#toPromise', function () {
describe('when a fetch is done', function () {
var actualPromise, expectedState, localState, getState;

beforeEach(function () {
expectedState = { foo: 'bar' };

actualPromise = store.fetch({
id: 'locally',
locally: function () {
return localState;
},
remotely: function () {
return new Promise(function (resolve) {
localState = expectedState;
resolve();
});
}
}).toPromise();
});

it('should resolve the promise', function () {
return expect(actualPromise).to.eventually.eql(expectedState);
});
});

describe('when a fetch fails', function () {
var actualPromise, expectedError;

beforeEach(function () {
expectedError = new Error('foo');

actualPromise = store.fetch({
id: 'locally',
locally: function () {},
remotely: function () {
return new Promise(function (resolve, reject) {
reject(expectedError);
});
}
}).toPromise();
});

it('should reject the promise', function () {
return expect(actualPromise).to.be.rejectedWith(expectedError);
});
});

describe('fetch.done().toPromise()', function () {
var result, expectedResult;

beforeEach(function () {
expectedResult = { foo: 'bar' };
result = fetch.done(expectedResult).toPromise();
});

it('should resolve the promise', function () {
return expect(result).to.eventually.equal(expectedResult);
});
});

describe('fetch.failed().toPromise()', function () {
var result, expectedError;

beforeEach(function () {
expectedError = new Error('foo');
result = fetch.failed(expectedError).toPromise();
});

it('should resolve the promise', function () {
return expect(result).to.be.rejectedWith(expectedError);
});
});
});

describe('#dependsOn', function () {
describe('when you pass it a fetch result', function () {
describe('when the fetch result is pending', function () {
Expand Down Expand Up @@ -301,7 +377,7 @@ describe('Store#fetch()', function () {
it('should return the in progress fetch', function () {
actualResult = store.fetch(fetchId, noop, noop);

expect(actualResult).to.eql(expectedResult);
expect(omitPromise(actualResult)).to.eql(omitPromise(expectedResult));
});
});

Expand Down Expand Up @@ -365,7 +441,7 @@ describe('Store#fetch()', function () {
});

it('should return a fetch not found result', function () {
expect(actualResult).to.eql(store.fetch.notFound('bar', store));
expect(omitPromise(actualResult)).to.eql(omitPromise(store.fetch.notFound('bar', store)));
});

it('should not call remotely', function () {
Expand Down Expand Up @@ -684,6 +760,10 @@ describe('Store#fetch()', function () {
});
});

function omitPromise(result) {
return _.omit(result, 'toPromise');
}

function noop() {
}
});

0 comments on commit 2ef0896

Please sign in to comment.