Skip to content

Commit

Permalink
[flux-todomvc] Replace dispatcher with new one
Browse files Browse the repository at this point in the history
  • Loading branch information
fisherwebdev committed Aug 1, 2014
1 parent a511d2f commit 1ba5beb
Show file tree
Hide file tree
Showing 3 changed files with 277 additions and 89 deletions.
4 changes: 2 additions & 2 deletions examples/flux-todomvc/js/dispatcher/AppDispatcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@

var Dispatcher = require('./Dispatcher');

var merge = require('react/lib/merge');
var copyProperties = require('react/lib/copyProperties');

var AppDispatcher = merge(Dispatcher.prototype, {
var AppDispatcher = copyProperties(new Dispatcher(), {

/**
* A bridge function between the views and the dispatcher, marking the action
Expand Down
309 changes: 222 additions & 87 deletions examples/flux-todomvc/js/dispatcher/Dispatcher.js
Original file line number Diff line number Diff line change
@@ -1,113 +1,248 @@
/*
* Copyright (c) 2014, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule Dispatcher
* @typechecks
*/

var invariant = require('./invariant');

var _lastID = 1;
var _prefix = 'ID_';

/**
* Copyright 2013-2014 Facebook, Inc.
* Dispatcher is used to broadcast payloads to registered callbacks. This is
* different from generic pub-sub systems in two ways:
*
* 1) Callbacks are not subscribed to particular events. Every payload is
* dispatched to every registered callback.
* 2) Callbacks can be deferred in whole or part until other callbacks have
* been executed.
*
* For example, consider this hypothetical flight destination form, which
* selects a default city when a country is selected:
*
* var flightDispatcher = new Dispatcher();
*
* // Keeps track of which country is selected
* var CountryStore = {country: null};
*
* // Keeps track of which city is selected
* var CityStore = {city: null};
*
* // Keeps track of the base flight price of the selected city
* var FlightPriceStore = {price: null}
*
* When a user changes the selected city, we dispatch the payload:
*
* flightDispatcher.dispatch({
* actionType: 'city-update',
* selectedCity: 'paris'
* });
*
* This payload is digested by `CityStore`:
*
* flightDispatcher.register(function(payload)) {
* if (payload.actionType === 'city-update') {
* CityStore.city = payload.selectedCity;
* }
* });
*
* When the user selects a country, we dispatch the payload:
*
* flightDispatcher.dispatch({
* actionType: 'country-update',
* selectedCountry: 'australia'
* });
*
* This payload is digested by both stores:
*
* CountryStore.dispatchToken = flightDispatcher.register(function(payload) {
* if (payload.actionType === 'country-update') {
* CountryStore.country = payload.selectedCountry;
* }
* });
*
* When the callback to update `CountryStore` is registered, we save a reference
* to the returned token. Using this token with `waitFor()`, we can guarantee
* that `CountryStore` is updated before the callback that updates `CityStore`
* needs to query its data.
*
* CityStore.dispatchToken = flightDispatcher.register(function(payload) {
* if (payload.actionType === 'country-update') {
* // `CountryStore.country` may not be updated.
* flightDispatcher.waitFor([CountryStore.dispatchToken]);
* // `CountryStore.country` is now guaranteed to be updated.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* // Select the default city for the new country
* CityStore.city = getDefaultCityForCountry(CountryStore.country);
* }
* });
*
* http://www.apache.org/licenses/LICENSE-2.0
* The usage of `waitFor()` can be chained, for example:
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* FlightPriceStore.dispatchToken =
* flightDispatcher.register(function(payload)) {
* switch (payload.actionType) {
* case 'country-update':
* flightDispatcher.waitFor([CityStore.dispatchToken]);
* FlightPriceStore.price =
* getFlightPriceStore(CountryStore.country, CityStore.city);
* break;
*
* Dispatcher
* case 'city-update':
* FlightPriceStore.price =
* FlightPriceStore(CountryStore.country, CityStore.city);
* break;
* }
* });
*
* The Dispatcher is capable of registering callbacks and invoking them.
* More robust implementations than this would include a way to order the
* callbacks for dependent Stores, and to guarantee that no two stores
* created circular dependencies.
* The `country-update` payload will be guaranteed to invoke the stores'
* registered callbacks in order: `CountryStore`, `CityStore`, then
* `FlightPriceStore`.
*/

var Promise = require('es6-promise').Promise;
var merge = require('react/lib/merge');

var _callbacks = [];
var _promises = [];

var Dispatcher = function() {};
Dispatcher.prototype = merge(Dispatcher.prototype, {
function Dispatcher() {"use strict";
this.$Dispatcher_callbacks = {};
this.$Dispatcher_isPending = {};
this.$Dispatcher_isHandled = {};
this.$Dispatcher_isDispatching = false;
this.$Dispatcher_pendingPayload = null;
}

/**
* Register a Store's callback so that it may be invoked by an action.
* @param {function} callback The callback to be registered.
* @return {number} The index of the callback within the _callbacks array.
* Registers a callback to be invoked with every dispatched payload. Returns
* a token that can be used with `waitFor()`.
*
* @param {function} callback
* @return {string}
*/
register: function(callback) {
_callbacks.push(callback);
return _callbacks.length - 1; // index
},
Dispatcher.prototype.register=function(callback) {"use strict";
var id = _prefix + _lastID++;
this.$Dispatcher_callbacks[id] = callback;
return id;
};

/**
* dispatch
* @param {object} payload The data from the action.
* Removes a callback based on its token.
*
* @param {string} id
*/
dispatch: function(payload) {
// First create array of promises for callbacks to reference.
var resolves = [];
var rejects = [];
_promises = _callbacks.map(function(_, i) {
return new Promise(function(resolve, reject) {
resolves[i] = resolve;
rejects[i] = reject;
});
});
// Dispatch to callbacks and resolve/reject promises.
_callbacks.forEach(function(callback, i) {
// Callback can return an obj, to resolve, or a promise, to chain.
// See waitFor() for why this might be useful.
Promise.resolve(callback(payload)).then(function() {
resolves[i](payload);
}, function() {
rejects[i](new Error('Dispatcher callback unsuccessful'));
});
});
_promises = [];
},
Dispatcher.prototype.unregister=function(id) {"use strict";
invariant(
this.$Dispatcher_callbacks[id],
'Dispatcher.unregister(...): `%s` does not map to a registered callback.',
id
);
delete this.$Dispatcher_callbacks[id];
};

/**
* Allows a store to wait for the registered callbacks of other stores
* to get invoked before its own does.
* This function is not used by this TodoMVC example application, but
* it is very useful in a larger, more complex application.
*
* Example usage where StoreB waits for StoreA:
* Waits for the callbacks specified to be invoked before continuing execution
* of the current callback. This method should only be used by a callback in
* response to a dispatched payload.
*
* var StoreA = merge(EventEmitter.prototype, {
* // other methods omitted
* @param {array<string>} ids
*/
Dispatcher.prototype.waitFor=function(ids) {"use strict";
invariant(
this.$Dispatcher_isDispatching,
'Dispatcher.waitFor(...): Must be invoked while dispatching.'
);
for (var ii = 0; ii < ids.length; ii++) {
var id = ids[ii];
if (this.$Dispatcher_isPending[id]) {
invariant(
this.$Dispatcher_isHandled[id],
'Dispatcher.waitFor(...): Circular dependency detected while ' +
'waiting for `%s`.',
id
);
continue;
}
invariant(
this.$Dispatcher_callbacks[id],
'Dispatcher.waitFor(...): `%s` does not map to a registered callback.',
id
);
this.$Dispatcher_invokeCallback(id);
}
};

/**
* Dispatches a payload to all registered callbacks.
*
* dispatchIndex: Dispatcher.register(function(payload) {
* // switch statement with lots of cases
* })
* }
* @param {object} payload
*/
Dispatcher.prototype.dispatch=function(payload) {"use strict";
invariant(
!this.$Dispatcher_isDispatching,
'Dispatch.dispatch(...): Cannot dispatch in the middle of a dispatch.'
);
this.$Dispatcher_startDispatching(payload);
try {
for (var id in this.$Dispatcher_callbacks) {
if (this.$Dispatcher_isPending[id]) {
continue;
}
this.$Dispatcher_invokeCallback(id);
}
} finally {
this.$Dispatcher_stopDispatching();
}
};

/**
* Is this Dispatcher currently dispatching.
*
* var StoreB = merge(EventEmitter.prototype, {
* // other methods omitted
* @return {boolean}
*/
Dispatcher.prototype.isDispatching=function() {"use strict";
return this.$Dispatcher_isDispatching;
};

/**
* Call the calback stored with the given id. Also do some internal
* bookkeeping.
*
* dispatchIndex: Dispatcher.register(function(payload) {
* switch(payload.action.actionType) {
* @param {string} id
* @internal
*/
Dispatcher.prototype.$Dispatcher_invokeCallback=function(id) {"use strict";
this.$Dispatcher_isPending[id] = true;
this.$Dispatcher_callbacks[id](this.$Dispatcher_pendingPayload);
this.$Dispatcher_isHandled[id] = true;
};

/**
* Set up bookkeeping needed when dispatching.
*
* case MyConstants.FOO_ACTION:
* Dispatcher.waitFor([StoreA.dispatchIndex], function() {
* // Do stuff only after StoreA's callback returns.
* });
* }
* })
* }
* @param {object} payload
* @internal
*/
Dispatcher.prototype.$Dispatcher_startDispatching=function(payload) {"use strict";
for (var id in this.$Dispatcher_callbacks) {
this.$Dispatcher_isPending[id] = false;
this.$Dispatcher_isHandled[id] = false;
}
this.$Dispatcher_pendingPayload = payload;
this.$Dispatcher_isDispatching = true;
};

/**
* Clear bookkeeping used for dispatching.
*
* It should be noted that if StoreB waits for StoreA, and StoreA waits for
* StoreB, a circular dependency will occur, but no error will be thrown.
* A more robust Dispatcher would issue a warning in this scenario.
* @internal
*/
waitFor: function(/*array*/ promiseIndexes, /*function*/ callback) {
var selectedPromises = promiseIndexes.map(function(index) {
return _promises[index];
});
return Promise.all(selectedPromises).then(callback);
}
Dispatcher.prototype.$Dispatcher_stopDispatching=function() {"use strict";
this.$Dispatcher_pendingPayload = null;
this.$Dispatcher_isDispatching = false;
};

});

module.exports = Dispatcher;
Loading

0 comments on commit 1ba5beb

Please sign in to comment.