Contact
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
| Download
Views: 51016
1
/**
2
* Copyright (c) 2014-2015, Facebook, Inc.
3
* All rights reserved.
4
*
5
* This source code is licensed under the BSD-style license found in the
6
* LICENSE file in the root directory of this source tree. An additional grant
7
* of patent rights can be found in the PATENTS file in the same directory.
8
*
9
* @providesModule Dispatcher
10
* @typechecks
11
* @preventMunge
12
*/
13
14
"use strict";
15
16
var invariant = require('./invariant');
17
18
var _prefix = 'ID_';
19
20
/**
21
* Dispatcher is used to broadcast payloads to registered callbacks. This is
22
* different from generic pub-sub systems in two ways:
23
*
24
* 1) Callbacks are not subscribed to particular events. Every payload is
25
* dispatched to every registered callback.
26
* 2) Callbacks can be deferred in whole or part until other callbacks have
27
* been executed.
28
*
29
* For example, consider this hypothetical flight destination form, which
30
* selects a default city when a country is selected:
31
*
32
* var flightDispatcher = new Dispatcher();
33
*
34
* // Keeps track of which country is selected
35
* var CountryStore = {country: null};
36
*
37
* // Keeps track of which city is selected
38
* var CityStore = {city: null};
39
*
40
* // Keeps track of the base flight price of the selected city
41
* var FlightPriceStore = {price: null}
42
*
43
* When a user changes the selected city, we dispatch the payload:
44
*
45
* flightDispatcher.dispatch({
46
* actionType: 'city-update',
47
* selectedCity: 'paris'
48
* });
49
*
50
* This payload is digested by `CityStore`:
51
*
52
* flightDispatcher.register(function(payload) {
53
* if (payload.actionType === 'city-update') {
54
* CityStore.city = payload.selectedCity;
55
* }
56
* });
57
*
58
* When the user selects a country, we dispatch the payload:
59
*
60
* flightDispatcher.dispatch({
61
* actionType: 'country-update',
62
* selectedCountry: 'australia'
63
* });
64
*
65
* This payload is digested by both stores:
66
*
67
* CountryStore.dispatchToken = flightDispatcher.register(function(payload) {
68
* if (payload.actionType === 'country-update') {
69
* CountryStore.country = payload.selectedCountry;
70
* }
71
* });
72
*
73
* When the callback to update `CountryStore` is registered, we save a reference
74
* to the returned token. Using this token with `waitFor()`, we can guarantee
75
* that `CountryStore` is updated before the callback that updates `CityStore`
76
* needs to query its data.
77
*
78
* CityStore.dispatchToken = flightDispatcher.register(function(payload) {
79
* if (payload.actionType === 'country-update') {
80
* // `CountryStore.country` may not be updated.
81
* flightDispatcher.waitFor([CountryStore.dispatchToken]);
82
* // `CountryStore.country` is now guaranteed to be updated.
83
*
84
* // Select the default city for the new country
85
* CityStore.city = getDefaultCityForCountry(CountryStore.country);
86
* }
87
* });
88
*
89
* The usage of `waitFor()` can be chained, for example:
90
*
91
* FlightPriceStore.dispatchToken =
92
* flightDispatcher.register(function(payload) {
93
* switch (payload.actionType) {
94
* case 'country-update':
95
* case 'city-update':
96
* flightDispatcher.waitFor([CityStore.dispatchToken]);
97
* FlightPriceStore.price =
98
* getFlightPriceStore(CountryStore.country, CityStore.city);
99
* break;
100
* }
101
* });
102
*
103
* The `country-update` payload will be guaranteed to invoke the stores'
104
* registered callbacks in order: `CountryStore`, `CityStore`, then
105
* `FlightPriceStore`.
106
*/
107
class Dispatcher {
108
constructor() {
109
this._lastID = 1;
110
this._callbacks = {};
111
this._isPending = {};
112
this._isHandled = {};
113
this._isDispatching = false;
114
this._pendingPayload = null;
115
}
116
117
/**
118
* Registers a callback to be invoked with every dispatched payload. Returns
119
* a token that can be used with `waitFor()`.
120
*
121
* @param {function} callback
122
* @return {string}
123
*/
124
register(callback) {
125
var id = _prefix + this._lastID++;
126
this._callbacks[id] = callback;
127
return id;
128
}
129
130
/**
131
* Removes a callback based on its token.
132
*
133
* @param {string} id
134
*/
135
unregister(id) {
136
invariant(
137
this._callbacks[id],
138
'Dispatcher.unregister(...): `%s` does not map to a registered callback.',
139
id
140
);
141
delete this._callbacks[id];
142
}
143
144
/**
145
* Waits for the callbacks specified to be invoked before continuing execution
146
* of the current callback. This method should only be used by a callback in
147
* response to a dispatched payload.
148
*
149
* @param {array<string>} ids
150
*/
151
waitFor(ids) {
152
invariant(
153
this._isDispatching,
154
'Dispatcher.waitFor(...): Must be invoked while dispatching.'
155
);
156
for (var ii = 0; ii < ids.length; ii++) {
157
var id = ids[ii];
158
if (this._isPending[id]) {
159
invariant(
160
this._isHandled[id],
161
'Dispatcher.waitFor(...): Circular dependency detected while ' +
162
'waiting for `%s`.',
163
id
164
);
165
continue;
166
}
167
invariant(
168
this._callbacks[id],
169
'Dispatcher.waitFor(...): `%s` does not map to a registered callback.',
170
id
171
);
172
this._invokeCallback(id);
173
}
174
}
175
176
/**
177
* Dispatches a payload to all registered callbacks.
178
*
179
* @param {object} payload
180
*/
181
dispatch(payload) {
182
invariant(
183
!this._isDispatching,
184
'Dispatch.dispatch(...): Cannot dispatch in the middle of a dispatch.'
185
);
186
this._startDispatching(payload);
187
try {
188
for (var id in this._callbacks) {
189
if (this._isPending[id]) {
190
continue;
191
}
192
this._invokeCallback(id);
193
}
194
} finally {
195
this._stopDispatching();
196
}
197
}
198
199
/**
200
* Is this Dispatcher currently dispatching.
201
*
202
* @return {boolean}
203
*/
204
isDispatching() {
205
return this._isDispatching;
206
}
207
208
/**
209
* Call the callback stored with the given id. Also do some internal
210
* bookkeeping.
211
*
212
* @param {string} id
213
* @internal
214
*/
215
_invokeCallback(id) {
216
this._isPending[id] = true;
217
this._callbacks[id](this._pendingPayload);
218
this._isHandled[id] = true;
219
}
220
221
/**
222
* Set up bookkeeping needed when dispatching.
223
*
224
* @param {object} payload
225
* @internal
226
*/
227
_startDispatching(payload) {
228
for (var id in this._callbacks) {
229
this._isPending[id] = false;
230
this._isHandled[id] = false;
231
}
232
this._pendingPayload = payload;
233
this._isDispatching = true;
234
}
235
236
/**
237
* Clear bookkeeping used for dispatching.
238
*
239
* @internal
240
*/
241
_stopDispatching() {
242
this._pendingPayload = null;
243
this._isDispatching = false;
244
}
245
}
246
247
module.exports = Dispatcher;
248
249