@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.
- package/.eslintrc +0 -1
- package/.jsdoc +57 -0
- package/CHANGELOG.md +33 -0
- package/README.md +17 -51
- package/package.json +24 -31
- package/src/client/Client.js +13 -7
- package/src/client/Context.js +12 -13
- package/src/client/ContextManager.js +1 -1
- package/src/client/PluginManager.js +1 -1
- package/src/client/SharedStateCollection.js +38 -0
- package/src/client/Socket.js +80 -44
- package/src/common/BasePlugin.js +31 -21
- package/src/common/BasePluginManager.js +34 -19
- package/src/common/BaseSharedState.js +304 -79
- package/src/common/BaseSharedStateCollection.js +324 -0
- package/src/common/BaseStateManager.js +353 -49
- package/src/common/BatchedTransport.js +73 -0
- package/src/common/ParameterBag.js +92 -30
- package/src/common/PromiseStore.js +53 -0
- package/src/common/SharedStatePrivate.js +87 -46
- package/src/common/constants.js +14 -1
- package/src/common/sockets-utils.js +3 -4
- package/src/server/Client.js +7 -2
- package/src/server/ContextManager.js +4 -4
- package/src/server/PluginManager.js +1 -1
- package/src/server/Server.js +263 -43
- package/src/server/SharedState.js +0 -1
- package/src/server/SharedStateCollection.js +39 -0
- package/src/server/Socket.js +55 -49
- package/src/server/Sockets.js +33 -11
- package/src/server/StateManager.js +187 -100
- package/src/server/audit-network-latency.worker.js +16 -4
- package/src/server/crypto.js +37 -0
- package/src/common/promise-store.js +0 -25
- package/src/common/utils.js +0 -22
- package/tsconfig.json +0 -21
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import cloneDeep from 'lodash.
|
|
1
|
+
import cloneDeep from 'lodash/cloneDeep.js';
|
|
2
2
|
import equal from 'fast-deep-equal';
|
|
3
3
|
|
|
4
4
|
export const sharedOptions = {
|
|
@@ -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(`[
|
|
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(`[
|
|
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(`[
|
|
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(`[
|
|
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(`[
|
|
112
|
+
throw new TypeError(`[SharedState] Invalid value "${value}" for enum parameter "${name}"`);
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
|
|
@@ -139,11 +139,11 @@ class ParameterBag {
|
|
|
139
139
|
const def = schema[name];
|
|
140
140
|
|
|
141
141
|
if (!Object.prototype.hasOwnProperty.call(def, 'type')) {
|
|
142
|
-
throw new TypeError(`[
|
|
142
|
+
throw new TypeError(`[StateManager.registerSchema] Invalid schema definition - param "${name}": "type" key is required`);
|
|
143
143
|
}
|
|
144
144
|
|
|
145
145
|
if (!Object.prototype.hasOwnProperty.call(types, def.type)) {
|
|
146
|
-
throw new TypeError(`[
|
|
146
|
+
throw new TypeError(`[StateManager.registerSchema] Invalid schema definition - param "${name}": "{ type: '${def.type}' }" does not exists`);
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
const required = types[def.type].required;
|
|
@@ -152,7 +152,7 @@ class ParameterBag {
|
|
|
152
152
|
if (def.event === true && key === 'default') {
|
|
153
153
|
// do nothing, default is always null for `event` params
|
|
154
154
|
} else if (!Object.prototype.hasOwnProperty.call(def, key)) {
|
|
155
|
-
throw new TypeError(`[
|
|
155
|
+
throw new TypeError(`[StateManager.registerSchema] Invalid schema definition - param "${name}" (type "${def.type}"): "${key}" key is required`);
|
|
156
156
|
}
|
|
157
157
|
});
|
|
158
158
|
}
|
|
@@ -160,7 +160,7 @@ class ParameterBag {
|
|
|
160
160
|
|
|
161
161
|
constructor(schema, initValues = {}) {
|
|
162
162
|
if (!schema) {
|
|
163
|
-
throw new Error(`[
|
|
163
|
+
throw new Error(`[ParameterBag] schema is mandatory`);
|
|
164
164
|
}
|
|
165
165
|
|
|
166
166
|
schema = cloneDeep(schema);
|
|
@@ -193,7 +193,7 @@ class ParameterBag {
|
|
|
193
193
|
// make shure initValues make sens according to the given schema
|
|
194
194
|
for (let name in initValues) {
|
|
195
195
|
if (!Object.prototype.hasOwnProperty.call(schema, name)) {
|
|
196
|
-
throw new ReferenceError(`[
|
|
196
|
+
throw new ReferenceError(`[StateManager.create] init value defined for undefined param "${name}"`);
|
|
197
197
|
}
|
|
198
198
|
}
|
|
199
199
|
|
|
@@ -226,7 +226,6 @@ class ParameterBag {
|
|
|
226
226
|
|
|
227
227
|
this._schema[name].initValue = initValue;
|
|
228
228
|
this._values[name] = initValue;
|
|
229
|
-
|
|
230
229
|
}
|
|
231
230
|
}
|
|
232
231
|
|
|
@@ -241,7 +240,8 @@ class ParameterBag {
|
|
|
241
240
|
}
|
|
242
241
|
|
|
243
242
|
/**
|
|
244
|
-
* Return values of all parameters as a flat object.
|
|
243
|
+
* Return values of all parameters as a flat object. If a parameter is of `any`
|
|
244
|
+
* type, a deep copy is made.
|
|
245
245
|
*
|
|
246
246
|
* @return {object}
|
|
247
247
|
*/
|
|
@@ -255,6 +255,25 @@ class ParameterBag {
|
|
|
255
255
|
return values;
|
|
256
256
|
}
|
|
257
257
|
|
|
258
|
+
/**
|
|
259
|
+
* Return values of all parameters as a flat object. Similar to `getValues` but
|
|
260
|
+
* returns a reference to the underlying value in case of `any` type. May be
|
|
261
|
+
* usefull if the underlying value is big (e.g. sensors recordings, etc.) and
|
|
262
|
+
* deep cloning expensive. Be aware that if changes are made on the returned
|
|
263
|
+
* object, the state of your application will become inconsistent.
|
|
264
|
+
*
|
|
265
|
+
* @return {object}
|
|
266
|
+
*/
|
|
267
|
+
getValuesUnsafe() {
|
|
268
|
+
let values = {};
|
|
269
|
+
|
|
270
|
+
for (let name in this._values) {
|
|
271
|
+
values[name] = this.getUnsafe(name);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return values;
|
|
275
|
+
}
|
|
276
|
+
|
|
258
277
|
/**
|
|
259
278
|
* Return the value of the given parameter. If the parameter is of `any` type,
|
|
260
279
|
* a deep copy is returned.
|
|
@@ -264,10 +283,12 @@ class ParameterBag {
|
|
|
264
283
|
*/
|
|
265
284
|
get(name) {
|
|
266
285
|
if (!this.has(name)) {
|
|
267
|
-
throw new ReferenceError(`[
|
|
286
|
+
throw new ReferenceError(`[SharedState] Cannot get value of undefined parameter "${name}"`);
|
|
268
287
|
}
|
|
269
288
|
|
|
270
289
|
if (this._schema[name].type === 'any') {
|
|
290
|
+
// we return a deep copy of the object as we don't want the client code to
|
|
291
|
+
// be able to modify our underlying data.
|
|
271
292
|
return cloneDeep(this._values[name]);
|
|
272
293
|
} else {
|
|
273
294
|
return this._values[name];
|
|
@@ -275,35 +296,72 @@ class ParameterBag {
|
|
|
275
296
|
}
|
|
276
297
|
|
|
277
298
|
/**
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
299
|
+
* Similar to `get` but returns a reference to the underlying value in case of
|
|
300
|
+
* `any` type. May be usefull if the underlying value is big (e.g. sensors
|
|
301
|
+
* recordings, etc.) and deep cloning expensive. Be aware that if changes are
|
|
302
|
+
* made on the returned object, the state of your application will become
|
|
303
|
+
* inconsistent.
|
|
281
304
|
*
|
|
282
305
|
* @param {string} name - Name of the parameter.
|
|
306
|
+
* @return {Mixed} - Value of the parameter.
|
|
307
|
+
*/
|
|
308
|
+
getUnsafe(name) {
|
|
309
|
+
if (!this.has(name)) {
|
|
310
|
+
throw new ReferenceError(`[SharedState] Cannot get value of undefined parameter "${name}"`);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return this._values[name];
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Check that the value is valid according to the schema and return it coerced
|
|
318
|
+
* to the schema definition
|
|
319
|
+
*
|
|
320
|
+
* @param {String} name - Name of the parameter.
|
|
283
321
|
* @param {Mixed} value - Value of the parameter.
|
|
284
|
-
* @param {boolean} [forcePropagation=false] - if true, propagate value even
|
|
285
|
-
* if the value has not changed.
|
|
286
|
-
* @return {Array} - [new value, updated flag].
|
|
287
322
|
*/
|
|
288
|
-
|
|
323
|
+
coerceValue(name, value) {
|
|
289
324
|
if (!this.has(name)) {
|
|
290
|
-
throw new ReferenceError(`[
|
|
325
|
+
throw new ReferenceError(`[SharedState] Cannot set value of undefined parameter "${name}"`);
|
|
291
326
|
}
|
|
292
327
|
|
|
293
328
|
const def = this._schema[name];
|
|
294
|
-
const { coerceFunction } = types[def.type];
|
|
295
329
|
|
|
296
330
|
if (value === null && def.nullable === false) {
|
|
297
|
-
throw new TypeError(`[
|
|
331
|
+
throw new TypeError(`[SharedState] Invalid value for ${def.type} param "${name}": value is null and param is not nullable`);
|
|
298
332
|
} else if (value === null && def.nullable === true) {
|
|
299
333
|
value = null;
|
|
300
334
|
} else {
|
|
335
|
+
const { coerceFunction } = types[def.type];
|
|
301
336
|
value = coerceFunction(name, def, value);
|
|
302
337
|
}
|
|
303
338
|
|
|
339
|
+
return value;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Set the value of a parameter. If the value of the parameter is updated
|
|
344
|
+
* (aka if previous value is different from new value) all registered
|
|
345
|
+
* callbacks are registered.
|
|
346
|
+
*
|
|
347
|
+
* @param {string} name - Name of the parameter.
|
|
348
|
+
* @param {Mixed} value - Value of the parameter.
|
|
349
|
+
* @return {Array} - [new value, updated flag].
|
|
350
|
+
*/
|
|
351
|
+
set(name, value) {
|
|
352
|
+
value = this.coerceValue(name, value);
|
|
304
353
|
const currentValue = this._values[name];
|
|
305
354
|
const updated = !equal(currentValue, value);
|
|
306
355
|
|
|
356
|
+
// we store a deep copy of the object as we don't want the client to be able
|
|
357
|
+
// to modify our underlying data, which leads to unexpected behavior where the
|
|
358
|
+
// deep equal check to returns true, and therefore the update is not triggered.
|
|
359
|
+
// @see tests/common.state-manager.spec.js
|
|
360
|
+
// 'should copy stored value for "any" type to have a predictable behavior'
|
|
361
|
+
if (this._schema[name].type === 'any') {
|
|
362
|
+
value = cloneDeep(value);
|
|
363
|
+
}
|
|
364
|
+
|
|
307
365
|
this._values[name] = value;
|
|
308
366
|
|
|
309
367
|
// return tuple so that the state manager can handle the `filterChange` option
|
|
@@ -335,30 +393,34 @@ class ParameterBag {
|
|
|
335
393
|
getSchema(name = null) {
|
|
336
394
|
if (name === null) {
|
|
337
395
|
return this._schema;
|
|
338
|
-
}
|
|
339
|
-
if (!this.has(name)) {
|
|
340
|
-
throw new ReferenceError(`[stateManager] Cannot get schema description of undefined parameter "${name}"`);
|
|
341
|
-
}
|
|
396
|
+
}
|
|
342
397
|
|
|
343
|
-
|
|
398
|
+
if (!this.has(name)) {
|
|
399
|
+
throw new ReferenceError(`[SharedState] Cannot get schema description of undefined parameter "${name}"`);
|
|
344
400
|
}
|
|
401
|
+
|
|
402
|
+
return this._schema[name];
|
|
345
403
|
}
|
|
346
404
|
|
|
347
405
|
// return the default value, if initValue has been given, return init values
|
|
348
406
|
getInitValues() {
|
|
349
407
|
const initValues = {};
|
|
408
|
+
|
|
350
409
|
for (let [name, def] of Object.entries(this._schema)) {
|
|
351
410
|
initValues[name] = def.initValue;
|
|
352
411
|
}
|
|
412
|
+
|
|
353
413
|
return initValues;
|
|
354
414
|
}
|
|
355
415
|
|
|
356
416
|
// return the default value, if initValue has been given, return init values
|
|
357
417
|
getDefaults() {
|
|
358
418
|
const defaults = {};
|
|
419
|
+
|
|
359
420
|
for (let [name, def] of Object.entries(this._schema)) {
|
|
360
|
-
defaults[name] = def.
|
|
421
|
+
defaults[name] = def.default;
|
|
361
422
|
}
|
|
423
|
+
|
|
362
424
|
return defaults;
|
|
363
425
|
}
|
|
364
426
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { idGenerator } from '@ircam/sc-utils';
|
|
2
|
+
|
|
3
|
+
export default class PromiseStore {
|
|
4
|
+
constructor(name) {
|
|
5
|
+
this.name = name;
|
|
6
|
+
this.store = new Map();
|
|
7
|
+
this.generateRequestId = idGenerator();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
add(resolve, reject, type, localParams) {
|
|
11
|
+
const reqId = this.generateRequestId.next().value;
|
|
12
|
+
this.store.set(reqId, { resolve, reject, type, localParams });
|
|
13
|
+
|
|
14
|
+
return reqId;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
resolve(reqId, data) {
|
|
18
|
+
if (this.store.has(reqId)) {
|
|
19
|
+
const { resolve, localParams } = this.store.get(reqId);
|
|
20
|
+
this.store.delete(reqId);
|
|
21
|
+
|
|
22
|
+
// re-merge local params into network response
|
|
23
|
+
if (localParams !== undefined) {
|
|
24
|
+
Object.assign(data, localParams);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
resolve(data);
|
|
28
|
+
} else {
|
|
29
|
+
throw new Error(`[${this.name}] cannot resolve request id (${reqId}), id does not exist`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
reject(reqId, msg) {
|
|
34
|
+
if (this.store.has(reqId)) {
|
|
35
|
+
const { reject } = this.store.get(reqId);
|
|
36
|
+
this.store.delete(reqId);
|
|
37
|
+
|
|
38
|
+
reject(new Error(msg));
|
|
39
|
+
} else {
|
|
40
|
+
throw new Error(`[${this.name}] cannot reject request id (${reqId}), id does not exist`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// reject all pending request
|
|
45
|
+
flush() {
|
|
46
|
+
for (let [_reqId, entry] of this.store) {
|
|
47
|
+
const { reject, type } = entry;
|
|
48
|
+
reject(new Error(`[${this.name}] Discard promise "${type}", cannot resolve`));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
this.store.clear();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -11,9 +11,29 @@ import {
|
|
|
11
11
|
UPDATE_NOTIFICATION,
|
|
12
12
|
} from '../common/constants.js';
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Filter update object according to filter list.
|
|
16
|
+
* Return identity is filter is null
|
|
17
|
+
* @param {object} updates
|
|
18
|
+
* @param {array|null} filter
|
|
19
|
+
*/
|
|
20
|
+
function filterUpdates(updates, filter) {
|
|
21
|
+
if (filter === null) {
|
|
22
|
+
return updates;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return filter.reduce((acc, key) => {
|
|
26
|
+
if (key in updates) {
|
|
27
|
+
acc[key] = updates[key];
|
|
28
|
+
}
|
|
29
|
+
return acc;
|
|
30
|
+
}, {});
|
|
31
|
+
}
|
|
32
|
+
|
|
14
33
|
/**
|
|
15
34
|
* The "real" state, this instance is kept private by the server.StateManager.
|
|
16
|
-
* It
|
|
35
|
+
* It can only be accessed through a SharedState proxy.
|
|
36
|
+
*
|
|
17
37
|
* @private
|
|
18
38
|
*/
|
|
19
39
|
class SharedStatePrivate {
|
|
@@ -29,8 +49,9 @@ class SharedStatePrivate {
|
|
|
29
49
|
this._creatorId = null;
|
|
30
50
|
}
|
|
31
51
|
|
|
32
|
-
_attachClient(remoteId, client, isOwner
|
|
33
|
-
|
|
52
|
+
_attachClient(remoteId, client, isOwner, filter) {
|
|
53
|
+
const clientInfos = { client, isOwner, filter };
|
|
54
|
+
this._attachedClients.set(remoteId, clientInfos);
|
|
34
55
|
|
|
35
56
|
if (isOwner) {
|
|
36
57
|
this._creatorRemoteId = remoteId;
|
|
@@ -59,20 +80,11 @@ class SharedStatePrivate {
|
|
|
59
80
|
}
|
|
60
81
|
|
|
61
82
|
if (hookAborted === false) {
|
|
62
|
-
|
|
83
|
+
let acknowledgedUpdates = {};
|
|
63
84
|
let hasUpdates = false;
|
|
64
85
|
|
|
65
86
|
for (let name in updates) {
|
|
66
|
-
// from v3.1.0 - the `filteredUpdates` check is made using 'fast-deep-equal'
|
|
67
|
-
// cf. https://github.com/epoberezkin/fast-deep-equal
|
|
68
|
-
// therefore unchanged objects are not considered changed
|
|
69
|
-
// nor propagated anymore.
|
|
70
|
-
// until v3.0.4 - we checked the `schema[name].type === 'any'`, to always consider
|
|
71
|
-
// objects as dirty, because if the state is attached locally, we
|
|
72
|
-
// compare the Object instances instead of their values.
|
|
73
|
-
// @note - this should be made more robust but how?
|
|
74
87
|
const [newValue, changed] = this._parameters.set(name, updates[name]);
|
|
75
|
-
|
|
76
88
|
// if `filterChange` is set to `false` we don't check if the value
|
|
77
89
|
// has been changed or not, it is always propagated to client states
|
|
78
90
|
const { event, filterChange } = this._parameters.getSchema(name);
|
|
@@ -83,55 +95,83 @@ class SharedStatePrivate {
|
|
|
83
95
|
}
|
|
84
96
|
|
|
85
97
|
if ((filterChange && changed) || !filterChange) {
|
|
86
|
-
|
|
98
|
+
acknowledgedUpdates[name] = newValue;
|
|
87
99
|
hasUpdates = true;
|
|
88
100
|
}
|
|
89
101
|
}
|
|
90
102
|
|
|
91
103
|
if (hasUpdates) {
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
// order would be broken,
|
|
99
|
-
|
|
100
|
-
// we need to handle cases where:
|
|
101
|
-
// client state (client.id: 2) sends a request
|
|
102
|
-
// server attached state (client.id: -1) spot a problem and overrides the value
|
|
103
|
-
// we want the remote client (id: 2) to receive in the right order:
|
|
104
|
+
// Normal case
|
|
105
|
+
//
|
|
106
|
+
// We need to handle cases where:
|
|
107
|
+
// - client state (client.id: 2) sends a request
|
|
108
|
+
// - server attached state (client.id: -1) spot a problem and overrides the value
|
|
109
|
+
// We want the remote client (id: 2) to receive in the right order:
|
|
104
110
|
// * 1. the value it requested,
|
|
105
111
|
// * 2. the value overriden by the server-side attached state (id: -1)
|
|
112
|
+
//
|
|
113
|
+
// such problem is now better solved with the the upateHook system, none
|
|
114
|
+
// nonetheway we don't want to introduce inconsistencies here
|
|
115
|
+
//
|
|
116
|
+
// Then we propagate server-side last, because as the server transport
|
|
117
|
+
// is synchronous it can break ordering if a subscription function makes
|
|
118
|
+
// itself an update in reaction to an update. Propagating to server last
|
|
119
|
+
// alllows to maintain network messages order consistent.
|
|
120
|
+
//
|
|
121
|
+
// @note - remoteId correspond to unique remote state id
|
|
106
122
|
|
|
107
|
-
//
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
123
|
+
// propagate RESPONSE to the client that originates the request if not the server
|
|
124
|
+
if (client.id !== -1) {
|
|
125
|
+
// no need to filter updates on requested, is blocked on client-side
|
|
126
|
+
client.transport.emit(
|
|
127
|
+
`${UPDATE_RESPONSE}-${this.id}-${remoteId}`,
|
|
128
|
+
reqId, acknowledgedUpdates, context,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// propagate NOTIFICATION to all attached clients except server
|
|
133
|
+
for (let [peerRemoteId, clientInfos] of this._attachedClients) {
|
|
134
|
+
const peer = clientInfos.client;
|
|
113
135
|
|
|
114
|
-
for (let [peerRemoteId, peer] of this._attachedClients.entries()) {
|
|
115
|
-
// propagate notification to all other attached clients except server
|
|
116
136
|
if (remoteId !== peerRemoteId && peer.id !== -1) {
|
|
117
|
-
|
|
137
|
+
const filter = clientInfos.filter;
|
|
138
|
+
const filteredUpdates = filterUpdates(acknowledgedUpdates, filter);
|
|
139
|
+
|
|
140
|
+
// propagate only if there something left after applying the filter
|
|
141
|
+
if (Object.keys(filteredUpdates).length > 0) {
|
|
142
|
+
peer.transport.emit(
|
|
143
|
+
`${UPDATE_NOTIFICATION}-${this.id}-${peerRemoteId}`,
|
|
144
|
+
filteredUpdates, context,
|
|
145
|
+
);
|
|
146
|
+
}
|
|
118
147
|
}
|
|
119
148
|
}
|
|
120
149
|
|
|
121
|
-
if
|
|
122
|
-
|
|
150
|
+
// propagate RESPONSE to server if it is the requester
|
|
151
|
+
if (client.id === -1) {
|
|
152
|
+
// no need to filter updates on requested, is blocked on client-side
|
|
153
|
+
client.transport.emit(
|
|
154
|
+
`${UPDATE_RESPONSE}-${this.id}-${remoteId}`,
|
|
155
|
+
reqId, acknowledgedUpdates, context,
|
|
156
|
+
);
|
|
123
157
|
}
|
|
124
158
|
|
|
125
|
-
|
|
126
|
-
|
|
159
|
+
// propagate NOTIFICATION to other state attached on the server
|
|
160
|
+
for (let [peerRemoteId, clientInfos] of this._attachedClients) {
|
|
161
|
+
const peer = clientInfos.client;
|
|
162
|
+
|
|
127
163
|
if (remoteId !== peerRemoteId && peer.id === -1) {
|
|
128
|
-
|
|
164
|
+
const filter = clientInfos.filter;
|
|
165
|
+
const filteredUpdates = filterUpdates(acknowledgedUpdates, filter);
|
|
166
|
+
|
|
167
|
+
if (Object.keys(filteredUpdates).length > 0) {
|
|
168
|
+
peer.transport.emit(
|
|
169
|
+
`${UPDATE_NOTIFICATION}-${this.id}-${peerRemoteId}`,
|
|
170
|
+
filteredUpdates, context,
|
|
171
|
+
);
|
|
172
|
+
}
|
|
129
173
|
}
|
|
130
174
|
}
|
|
131
|
-
|
|
132
|
-
if (client.id === -1) {
|
|
133
|
-
client.transport.emit(`${UPDATE_RESPONSE}-${this.id}-${remoteId}`, reqId, filteredUpdates, context);
|
|
134
|
-
}
|
|
135
175
|
} else {
|
|
136
176
|
// propagate back to the requester that the update has been aborted
|
|
137
177
|
// ignore all other attached clients.
|
|
@@ -152,14 +192,15 @@ class SharedStatePrivate {
|
|
|
152
192
|
if (isOwner) {
|
|
153
193
|
// delete only if creator
|
|
154
194
|
client.transport.addListener(`${DELETE_REQUEST}-${this.id}-${remoteId}`, (reqId) => {
|
|
155
|
-
this._manager.
|
|
195
|
+
this._manager._sharedStatePrivateById.delete(this.id);
|
|
156
196
|
// --------------------------------------------------------------------
|
|
157
197
|
// WARNING - MAKE SURE WE DON'T HAVE PROBLEM W/ THAT
|
|
158
198
|
// --------------------------------------------------------------------
|
|
159
199
|
// @todo - propagate server-side last, because if a subscription function sends a
|
|
160
200
|
// message to a client, network messages order are kept coherent
|
|
161
201
|
// this._subscriptions.forEach(func => func(updated));
|
|
162
|
-
for (let [remoteId,
|
|
202
|
+
for (let [remoteId, clientInfos] of this._attachedClients) {
|
|
203
|
+
const attached = clientInfos.client;
|
|
163
204
|
this._detachClient(remoteId, attached);
|
|
164
205
|
|
|
165
206
|
if (remoteId === this._creatorRemoteId) {
|
package/src/common/constants.js
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
// id of the server when owner of a state
|
|
2
2
|
export const SERVER_ID = -1;
|
|
3
3
|
|
|
4
|
+
export const PING_INTERVAL = 10 * 1000;
|
|
5
|
+
export const PING_LATENCY_TOLERANCE = 1000;
|
|
6
|
+
export const PING_MESSAGE = 'h:ping';
|
|
7
|
+
export const PONG_MESSAGE = 'h:pong';
|
|
8
|
+
|
|
9
|
+
// batched transport channel
|
|
10
|
+
export const BATCHED_TRANSPORT_CHANNEL = 'b:t';
|
|
11
|
+
|
|
4
12
|
// shared states channels
|
|
5
13
|
export const CREATE_REQUEST = 's:c:req';
|
|
6
14
|
export const CREATE_RESPONSE = 's:c:res';
|
|
@@ -21,6 +29,7 @@ export const DETACH_ERROR = 's:dt:err';
|
|
|
21
29
|
|
|
22
30
|
export const OBSERVE_REQUEST = 's:o:req';
|
|
23
31
|
export const OBSERVE_RESPONSE = 's:o:res';
|
|
32
|
+
export const OBSERVE_ERROR = 's:o:err';
|
|
24
33
|
export const OBSERVE_NOTIFICATION = 's:o:not';
|
|
25
34
|
|
|
26
35
|
export const UNOBSERVE_NOTIFICATION = 's:uo:not';
|
|
@@ -32,6 +41,10 @@ export const UPDATE_NOTIFICATION = 's:u:not';
|
|
|
32
41
|
|
|
33
42
|
export const DELETE_SCHEMA = 's:d:s';
|
|
34
43
|
|
|
44
|
+
export const GET_SCHEMA_REQUEST = 's:s:req';
|
|
45
|
+
export const GET_SCHEMA_RESPONSE = 's:s:res';
|
|
46
|
+
export const GET_SCHEMA_ERROR = 's:s:err';
|
|
47
|
+
|
|
35
48
|
// context channels
|
|
36
49
|
export const CONTEXT_ENTER_REQUEST = 'c:en:req';
|
|
37
50
|
export const CONTEXT_ENTER_RESPONSE = 'c:en:res';
|
|
@@ -47,5 +60,5 @@ export const CLIENT_HANDSHAKE_RESPONSE = 'cl:h:res';
|
|
|
47
60
|
export const CLIENT_HANDSHAKE_ERROR = 'cl:h:err';
|
|
48
61
|
|
|
49
62
|
// audit state schema name
|
|
50
|
-
export const AUDIT_STATE_NAME = 's:
|
|
63
|
+
export const AUDIT_STATE_NAME = 'p:s:audit';
|
|
51
64
|
export const PRIVATE_STATES = [AUDIT_STATE_NAME];
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import 'fast-text-encoding';
|
|
2
|
-
import root from 'window-or-global';
|
|
3
2
|
|
|
4
|
-
const encoder = new
|
|
5
|
-
const decoder = new
|
|
3
|
+
const encoder = new TextEncoder('utf-8');
|
|
4
|
+
const decoder = new TextDecoder('utf-8');
|
|
6
5
|
|
|
7
6
|
const types = [
|
|
8
7
|
'Int8Array',
|
|
@@ -57,7 +56,7 @@ export function unpackBinaryMessage(buffer /* arraybuffer */) {
|
|
|
57
56
|
const channel = decoder.decode(channelBuffer);
|
|
58
57
|
const type = types[typeIndex];
|
|
59
58
|
// slice (copy) the underlying ArrayBuffer to create a clean TypedArray
|
|
60
|
-
const data = new
|
|
59
|
+
const data = new globalThis[type](buffer.slice(startOffset));
|
|
61
60
|
|
|
62
61
|
return [channel, data];
|
|
63
62
|
}
|
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 {
|
|
@@ -78,7 +78,7 @@ import Server from './Server.js';
|
|
|
78
78
|
class PluginManager extends BasePluginManager {
|
|
79
79
|
constructor(server) {
|
|
80
80
|
if (!(server instanceof Server)) {
|
|
81
|
-
throw new Error(`[soundworks.PluginManager] Invalid argument, "new PluginManager(server)" should receive an instance of "soundworks.Server as argument
|
|
81
|
+
throw new Error(`[soundworks.PluginManager] Invalid argument, "new PluginManager(server)" should receive an instance of "soundworks.Server" as argument`);
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
super(server);
|