@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.
- package/.eslintrc +0 -1
- package/.jsdoc +57 -0
- package/CHANGELOG.md +26 -0
- package/README.md +17 -51
- package/package.json +24 -31
- package/src/client/Client.js +11 -5
- 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 +60 -44
- package/src/common/BasePlugin.js +31 -21
- package/src/common/BasePluginManager.js +34 -19
- package/src/common/BaseSharedState.js +201 -68
- package/src/common/BaseSharedStateCollection.js +279 -0
- package/src/common/BaseStateManager.js +257 -36
- package/src/common/BatchedTransport.js +66 -0
- package/src/common/ParameterBag.js +92 -30
- package/src/common/PromiseStore.js +48 -0
- package/src/common/SharedStatePrivate.js +52 -43
- 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 +255 -41
- package/src/server/SharedState.js +0 -1
- package/src/server/SharedStateCollection.js +39 -0
- package/src/server/Socket.js +50 -49
- package/src/server/Sockets.js +31 -11
- package/src/server/StateManager.js +162 -90
- 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
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { BATCHED_TRANSPORT_CHANNEL } from './constants.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* This class proxies transports given the SharedStateManager to batch messages
|
|
5
|
+
* @private
|
|
6
|
+
*/
|
|
7
|
+
class BatchedTransport {
|
|
8
|
+
constructor(transport) {
|
|
9
|
+
this._transport = transport;
|
|
10
|
+
this._stack = [];
|
|
11
|
+
this._listeners = new Map();
|
|
12
|
+
this._sending = false;
|
|
13
|
+
|
|
14
|
+
this._transport.addListener(BATCHED_TRANSPORT_CHANNEL, stack => {
|
|
15
|
+
stack.forEach(entry => {
|
|
16
|
+
const [channel, args] = entry;
|
|
17
|
+
// server side the transport is the same EventEmitter instance
|
|
18
|
+
// for both state manager server and client, so channel might not exist
|
|
19
|
+
// one side or the other.
|
|
20
|
+
if (this._listeners.has(channel)) {
|
|
21
|
+
const callbacks = this._listeners.get(channel);
|
|
22
|
+
callbacks.forEach(callback => callback(...args));
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
addListener(channel, callback) {
|
|
29
|
+
if (!this._listeners.has(channel)) {
|
|
30
|
+
this._listeners.set(channel, new Set());
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const callbacks = this._listeners.get(channel);
|
|
34
|
+
callbacks.add(callback);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async emit(channel, ...args) {
|
|
38
|
+
this._stack.push([channel, args]);
|
|
39
|
+
|
|
40
|
+
if (!this._sending) {
|
|
41
|
+
this._sending = true;
|
|
42
|
+
this._sending = await false;
|
|
43
|
+
const stack = this._stack;
|
|
44
|
+
this._stack = [];
|
|
45
|
+
this._transport.emit(BATCHED_TRANSPORT_CHANNEL, stack);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
removeListener(channel, callback) {
|
|
50
|
+
if (!this._listeners.has(channel)) {
|
|
51
|
+
const callbacks = this._listeners.get(channel);
|
|
52
|
+
callbacks.delete(callback);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
removeAllListeners(channel = null) {
|
|
57
|
+
if (channel === null) {
|
|
58
|
+
this._listeners.clear();
|
|
59
|
+
} else if (this._listeners.has(channel)) {
|
|
60
|
+
const callbacks = this._listeners.get(channel);
|
|
61
|
+
callbacks.clear();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export default BatchedTransport;
|
|
@@ -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,48 @@
|
|
|
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) {
|
|
11
|
+
const reqId = this.generateRequestId.next().value;
|
|
12
|
+
this.store.set(reqId, { resolve, reject, type });
|
|
13
|
+
|
|
14
|
+
return reqId;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
resolve(reqId, data) {
|
|
18
|
+
if (this.store.has(reqId)) {
|
|
19
|
+
const { resolve } = this.store.get(reqId);
|
|
20
|
+
this.store.delete(reqId);
|
|
21
|
+
|
|
22
|
+
resolve(data);
|
|
23
|
+
} else {
|
|
24
|
+
throw new Error(`[${this.name}] cannot resolve request id (${reqId}), id does not exist`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
reject(reqId, msg) {
|
|
29
|
+
if (this.store.has(reqId)) {
|
|
30
|
+
const { reject } = this.store.get(reqId);
|
|
31
|
+
this.store.delete(reqId);
|
|
32
|
+
|
|
33
|
+
reject(new Error(msg));
|
|
34
|
+
} else {
|
|
35
|
+
throw new Error(`[${this.name}] cannot reject request id (${reqId}), id does not exist`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// reject all pendeing request
|
|
40
|
+
flush() {
|
|
41
|
+
for (let [_reqId, entry] of this.store) {
|
|
42
|
+
const { reject, type } = entry;
|
|
43
|
+
reject(new Error(`[${this.name}] Discard promise "${type}", cannot resolve`));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
this.store.clear();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -13,7 +13,8 @@ import {
|
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* The "real" state, this instance is kept private by the server.StateManager.
|
|
16
|
-
* It
|
|
16
|
+
* It can only be accessed through a SharedState proxy.
|
|
17
|
+
*
|
|
17
18
|
* @private
|
|
18
19
|
*/
|
|
19
20
|
class SharedStatePrivate {
|
|
@@ -30,7 +31,8 @@ class SharedStatePrivate {
|
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
_attachClient(remoteId, client, isOwner = false) {
|
|
33
|
-
|
|
34
|
+
const clientInfos = { client, isOwner };
|
|
35
|
+
this._attachedClients.set(remoteId, clientInfos);
|
|
34
36
|
|
|
35
37
|
if (isOwner) {
|
|
36
38
|
this._creatorRemoteId = remoteId;
|
|
@@ -63,16 +65,7 @@ class SharedStatePrivate {
|
|
|
63
65
|
let hasUpdates = false;
|
|
64
66
|
|
|
65
67
|
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
68
|
const [newValue, changed] = this._parameters.set(name, updates[name]);
|
|
75
|
-
|
|
76
69
|
// if `filterChange` is set to `false` we don't check if the value
|
|
77
70
|
// has been changed or not, it is always propagated to client states
|
|
78
71
|
const { event, filterChange } = this._parameters.getSchema(name);
|
|
@@ -89,49 +82,64 @@ class SharedStatePrivate {
|
|
|
89
82
|
}
|
|
90
83
|
|
|
91
84
|
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:
|
|
85
|
+
// Normal case
|
|
86
|
+
//
|
|
87
|
+
// We need to handle cases where:
|
|
88
|
+
// - client state (client.id: 2) sends a request
|
|
89
|
+
// - server attached state (client.id: -1) spot a problem and overrides the value
|
|
90
|
+
// We want the remote client (id: 2) to receive in the right order:
|
|
104
91
|
// * 1. the value it requested,
|
|
105
92
|
// * 2. the value overriden by the server-side attached state (id: -1)
|
|
93
|
+
//
|
|
94
|
+
// such problem is now better solved with the the upateHook system, none
|
|
95
|
+
// nonetheway we don't want to introduce inconsistencies here
|
|
96
|
+
//
|
|
97
|
+
// Then we propagate server-side last, because as the server transport
|
|
98
|
+
// is synchronous it can break ordering if a subscription function makes
|
|
99
|
+
// itself an update in reaction to an update. Propagating to server last
|
|
100
|
+
// alllows to maintain network messages order consistent.
|
|
101
|
+
//
|
|
102
|
+
// @note - remoteId correspond to unique remote state id
|
|
103
|
+
|
|
104
|
+
// propagate RESPONSE to the client that originates the request if not the server
|
|
105
|
+
if (client.id !== -1) {
|
|
106
|
+
client.transport.emit(
|
|
107
|
+
`${UPDATE_RESPONSE}-${this.id}-${remoteId}`,
|
|
108
|
+
reqId, filteredUpdates, context,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
106
111
|
|
|
107
|
-
//
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
// -> get values from reducer
|
|
111
|
-
//. -> dispatch to everybody
|
|
112
|
-
// }
|
|
112
|
+
// propagate NOTIFICATION to all other attached clients except server
|
|
113
|
+
for (let [peerRemoteId, clientInfos] of this._attachedClients) {
|
|
114
|
+
const peer = clientInfos.client;
|
|
113
115
|
|
|
114
|
-
for (let [peerRemoteId, peer] of this._attachedClients.entries()) {
|
|
115
|
-
// propagate notification to all other attached clients except server
|
|
116
116
|
if (remoteId !== peerRemoteId && peer.id !== -1) {
|
|
117
|
-
peer.transport.emit(
|
|
117
|
+
peer.transport.emit(
|
|
118
|
+
`${UPDATE_NOTIFICATION}-${this.id}-${peerRemoteId}`,
|
|
119
|
+
filteredUpdates, context,
|
|
120
|
+
);
|
|
118
121
|
}
|
|
119
122
|
}
|
|
120
123
|
|
|
121
|
-
if
|
|
122
|
-
|
|
124
|
+
// propagate RESPONSE to server if it is the requester
|
|
125
|
+
if (client.id === -1) {
|
|
126
|
+
client.transport.emit(
|
|
127
|
+
`${UPDATE_RESPONSE}-${this.id}-${remoteId}`,
|
|
128
|
+
reqId, filteredUpdates, context,
|
|
129
|
+
);
|
|
123
130
|
}
|
|
124
131
|
|
|
125
|
-
|
|
126
|
-
|
|
132
|
+
// propagate NOTIFICATION other attached state that belongs to server
|
|
133
|
+
for (let [peerRemoteId, clientInfos] of this._attachedClients) {
|
|
134
|
+
const peer = clientInfos.client;
|
|
135
|
+
|
|
127
136
|
if (remoteId !== peerRemoteId && peer.id === -1) {
|
|
128
|
-
peer.transport.emit(
|
|
137
|
+
peer.transport.emit(
|
|
138
|
+
`${UPDATE_NOTIFICATION}-${this.id}-${peerRemoteId}`,
|
|
139
|
+
filteredUpdates, context,
|
|
140
|
+
);
|
|
129
141
|
}
|
|
130
142
|
}
|
|
131
|
-
|
|
132
|
-
if (client.id === -1) {
|
|
133
|
-
client.transport.emit(`${UPDATE_RESPONSE}-${this.id}-${remoteId}`, reqId, filteredUpdates, context);
|
|
134
|
-
}
|
|
135
143
|
} else {
|
|
136
144
|
// propagate back to the requester that the update has been aborted
|
|
137
145
|
// ignore all other attached clients.
|
|
@@ -152,14 +160,15 @@ class SharedStatePrivate {
|
|
|
152
160
|
if (isOwner) {
|
|
153
161
|
// delete only if creator
|
|
154
162
|
client.transport.addListener(`${DELETE_REQUEST}-${this.id}-${remoteId}`, (reqId) => {
|
|
155
|
-
this._manager.
|
|
163
|
+
this._manager._sharedStatePrivateById.delete(this.id);
|
|
156
164
|
// --------------------------------------------------------------------
|
|
157
165
|
// WARNING - MAKE SURE WE DON'T HAVE PROBLEM W/ THAT
|
|
158
166
|
// --------------------------------------------------------------------
|
|
159
167
|
// @todo - propagate server-side last, because if a subscription function sends a
|
|
160
168
|
// message to a client, network messages order are kept coherent
|
|
161
169
|
// this._subscriptions.forEach(func => func(updated));
|
|
162
|
-
for (let [remoteId,
|
|
170
|
+
for (let [remoteId, clientInfos] of this._attachedClients) {
|
|
171
|
+
const attached = clientInfos.client;
|
|
163
172
|
this._detachClient(remoteId, attached);
|
|
164
173
|
|
|
165
174
|
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);
|