Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Download
80529 views
1
/*
2
* Copyright (c) 2014, 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
*/
12
13
"use strict";
14
15
var invariant = require('./invariant');
16
17
var _lastID = 1;
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
* flightDispatcher.waitFor([CityStore.dispatchToken]);
96
* FlightPriceStore.price =
97
* getFlightPriceStore(CountryStore.country, CityStore.city);
98
* break;
99
*
100
* case 'city-update':
101
* FlightPriceStore.price =
102
* FlightPriceStore(CountryStore.country, CityStore.city);
103
* break;
104
* }
105
* });
106
*
107
* The `country-update` payload will be guaranteed to invoke the stores'
108
* registered callbacks in order: `CountryStore`, `CityStore`, then
109
* `FlightPriceStore`.
110
*/
111
112
function Dispatcher() {
113
this.$Dispatcher_callbacks = {};
114
this.$Dispatcher_isPending = {};
115
this.$Dispatcher_isHandled = {};
116
this.$Dispatcher_isDispatching = false;
117
this.$Dispatcher_pendingPayload = null;
118
}
119
120
/**
121
* Registers a callback to be invoked with every dispatched payload. Returns
122
* a token that can be used with `waitFor()`.
123
*
124
* @param {function} callback
125
* @return {string}
126
*/
127
Dispatcher.prototype.register=function(callback) {
128
var id = _prefix + _lastID++;
129
this.$Dispatcher_callbacks[id] = callback;
130
return id;
131
};
132
133
/**
134
* Removes a callback based on its token.
135
*
136
* @param {string} id
137
*/
138
Dispatcher.prototype.unregister=function(id) {
139
invariant(
140
this.$Dispatcher_callbacks[id],
141
'Dispatcher.unregister(...): `%s` does not map to a registered callback.',
142
id
143
);
144
delete this.$Dispatcher_callbacks[id];
145
};
146
147
/**
148
* Waits for the callbacks specified to be invoked before continuing execution
149
* of the current callback. This method should only be used by a callback in
150
* response to a dispatched payload.
151
*
152
* @param {array<string>} ids
153
*/
154
Dispatcher.prototype.waitFor=function(ids) {
155
invariant(
156
this.$Dispatcher_isDispatching,
157
'Dispatcher.waitFor(...): Must be invoked while dispatching.'
158
);
159
for (var ii = 0; ii < ids.length; ii++) {
160
var id = ids[ii];
161
if (this.$Dispatcher_isPending[id]) {
162
invariant(
163
this.$Dispatcher_isHandled[id],
164
'Dispatcher.waitFor(...): Circular dependency detected while ' +
165
'waiting for `%s`.',
166
id
167
);
168
continue;
169
}
170
invariant(
171
this.$Dispatcher_callbacks[id],
172
'Dispatcher.waitFor(...): `%s` does not map to a registered callback.',
173
id
174
);
175
this.$Dispatcher_invokeCallback(id);
176
}
177
};
178
179
/**
180
* Dispatches a payload to all registered callbacks.
181
*
182
* @param {object} payload
183
*/
184
Dispatcher.prototype.dispatch=function(payload) {
185
invariant(
186
!this.$Dispatcher_isDispatching,
187
'Dispatch.dispatch(...): Cannot dispatch in the middle of a dispatch.'
188
);
189
this.$Dispatcher_startDispatching(payload);
190
try {
191
for (var id in this.$Dispatcher_callbacks) {
192
if (this.$Dispatcher_isPending[id]) {
193
continue;
194
}
195
this.$Dispatcher_invokeCallback(id);
196
}
197
} finally {
198
this.$Dispatcher_stopDispatching();
199
}
200
};
201
202
/**
203
* Is this Dispatcher currently dispatching.
204
*
205
* @return {boolean}
206
*/
207
Dispatcher.prototype.isDispatching=function() {
208
return this.$Dispatcher_isDispatching;
209
};
210
211
/**
212
* Call the callback stored with the given id. Also do some internal
213
* bookkeeping.
214
*
215
* @param {string} id
216
* @internal
217
*/
218
Dispatcher.prototype.$Dispatcher_invokeCallback=function(id) {
219
this.$Dispatcher_isPending[id] = true;
220
this.$Dispatcher_callbacks[id](this.$Dispatcher_pendingPayload);
221
this.$Dispatcher_isHandled[id] = true;
222
};
223
224
/**
225
* Set up bookkeeping needed when dispatching.
226
*
227
* @param {object} payload
228
* @internal
229
*/
230
Dispatcher.prototype.$Dispatcher_startDispatching=function(payload) {
231
for (var id in this.$Dispatcher_callbacks) {
232
this.$Dispatcher_isPending[id] = false;
233
this.$Dispatcher_isHandled[id] = false;
234
}
235
this.$Dispatcher_pendingPayload = payload;
236
this.$Dispatcher_isDispatching = true;
237
};
238
239
/**
240
* Clear bookkeeping used for dispatching.
241
*
242
* @internal
243
*/
244
Dispatcher.prototype.$Dispatcher_stopDispatching=function() {
245
this.$Dispatcher_pendingPayload = null;
246
this.$Dispatcher_isDispatching = false;
247
};
248
249
250
module.exports = Dispatcher;
251
252