@soundworks/core 4.0.0-alpha.4 → 4.0.0-alpha.6

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,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,16 @@ 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
+ * Note that the states that are created by the same node are not propagated through
183
+ * the observe callback.
184
+ *
185
+ * @param {String} [schemaName] - optionnal schema name to filter the observed
186
+ * states.
174
187
  * @param {server.StateManager~ObserveCallback|client.StateManager~ObserveCallback}
175
188
  * 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.
189
+ * @returns {Promise<Function>} - Returns a Promise that resolves when the given
190
+ * callback as been executed on each existing states. The promise value is a
191
+ * function which allows to stop observing the states on the network.
179
192
  * @example
180
193
  * client.stateManager.observe(async (schemaName, stateId, nodeId) => {
181
194
  * if (schemaName === 'something') {
@@ -184,16 +197,57 @@ class BaseStateManager {
184
197
  * }
185
198
  * });
186
199
  */
187
- async observe(callback) {
188
- this._observeListeners.add(callback);
189
- // store function
200
+ // note: all filtering is done only on client-side as it is really more simple to
201
+ // handle this way and the network overhead is very low for observe notifications:
202
+ // i.e. schemaName, stateId, nodeId
203
+ async observe(...args) {
204
+ let filterSchemaName;
205
+ let callback;
206
+
207
+ if (args.length === 1) {
208
+ filterSchemaName = '*';
209
+ callback = args[0];
210
+
211
+ if (!isFunction(callback)) {
212
+ throw new Error(`[stateManager] Invalid arguments, valid signatures are "stateManager.observe(callback)" or "stateManager.observe(schemaName, callback)"`);
213
+ }
214
+ } else if (args.length === 2) {
215
+ filterSchemaName = args[0];
216
+ callback = args[1];
217
+
218
+ if (!isString(filterSchemaName) || !isFunction(callback)) {
219
+ throw new Error(`[stateManager] Invalid arguments, valid signatures are "stateManager.observe(callback)" or "stateManager.observe(schemaName, callback)"`);
220
+ }
221
+ } else {
222
+ throw new Error(`[stateManager] Invalid arguments, valid signatures are "stateManager.observe(callback)" or "stateManager.observe(schemaName, callback)"`);
223
+ }
224
+
225
+ // resend request to get updated list of states
190
226
  return new Promise((resolve, reject) => {
191
227
  const reqId = storeRequestPromise(resolve, reject);
192
- // store the callback to be executed on the response
193
- this._observeRequestCallbacks.set(reqId, callback);
228
+ // store the callback for execution on the response. the returned Promise
229
+ // is fullfiled once callback has been executed with each existing states
230
+ this._observeRequestCallbacks.set(reqId, [callback, filterSchemaName]);
231
+ // store the callback for execution on subsequent notifications
232
+ this._observeListeners.set(callback, filterSchemaName);
233
+
194
234
  this.client.transport.emit(OBSERVE_REQUEST, reqId);
195
235
  });
196
236
  }
237
+
238
+ /**
239
+ * Returns a collection of all the states created from the schema name. Except
240
+ * the ones created by the current node.
241
+ *
242
+ * @param {string} schemaName - Name of the schema.
243
+ * @returns {server.SharedStateCollection|client.SharedStateCollection}
244
+ */
245
+ async getCollection(schemaName) {
246
+ const collection = new SharedStateCollection(this, schemaName);
247
+ await collection._init();
248
+
249
+ return collection;
250
+ }
197
251
  }
198
252
 
199
253
  export default BaseStateManager;
@@ -17,7 +17,7 @@ export const types = {
17
17
  },
18
18
  coerceFunction: (name, def, value) => {
19
19
  if (typeof value !== 'boolean') {
20
- throw new TypeError(`[SharedState] Invalid value for boolean param "${name}": ${value}`);
20
+ throw new TypeError(`[SharedState] Invalid value "${value}" for boolean parameter "${name}"`);
21
21
  }
22
22
 
23
23
  return value;
@@ -30,7 +30,7 @@ export const types = {
30
30
  },
31
31
  coerceFunction: (name, def, value) => {
32
32
  if (typeof value !== 'string') {
33
- throw new TypeError(`[SharedState] Invalid value for string param "${name}": ${value}`);
33
+ throw new TypeError(`[SharedState] Invalid value "${value}" for string parameter "${name}"`);
34
34
  }
35
35
 
36
36
  return value;
@@ -63,7 +63,7 @@ export const types = {
63
63
  },
64
64
  coerceFunction: (name, def, value) => {
65
65
  if (!(typeof value === 'number' && Math.floor(value) === value)) {
66
- throw new TypeError(`[SharedState] Invalid value for integer param "${name}": ${value}`);
66
+ throw new TypeError(`[SharedState] Invalid value "${value}" for integer parameter "${name}"`);
67
67
  }
68
68
 
69
69
  return Math.max(def.min, Math.min(def.max, value));
@@ -96,7 +96,7 @@ export const types = {
96
96
  },
97
97
  coerceFunction: (name, def, value) => {
98
98
  if (typeof value !== 'number' || value !== value) { // reject NaN
99
- throw new TypeError(`[SharedState] Invalid value for float param "${name}": ${value}`);
99
+ throw new TypeError(`[SharedState] Invalid value "${value}" for float parameter "${name}"`);
100
100
  }
101
101
 
102
102
  return Math.max(def.min, Math.min(def.max, value));
@@ -109,7 +109,7 @@ export const types = {
109
109
  },
110
110
  coerceFunction: (name, def, value) => {
111
111
  if (def.list.indexOf(value) === -1) {
112
- throw new TypeError(`[SharedState] Invalid value for enum param "${name}": ${value}`);
112
+ throw new TypeError(`[SharedState] Invalid value "${value}" for enum parameter "${name}"`);
113
113
  }
114
114
 
115
115
 
@@ -241,7 +241,8 @@ class ParameterBag {
241
241
  }
242
242
 
243
243
  /**
244
- * Return values of all parameters as a flat object.
244
+ * Return values of all parameters as a flat object. If a parameter is of `any`
245
+ * type, a deep copy is made.
245
246
  *
246
247
  * @return {object}
247
248
  */
@@ -255,6 +256,25 @@ class ParameterBag {
255
256
  return values;
256
257
  }
257
258
 
259
+ /**
260
+ * Return values of all parameters as a flat object. Similar to `getValues` but
261
+ * returns a reference to the underlying value in case of `any` type. May be
262
+ * usefull if the underlying value is big (e.g. sensors recordings, etc.) and
263
+ * deep cloning expensive. Be aware that if changes are made on the returned
264
+ * object, the state of your application will become inconsistent.
265
+ *
266
+ * @return {object}
267
+ */
268
+ getValuesUnsafe() {
269
+ let values = {};
270
+
271
+ for (let name in this._values) {
272
+ values[name] = this.getUnsafe(name);
273
+ }
274
+
275
+ return values;
276
+ }
277
+
258
278
  /**
259
279
  * Return the value of the given parameter. If the parameter is of `any` type,
260
280
  * a deep copy is returned.
@@ -277,39 +297,66 @@ class ParameterBag {
277
297
  }
278
298
 
279
299
  /**
280
- * Set the value of a parameter. If the value of the parameter is updated
281
- * (aka if previous value is different from new value) all registered
282
- * callbacks are registered.
300
+ * Similar to `get` but returns a reference to the underlying value in case of
301
+ * `any` type. May be usefull if the underlying value is big (e.g. sensors
302
+ * recordings, etc.) and deep cloning expensive. Be aware that if changes are
303
+ * made on the returned object, the state of your application will become
304
+ * inconsistent.
283
305
  *
284
306
  * @param {string} name - Name of the parameter.
307
+ * @return {Mixed} - Value of the parameter.
308
+ */
309
+ getUnsafe(name) {
310
+ if (!this.has(name)) {
311
+ throw new ReferenceError(`[SharedState] Cannot get value of undefined parameter "${name}"`);
312
+ }
313
+
314
+ return this._values[name];
315
+ }
316
+
317
+ /**
318
+ * Check that the value is valid according to the schema and return it coerced
319
+ * to the schema definition
320
+ *
321
+ * @param {String} name - Name of the parameter.
285
322
  * @param {Mixed} value - Value of the parameter.
286
- * @param {boolean} [forcePropagation=false] - if true, propagate value even
287
- * if the value has not changed.
288
- * @return {Array} - [new value, updated flag].
289
323
  */
290
- set(name, value) {
324
+ coerceValue(name, value) {
291
325
  if (!this.has(name)) {
292
326
  throw new ReferenceError(`[SharedState] Cannot set value of undefined parameter "${name}"`);
293
327
  }
294
328
 
295
329
  const def = this._schema[name];
296
- const { coerceFunction } = types[def.type];
297
330
 
298
331
  if (value === null && def.nullable === false) {
299
332
  throw new TypeError(`[SharedState] Invalid value for ${def.type} param "${name}": value is null and param is not nullable`);
300
333
  } else if (value === null && def.nullable === true) {
301
334
  value = null;
302
335
  } else {
336
+ const { coerceFunction } = types[def.type];
303
337
  value = coerceFunction(name, def, value);
304
338
  }
305
339
 
340
+ return value;
341
+ }
342
+
343
+ /**
344
+ * Set the value of a parameter. If the value of the parameter is updated
345
+ * (aka if previous value is different from new value) all registered
346
+ * callbacks are registered.
347
+ *
348
+ * @param {string} name - Name of the parameter.
349
+ * @param {Mixed} value - Value of the parameter.
350
+ * @return {Array} - [new value, updated flag].
351
+ */
352
+ set(name, value) {
353
+ value = this.coerceValue(name, value);
306
354
  const currentValue = this._values[name];
307
355
  const updated = !equal(currentValue, value);
308
356
 
309
357
  // we store a deep copy of the object as we don't want the client to be able
310
- // to modify our underlying data, which leads unexpected behavior where the
358
+ // to modify our underlying data, which leads to unexpected behavior where the
311
359
  // deep equal check to returns true, and therefore the update is not triggered.
312
- //
313
360
  // @see tests/common.state-manager.spec.js
314
361
  // 'should copy stored value for "any" type to have a predictable behavior'
315
362
  if (this._schema[name].type === 'any') {
@@ -1,4 +1,4 @@
1
- import { idGenerator } from './utils.js';
1
+ import { idGenerator } from '@ircam/sc-utils';
2
2
 
3
3
  const generateRequestId = idGenerator();
4
4
  const requestPromises = new Map();
@@ -1,7 +1,6 @@
1
+ import { idGenerator } from '@ircam/sc-utils';
1
2
  import { v4 as uuid } from 'uuid';
2
3
 
3
- import { idGenerator } from '../common/utils.js';
4
-
5
4
  const generateId = idGenerator();
6
5
 
7
6
  /**
@@ -44,6 +43,12 @@ class Client {
44
43
  * @type {server.Socket}
45
44
  */
46
45
  this.socket = socket;
46
+
47
+ /**
48
+ * Is set in server._onSocketConnection
49
+ * @private
50
+ */
51
+ this.token = null;
47
52
  }
48
53
  }
49
54
 
@@ -114,7 +114,7 @@ class ContextManager {
114
114
  this._contextStartPromises.set(contextName, startPromise);
115
115
  await startPromise;
116
116
  context.status = 'started';
117
- } catch(err) {
117
+ } catch (err) {
118
118
  context.status = 'errored';
119
119
  throw new Error(err);
120
120
  }
@@ -144,7 +144,7 @@ class ContextManager {
144
144
  let context;
145
145
  try {
146
146
  context = await this.get(contextName);
147
- } catch(err) {
147
+ } catch (err) {
148
148
  client.socket.send(
149
149
  CONTEXT_ENTER_ERROR,
150
150
  reqId,
@@ -185,7 +185,7 @@ class ContextManager {
185
185
  await context.enter(client);
186
186
 
187
187
  client.socket.send(CONTEXT_ENTER_RESPONSE, reqId, contextName);
188
- } catch(err) {
188
+ } catch (err) {
189
189
  client.socket.send(CONTEXT_ENTER_ERROR, reqId, contextName, err.message);
190
190
  }
191
191
  });
@@ -207,7 +207,7 @@ class ContextManager {
207
207
  try {
208
208
  await context.exit(client);
209
209
  client.socket.send(CONTEXT_EXIT_RESPONSE, reqId, contextName);
210
- } catch(err) {
210
+ } catch (err) {
211
211
  client.socket.send(CONTEXT_EXIT_ERROR, reqId, contextName, err.message);
212
212
  }
213
213
  } else {