@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.
- package/.github/workflows/gh-pages.yml +31 -0
- package/.github/workflows/unit-tests.yml +18 -0
- package/README.md +2 -2
- package/package.json +6 -7
- package/src/client/Client.js +11 -5
- package/src/client/Context.js +2 -2
- package/src/client/ContextManager.js +1 -1
- package/src/client/SharedStateCollection.js +35 -0
- package/src/client/Socket.js +17 -9
- package/src/common/BasePluginManager.js +4 -5
- package/src/common/BaseSharedState.js +59 -15
- package/src/common/BaseSharedStateCollection.js +179 -0
- package/src/common/BaseStateManager.js +70 -16
- package/src/common/ParameterBag.js +63 -16
- package/src/common/promise-store.js +1 -1
- package/src/server/Client.js +7 -2
- package/src/server/ContextManager.js +4 -4
- package/src/server/Server.js +197 -27
- package/src/server/SharedState.js +0 -1
- package/src/server/SharedStateCollection.js +36 -0
- package/src/server/Sockets.js +25 -4
- package/src/server/StateManager.js +7 -5
- package/src/server/crypto.js +38 -0
- package/src/common/utils.js +0 -28
|
@@ -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
|
|
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
|
-
|
|
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, (
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
*
|
|
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>} -
|
|
177
|
-
* as been executed
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
|
193
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
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
|
-
|
|
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') {
|
package/src/server/Client.js
CHANGED
|
@@ -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 {
|