/*1* Copyright (c) 2014, Facebook, Inc.2* All rights reserved.3*4* This source code is licensed under the BSD-style license found in the5* LICENSE file in the root directory of this source tree. An additional grant6* of patent rights can be found in the PATENTS file in the same directory.7*8* @providesModule Dispatcher9* @typechecks10*/1112"use strict";1314var invariant = require('./invariant');1516var _lastID = 1;17var _prefix = 'ID_';1819/**20* Dispatcher is used to broadcast payloads to registered callbacks. This is21* different from generic pub-sub systems in two ways:22*23* 1) Callbacks are not subscribed to particular events. Every payload is24* dispatched to every registered callback.25* 2) Callbacks can be deferred in whole or part until other callbacks have26* been executed.27*28* For example, consider this hypothetical flight destination form, which29* selects a default city when a country is selected:30*31* var flightDispatcher = new Dispatcher();32*33* // Keeps track of which country is selected34* var CountryStore = {country: null};35*36* // Keeps track of which city is selected37* var CityStore = {city: null};38*39* // Keeps track of the base flight price of the selected city40* var FlightPriceStore = {price: null}41*42* When a user changes the selected city, we dispatch the payload:43*44* flightDispatcher.dispatch({45* actionType: 'city-update',46* selectedCity: 'paris'47* });48*49* This payload is digested by `CityStore`:50*51* flightDispatcher.register(function(payload) {52* if (payload.actionType === 'city-update') {53* CityStore.city = payload.selectedCity;54* }55* });56*57* When the user selects a country, we dispatch the payload:58*59* flightDispatcher.dispatch({60* actionType: 'country-update',61* selectedCountry: 'australia'62* });63*64* This payload is digested by both stores:65*66* CountryStore.dispatchToken = flightDispatcher.register(function(payload) {67* if (payload.actionType === 'country-update') {68* CountryStore.country = payload.selectedCountry;69* }70* });71*72* When the callback to update `CountryStore` is registered, we save a reference73* to the returned token. Using this token with `waitFor()`, we can guarantee74* that `CountryStore` is updated before the callback that updates `CityStore`75* needs to query its data.76*77* CityStore.dispatchToken = flightDispatcher.register(function(payload) {78* if (payload.actionType === 'country-update') {79* // `CountryStore.country` may not be updated.80* flightDispatcher.waitFor([CountryStore.dispatchToken]);81* // `CountryStore.country` is now guaranteed to be updated.82*83* // Select the default city for the new country84* CityStore.city = getDefaultCityForCountry(CountryStore.country);85* }86* });87*88* The usage of `waitFor()` can be chained, for example:89*90* FlightPriceStore.dispatchToken =91* flightDispatcher.register(function(payload) {92* switch (payload.actionType) {93* case 'country-update':94* flightDispatcher.waitFor([CityStore.dispatchToken]);95* FlightPriceStore.price =96* getFlightPriceStore(CountryStore.country, CityStore.city);97* break;98*99* case 'city-update':100* FlightPriceStore.price =101* FlightPriceStore(CountryStore.country, CityStore.city);102* break;103* }104* });105*106* The `country-update` payload will be guaranteed to invoke the stores'107* registered callbacks in order: `CountryStore`, `CityStore`, then108* `FlightPriceStore`.109*/110111function Dispatcher() {112this.$Dispatcher_callbacks = {};113this.$Dispatcher_isPending = {};114this.$Dispatcher_isHandled = {};115this.$Dispatcher_isDispatching = false;116this.$Dispatcher_pendingPayload = null;117}118119/**120* Registers a callback to be invoked with every dispatched payload. Returns121* a token that can be used with `waitFor()`.122*123* @param {function} callback124* @return {string}125*/126Dispatcher.prototype.register=function(callback) {127var id = _prefix + _lastID++;128this.$Dispatcher_callbacks[id] = callback;129return id;130};131132/**133* Removes a callback based on its token.134*135* @param {string} id136*/137Dispatcher.prototype.unregister=function(id) {138invariant(139this.$Dispatcher_callbacks[id],140'Dispatcher.unregister(...): `%s` does not map to a registered callback.',141id142);143delete this.$Dispatcher_callbacks[id];144};145146/**147* Waits for the callbacks specified to be invoked before continuing execution148* of the current callback. This method should only be used by a callback in149* response to a dispatched payload.150*151* @param {array<string>} ids152*/153Dispatcher.prototype.waitFor=function(ids) {154invariant(155this.$Dispatcher_isDispatching,156'Dispatcher.waitFor(...): Must be invoked while dispatching.'157);158for (var ii = 0; ii < ids.length; ii++) {159var id = ids[ii];160if (this.$Dispatcher_isPending[id]) {161invariant(162this.$Dispatcher_isHandled[id],163'Dispatcher.waitFor(...): Circular dependency detected while ' +164'waiting for `%s`.',165id166);167continue;168}169invariant(170this.$Dispatcher_callbacks[id],171'Dispatcher.waitFor(...): `%s` does not map to a registered callback.',172id173);174this.$Dispatcher_invokeCallback(id);175}176};177178/**179* Dispatches a payload to all registered callbacks.180*181* @param {object} payload182*/183Dispatcher.prototype.dispatch=function(payload) {184invariant(185!this.$Dispatcher_isDispatching,186'Dispatch.dispatch(...): Cannot dispatch in the middle of a dispatch.'187);188this.$Dispatcher_startDispatching(payload);189try {190for (var id in this.$Dispatcher_callbacks) {191if (this.$Dispatcher_isPending[id]) {192continue;193}194this.$Dispatcher_invokeCallback(id);195}196} finally {197this.$Dispatcher_stopDispatching();198}199};200201/**202* Is this Dispatcher currently dispatching.203*204* @return {boolean}205*/206Dispatcher.prototype.isDispatching=function() {207return this.$Dispatcher_isDispatching;208};209210/**211* Call the callback stored with the given id. Also do some internal212* bookkeeping.213*214* @param {string} id215* @internal216*/217Dispatcher.prototype.$Dispatcher_invokeCallback=function(id) {218this.$Dispatcher_isPending[id] = true;219this.$Dispatcher_callbacks[id](this.$Dispatcher_pendingPayload);220this.$Dispatcher_isHandled[id] = true;221};222223/**224* Set up bookkeeping needed when dispatching.225*226* @param {object} payload227* @internal228*/229Dispatcher.prototype.$Dispatcher_startDispatching=function(payload) {230for (var id in this.$Dispatcher_callbacks) {231this.$Dispatcher_isPending[id] = false;232this.$Dispatcher_isHandled[id] = false;233}234this.$Dispatcher_pendingPayload = payload;235this.$Dispatcher_isDispatching = true;236};237238/**239* Clear bookkeeping used for dispatching.240*241* @internal242*/243Dispatcher.prototype.$Dispatcher_stopDispatching=function() {244this.$Dispatcher_pendingPayload = null;245this.$Dispatcher_isDispatching = false;246};247248249module.exports = Dispatcher;250251252