@soundworks/core 4.0.0-alpha.1 → 4.0.0-alpha.11

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,3 +1,4 @@
1
+ import { isPlainObject } from '@ircam/sc-utils';
1
2
  import ParameterBag from './ParameterBag.js';
2
3
  import {
3
4
  DELETE_REQUEST,
@@ -20,10 +21,12 @@ import {
20
21
 
21
22
  class BaseSharedState {
22
23
  constructor(id, remoteId, schemaName, schema, client, isOwner, manager, initValues = {}) {
23
- this.id = id;
24
- this.remoteId = remoteId;
25
- this.schemaName = schemaName;
26
-
24
+ /** @private */
25
+ this._id = id;
26
+ /** @private */
27
+ this._remoteId = remoteId;
28
+ /** @private */
29
+ this._schemaName = schemaName;
27
30
  /** @private */
28
31
  this._isOwner = isOwner; // may be the server or any client
29
32
  /** @private */
@@ -31,10 +34,12 @@ class BaseSharedState {
31
34
  /** @private */
32
35
  this._manager = manager;
33
36
 
37
+ this._detached = false;
38
+
34
39
  try {
35
40
  /** @private */
36
41
  this._parameters = new ParameterBag(schema, initValues);
37
- } catch(err) {
42
+ } catch (err) {
38
43
  console.error(err.stack);
39
44
 
40
45
  throw new Error(`Error creating or attaching state "${schemaName}" w/ values:\n
@@ -87,14 +92,25 @@ ${JSON.stringify(initValues, null, 2)}`);
87
92
  });
88
93
 
89
94
  // ---------------------------------------------
90
- // DELETE initiated by creator, or schema deleted
95
+ // state has been deleted by its creator or the schema has been deleted
91
96
  // ---------------------------------------------
92
- this._client.transport.addListener(`${DELETE_NOTIFICATION}-${this.id}-${this.remoteId}`, () => {
97
+ this._client.transport.addListener(`${DELETE_NOTIFICATION}-${this.id}-${this.remoteId}`, async () => {
93
98
  this._manager._statesById.delete(this.id);
94
99
  this._clearTransport();
95
100
 
96
- this._onDetachCallbacks.forEach(callback => callback());
97
- this._onDeleteCallbacks.forEach(callback => callback());
101
+ for (let callback of this._onDetachCallbacks) {
102
+ try {
103
+ await callback();
104
+ } catch(err) {
105
+ console.error(err.message);
106
+ }
107
+ }
108
+
109
+ if (this._isOwner) {
110
+ for (let callback of this._onDeleteCallbacks) {
111
+ await callback();
112
+ }
113
+ }
98
114
 
99
115
  this._clearDetach();
100
116
  });
@@ -102,14 +118,19 @@ ${JSON.stringify(initValues, null, 2)}`);
102
118
 
103
119
  if (this._isOwner) {
104
120
  // ---------------------------------------------
105
- // DELETE (can only delete if creator)
121
+ // the creator has called `.delete()`
106
122
  // ---------------------------------------------
107
- this._client.transport.addListener(`${DELETE_RESPONSE}-${this.id}-${this.remoteId}`, (reqId) => {
123
+ this._client.transport.addListener(`${DELETE_RESPONSE}-${this.id}-${this.remoteId}`, async (reqId) => {
108
124
  this._manager._statesById.delete(this.id);
109
125
  this._clearTransport();
110
126
 
111
- this._onDetachCallbacks.forEach(callback => callback());
112
- this._onDeleteCallbacks.forEach(callback => callback());
127
+ for (let callback of this._onDetachCallbacks) {
128
+ await callback();
129
+ }
130
+
131
+ for (let callback of this._onDeleteCallbacks) {
132
+ await callback();
133
+ }
113
134
 
114
135
  this._clearDetach();
115
136
  resolveRequest(reqId, this);
@@ -121,13 +142,15 @@ ${JSON.stringify(initValues, null, 2)}`);
121
142
 
122
143
  } else {
123
144
  // ---------------------------------------------
124
- // DETACH
145
+ // the attached node has called `.detach()`
125
146
  // ---------------------------------------------
126
- this._client.transport.addListener(`${DETACH_RESPONSE}-${this.id}-${this.remoteId}`, (reqId) => {
147
+ this._client.transport.addListener(`${DETACH_RESPONSE}-${this.id}-${this.remoteId}`, async (reqId) => {
127
148
  this._manager._statesById.delete(this.id);
128
149
  this._clearTransport();
129
150
 
130
- this._onDetachCallbacks.forEach(func => func());
151
+ for (let callback of this._onDetachCallbacks) {
152
+ await callback();
153
+ }
131
154
 
132
155
  this._clearDetach();
133
156
  resolveRequest(reqId, this);
@@ -137,20 +160,51 @@ ${JSON.stringify(initValues, null, 2)}`);
137
160
  this._client.transport.addListener(`${DETACH_ERROR}-${this.id}`, (reqId, msg) => {
138
161
  rejectRequest(reqId, msg);
139
162
  });
140
-
141
163
  }
142
164
  }
143
165
 
166
+ /**
167
+ * Id of the state
168
+ * @type {Number}
169
+ * @readonly
170
+ */
171
+ get id() {
172
+ return this._id;
173
+ }
174
+
175
+ /**
176
+ * Unique id of the state for the current node
177
+ * @readonly
178
+ * @type {Number}
179
+ * @private
180
+ */
181
+ get remoteId() {
182
+ return this._remoteId;
183
+ }
184
+
185
+ /**
186
+ * Name of the schema
187
+ * @type {String}
188
+ * @readonly
189
+ */
190
+ get schemaName() {
191
+ return this._schemaName;
192
+ }
193
+
194
+ /**
195
+ * Indicates if the node is the owner of the state
196
+ * @type {Boolean}
197
+ * @readonly
198
+ */
199
+ get isOwner() {
200
+ return this._isOwner;
201
+ }
202
+
144
203
  /** @private */
145
204
  _clearDetach() {
146
205
  this._onDetachCallbacks.clear();
147
206
  this._onDeleteCallbacks.clear();
148
-
149
- // Monkey patch detach so it throws if called twice. Doing nothing blocks
150
- // the process on a second `detach` call as the Promise never resolves
151
- this.detach = () => {
152
- throw new Error(`[stateManager] State "${this.schemaName} (${this.id})" already detached, cannot detach twice`);
153
- };
207
+ this._detached = true;
154
208
  }
155
209
 
156
210
  /** @private */
@@ -255,16 +309,23 @@ ${JSON.stringify(initValues, null, 2)}`);
255
309
  * const updates = await state.set({ myParam: Math.random() });
256
310
  */
257
311
  async set(updates, context = null) {
312
+ if (!isPlainObject(updates)) {
313
+ throw new ReferenceError(`[SharedState] State "${this.schemaName}": state.set(updates[, context]) should receive an object as first parameter`);
314
+ }
315
+
316
+ if (context !== null && !isPlainObject(context)) {
317
+ throw new ReferenceError(`[SharedState] State "${this.schemaName}": state.set(updates[, context]) should receive an object as second parameter`);
318
+ }
319
+
258
320
  // handle immediate option
259
321
  const immediateNewValues = {};
260
322
  const immediateOldValues = {};
261
323
  let propagateNow = false;
262
324
 
263
325
  for (let name in updates) {
264
- // throw early (client-side and not only server-side) if parameter is undefined
265
- if (!this._parameters.has(name)) {
266
- throw new ReferenceError(`[stateManager] Cannot set value of undefined parameter "${name}"`);
267
- }
326
+ // try to coerce value early, so that eventual errors are triggered early
327
+ // on the node requesting the update
328
+ const _ = this._parameters.coerceValue(name, updates[name]);
268
329
 
269
330
  // @note: general idea...
270
331
  // if immediate=true
@@ -303,21 +364,41 @@ ${JSON.stringify(initValues, null, 2)}`);
303
364
  }
304
365
 
305
366
  /**
306
- * Get the value of a paramter of the state.
367
+ * Get the value of a parameter of the state. If the parameter is of `any` type,
368
+ * a deep copy is returned.
307
369
  *
308
370
  * @param {string} name - Name of the param.
309
371
  * @throws Throws if `name` does not correspond to an existing field
310
372
  * of the state.
311
373
  * @return {mixed}
312
374
  * @example
313
- * const value = state.get('name');
375
+ * const value = state.get('paramName');
314
376
  */
315
377
  get(name) {
316
378
  return this._parameters.get(name);
317
379
  }
318
380
 
319
381
  /**
320
- * Get all the key / value pairs of the state.
382
+ * Similar to `get` but returns a reference to the underlying value in case of
383
+ * `any` type. May be usefull if the underlying value is big (e.g. sensors
384
+ * recordings, etc.) and deep cloning expensive. Be aware that if changes are
385
+ * made on the returned object, the state of your application will become
386
+ * inconsistent.
387
+ *
388
+ * @param {string} name - Name of the param.
389
+ * @throws Throws if `name` does not correspond to an existing field
390
+ * of the state.
391
+ * @return {mixed}
392
+ * @example
393
+ * const value = state.getUnsafe('paramName');
394
+ */
395
+ getUnsafe(name) {
396
+ return this._parameters.getUnsafe(name);
397
+ }
398
+
399
+ /**
400
+ * Get all the key / value pairs of the state. If a parameter is of `any`
401
+ * type, a deep copy is made.
321
402
  *
322
403
  * @return {object}
323
404
  * @example
@@ -327,6 +408,23 @@ ${JSON.stringify(initValues, null, 2)}`);
327
408
  return this._parameters.getValues();
328
409
  }
329
410
 
411
+ /**
412
+ * Get all the key / value pairs of the state. If a parameter is of `any`
413
+ * type, a deep copy is made.
414
+ * Similar to `getValues` but returns a reference to the underlying value in
415
+ * case of `any` type. May be usefull if the underlying value is big (e.g.
416
+ * sensors recordings, etc.) and deep cloning expensive. Be aware that if
417
+ * changes are made on the returned object, the state of your application will
418
+ * become inconsistent.
419
+ *
420
+ * @return {object}
421
+ * @example
422
+ * const values = state.getValues();
423
+ */
424
+ getValuesUnsafe() {
425
+ return this._parameters.getValuesUnsafe();
426
+ }
427
+
330
428
  /**
331
429
  * Get the schema from which the state has been created.
332
430
  *
@@ -367,7 +465,7 @@ ${JSON.stringify(initValues, null, 2)}`);
367
465
 
368
466
  /**
369
467
  * Detach from the state. If the client is the creator of the state, the state
370
- * is deleted and all attached nodes get notified
468
+ * is deleted and all attached nodes get notified.
371
469
  *
372
470
  * @example
373
471
  * const state = await client.state.attach('globals');
@@ -375,6 +473,10 @@ ${JSON.stringify(initValues, null, 2)}`);
375
473
  * await state.detach();
376
474
  */
377
475
  async detach() {
476
+ if (this._detached) {
477
+ throw new Error(`[SharedState] State "${this.schemaName} (${this.id})" already detached, cannot detach twice`);
478
+ }
479
+
378
480
  this._onUpdateCallbacks.clear();
379
481
 
380
482
  if (this._isOwner) {
@@ -392,9 +494,11 @@ ${JSON.stringify(initValues, null, 2)}`);
392
494
 
393
495
  /**
394
496
  * Delete the state. Only the creator/owner of the state can use this method.
395
- * All nodes attached to the state will be deteched, triggering the `onDetach`
396
- * callback. The creator of the state will also have its `onDelete` callback
397
- * triggered.
497
+ *
498
+ * All nodes attached to the state will be detached, triggering any registered
499
+ * `onDetach` callbacks. The creator of the state will also have its own `onDelete`
500
+ * callback triggered. The local `onDeatch` and `onDelete` callbacks will be
501
+ * executed *before* the returned Promise resolves
398
502
  *
399
503
  * @throws Throws if the method is called by a node which is not the owner of
400
504
  * the state.
@@ -405,16 +509,20 @@ ${JSON.stringify(initValues, null, 2)}`);
405
509
  */
406
510
  async delete() {
407
511
  if (this._isOwner) {
512
+ if (this._detached) {
513
+ throw new Error(`[SharedState] State "${this.schemaName} (${this.id})" already deleted, cannot delete twice`);
514
+ }
515
+
408
516
  return this.detach();
409
517
  } else {
410
- throw new Error(`[stateManager] can delete state "${this.schemaName}", only owner of the state (i.e. the node that "create[d]" it) can delete it`);
518
+ throw new Error(`[SharedState] Cannot delete state "${this.schemaName}", only the owner of the state (i.e. the node that created it) can delete the state. Use "detach" instead.`);
411
519
  }
412
520
  }
413
521
 
414
522
  /**
415
523
  * Subscribe to state updates.
416
524
  *
417
- * @param {client.SharedState~onUpdateCallback|client.SharedState~onUpdateCallback} callback
525
+ * @param {client.SharedState~onUpdateCallback|server.SharedState~onUpdateCallback} callback
418
526
  * Callback to execute when an update is applied on the state.
419
527
  * @param {Boolean} [executeListener=false] - Execute the callback immediately
420
528
  * with current state values. (`oldValues` will be set to `{}`, and `context` to `null`)
@@ -447,10 +555,11 @@ ${JSON.stringify(initValues, null, 2)}`);
447
555
  }
448
556
 
449
557
  /**
450
- * Register a function to execute when detaching from the state
558
+ * Register a function to execute when detaching from the state. The function
559
+ * will be executed before the `detach` promise resolves.
451
560
  *
452
- * @param {Function} callback - callback to execute when detaching from the state.
453
- * wether the client as called `detach`, or the state has been deleted by its
561
+ * @param {Function} callback - Callback to execute when detaching from the state.
562
+ * Whether the client as called `detach`, or the state has been deleted by its
454
563
  * creator.
455
564
  */
456
565
  onDetach(callback) {
@@ -460,9 +569,10 @@ ${JSON.stringify(initValues, null, 2)}`);
460
569
 
461
570
  /**
462
571
  * Register a function to execute when the state is deleted. Only called if the
463
- * node was the creator of the state. Is called after `onDetach`
572
+ * node was the creator of the state. Is called after `onDetach` and executed
573
+ * before the `delete` Promise resolves.
464
574
  *
465
- * @param {Function} callback - callback to execute when the state is deleted.
575
+ * @param {Function} callback - Callback to execute when the state is deleted.
466
576
  */
467
577
  onDelete(callback) {
468
578
  this._onDeleteCallbacks.add(callback);
@@ -0,0 +1,191 @@
1
+ /**
2
+ * @private
3
+ */
4
+ class BaseSharedStateCollection {
5
+ constructor(stateManager, schemaName) {
6
+ this._stateManager = stateManager;
7
+ this._schemaName = schemaName;
8
+ this._states = [];
9
+ this._onUpdateCallbacks = new Set();
10
+ this._onAttachCallbacks = new Set();
11
+ this._onDetachCallbacks = new Set();
12
+ this._unobserve = null;
13
+ }
14
+
15
+ async _init() {
16
+ this._unobserve = await this._stateManager.observe(this._schemaName, async (schemaName, stateId, nodeId) => {
17
+ const state = await this._stateManager.attach(schemaName, stateId);
18
+
19
+ this._states.push(state);
20
+
21
+ state.onDetach(() => {
22
+ const index = this._states.indexOf(state);
23
+ this._states.splice(index, 1);
24
+
25
+ this._onDetachCallbacks.forEach(callback => callback(state));
26
+ });
27
+
28
+ state.onUpdate((newValues, oldValues, context) => {
29
+ Array.from(this._onUpdateCallbacks).forEach(callback => {
30
+ callback(state, newValues, oldValues, context);
31
+ });
32
+ });
33
+
34
+ this._onAttachCallbacks.forEach(callback => callback(state));
35
+ });
36
+ }
37
+
38
+ /**
39
+ * Size of the collection
40
+ * @type {number}
41
+ */
42
+ get length() {
43
+ return this._states.length;
44
+ }
45
+
46
+ /**
47
+ * Detach from the collection, i.e. detach all underlying shared states.
48
+ * @type {number}
49
+ */
50
+ async detach() {
51
+ this._unobserve();
52
+
53
+ const promises = Array.from(this._states).map(state => state.detach());
54
+ await Promise.all(promises);
55
+
56
+ this._onUpdateCallbacks.clear();
57
+ this._onDetachCallbacks.clear();
58
+ }
59
+
60
+ /**
61
+ * Return the current values of all the states in the collection.
62
+ * @return {Object[]}
63
+ */
64
+ getValues() {
65
+ return this._states.map(state => state.getValues());
66
+ }
67
+
68
+ /**
69
+ * Return the current param value of all the states in the collection.
70
+ *
71
+ * @param {String} name - Name of the parameter
72
+ * @return {any[]}
73
+ */
74
+ get(name) {
75
+ return this._states.map(state => state.get(name));
76
+ }
77
+
78
+ /**
79
+ * Update all states of the collection with given values.
80
+ * @param {object} updates - key / value pairs of updates to apply to the state.
81
+ * @param {mixed} [context=null] - optionnal contextual object that will be propagated
82
+ * alongside the updates of the state. The context is valid only for the
83
+ * current call and will be passed as third argument to all update listeners.
84
+ */
85
+ async set(updates, context = null) {
86
+ const promises = this._states.map(state => state.set(updates, context));
87
+ return Promise.all(promises);
88
+ }
89
+
90
+ /**
91
+ * Subscribe to any state update of the collection.
92
+ *
93
+ * @param {server.SharedStateCollection~onUpdateCallback|client.SharedStateCollection~onUpdateCallback}
94
+ * callback - Callback to execute when an update is applied on a state.
95
+ * @param {Boolean} [executeListener=false] - Execute the callback immediately
96
+ * for all underlying states with current state values. (`oldValues` will be
97
+ * set to `{}`, and `context` to `null`)
98
+ * @returns {Function} - Function that delete the registered listener.
99
+ */
100
+ onUpdate(callback, executeListener = false) {
101
+ this._onUpdateCallbacks.add(callback);
102
+
103
+ if (executeListener === true) {
104
+ this._states.forEach(state => {
105
+ const currentValues = state.getValues();
106
+ const oldValues = {};
107
+ const context = null;
108
+
109
+ callback(state, currentValues, oldValues, context);
110
+ });
111
+ }
112
+
113
+ return () => this._onUpdateCallbacks.delete(callback);
114
+ }
115
+
116
+ /**
117
+ * Register a function to execute when a state is added to the collection.
118
+ *
119
+ * @param {Function} callback - callback to execute when a state is added to
120
+ * the collection.
121
+ */
122
+ onAttach(callback) {
123
+ this._onAttachCallbacks.add(callback);
124
+ }
125
+
126
+ /**
127
+ * Register a function to execute when a state is removed from the collection.
128
+ *
129
+ * @param {Function} callback - callback to execute when a state is removed
130
+ * from the collection.
131
+ */
132
+ onDetach(callback) {
133
+ this._onDetachCallbacks.add(callback);
134
+ }
135
+
136
+ /**
137
+ * Execute the given function once for each states of the collection (see `Array.forEach`).
138
+ *
139
+ * @param {Function} func - A function to execute for each element in the array.
140
+ * Its return value is discarded.
141
+ */
142
+ forEach(func) {
143
+ this._states.forEach(func);
144
+ }
145
+
146
+ /**
147
+ * Creates a new array populated with the results of calling a provided function
148
+ * on every state of the collection (see `Array.map`).
149
+ *
150
+ * @param {Function} func - A function to execute for each element in the array.
151
+ * Its return value is added as a single element in the new array.
152
+ */
153
+ map(func) {
154
+ return this._states.map(func);
155
+ }
156
+
157
+ /**
158
+ * Creates a shallow copy of a portion of the collection, filtered down to just
159
+ * the estates that pass the test implemented by the provided function (see `Array.filter`).
160
+ *
161
+ * @param {Function} func - A function to execute for each element in the array.
162
+ * It should return a truthy to keep the element in the resulting array, and a f
163
+ * alsy value otherwise.
164
+ */
165
+ filter(func) {
166
+ return this._states.filter(func);
167
+ }
168
+
169
+ /**
170
+ * Sort the elements of the collection in place (see `Array.sort`).
171
+ *
172
+ * @param {Function} func - Function that defines the sort order.
173
+ */
174
+ sort(func) {
175
+ this._states.sort(func);
176
+ }
177
+
178
+ /**
179
+ * Returns the first element of the collection that satisfies the provided testing
180
+ * function. If no values satisfy the testing function, undefined is returned.
181
+ *
182
+ * @param {Function} func - Function to execute for each element in the array.
183
+ * It should return a truthy value to indicate a matching element has been found.
184
+ * @return {}
185
+ */
186
+ find(func) {
187
+ return this._states.find(func);
188
+ }
189
+ }
190
+
191
+ export default BaseSharedStateCollection;
@@ -1,4 +1,6 @@
1
+ import { isString, isFunction } from '@ircam/sc-utils';
1
2
  import SharedState from './BaseSharedState.js';
3
+ import SharedStateCollection from './BaseSharedStateCollection.js';
2
4
  import {
3
5
  CREATE_REQUEST,
4
6
  CREATE_RESPONSE,
@@ -31,7 +33,7 @@ class BaseStateManager {
31
33
  this.client = { id, transport };
32
34
 
33
35
  this._statesById = new Map();
34
- this._observeListeners = new Set();
36
+ this._observeListeners = new Map(); // Map <callback, filterSchemaName>
35
37
  this._cachedSchemas = new Map();
36
38
  this._observeRequestCallbacks = new Map();
37
39
 
@@ -88,28 +90,35 @@ class BaseStateManager {
88
90
  this.client.transport.addListener(OBSERVE_RESPONSE, async (reqId, ...list) => {
89
91
  // retrieve the callback that have been stored in observe to make sure
90
92
  // we don't call another callback that may have been registered earlier.
91
- const callback = this._observeRequestCallbacks.get(reqId);
93
+ const [callback, filterSchemaName] = this._observeRequestCallbacks.get(reqId);
92
94
  this._observeRequestCallbacks.delete(reqId);
93
95
 
94
96
  const promises = list.map(([schemaName, stateId, nodeId]) => {
95
- return callback(schemaName, stateId, nodeId);
97
+ if (filterSchemaName === '*' || filterSchemaName === schemaName) {
98
+ return callback(schemaName, stateId, nodeId);
99
+ } else {
100
+ return Promise.resolve();
101
+ }
96
102
  });
97
103
 
98
104
  await Promise.all(promises);
99
105
 
100
106
  const unsubscribe = () => {
101
107
  this._observeListeners.delete(callback);
102
-
108
+ // no more listeners, we can stop receiving notification from the server
103
109
  if (this._observeListeners.size === 0) {
104
110
  this.client.transport.emit(UNOBSERVE_NOTIFICATION);
105
111
  }
106
112
  };
113
+
107
114
  resolveRequest(reqId, unsubscribe);
108
115
  });
109
116
 
110
- this.client.transport.addListener(OBSERVE_NOTIFICATION, (...list) => {
111
- list.forEach(([schemaName, stateId, nodeId]) => {
112
- this._observeListeners.forEach(callback => callback(schemaName, stateId, nodeId));
117
+ this.client.transport.addListener(OBSERVE_NOTIFICATION, (schemaName, stateId, nodeId) => {
118
+ this._observeListeners.forEach((filterSchemaName, callback) => {
119
+ if (filterSchemaName === '*' || filterSchemaName === schemaName) {
120
+ callback(schemaName, stateId, nodeId);
121
+ }
113
122
  });
114
123
  });
115
124
 
@@ -170,12 +179,19 @@ class BaseStateManager {
170
179
  * the state of all other clients of the application, to monitor them and/or take
171
180
  * control over them from a single point.
172
181
  *
173
- * @todo Optionnal schema name
182
+ * Notes:
183
+ * - The states that are created by the same node are not propagated through
184
+ * the observe callback.
185
+ * - The order of execution is not guaranted, i.e. an state attached in the
186
+ * `observe` callback could be created before the `async create` method resolves.
187
+ *
188
+ * @param {String} [schemaName] - optionnal schema name to filter the observed
189
+ * states.
174
190
  * @param {server.StateManager~ObserveCallback|client.StateManager~ObserveCallback}
175
191
  * 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.
192
+ * @returns {Promise<Function>} - Returns a Promise that resolves when the given
193
+ * callback as been executed on each existing states. The promise value is a
194
+ * function which allows to stop observing the states on the network.
179
195
  * @example
180
196
  * client.stateManager.observe(async (schemaName, stateId, nodeId) => {
181
197
  * if (schemaName === 'something') {
@@ -184,16 +200,57 @@ class BaseStateManager {
184
200
  * }
185
201
  * });
186
202
  */
187
- async observe(callback) {
188
- this._observeListeners.add(callback);
189
- // store function
203
+ // note: all filtering is done only on client-side as it is really more simple to
204
+ // handle this way and the network overhead is very low for observe notifications:
205
+ // i.e. schemaName, stateId, nodeId
206
+ async observe(...args) {
207
+ let filterSchemaName;
208
+ let callback;
209
+
210
+ if (args.length === 1) {
211
+ filterSchemaName = '*';
212
+ callback = args[0];
213
+
214
+ if (!isFunction(callback)) {
215
+ throw new Error(`[stateManager] Invalid arguments, valid signatures are "stateManager.observe(callback)" or "stateManager.observe(schemaName, callback)"`);
216
+ }
217
+ } else if (args.length === 2) {
218
+ filterSchemaName = args[0];
219
+ callback = args[1];
220
+
221
+ if (!isString(filterSchemaName) || !isFunction(callback)) {
222
+ throw new Error(`[stateManager] Invalid arguments, valid signatures are "stateManager.observe(callback)" or "stateManager.observe(schemaName, callback)"`);
223
+ }
224
+ } else {
225
+ throw new Error(`[stateManager] Invalid arguments, valid signatures are "stateManager.observe(callback)" or "stateManager.observe(schemaName, callback)"`);
226
+ }
227
+
228
+ // resend request to get updated list of states
190
229
  return new Promise((resolve, reject) => {
191
230
  const reqId = storeRequestPromise(resolve, reject);
192
- // store the callback to be executed on the response
193
- this._observeRequestCallbacks.set(reqId, callback);
231
+ // store the callback for execution on the response. the returned Promise
232
+ // is fullfiled once callback has been executed with each existing states
233
+ this._observeRequestCallbacks.set(reqId, [callback, filterSchemaName]);
234
+ // store the callback for execution on subsequent notifications
235
+ this._observeListeners.set(callback, filterSchemaName);
236
+
194
237
  this.client.transport.emit(OBSERVE_REQUEST, reqId);
195
238
  });
196
239
  }
240
+
241
+ /**
242
+ * Returns a collection of all the states created from the schema name. Except
243
+ * the ones created by the current node.
244
+ *
245
+ * @param {string} schemaName - Name of the schema.
246
+ * @returns {server.SharedStateCollection|client.SharedStateCollection}
247
+ */
248
+ async getCollection(schemaName) {
249
+ const collection = new SharedStateCollection(this, schemaName);
250
+ await collection._init();
251
+
252
+ return collection;
253
+ }
197
254
  }
198
255
 
199
256
  export default BaseStateManager;