/**1* Copyright (c) 2014-2015, 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* @preventMunge11*/1213"use strict";1415var invariant = require('./invariant');1617var _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* case 'city-update':95* flightDispatcher.waitFor([CityStore.dispatchToken]);96* FlightPriceStore.price =97* getFlightPriceStore(CountryStore.country, CityStore.city);98* break;99* }100* });101*102* The `country-update` payload will be guaranteed to invoke the stores'103* registered callbacks in order: `CountryStore`, `CityStore`, then104* `FlightPriceStore`.105*/106class Dispatcher {107constructor() {108this._lastID = 1;109this._callbacks = {};110this._isPending = {};111this._isHandled = {};112this._isDispatching = false;113this._pendingPayload = null;114}115116/**117* Registers a callback to be invoked with every dispatched payload. Returns118* a token that can be used with `waitFor()`.119*120* @param {function} callback121* @return {string}122*/123register(callback) {124var id = _prefix + this._lastID++;125this._callbacks[id] = callback;126return id;127}128129/**130* Removes a callback based on its token.131*132* @param {string} id133*/134unregister(id) {135invariant(136this._callbacks[id],137'Dispatcher.unregister(...): `%s` does not map to a registered callback.',138id139);140delete this._callbacks[id];141}142143/**144* Waits for the callbacks specified to be invoked before continuing execution145* of the current callback. This method should only be used by a callback in146* response to a dispatched payload.147*148* @param {array<string>} ids149*/150waitFor(ids) {151invariant(152this._isDispatching,153'Dispatcher.waitFor(...): Must be invoked while dispatching.'154);155for (var ii = 0; ii < ids.length; ii++) {156var id = ids[ii];157if (this._isPending[id]) {158invariant(159this._isHandled[id],160'Dispatcher.waitFor(...): Circular dependency detected while ' +161'waiting for `%s`.',162id163);164continue;165}166invariant(167this._callbacks[id],168'Dispatcher.waitFor(...): `%s` does not map to a registered callback.',169id170);171this._invokeCallback(id);172}173}174175/**176* Dispatches a payload to all registered callbacks.177*178* @param {object} payload179*/180dispatch(payload) {181invariant(182!this._isDispatching,183'Dispatch.dispatch(...): Cannot dispatch in the middle of a dispatch.'184);185this._startDispatching(payload);186try {187for (var id in this._callbacks) {188if (this._isPending[id]) {189continue;190}191this._invokeCallback(id);192}193} finally {194this._stopDispatching();195}196}197198/**199* Is this Dispatcher currently dispatching.200*201* @return {boolean}202*/203isDispatching() {204return this._isDispatching;205}206207/**208* Call the callback stored with the given id. Also do some internal209* bookkeeping.210*211* @param {string} id212* @internal213*/214_invokeCallback(id) {215this._isPending[id] = true;216this._callbacks[id](this._pendingPayload);217this._isHandled[id] = true;218}219220/**221* Set up bookkeeping needed when dispatching.222*223* @param {object} payload224* @internal225*/226_startDispatching(payload) {227for (var id in this._callbacks) {228this._isPending[id] = false;229this._isHandled[id] = false;230}231this._pendingPayload = payload;232this._isDispatching = true;233}234235/**236* Clear bookkeeping used for dispatching.237*238* @internal239*/240_stopDispatching() {241this._pendingPayload = null;242this._isDispatching = false;243}244}245246module.exports = Dispatcher;247248249