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

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.
@@ -0,0 +1,279 @@
1
+ /** @private */
2
+ class BaseSharedStateCollection {
3
+ constructor(stateManager, schemaName, options = {}) {
4
+ this._stateManager = stateManager;
5
+ this._schemaName = schemaName;
6
+ this._options = Object.assign({ excludeLocal: false }, options);
7
+
8
+ this._schema = null;
9
+ this._states = [];
10
+
11
+ this._onUpdateCallbacks = new Set();
12
+ this._onAttachCallbacks = new Set();
13
+ this._onDetachCallbacks = new Set();
14
+ this._unobserve = null;
15
+ }
16
+
17
+ /** @private */
18
+ async _init() {
19
+ this._schema = await this._stateManager.getSchema(this._schemaName);
20
+
21
+ this._unobserve = await this._stateManager.observe(this._schemaName, async (schemaName, stateId) => {
22
+ const state = await this._stateManager.attach(schemaName, stateId);
23
+ this._states.push(state);
24
+
25
+ state.onDetach(() => {
26
+ const index = this._states.indexOf(state);
27
+ this._states.splice(index, 1);
28
+
29
+ this._onDetachCallbacks.forEach(callback => callback(state));
30
+ });
31
+
32
+ state.onUpdate((newValues, oldValues, context) => {
33
+ Array.from(this._onUpdateCallbacks).forEach(callback => {
34
+ callback(state, newValues, oldValues, context);
35
+ });
36
+ });
37
+
38
+ this._onAttachCallbacks.forEach(callback => callback(state));
39
+ }, this._options);
40
+ }
41
+
42
+ /**
43
+ * Size of the collection, alias `size`
44
+ * @type {number}
45
+ * @readonly
46
+ */
47
+ get length() {
48
+ return this._states.length;
49
+ }
50
+
51
+ /**
52
+ * Size of the collection, , alias `length`
53
+ * @type {number}
54
+ * @readonly
55
+ */
56
+ get size() {
57
+ return this._states.length;
58
+ }
59
+
60
+ /**
61
+ * Name of the schema from which the collection has been created.
62
+ * @type {String}
63
+ * @readonly
64
+ */
65
+ get schemaName() {
66
+ return this._schemaName;
67
+ }
68
+
69
+ /**
70
+ * Definition of schema from which the collection has been created.
71
+ *
72
+ * @param {string} [name=null] - If given, returns only the definition
73
+ * corresponding to the given param name.
74
+ * @throws Throws if `name` does not correspond to an existing field
75
+ * of the schema.
76
+ * @return {object}
77
+ * @example
78
+ * const schema = collection.getSchema();
79
+ */
80
+ getSchema(name = null) {
81
+ if (name) {
82
+ return this._schema[name];
83
+ }
84
+
85
+ return this._schema;
86
+ }
87
+
88
+ /**
89
+ * Get the default values as declared in the schema.
90
+ *
91
+ * @return {object}
92
+ * @example
93
+ * const defaults = state.getDefaults();
94
+ */
95
+ getDefaults() {
96
+ const defaults = {};
97
+ for (let name in this._schema) {
98
+ defaults[name] = this._schema[name].default;
99
+ }
100
+ return defaults;
101
+ }
102
+
103
+ /**
104
+ * Return the current values of all the states in the collection.
105
+ * @return {Object[]}
106
+ */
107
+ getValues() {
108
+ return this._states.map(state => state.getValues());
109
+ }
110
+
111
+ /**
112
+ * Return the current param value of all the states in the collection.
113
+ *
114
+ * @param {String} name - Name of the parameter
115
+ * @return {any[]}
116
+ */
117
+ get(name) {
118
+ return this._states.map(state => state.get(name));
119
+ }
120
+
121
+ /**
122
+ * Update all states of the collection with given values.
123
+ * @param {object} updates - key / value pairs of updates to apply to the state.
124
+ * @param {mixed} [context=null] - optionnal contextual object that will be propagated
125
+ * alongside the updates of the state. The context is valid only for the
126
+ * current call and will be passed as third argument to all update listeners.
127
+ */
128
+ async set(updates, context = null) {
129
+ // hot fix for https://github.com/collective-soundworks/soundworks/issues/85
130
+ // to be cleaned soon
131
+ const promises = this._states.map(state => state.set(updates, context));
132
+ return Promise.all(promises);
133
+ }
134
+
135
+ /**
136
+ * Subscribe to any state update of the collection.
137
+ *
138
+ * @param {server.SharedStateCollection~onUpdateCallback|client.SharedStateCollection~onUpdateCallback}
139
+ * callback - Callback to execute when an update is applied on a state.
140
+ * @param {Boolean} [executeListener=false] - Execute the callback immediately
141
+ * for all underlying states with current state values. (`oldValues` will be
142
+ * set to `{}`, and `context` to `null`)
143
+ * @returns {Function} - Function that delete the registered listener.
144
+ */
145
+ onUpdate(callback, executeListener = false) {
146
+ this._onUpdateCallbacks.add(callback);
147
+
148
+ if (executeListener === true) {
149
+ this._states.forEach(state => {
150
+ const currentValues = state.getValues();
151
+ const oldValues = {};
152
+ const context = null;
153
+
154
+ callback(state, currentValues, oldValues, context);
155
+ });
156
+ }
157
+
158
+ return () => this._onUpdateCallbacks.delete(callback);
159
+ }
160
+
161
+ /**
162
+ * Register a function to execute when a state is added to the collection.
163
+ *
164
+ * @param {Function} callback - callback to execute when a state is added to
165
+ * the collection.
166
+ * @param {Function} executeListener - execute the callback with the states
167
+ * already present in the collection.
168
+ * @returns {Function} - Function that delete the registered listener.
169
+ */
170
+ onAttach(callback, executeListener = false) {
171
+ if (executeListener === true) {
172
+ this._states.forEach(state => callback(state));
173
+ }
174
+
175
+ this._onAttachCallbacks.add(callback);
176
+
177
+ return () => this._onAttachCallbacks.delete(callback);
178
+ }
179
+
180
+ /**
181
+ * Register a function to execute when a state is removed from the collection.
182
+ *
183
+ * @param {Function} callback - callback to execute when a state is removed
184
+ * from the collection.
185
+ * @returns {Function} - Function that delete the registered listener.
186
+ */
187
+ onDetach(callback) {
188
+ this._onDetachCallbacks.add(callback);
189
+
190
+ return () => this._onDetachCallbacks.delete(callback);
191
+ }
192
+
193
+ /**
194
+ * Detach from the collection, i.e. detach all underlying shared states.
195
+ * @type {number}
196
+ */
197
+ async detach() {
198
+ this._unobserve();
199
+ this._onUpdateCallbacks.clear();
200
+
201
+ const promises = this._states.map(state => state.detach());
202
+ await Promise.all(promises);
203
+
204
+ this._onDetachCallbacks.clear();
205
+ }
206
+
207
+ /**
208
+ * Execute the given function once for each states of the collection (see `Array.forEach`).
209
+ *
210
+ * @param {Function} func - A function to execute for each element in the array.
211
+ * Its return value is discarded.
212
+ */
213
+ forEach(func) {
214
+ this._states.forEach(func);
215
+ }
216
+
217
+ /**
218
+ * Creates a new array populated with the results of calling a provided function
219
+ * on every state of the collection (see `Array.map`).
220
+ *
221
+ * @param {Function} func - A function to execute for each element in the array.
222
+ * Its return value is added as a single element in the new array.
223
+ */
224
+ map(func) {
225
+ return this._states.map(func);
226
+ }
227
+
228
+ /**
229
+ * Creates a shallow copy of a portion of the collection, filtered down to just
230
+ * the estates that pass the test implemented by the provided function (see `Array.filter`).
231
+ *
232
+ * @param {Function} func - A function to execute for each element in the array.
233
+ * It should return a truthy to keep the element in the resulting array, and a f
234
+ * alsy value otherwise.
235
+ */
236
+ filter(func) {
237
+ return this._states.filter(func);
238
+ }
239
+
240
+ /**
241
+ * Sort the elements of the collection in place (see `Array.sort`).
242
+ *
243
+ * @param {Function} func - Function that defines the sort order.
244
+ */
245
+ sort(func) {
246
+ this._states.sort(func);
247
+ }
248
+
249
+ /**
250
+ * Returns the first element of the collection that satisfies the provided testing
251
+ * function. If no values satisfy the testing function, undefined is returned.
252
+ *
253
+ * @param {Function} func - Function to execute for each element in the array.
254
+ * It should return a truthy value to indicate a matching element has been found.
255
+ * @return {}
256
+ */
257
+ find(func) {
258
+ return this._states.find(func);
259
+ }
260
+
261
+ /**
262
+ * Iterable API, e.g. for use in `for .. of` loops
263
+ */
264
+ [Symbol.iterator]() {
265
+ let index = 0;
266
+
267
+ return {
268
+ next: () => {
269
+ if (index >= this._states.length) {
270
+ return { value: undefined, done: true };
271
+ } else {
272
+ return { value: this._states[index++], done: false };
273
+ }
274
+ },
275
+ };
276
+ }
277
+ }
278
+
279
+ export default BaseSharedStateCollection;
@@ -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,15 +13,14 @@ 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
24
 
21
25
  /**
22
26
  * @private
@@ -28,12 +32,16 @@ class BaseStateManager {
28
32
  * Must implement a basic EventEmitter API.
29
33
  */
30
34
  constructor(id, transport) {
31
- this.client = { id, transport };
35
+ // proxy transport with BatchedTransport;
36
+ this.client = { id, transport: new BatchedTransport(transport) };
37
+
38
+ this._statesById = new Map(); // <id, state>
39
+ this._cachedSchemas = new Map(); // <shemaName, definition>
40
+
41
+ this._observeListeners = new Set(); // Set <[observedSchemaName, callback, options]>
42
+ this._observeRequestCallbacks = new Map(); // Map <reqId, [observedSchemaName, callback, options]>
32
43
 
33
- this._statesById = new Map();
34
- this._observeListeners = new Set();
35
- this._cachedSchemas = new Map();
36
- this._observeRequestCallbacks = new Map();
44
+ this._promiseStore = new PromiseStore();
37
45
 
38
46
  // ---------------------------------------------
39
47
  // CREATE
@@ -51,11 +59,11 @@ class BaseStateManager {
51
59
  const state = new SharedState(stateId, remoteId, schemaName, schema, this.client, true, this, initValues);
52
60
  this._statesById.set(state.id, state);
53
61
 
54
- resolveRequest(reqId, state);
62
+ this._promiseStore.resolve(reqId, state);
55
63
  });
56
64
 
57
65
  this.client.transport.addListener(CREATE_ERROR, (reqId, msg) => {
58
- rejectRequest(reqId, msg);
66
+ this._promiseStore.reject(reqId, msg);
59
67
  });
60
68
 
61
69
  // ---------------------------------------------
@@ -75,11 +83,11 @@ class BaseStateManager {
75
83
  const state = new SharedState(stateId, remoteId, schemaName, schema, this.client, false, this, currentValues);
76
84
  this._statesById.set(state.id, state);
77
85
 
78
- resolveRequest(reqId, state);
86
+ this._promiseStore.resolve(reqId, state);
79
87
  });
80
88
 
81
89
  this.client.transport.addListener(ATTACH_ERROR, (reqId, msg) => {
82
- rejectRequest(reqId, msg);
90
+ this._promiseStore.reject(reqId, msg);
83
91
  });
84
92
 
85
93
  // ---------------------------------------------
@@ -88,28 +96,52 @@ class BaseStateManager {
88
96
  this.client.transport.addListener(OBSERVE_RESPONSE, async (reqId, ...list) => {
89
97
  // retrieve the callback that have been stored in observe to make sure
90
98
  // we don't call another callback that may have been registered earlier.
91
- const callback = this._observeRequestCallbacks.get(reqId);
99
+ const observeInfos = this._observeRequestCallbacks.get(reqId);
100
+ const [observedSchemaName, callback, options] = observeInfos;
101
+
102
+ // move observeInfos from `_observeRequestCallbacks` to `_observeListeners`
103
+ // to guarantee order of execution, @see not in `.observe`
92
104
  this._observeRequestCallbacks.delete(reqId);
105
+ this._observeListeners.add(observeInfos);
93
106
 
94
107
  const promises = list.map(([schemaName, stateId, nodeId]) => {
95
- return callback(schemaName, stateId, nodeId);
108
+ const filter = this._filterObserve(observedSchemaName, schemaName, nodeId, options);
109
+
110
+ if (!filter) {
111
+ return callback(schemaName, stateId, nodeId);
112
+ } else {
113
+ return Promise.resolve();
114
+ }
96
115
  });
97
116
 
98
117
  await Promise.all(promises);
99
118
 
100
119
  const unsubscribe = () => {
101
- this._observeListeners.delete(callback);
120
+ this._observeListeners.delete(observeInfos);
102
121
 
122
+ // no more listeners, we can stop receiving notifications from the server
103
123
  if (this._observeListeners.size === 0) {
104
124
  this.client.transport.emit(UNOBSERVE_NOTIFICATION);
105
125
  }
106
126
  };
107
- resolveRequest(reqId, unsubscribe);
127
+
128
+ this._promiseStore.resolve(reqId, unsubscribe);
129
+ });
130
+
131
+ // Observe error can occur if observed schema name does not exists
132
+ this.client.transport.addListener(OBSERVE_ERROR, (reqId, msg) => {
133
+ this._observeRequestCallbacks.delete(reqId);
134
+ this._promiseStore.reject(reqId, msg);
108
135
  });
109
136
 
110
- this.client.transport.addListener(OBSERVE_NOTIFICATION, (...list) => {
111
- list.forEach(([schemaName, stateId, nodeId]) => {
112
- this._observeListeners.forEach(callback => callback(schemaName, stateId, nodeId));
137
+ this.client.transport.addListener(OBSERVE_NOTIFICATION, (schemaName, stateId, nodeId) => {
138
+ this._observeListeners.forEach(observeInfos => {
139
+ const [observedSchemaName, callback, options] = observeInfos;
140
+ const filter = this._filterObserve(observedSchemaName, schemaName, nodeId, options);
141
+
142
+ if (!filter) {
143
+ callback(schemaName, stateId, nodeId);
144
+ }
113
145
  });
114
146
  });
115
147
 
@@ -119,6 +151,59 @@ class BaseStateManager {
119
151
  this.client.transport.addListener(DELETE_SCHEMA, schemaName => {
120
152
  this._cachedSchemas.delete(schemaName);
121
153
  });
154
+
155
+ // ---------------------------------------------
156
+ // GET SCHEMA
157
+ // ---------------------------------------------
158
+ this.client.transport.addListener(GET_SCHEMA_RESPONSE, (reqId, schemaName, schema) => {
159
+ if (!this._cachedSchemas.has(schemaName)) {
160
+ this._cachedSchemas.set(schemaName, schema);
161
+ }
162
+ // return a populated schema
163
+ const parameterBag = new ParameterBag(schema);
164
+ this._promiseStore.resolve(reqId, parameterBag.getSchema());
165
+ });
166
+
167
+ this.client.transport.addListener(GET_SCHEMA_ERROR, (reqId, msg) => {
168
+ this._promiseStore.reject(reqId, msg);
169
+ });
170
+ }
171
+
172
+ /** @private */
173
+ _filterObserve(observedSchemaName, schemaName, creatorId, options) {
174
+ let filter = true;
175
+ // schema name filter filer
176
+ if (observedSchemaName === null || observedSchemaName === schemaName) {
177
+ filter = false;
178
+ }
179
+ // filter state created by client if excludeLocal is true
180
+ if (options.excludeLocal === true && creatorId === this.client.id) {
181
+ filter = true;
182
+ }
183
+
184
+ return filter;
185
+ }
186
+
187
+ /**
188
+ * Return the schema from a given registered schema name
189
+ *
190
+ * @param {String} schemaName - Name of the schema as given on registration
191
+ * (cf. ServerStateManager)
192
+ * @example
193
+ * const schema = await client.stateManager.getSchema('my-schema');
194
+ */
195
+ async getSchema(schemaName) {
196
+ if (this._cachedSchemas.has(schemaName)) {
197
+ const schema = this._cachedSchemas.get(schemaName);
198
+ // return a populated schema
199
+ const parameterBag = new ParameterBag(schema);
200
+ return parameterBag.getSchema();
201
+ }
202
+
203
+ return new Promise((resolve, reject) => {
204
+ const reqId = this._promiseStore.add(resolve, reject, 'get-schema');
205
+ this.client.transport.emit(GET_SCHEMA_REQUEST, reqId, schemaName);
206
+ });
122
207
  }
123
208
 
124
209
  /**
@@ -135,7 +220,7 @@ class BaseStateManager {
135
220
  */
136
221
  async create(schemaName, initValues = {}) {
137
222
  return new Promise((resolve, reject) => {
138
- const reqId = storeRequestPromise(resolve, reject);
223
+ const reqId = this._promiseStore.add(resolve, reject, 'create-request');
139
224
  const requireSchema = this._cachedSchemas.has(schemaName) ? false : true;
140
225
  this.client.transport.emit(CREATE_REQUEST, reqId, schemaName, requireSchema, initValues);
141
226
  });
@@ -157,8 +242,7 @@ class BaseStateManager {
157
242
  */
158
243
  async attach(schemaName, stateId = null) {
159
244
  return new Promise((resolve, reject) => {
160
- // @todo - add a timeout
161
- const reqId = storeRequestPromise(resolve, reject);
245
+ const reqId = this._promiseStore.add(resolve, reject, 'attach-request');
162
246
  const requireSchema = this._cachedSchemas.has(schemaName) ? false : true;
163
247
  this.client.transport.emit(ATTACH_REQUEST, reqId, schemaName, stateId, requireSchema);
164
248
  });
@@ -170,30 +254,167 @@ class BaseStateManager {
170
254
  * the state of all other clients of the application, to monitor them and/or take
171
255
  * control over them from a single point.
172
256
  *
173
- * @todo Optionnal schema name
257
+ * Notes:
258
+ * - The order of execution is not guaranted between nodes, i.e. an state attached
259
+ * in the `observe` callback could be created before the `async create` method resolves.
260
+ * - Filtering, i.e. `observedSchemaName` and `options.excludeLocal` are handled
261
+ * on the node side, the server just notify all state creation activity and
262
+ * the node executes the given callbacks according to the different filter rules.
263
+ * Such strategy allows to share the observe notifications between all observers.
264
+ *
265
+ * Alternative signatures:
266
+ * - .observe(callback)
267
+ * - .observe(schemaName, callback)
268
+ * - .observe(callback, options)
269
+ * - .observe(schemaName, callback, options)
270
+ *
271
+ * @param {string} [schemaName] - optionnal schema name to filter the observed
272
+ * states.
174
273
  * @param {server.StateManager~ObserveCallback|client.StateManager~ObserveCallback}
175
274
  * 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.
275
+ * @param {object} options - Options.
276
+ * @param {boolean} [options.excludeLocal = false] - If set to true, exclude states
277
+ * created locallly, i.e. by the same node, from the collection.
278
+ * @returns {Promise<Function>} - Returns a Promise that resolves when the given
279
+ * callback as been executed on each existing states. The promise value is a
280
+ * function which allows to stop observing the states on the network.
179
281
  * @example
180
- * client.stateManager.observe(async (schemaName, stateId, nodeId) => {
282
+ * client.stateManager.observe(async (schemaName, stateId) => {
181
283
  * if (schemaName === 'something') {
182
284
  * const state = await this.client.stateManager.attach(schemaName, stateId);
183
285
  * console.log(state.getValues());
184
286
  * }
185
287
  * });
186
288
  */
187
- async observe(callback) {
188
- this._observeListeners.add(callback);
189
- // store function
289
+ // note: all filtering is done only on client-side as it is really more simple to
290
+ // handle this way and the network overhead is very low for observe notifications:
291
+ // i.e. schemaName, stateId, nodeId
292
+ async observe(...args) {
293
+
294
+ const defaultOptions = {
295
+ excludeLocal: false,
296
+ };
297
+
298
+ let observedSchemaName;
299
+ let callback;
300
+ let options;
301
+
302
+ switch (args.length) {
303
+ case 1: {
304
+ // variation: .observe(callback)
305
+ if (!isFunction(args[0])) {
306
+ throw new TypeError(`[stateManager] Invalid arguments, argument 1 should be a function"`);
307
+ }
308
+
309
+ observedSchemaName = null;
310
+ callback = args[0];
311
+ options = defaultOptions;
312
+ break;
313
+ }
314
+ case 2: {
315
+ // variation: .observe(schemaName, callback)
316
+ if (isString(args[0])) {
317
+ if (!isFunction(args[1])) {
318
+ throw new TypeError(`[stateManager] Invalid arguments, argument 2 should be a function"`);
319
+ }
320
+
321
+ observedSchemaName = args[0];
322
+ callback = args[1];
323
+ options = defaultOptions;
324
+
325
+ // variation: .observe(callback, options) API
326
+ } else if (isFunction(args[0])) {
327
+ if (!isPlainObject(args[1])) {
328
+ throw new TypeError(`[stateManager] Invalid arguments, argument 2 should be an object"`);
329
+ }
330
+
331
+ observedSchemaName = null;
332
+ callback = args[0];
333
+ options = Object.assign(defaultOptions, args[1]);
334
+
335
+ } else {
336
+ throw new Error(`[stateManager] Invalid signature, refer to the StateManager.observe documentation"`);
337
+ }
338
+
339
+ break;
340
+ }
341
+ case 3: {
342
+ // variation: .observe(schemaName, callback, options)
343
+ if (!isString(args[0])) {
344
+ throw new TypeError(`[stateManager] Invalid arguments, argument 1 should be a string"`);
345
+ }
346
+
347
+ if (!isFunction(args[1])) {
348
+ throw new TypeError(`[stateManager] Invalid arguments, argument 2 should be a function"`);
349
+ }
350
+
351
+ if (!isPlainObject(args[2])) {
352
+ throw new TypeError(`[stateManager] Invalid arguments, argument 2 should be an object"`);
353
+ }
354
+
355
+ observedSchemaName = args[0];
356
+ callback = args[1];
357
+ options = Object.assign(defaultOptions, args[2]);
358
+
359
+ break;
360
+ }
361
+ // throw in all other cases
362
+ default: {
363
+ throw new Error(`[stateManager] Invalid signature, refer to the StateManager.observe documentation"`);
364
+ }
365
+ }
366
+
367
+ // resend request to get updated list of states
190
368
  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);
369
+ const reqId = this._promiseStore.add(resolve, reject, 'observe-request');
370
+ // store the callback for execution on the response. the returned Promise
371
+ // is fullfiled once callback has been executed with each existing states
372
+ const observeInfos = [observedSchemaName, callback, options];
373
+ this._observeRequestCallbacks.set(reqId, observeInfos);
374
+
375
+ // NOTE: do not store in `_observeListeners` yet as it can produce race
376
+ // conditions, e.g.:
377
+ // ```
378
+ // await client.stateManager.observe(async (schemaName, stateId, nodeId) => {});
379
+ // // client now receives OBSERVE_NOTIFICATIONS
380
+ // await otherClient.stateManager.create('a');
381
+ // // second observer added in between
382
+ // client.stateManager.observe(async (schemaName, stateId, nodeId) => {});
383
+ // ````
384
+ // OBSERVE_NOTIFICATION is received before the OBSERVE_RESPONSE, then the
385
+ // second observer is called twice:
386
+ // - OBSERVE_RESPONSE 1 []
387
+ // - OBSERVE_NOTIFICATION [ 'a', 1, 0 ]
388
+ // - OBSERVE_NOTIFICATION [ 'a', 1, 0 ] // this should not happen
389
+ // - OBSERVE_RESPONSE 1 [ [ 'a', 1, 0 ] ]
390
+ //
391
+ // cf. unit test `observe should properly behave in race condition`
392
+
393
+ this.client.transport.emit(OBSERVE_REQUEST, reqId, observedSchemaName);
195
394
  });
196
395
  }
396
+
397
+ /**
398
+ * Returns a collection of all the states created from the schema name.
399
+ *
400
+ * @param {string} schemaName - Name of the schema.
401
+ * @param {object} options - Options.
402
+ * @param {boolean} [options.excludeLocal = false] - If set to true, exclude states
403
+ * created locallly, i.e. by the same node, from the collection.
404
+ * @returns {server.SharedStateCollection|client.SharedStateCollection}
405
+ */
406
+ async getCollection(schemaName, options) {
407
+ const collection = new SharedStateCollection(this, schemaName, options);
408
+
409
+ try {
410
+ await collection._init();
411
+ } catch (err) {
412
+ console.log(err.message);
413
+ throw new Error(`[stateManager] Cannot create collection, schema "${schemaName}" does not exists`);
414
+ }
415
+
416
+ return collection;
417
+ }
197
418
  }
198
419
 
199
420
  export default BaseStateManager;