Skip to content

Instantly share code, notes, and snippets.

@mjackson
Created August 21, 2014 19:55
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mjackson/06c5c8c53a9e10de6275 to your computer and use it in GitHub Desktop.
Save mjackson/06c5c8c53a9e10de6275 to your computer and use it in GitHub Desktop.
An async version of Facebook's flux dispatcher
var d = require('d');
var warning = require('react/lib/warning');
var Promise = require('es6-promise').Promise;
function isNotNull(object) {
return object != null;
}
function Dispatcher() {
this._currentActionName = null;
this._handlers = [];
this._promises = [];
}
Object.defineProperties(Dispatcher.prototype, {
/**
* A mixin that React components can use to send actions through
* this dispatcher.
*
* Example:
*
* var React = require('react');
* var Dispatcher = require('react-dispatcher');
* var Router = require('react-router');
*
* Dispatcher.registerHandler(function (actionName, actionArgs) {
* if (actionName === 'login') {
* var username = actionArgs[0], password = actionArgs[1];
*
* // return a promise that fulfills if the username/password
* // are good, rejects if there's a problem
* return checkCredentials(username, password);
* }
* });
*
* var Login = React.createClass({
*
* mixins: [ Dispatcher.ActionSender ],
*
* getInitialState: function () {
* return { errorMessage: '' };
* },
*
* handleLoginError: function (error) {
* this.setState({ errorMessage: error.message });
* },
*
* handleSubmit: function () {
* var username = this.refs.username.getDOMNode().value;
* var password = this.refs.password.getDOMNode().value;
*
* this.sendAction('login', username, password).then(function () {
* Router.transitionTo('/home/' + username);
* }, this.handleLoginError);
* },
*
* render: function () {
* var error;
* if (this.state.errorMessage)
* error = <div className="error">{this.state.errorMessage}</div>;
*
* return (
* {error}
* <form onSubmit={this.handleSubmit}>
* <input ref="username" type="text" name="username"/>
* <input ref="password" type="password" name="password"/>
* </form>
* );
* }
*
* });
*/
ActionSender: d.gs(function () {
var dispatcher = this;
return {
sendAction: function () {
return dispatcher.sendAction.apply(dispatcher, arguments);
}
};
}),
/**
* Registers the given handler function to run when a new action
* is dispatched and returns an opaque value that other handlers
* can use to wait for the given handler to finish processing an
* action.
*
* Example:
*
* function myHandler(actionName, actionArgs) {
* // return a value or a promise for a value
* }
*
* // save the ID so other handlers can waitFor myHandler
* var myHandlerID = Dispatcher.registerHandler(handler);
*
* Dispatcher.registerHandler(function (actionName, actionArgs) {
* return Dispatcher.waitFor(myHandlerID).then(function (value) {
* // value is the result from myHandler
* });
* });
*/
registerHandler: d(function (handler) {
this._handlers.push(handler);
return this._handlers.length - 1;
}),
/**
* Creates and sends a new action with the given name and arguments to all
* registered handlers. Arguments may be an array or a variable-length
* argument list. Handlers will be called with two arguments: 1) the action
* name and 2) an array of action arguments.
*
* Handlers may return a promise if they need to handle the action asynchronously.
* Returns a promise that resolves when all handlers are done.
*
* Note: This function throws if you try to dispatch an action while another
* is being dispatched. This does not mean that handlers need to wait for all
* async handlers to finish before dispatching another action, only that they
* shouldn't dispatch another action in the same turn of the event loop.
*/
dispatchAction: d(function (actionName, actionArgs) {
if (this._promises.length)
throw new Error('Dispatch of "' + this._currentActionName + '" is still in progress');
// Update this._currentActionName, for error messages.
this._currentActionName = actionName;
if (!Array.isArray(actionArgs))
actionArgs = Array.prototype.slice.call(arguments, 1);
// Populate this._promises so handlers can reference others in waitFor.
var resolves = [], rejects = [];
this._promises = this._handlers.map(function (handler, i) {
return new Promise(function (resolve, reject) {
resolves[i] = resolve;
rejects[i] = reject;
});
});
// Dispatch to handlers and resolve/reject promises.
var results = this._handlers.map(function (handler, i) {
var result = handler.call(this, actionName, actionArgs);
// Stores can return a promise if they need to make a network request
// or waitFor another store to finish processing an action.
Promise.resolve(result).then(resolves[i], rejects[i]);
return result;
}, this);
// Emit a warning if nobody handled the action.
warning(
results.some(isNotNull),
'No handlers handled action: ' + actionName
);
var allResults = Promise.all(this._promises);
// Clear the promises array. Stores that use waitFor must do so immediately.
this._promises = [];
// Return a promise that resolves when all handlers are done.
return allResults;
}),
waitFor: d(function (handlerIDs) {
if (!this._promises.length)
throw new Error('Handlers must use waitFor in the first turn of the dispatch cycle');
if (Array.isArray(handlerIDs))
return Promise.all(handlerIDs.map(this._getPromise.bind(this)));
return Promise.resolve(this._getPromise(handlerIDs));
}),
_getPromise: d(function (index) {
if (!this._promises[index])
throw new Error('Unregistered dispatcher id: ' + index);
return this._promises[index];
})
});
module.exports = Dispatcher;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment