@soundworks/core 4.0.0-alpha.2 → 4.0.0-alpha.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,9 @@
1
+ import { isString, isFunction, isPlainObject } from '@ircam/sc-utils';
1
2
  import SharedState from './BaseSharedState.js';
3
+ import SharedStateCollection from './BaseSharedStateCollection.js';
4
+ import BatchedTransport from './BatchedTransport.js';
5
+ import ParameterBag from './ParameterBag.js';
6
+ import PromiseStore from './PromiseStore.js';
2
7
  import {
3
8
  CREATE_REQUEST,
4
9
  CREATE_RESPONSE,
@@ -8,38 +13,43 @@ import {
8
13
  ATTACH_ERROR,
9
14
  OBSERVE_REQUEST,
10
15
  OBSERVE_RESPONSE,
16
+ OBSERVE_ERROR,
11
17
  OBSERVE_NOTIFICATION,
12
18
  UNOBSERVE_NOTIFICATION,
13
19
  DELETE_SCHEMA,
20
+ GET_SCHEMA_REQUEST,
21
+ GET_SCHEMA_RESPONSE,
22
+ GET_SCHEMA_ERROR,
14
23
  } from './constants.js';
15
- import {
16
- storeRequestPromise,
17
- resolveRequest,
18
- rejectRequest,
19
- } from './promise-store.js';
20
-
21
- /**
22
- * @private
23
- */
24
+
25
+ /** @private */
24
26
  class BaseStateManager {
27
+ constructor() {
28
+ this._statesById = new Map(); // <id, state>
29
+ this._cachedSchemas = new Map(); // <shemaName, definition>
30
+
31
+ this._observeListeners = new Set(); // Set <[observedSchemaName, callback, options]>
32
+ this._observeRequestCallbacks = new Map(); // Map <reqId, [observedSchemaName, callback, options]>
33
+
34
+ this._promiseStore = new PromiseStore();
35
+ }
36
+
25
37
  /**
38
+ * Executed on `client.init`
26
39
  * @param {Number} id - Id of the node.
27
- * @param {Object} transport - Transport to use for synchronizing the state.
40
+ * @param {Object} transport - Transport to use for synchronizing the states.
28
41
  * Must implement a basic EventEmitter API.
29
42
  */
30
- constructor(id, transport) {
31
- this.client = { id, transport };
32
-
33
- this._statesById = new Map();
34
- this._observeListeners = new Set();
35
- this._cachedSchemas = new Map();
36
- this._observeRequestCallbacks = new Map();
43
+ /** @private */
44
+ init(id, transport) {
45
+ const batchedTransport = new BatchedTransport(transport);
46
+ this.client = { id, transport: batchedTransport };
37
47
 
38
48
  // ---------------------------------------------
39
49
  // CREATE
40
50
  // ---------------------------------------------
41
51
 
42
- this.client.transport.addListener(CREATE_RESPONSE, (reqId, stateId, remoteId, schemaName, schema, initValues) => {
52
+ this.client.transport.addListener(CREATE_RESPONSE, (reqId, stateId, remoteId, schemaName, schema, initValues, filter) => {
43
53
 
44
54
  // cache schema when first dealing it to save some bandwidth
45
55
  if (!this._cachedSchemas.has(schemaName)) {
@@ -48,20 +58,20 @@ class BaseStateManager {
48
58
 
49
59
  schema = this._cachedSchemas.get(schemaName);
50
60
 
51
- const state = new SharedState(stateId, remoteId, schemaName, schema, this.client, true, this, initValues);
61
+ const state = new SharedState(stateId, remoteId, schemaName, schema, this.client, true, this, initValues, null);
52
62
  this._statesById.set(state.id, state);
53
63
 
54
- resolveRequest(reqId, state);
64
+ this._promiseStore.resolve(reqId, state);
55
65
  });
56
66
 
57
67
  this.client.transport.addListener(CREATE_ERROR, (reqId, msg) => {
58
- rejectRequest(reqId, msg);
68
+ this._promiseStore.reject(reqId, msg);
59
69
  });
60
70
 
61
71
  // ---------------------------------------------
62
72
  // ATTACH (when creator, is attached by default)
63
73
  // ---------------------------------------------
64
- this.client.transport.addListener(ATTACH_RESPONSE, (reqId, stateId, remoteId, schemaName, schema, currentValues) => {
74
+ this.client.transport.addListener(ATTACH_RESPONSE, (reqId, stateId, remoteId, schemaName, schema, currentValues, filter) => {
65
75
  // cache schema when first dealing with it to save some bandwidth
66
76
  // note: when we make the schemas dynamic at some point
67
77
  // the server should be able to invalidate the schema and send
@@ -72,14 +82,14 @@ class BaseStateManager {
72
82
 
73
83
  schema = this._cachedSchemas.get(schemaName);
74
84
 
75
- const state = new SharedState(stateId, remoteId, schemaName, schema, this.client, false, this, currentValues);
85
+ const state = new SharedState(stateId, remoteId, schemaName, schema, this.client, false, this, currentValues, filter);
76
86
  this._statesById.set(state.id, state);
77
87
 
78
- resolveRequest(reqId, state);
88
+ this._promiseStore.resolve(reqId, state);
79
89
  });
80
90
 
81
91
  this.client.transport.addListener(ATTACH_ERROR, (reqId, msg) => {
82
- rejectRequest(reqId, msg);
92
+ this._promiseStore.reject(reqId, msg);
83
93
  });
84
94
 
85
95
  // ---------------------------------------------
@@ -88,28 +98,52 @@ class BaseStateManager {
88
98
  this.client.transport.addListener(OBSERVE_RESPONSE, async (reqId, ...list) => {
89
99
  // retrieve the callback that have been stored in observe to make sure
90
100
  // we don't call another callback that may have been registered earlier.
91
- const callback = this._observeRequestCallbacks.get(reqId);
101
+ const observeInfos = this._observeRequestCallbacks.get(reqId);
102
+ const [observedSchemaName, callback, options] = observeInfos;
103
+
104
+ // move observeInfos from `_observeRequestCallbacks` to `_observeListeners`
105
+ // to guarantee order of execution, @see not in `.observe`
92
106
  this._observeRequestCallbacks.delete(reqId);
107
+ this._observeListeners.add(observeInfos);
93
108
 
94
109
  const promises = list.map(([schemaName, stateId, nodeId]) => {
95
- return callback(schemaName, stateId, nodeId);
110
+ const filter = this._filterObserve(observedSchemaName, schemaName, nodeId, options);
111
+
112
+ if (!filter) {
113
+ return callback(schemaName, stateId, nodeId);
114
+ } else {
115
+ return Promise.resolve();
116
+ }
96
117
  });
97
118
 
98
119
  await Promise.all(promises);
99
120
 
100
121
  const unsubscribe = () => {
101
- this._observeListeners.delete(callback);
122
+ this._observeListeners.delete(observeInfos);
102
123
 
124
+ // no more listeners, we can stop receiving notifications from the server
103
125
  if (this._observeListeners.size === 0) {
104
126
  this.client.transport.emit(UNOBSERVE_NOTIFICATION);
105
127
  }
106
128
  };
107
- resolveRequest(reqId, unsubscribe);
129
+
130
+ this._promiseStore.resolve(reqId, unsubscribe);
131
+ });
132
+
133
+ // Observe error can occur if observed schema name does not exists
134
+ this.client.transport.addListener(OBSERVE_ERROR, (reqId, msg) => {
135
+ this._observeRequestCallbacks.delete(reqId);
136
+ this._promiseStore.reject(reqId, msg);
108
137
  });
109
138
 
110
- this.client.transport.addListener(OBSERVE_NOTIFICATION, (...list) => {
111
- list.forEach(([schemaName, stateId, nodeId]) => {
112
- this._observeListeners.forEach(callback => callback(schemaName, stateId, nodeId));
139
+ this.client.transport.addListener(OBSERVE_NOTIFICATION, (schemaName, stateId, nodeId) => {
140
+ this._observeListeners.forEach(observeInfos => {
141
+ const [observedSchemaName, callback, options] = observeInfos;
142
+ const filter = this._filterObserve(observedSchemaName, schemaName, nodeId, options);
143
+
144
+ if (!filter) {
145
+ callback(schemaName, stateId, nodeId);
146
+ }
113
147
  });
114
148
  });
115
149
 
@@ -119,6 +153,59 @@ class BaseStateManager {
119
153
  this.client.transport.addListener(DELETE_SCHEMA, schemaName => {
120
154
  this._cachedSchemas.delete(schemaName);
121
155
  });
156
+
157
+ // ---------------------------------------------
158
+ // GET SCHEMA
159
+ // ---------------------------------------------
160
+ this.client.transport.addListener(GET_SCHEMA_RESPONSE, (reqId, schemaName, schema) => {
161
+ if (!this._cachedSchemas.has(schemaName)) {
162
+ this._cachedSchemas.set(schemaName, schema);
163
+ }
164
+ // return a populated schema
165
+ const parameterBag = new ParameterBag(schema);
166
+ this._promiseStore.resolve(reqId, parameterBag.getSchema());
167
+ });
168
+
169
+ this.client.transport.addListener(GET_SCHEMA_ERROR, (reqId, msg) => {
170
+ this._promiseStore.reject(reqId, msg);
171
+ });
172
+ }
173
+
174
+ /** @private */
175
+ _filterObserve(observedSchemaName, schemaName, creatorId, options) {
176
+ let filter = true;
177
+ // schema name filter filer
178
+ if (observedSchemaName === null || observedSchemaName === schemaName) {
179
+ filter = false;
180
+ }
181
+ // filter state created by client if excludeLocal is true
182
+ if (options.excludeLocal === true && creatorId === this.client.id) {
183
+ filter = true;
184
+ }
185
+
186
+ return filter;
187
+ }
188
+
189
+ /**
190
+ * Return the schema from a given registered schema name
191
+ *
192
+ * @param {String} schemaName - Name of the schema as given on registration
193
+ * (cf. ServerStateManager)
194
+ * @example
195
+ * const schema = await client.stateManager.getSchema('my-schema');
196
+ */
197
+ async getSchema(schemaName) {
198
+ if (this._cachedSchemas.has(schemaName)) {
199
+ const schema = this._cachedSchemas.get(schemaName);
200
+ // return a populated schema
201
+ const parameterBag = new ParameterBag(schema);
202
+ return parameterBag.getSchema();
203
+ }
204
+
205
+ return new Promise((resolve, reject) => {
206
+ const reqId = this._promiseStore.add(resolve, reject, 'get-schema');
207
+ this.client.transport.emit(GET_SCHEMA_REQUEST, reqId, schemaName);
208
+ });
122
209
  }
123
210
 
124
211
  /**
@@ -135,7 +222,7 @@ class BaseStateManager {
135
222
  */
136
223
  async create(schemaName, initValues = {}) {
137
224
  return new Promise((resolve, reject) => {
138
- const reqId = storeRequestPromise(resolve, reject);
225
+ const reqId = this._promiseStore.add(resolve, reject, 'create-request');
139
226
  const requireSchema = this._cachedSchemas.has(schemaName) ? false : true;
140
227
  this.client.transport.emit(CREATE_REQUEST, reqId, schemaName, requireSchema, initValues);
141
228
  });
@@ -144,23 +231,64 @@ class BaseStateManager {
144
231
  /**
145
232
  * Attach to an existing `SharedState` instance.
146
233
  *
234
+ * Alternative signatures:
235
+ * - `stateManager.attach(schemaName)`
236
+ * - `stateManager.attach(schemaName, stateId)`
237
+ * - `stateManager.attach(schemaName, filter)`
238
+ * - `stateManager.attach(schemaName, stateId, filter)`
239
+ *
147
240
  * @param {String} schemaName - Name of the schema as given on registration
148
241
  * (cf. ServerStateManager)
149
242
  * @param {Object} [stateId=null] - Id of the state to attach to. If `null`,
150
243
  * attach to the first state found with the given schema name (usefull for
151
244
  * globally shared states owned by the server).
245
+ * @param {array|null} [filter=null] - Filter parameters of interest in the
246
+ * returned state. If set to `null`, no filter applied.
152
247
  * @returns {client.SharedState|server.SharedState}
153
248
  * @see {@link client.SharedState}
154
249
  * @see {@link server.SharedState}
250
+ *
155
251
  * @example
156
252
  * const state = await client.stateManager.attach('my-schema');
157
253
  */
158
- async attach(schemaName, stateId = null) {
254
+ async attach(schemaName, stateIdOrFilter = null, filter = null) {
255
+ let stateId = null;
256
+
257
+ if (!isString(schemaName)) {
258
+ throw new TypeError(`Cannot execute 'attach' on 'StateManager': argument 1 should be either a number or an array`);
259
+ }
260
+
261
+ if (arguments.length === 2) {
262
+ if (stateIdOrFilter === null) {
263
+ stateId = null;
264
+ filter = null;
265
+ } else if (Number.isFinite(stateIdOrFilter)) {
266
+ stateId = stateIdOrFilter;
267
+ filter = null;
268
+ } else if (Array.isArray(stateIdOrFilter)) {
269
+ stateId = null;
270
+ filter = stateIdOrFilter;
271
+ } else {
272
+ throw new TypeError(`Cannot execute 'attach' on 'StateManager': argument 2 should be either null, a number or an array`);
273
+ }
274
+ }
275
+
276
+ if (arguments.length === 3) {
277
+ stateId = stateIdOrFilter;
278
+
279
+ if (stateId !== null && !Number.isFinite(stateId)) {
280
+ throw new TypeError(`Cannot execute 'attach' on 'StateManager': argument 2 should be either null or a number`);
281
+ }
282
+
283
+ if (filter !== null && !Array.isArray(filter)) {
284
+ throw new TypeError(`Cannot execute 'attach' on 'StateManager': argument 3 should be either null or an array`);
285
+ }
286
+ }
287
+
159
288
  return new Promise((resolve, reject) => {
160
- // @todo - add a timeout
161
- const reqId = storeRequestPromise(resolve, reject);
289
+ const reqId = this._promiseStore.add(resolve, reject, 'attach-request');
162
290
  const requireSchema = this._cachedSchemas.has(schemaName) ? false : true;
163
- this.client.transport.emit(ATTACH_REQUEST, reqId, schemaName, stateId, requireSchema);
291
+ this.client.transport.emit(ATTACH_REQUEST, reqId, schemaName, stateId, requireSchema, filter);
164
292
  });
165
293
  }
166
294
 
@@ -170,30 +298,206 @@ class BaseStateManager {
170
298
  * the state of all other clients of the application, to monitor them and/or take
171
299
  * control over them from a single point.
172
300
  *
173
- * @todo Optionnal schema name
301
+ * Notes:
302
+ * - The order of execution is not guaranted between nodes, i.e. an state attached
303
+ * in the `observe` callback could be created before the `async create` method resolves.
304
+ * - Filtering, i.e. `observedSchemaName` and `options.excludeLocal` are handled
305
+ * on the node side, the server just notify all state creation activity and
306
+ * the node executes the given callbacks according to the different filter rules.
307
+ * Such strategy allows to share the observe notifications between all observers.
308
+ *
309
+ * Alternative signatures:
310
+ * - `stateManager.observe(callback)`
311
+ * - `stateManager.observe(schemaName, callback)`
312
+ * - `stateManager.observe(callback, options)`
313
+ * - `stateManager.observe(schemaName, callback, options)`
314
+ *
315
+ * @param {string} [schemaName] - optionnal schema name to filter the observed
316
+ * states.
174
317
  * @param {server.StateManager~ObserveCallback|client.StateManager~ObserveCallback}
175
318
  * callback - Function to be called when a new state is created on the network.
176
- * @returns {Promise<Function>} - Return a Promise that resolves when the callback
177
- * as been executed for the first time. The promise value is a function which
178
- * allows to stop observing the network.
319
+ * @param {object} options - Options.
320
+ * @param {boolean} [options.excludeLocal = false] - If set to true, exclude states
321
+ * created locallly, i.e. by the same node, from the collection.
322
+ * @returns {Promise<Function>} - Returns a Promise that resolves when the given
323
+ * callback as been executed on each existing states. The promise value is a
324
+ * function which allows to stop observing the states on the network.
325
+ *
179
326
  * @example
180
- * client.stateManager.observe(async (schemaName, stateId, nodeId) => {
327
+ * client.stateManager.observe(async (schemaName, stateId) => {
181
328
  * if (schemaName === 'something') {
182
329
  * const state = await this.client.stateManager.attach(schemaName, stateId);
183
330
  * console.log(state.getValues());
184
331
  * }
185
332
  * });
186
333
  */
187
- async observe(callback) {
188
- this._observeListeners.add(callback);
189
- // store function
334
+ // note: all filtering is done only on client-side as it is really more simple to
335
+ // handle this way and the network overhead is very low for observe notifications:
336
+ // i.e. schemaName, stateId, nodeId
337
+ async observe(...args) {
338
+
339
+ const defaultOptions = {
340
+ excludeLocal: false,
341
+ };
342
+
343
+ let observedSchemaName;
344
+ let callback;
345
+ let options;
346
+
347
+ switch (args.length) {
348
+ case 1: {
349
+ // variation: .observe(callback)
350
+ if (!isFunction(args[0])) {
351
+ throw new TypeError(`[stateManager] Invalid arguments, argument 1 should be a function"`);
352
+ }
353
+
354
+ observedSchemaName = null;
355
+ callback = args[0];
356
+ options = defaultOptions;
357
+ break;
358
+ }
359
+ case 2: {
360
+ // variation: .observe(schemaName, callback)
361
+ if (isString(args[0])) {
362
+ if (!isFunction(args[1])) {
363
+ throw new TypeError(`[stateManager] Invalid arguments, argument 2 should be a function"`);
364
+ }
365
+
366
+ observedSchemaName = args[0];
367
+ callback = args[1];
368
+ options = defaultOptions;
369
+
370
+ // variation: .observe(callback, options) API
371
+ } else if (isFunction(args[0])) {
372
+ if (!isPlainObject(args[1])) {
373
+ throw new TypeError(`[stateManager] Invalid arguments, argument 2 should be an object"`);
374
+ }
375
+
376
+ observedSchemaName = null;
377
+ callback = args[0];
378
+ options = Object.assign(defaultOptions, args[1]);
379
+
380
+ } else {
381
+ throw new TypeError(`[stateManager] Invalid signature, refer to the StateManager.observe documentation"`);
382
+ }
383
+
384
+ break;
385
+ }
386
+ case 3: {
387
+ // variation: .observe(schemaName, callback, options)
388
+ if (!isString(args[0])) {
389
+ throw new TypeError(`[stateManager] Invalid arguments, argument 1 should be a string"`);
390
+ }
391
+
392
+ if (!isFunction(args[1])) {
393
+ throw new TypeError(`[stateManager] Invalid arguments, argument 2 should be a function"`);
394
+ }
395
+
396
+ if (!isPlainObject(args[2])) {
397
+ throw new TypeError(`[stateManager] Invalid arguments, argument 2 should be an object"`);
398
+ }
399
+
400
+ observedSchemaName = args[0];
401
+ callback = args[1];
402
+ options = Object.assign(defaultOptions, args[2]);
403
+
404
+ break;
405
+ }
406
+ // throw in all other cases
407
+ default: {
408
+ throw new Error(`[stateManager] Invalid signature, refer to the StateManager.observe documentation"`);
409
+ }
410
+ }
411
+
412
+ // resend request to get updated list of states
190
413
  return new Promise((resolve, reject) => {
191
- const reqId = storeRequestPromise(resolve, reject);
192
- // store the callback to be executed on the response
193
- this._observeRequestCallbacks.set(reqId, callback);
194
- this.client.transport.emit(OBSERVE_REQUEST, reqId);
414
+ const reqId = this._promiseStore.add(resolve, reject, 'observe-request');
415
+ // store the callback for execution on the response. the returned Promise
416
+ // is fullfiled once callback has been executed with each existing states
417
+ const observeInfos = [observedSchemaName, callback, options];
418
+ this._observeRequestCallbacks.set(reqId, observeInfos);
419
+
420
+ // NOTE: do not store in `_observeListeners` yet as it can produce race
421
+ // conditions, e.g.:
422
+ // ```
423
+ // await client.stateManager.observe(async (schemaName, stateId, nodeId) => {});
424
+ // // client now receives OBSERVE_NOTIFICATIONS
425
+ // await otherClient.stateManager.create('a');
426
+ // // second observer added in between
427
+ // client.stateManager.observe(async (schemaName, stateId, nodeId) => {});
428
+ // ````
429
+ // OBSERVE_NOTIFICATION is received before the OBSERVE_RESPONSE, then the
430
+ // second observer is called twice:
431
+ // - OBSERVE_RESPONSE 1 []
432
+ // - OBSERVE_NOTIFICATION [ 'a', 1, 0 ]
433
+ // - OBSERVE_NOTIFICATION [ 'a', 1, 0 ] // this should not happen
434
+ // - OBSERVE_RESPONSE 1 [ [ 'a', 1, 0 ] ]
435
+ //
436
+ // cf. unit test `observe should properly behave in race condition`
437
+
438
+ this.client.transport.emit(OBSERVE_REQUEST, reqId, observedSchemaName);
195
439
  });
196
440
  }
441
+
442
+ /**
443
+ * Returns a collection of all the states created from the schema name.
444
+ *
445
+ * Alternitive signatures:
446
+ * - `stateManager.getCollection(schemaName)`
447
+ * - `stateManager.getCollection(schemaName, filter)`
448
+ * - `stateManager.getCollection(schemaName, options)`
449
+ * - `stateManager.getCollection(schemaName, filter, options)`
450
+ *
451
+ * @param {string} schemaName - Name of the schema.
452
+ * @param {array|null} [filter=null] - Filter parameter of interest for each
453
+ * state of the collection. If set to `null`, no filter applied.
454
+ * @param {object} [options={}] - Options.
455
+ * @param {boolean} [options.excludeLocal=false] - If set to true, exclude states
456
+ * created by the same node from the collection.
457
+ * @returns {server.SharedStateCollection|client.SharedStateCollection}
458
+ *
459
+ * @example
460
+ * const collection = await client.stateManager.getCollection(schemaName);
461
+ */
462
+ async getCollection(schemaName, filterOrOptions = null, options = {}) {
463
+ if (!isString(schemaName)) {
464
+ throw new TypeError(`[stateManager] Cannot execute 'getCollection' on 'StateManager': 'schemaName' should be a string"`);
465
+ }
466
+
467
+ let filter;
468
+
469
+ if (arguments.length === 2) {
470
+ if (filterOrOptions === null) {
471
+ filter = null;
472
+ options = null;
473
+ } else if (Array.isArray(filterOrOptions)) {
474
+ filter = filterOrOptions;
475
+ options = {};
476
+ } else if (typeof filterOrOptions === 'object') {
477
+ filter = null;
478
+ options = filterOrOptions;
479
+ } else {
480
+ throw new TypeError(`[stateManager] Cannot execute 'getCollection' on 'StateManager': argument 2 should be either null, an array or an object"`);
481
+ }
482
+ }
483
+
484
+ if (arguments.length === 3) {
485
+ filter = filterOrOptions;
486
+
487
+ if (filter !== null && !Array.isArray(filter)) {
488
+ throw new TypeError(`[stateManager] Cannot execute 'getCollection' on 'StateManager': 'filter' should be either an array or null"`);
489
+ }
490
+
491
+ if (options === null || typeof options !== 'object') {
492
+ throw new TypeError(`[stateManager] Cannot execute 'getCollection' on 'StateManager': 'options' should be an object"`);
493
+ }
494
+ }
495
+
496
+ const collection = new SharedStateCollection(this, schemaName, filter, options);
497
+ await collection._init();
498
+
499
+ return collection;
500
+ }
197
501
  }
198
502
 
199
503
  export default BaseStateManager;
@@ -0,0 +1,73 @@
1
+ import { delay } from '@ircam/sc-utils';
2
+
3
+ import { BATCHED_TRANSPORT_CHANNEL } from './constants.js';
4
+
5
+ /**
6
+ * This class proxies transports given the SharedStateManager to batch messages
7
+ *
8
+ * @param {number} [options.wait=0] - Wait for given number of milliseconds
9
+ * for stacking messages before emitting on the network. If 0 is given, network
10
+ * message is emitted on next microtask
11
+ * @private
12
+ */
13
+ class BatchedTransport {
14
+ constructor(transport) {
15
+ this._transport = transport;
16
+ this._listeners = new Map();
17
+ this._stack = [];
18
+ this._sending = false;
19
+
20
+ this._transport.addListener(BATCHED_TRANSPORT_CHANNEL, stack => {
21
+ stack.forEach(entry => {
22
+ const [channel, args] = entry;
23
+ // server side the transport is the same EventEmitter instance
24
+ // for both state manager server and client, so channel might not exist
25
+ // one side or the other.
26
+ if (this._listeners.has(channel)) {
27
+ const callbacks = this._listeners.get(channel);
28
+ callbacks.forEach(callback => callback(...args));
29
+ }
30
+ });
31
+ });
32
+ }
33
+
34
+ addListener(channel, callback) {
35
+ if (!this._listeners.has(channel)) {
36
+ this._listeners.set(channel, new Set());
37
+ }
38
+
39
+ const callbacks = this._listeners.get(channel);
40
+ callbacks.add(callback);
41
+ }
42
+
43
+ async emit(channel, ...args) {
44
+ this._stack.push([channel, args]);
45
+
46
+ if (!this._sending) {
47
+ this._sending = true;
48
+ await false;
49
+ this._sending = false;
50
+ const stack = this._stack;
51
+ this._stack = [];
52
+ this._transport.emit(BATCHED_TRANSPORT_CHANNEL, stack);
53
+ }
54
+ }
55
+
56
+ removeListener(channel, callback) {
57
+ if (!this._listeners.has(channel)) {
58
+ const callbacks = this._listeners.get(channel);
59
+ callbacks.delete(callback);
60
+ }
61
+ }
62
+
63
+ removeAllListeners(channel = null) {
64
+ if (channel === null) {
65
+ this._listeners.clear();
66
+ } else if (this._listeners.has(channel)) {
67
+ const callbacks = this._listeners.get(channel);
68
+ callbacks.clear();
69
+ }
70
+ }
71
+ }
72
+
73
+ export default BatchedTransport;