@soundworks/core 4.0.0-alpha.13 → 4.0.0-alpha.15
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/CHANGELOG.md +4 -0
- package/README.md +13 -28
- package/package.json +17 -23
- package/src/client/Context.js +10 -11
- package/src/client/SharedStateCollection.js +5 -2
- package/src/client/Socket.js +0 -1
- package/src/common/BasePlugin.js +1 -1
- package/src/common/BasePluginManager.js +1 -1
- package/src/common/BaseSharedState.js +31 -27
- package/src/common/BaseSharedStateCollection.js +33 -10
- package/src/common/BaseStateManager.js +173 -53
- package/src/common/ParameterBag.js +11 -7
- package/src/common/PromiseStore.js +41 -0
- package/src/common/SharedStatePrivate.js +24 -27
- package/src/common/constants.js +2 -1
- package/src/common/sockets-utils.js +3 -4
- package/src/server/Server.js +2 -3
- package/src/server/SharedStateCollection.js +5 -2
- package/src/server/Socket.js +1 -1
- package/src/server/StateManager.js +34 -22
- package/src/server/audit-network-latency.worker.js +1 -1
- package/src/server/crypto.js +6 -7
- package/src/common/promise-store.js +0 -25
- package/tsconfig.json +0 -21
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { isString, isFunction } from '@ircam/sc-utils';
|
|
1
|
+
import { isString, isFunction, isPlainObject } from '@ircam/sc-utils';
|
|
2
2
|
import SharedState from './BaseSharedState.js';
|
|
3
3
|
import SharedStateCollection from './BaseSharedStateCollection.js';
|
|
4
|
+
import PromiseStore from './PromiseStore.js';
|
|
4
5
|
import {
|
|
5
6
|
CREATE_REQUEST,
|
|
6
7
|
CREATE_RESPONSE,
|
|
@@ -10,15 +11,11 @@ import {
|
|
|
10
11
|
ATTACH_ERROR,
|
|
11
12
|
OBSERVE_REQUEST,
|
|
12
13
|
OBSERVE_RESPONSE,
|
|
14
|
+
OBSERVE_ERROR,
|
|
13
15
|
OBSERVE_NOTIFICATION,
|
|
14
16
|
UNOBSERVE_NOTIFICATION,
|
|
15
17
|
DELETE_SCHEMA,
|
|
16
18
|
} from './constants.js';
|
|
17
|
-
import {
|
|
18
|
-
storeRequestPromise,
|
|
19
|
-
resolveRequest,
|
|
20
|
-
rejectRequest,
|
|
21
|
-
} from './promise-store.js';
|
|
22
19
|
|
|
23
20
|
/**
|
|
24
21
|
* @private
|
|
@@ -32,10 +29,13 @@ class BaseStateManager {
|
|
|
32
29
|
constructor(id, transport) {
|
|
33
30
|
this.client = { id, transport };
|
|
34
31
|
|
|
35
|
-
this._statesById = new Map();
|
|
36
|
-
this.
|
|
37
|
-
|
|
38
|
-
this.
|
|
32
|
+
this._statesById = new Map(); // <id, state>
|
|
33
|
+
this._cachedSchemas = new Map(); // <shemaName, definition>
|
|
34
|
+
|
|
35
|
+
this._observeListeners = new Set(); // Set <[observedSchemaName, callback, options]>
|
|
36
|
+
this._observeRequestCallbacks = new Map(); // Map <reqId, [observedSchemaName, callback, options]>
|
|
37
|
+
|
|
38
|
+
this._promiseStore = new PromiseStore();
|
|
39
39
|
|
|
40
40
|
// ---------------------------------------------
|
|
41
41
|
// CREATE
|
|
@@ -53,11 +53,11 @@ class BaseStateManager {
|
|
|
53
53
|
const state = new SharedState(stateId, remoteId, schemaName, schema, this.client, true, this, initValues);
|
|
54
54
|
this._statesById.set(state.id, state);
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
this._promiseStore.resolve(reqId, state);
|
|
57
57
|
});
|
|
58
58
|
|
|
59
59
|
this.client.transport.addListener(CREATE_ERROR, (reqId, msg) => {
|
|
60
|
-
|
|
60
|
+
this._promiseStore.reject(reqId, msg);
|
|
61
61
|
});
|
|
62
62
|
|
|
63
63
|
// ---------------------------------------------
|
|
@@ -77,11 +77,11 @@ class BaseStateManager {
|
|
|
77
77
|
const state = new SharedState(stateId, remoteId, schemaName, schema, this.client, false, this, currentValues);
|
|
78
78
|
this._statesById.set(state.id, state);
|
|
79
79
|
|
|
80
|
-
|
|
80
|
+
this._promiseStore.resolve(reqId, state);
|
|
81
81
|
});
|
|
82
82
|
|
|
83
83
|
this.client.transport.addListener(ATTACH_ERROR, (reqId, msg) => {
|
|
84
|
-
|
|
84
|
+
this._promiseStore.reject(reqId, msg);
|
|
85
85
|
});
|
|
86
86
|
|
|
87
87
|
// ---------------------------------------------
|
|
@@ -90,11 +90,18 @@ class BaseStateManager {
|
|
|
90
90
|
this.client.transport.addListener(OBSERVE_RESPONSE, async (reqId, ...list) => {
|
|
91
91
|
// retrieve the callback that have been stored in observe to make sure
|
|
92
92
|
// we don't call another callback that may have been registered earlier.
|
|
93
|
-
const
|
|
93
|
+
const observeInfos = this._observeRequestCallbacks.get(reqId);
|
|
94
|
+
const [observedSchemaName, callback, options] = observeInfos;
|
|
95
|
+
|
|
96
|
+
// move observeInfos from `_observeRequestCallbacks` to `_observeListeners`
|
|
97
|
+
// to guarantee order of execution, @see not in `.observe`
|
|
94
98
|
this._observeRequestCallbacks.delete(reqId);
|
|
99
|
+
this._observeListeners.add(observeInfos);
|
|
95
100
|
|
|
96
101
|
const promises = list.map(([schemaName, stateId, nodeId]) => {
|
|
97
|
-
|
|
102
|
+
const filter = this._filterObserve(observedSchemaName, schemaName, nodeId, options);
|
|
103
|
+
|
|
104
|
+
if (!filter) {
|
|
98
105
|
return callback(schemaName, stateId, nodeId);
|
|
99
106
|
} else {
|
|
100
107
|
return Promise.resolve();
|
|
@@ -104,19 +111,29 @@ class BaseStateManager {
|
|
|
104
111
|
await Promise.all(promises);
|
|
105
112
|
|
|
106
113
|
const unsubscribe = () => {
|
|
107
|
-
this._observeListeners.delete(
|
|
108
|
-
|
|
114
|
+
this._observeListeners.delete(observeInfos);
|
|
115
|
+
|
|
116
|
+
// no more listeners, we can stop receiving notifications from the server
|
|
109
117
|
if (this._observeListeners.size === 0) {
|
|
110
118
|
this.client.transport.emit(UNOBSERVE_NOTIFICATION);
|
|
111
119
|
}
|
|
112
120
|
};
|
|
113
121
|
|
|
114
|
-
|
|
122
|
+
this._promiseStore.resolve(reqId, unsubscribe);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Observe error can occur if observed schema name does not exists
|
|
126
|
+
this.client.transport.addListener(OBSERVE_ERROR, (reqId, msg) => {
|
|
127
|
+
this._observeRequestCallbacks.delete(reqId);
|
|
128
|
+
this._promiseStore.reject(reqId, msg);
|
|
115
129
|
});
|
|
116
130
|
|
|
117
131
|
this.client.transport.addListener(OBSERVE_NOTIFICATION, (schemaName, stateId, nodeId) => {
|
|
118
|
-
this._observeListeners.forEach(
|
|
119
|
-
|
|
132
|
+
this._observeListeners.forEach(observeInfos => {
|
|
133
|
+
const [observedSchemaName, callback, options] = observeInfos;
|
|
134
|
+
const filter = this._filterObserve(observedSchemaName, schemaName, nodeId, options);
|
|
135
|
+
|
|
136
|
+
if (!filter) {
|
|
120
137
|
callback(schemaName, stateId, nodeId);
|
|
121
138
|
}
|
|
122
139
|
});
|
|
@@ -130,6 +147,21 @@ class BaseStateManager {
|
|
|
130
147
|
});
|
|
131
148
|
}
|
|
132
149
|
|
|
150
|
+
/** @private */
|
|
151
|
+
_filterObserve(observedSchemaName, schemaName, creatorId, options) {
|
|
152
|
+
let filter = true;
|
|
153
|
+
// schema name filter filer
|
|
154
|
+
if (observedSchemaName === null || observedSchemaName === schemaName) {
|
|
155
|
+
filter = false;
|
|
156
|
+
}
|
|
157
|
+
// filter state created by client if excludeLocal is true
|
|
158
|
+
if (options.excludeLocal === true && creatorId === this.client.id) {
|
|
159
|
+
filter = true;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return filter;
|
|
163
|
+
}
|
|
164
|
+
|
|
133
165
|
/**
|
|
134
166
|
* Create a `SharedState` instance from a registered schema.
|
|
135
167
|
*
|
|
@@ -144,7 +176,7 @@ class BaseStateManager {
|
|
|
144
176
|
*/
|
|
145
177
|
async create(schemaName, initValues = {}) {
|
|
146
178
|
return new Promise((resolve, reject) => {
|
|
147
|
-
const reqId =
|
|
179
|
+
const reqId = this._promiseStore.add(resolve, reject, 'create-create');
|
|
148
180
|
const requireSchema = this._cachedSchemas.has(schemaName) ? false : true;
|
|
149
181
|
this.client.transport.emit(CREATE_REQUEST, reqId, schemaName, requireSchema, initValues);
|
|
150
182
|
});
|
|
@@ -167,7 +199,7 @@ class BaseStateManager {
|
|
|
167
199
|
async attach(schemaName, stateId = null) {
|
|
168
200
|
return new Promise((resolve, reject) => {
|
|
169
201
|
// @todo - add a timeout
|
|
170
|
-
const reqId =
|
|
202
|
+
const reqId = this._promiseStore.add(resolve, reject, 'attach-request');
|
|
171
203
|
const requireSchema = this._cachedSchemas.has(schemaName) ? false : true;
|
|
172
204
|
this.client.transport.emit(ATTACH_REQUEST, reqId, schemaName, stateId, requireSchema);
|
|
173
205
|
});
|
|
@@ -180,20 +212,31 @@ class BaseStateManager {
|
|
|
180
212
|
* control over them from a single point.
|
|
181
213
|
*
|
|
182
214
|
* Notes:
|
|
183
|
-
* - The
|
|
184
|
-
* the observe callback.
|
|
185
|
-
* -
|
|
186
|
-
*
|
|
215
|
+
* - The order of execution is not guaranted between nodes, i.e. an state attached
|
|
216
|
+
* in the `observe` callback could be created before the `async create` method resolves.
|
|
217
|
+
* - Filtering, i.e. `observedSchemaName` and `options.excludeLocal` are handled
|
|
218
|
+
* on the node side, the server just notify all state creation activity and
|
|
219
|
+
* the node executes the given callbacks according to the different filter rules.
|
|
220
|
+
* Such strategy allows to share the observe notifications between all observers.
|
|
187
221
|
*
|
|
188
|
-
*
|
|
222
|
+
* Alternative signatures:
|
|
223
|
+
* - .observe(callback)
|
|
224
|
+
* - .observe(schemaName, callback)
|
|
225
|
+
* - .observe(callback, options)
|
|
226
|
+
* - .observe(schemaName, callback, options)
|
|
227
|
+
*
|
|
228
|
+
* @param {string} [schemaName] - optionnal schema name to filter the observed
|
|
189
229
|
* states.
|
|
190
230
|
* @param {server.StateManager~ObserveCallback|client.StateManager~ObserveCallback}
|
|
191
231
|
* callback - Function to be called when a new state is created on the network.
|
|
232
|
+
* @param {object} options - Options.
|
|
233
|
+
* @param {boolean} [options.excludeLocal = false] - If set to true, exclude states
|
|
234
|
+
* created locallly, i.e. by the same node, from the collection.
|
|
192
235
|
* @returns {Promise<Function>} - Returns a Promise that resolves when the given
|
|
193
236
|
* callback as been executed on each existing states. The promise value is a
|
|
194
237
|
* function which allows to stop observing the states on the network.
|
|
195
238
|
* @example
|
|
196
|
-
* client.stateManager.observe(async (schemaName, stateId
|
|
239
|
+
* client.stateManager.observe(async (schemaName, stateId) => {
|
|
197
240
|
* if (schemaName === 'something') {
|
|
198
241
|
* const state = await this.client.stateManager.attach(schemaName, stateId);
|
|
199
242
|
* console.log(state.getValues());
|
|
@@ -204,50 +247,127 @@ class BaseStateManager {
|
|
|
204
247
|
// handle this way and the network overhead is very low for observe notifications:
|
|
205
248
|
// i.e. schemaName, stateId, nodeId
|
|
206
249
|
async observe(...args) {
|
|
207
|
-
|
|
250
|
+
|
|
251
|
+
const defaultOptions = {
|
|
252
|
+
excludeLocal: false,
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
let observedSchemaName;
|
|
208
256
|
let callback;
|
|
257
|
+
let options;
|
|
209
258
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
259
|
+
switch (args.length) {
|
|
260
|
+
case 1: {
|
|
261
|
+
// variation: .observe(callback)
|
|
262
|
+
if (!isFunction(args[0])) {
|
|
263
|
+
throw new TypeError(`[stateManager] Invalid arguments, argument 1 should be a function"`);
|
|
264
|
+
}
|
|
213
265
|
|
|
214
|
-
|
|
215
|
-
|
|
266
|
+
observedSchemaName = null;
|
|
267
|
+
callback = args[0];
|
|
268
|
+
options = defaultOptions;
|
|
269
|
+
break;
|
|
216
270
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
271
|
+
case 2: {
|
|
272
|
+
// variation: .observe(schemaName, callback)
|
|
273
|
+
if (isString(args[0])) {
|
|
274
|
+
if (!isFunction(args[1])) {
|
|
275
|
+
throw new TypeError(`[stateManager] Invalid arguments, argument 2 should be a function"`);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
observedSchemaName = args[0];
|
|
279
|
+
callback = args[1];
|
|
280
|
+
options = defaultOptions;
|
|
281
|
+
|
|
282
|
+
// variation: .observe(callback, options) API
|
|
283
|
+
} else if (isFunction(args[0])) {
|
|
284
|
+
if (!isPlainObject(args[1])) {
|
|
285
|
+
throw new TypeError(`[stateManager] Invalid arguments, argument 2 should be an object"`);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
observedSchemaName = null;
|
|
289
|
+
callback = args[0];
|
|
290
|
+
options = Object.assign(defaultOptions, args[1]);
|
|
291
|
+
|
|
292
|
+
} else {
|
|
293
|
+
throw new Error(`[stateManager] Invalid signature, refer to the StateManager.observe documentation"`);
|
|
294
|
+
}
|
|
220
295
|
|
|
221
|
-
|
|
222
|
-
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
case 3: {
|
|
299
|
+
// variation: .observe(schemaName, callback, options)
|
|
300
|
+
if (!isString(args[0])) {
|
|
301
|
+
throw new TypeError(`[stateManager] Invalid arguments, argument 1 should be a string"`);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (!isFunction(args[1])) {
|
|
305
|
+
throw new TypeError(`[stateManager] Invalid arguments, argument 2 should be a function"`);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (!isPlainObject(args[2])) {
|
|
309
|
+
throw new TypeError(`[stateManager] Invalid arguments, argument 2 should be an object"`);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
observedSchemaName = args[0];
|
|
313
|
+
callback = args[1];
|
|
314
|
+
options = Object.assign(defaultOptions, args[2]);
|
|
315
|
+
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
318
|
+
// throw in all other cases
|
|
319
|
+
default: {
|
|
320
|
+
throw new Error(`[stateManager] Invalid signature, refer to the StateManager.observe documentation"`);
|
|
223
321
|
}
|
|
224
|
-
} else {
|
|
225
|
-
throw new Error(`[stateManager] Invalid arguments, valid signatures are "stateManager.observe(callback)" or "stateManager.observe(schemaName, callback)"`);
|
|
226
322
|
}
|
|
227
323
|
|
|
228
324
|
// resend request to get updated list of states
|
|
229
325
|
return new Promise((resolve, reject) => {
|
|
230
|
-
const reqId =
|
|
326
|
+
const reqId = this._promiseStore.add(resolve, reject, 'observe-request');
|
|
231
327
|
// store the callback for execution on the response. the returned Promise
|
|
232
328
|
// is fullfiled once callback has been executed with each existing states
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
329
|
+
const observeInfos = [observedSchemaName, callback, options];
|
|
330
|
+
this._observeRequestCallbacks.set(reqId, observeInfos);
|
|
331
|
+
|
|
332
|
+
// NOTE: do not store in `_observeListeners` yet as it can produce race
|
|
333
|
+
// conditions, e.g.:
|
|
334
|
+
// ```
|
|
335
|
+
// await client.stateManager.observe(async (schemaName, stateId, nodeId) => {});
|
|
336
|
+
// // client now receives OBSERVE_NOTIFICATIONS
|
|
337
|
+
// await otherClient.stateManager.create('a');
|
|
338
|
+
// // second observer added in between
|
|
339
|
+
// client.stateManager.observe(async (schemaName, stateId, nodeId) => {});
|
|
340
|
+
// ````
|
|
341
|
+
// OBSERVE_NOTIFICATION is received before the OBSERVE_RESPONSE, then the
|
|
342
|
+
// second observer is called twice:
|
|
343
|
+
// - OBSERVE_RESPONSE 1 []
|
|
344
|
+
// - OBSERVE_NOTIFICATION [ 'a', 1, 0 ]
|
|
345
|
+
// - OBSERVE_NOTIFICATION [ 'a', 1, 0 ] // this should not happen
|
|
346
|
+
// - OBSERVE_RESPONSE 1 [ [ 'a', 1, 0 ] ]
|
|
347
|
+
//
|
|
348
|
+
// cf. unit test `observe should properly behave in race condition`
|
|
349
|
+
|
|
350
|
+
this.client.transport.emit(OBSERVE_REQUEST, reqId, observedSchemaName);
|
|
238
351
|
});
|
|
239
352
|
}
|
|
240
353
|
|
|
241
354
|
/**
|
|
242
|
-
* Returns a collection of all the states created from the schema name.
|
|
243
|
-
* the ones created by the current node.
|
|
355
|
+
* Returns a collection of all the states created from the schema name.
|
|
244
356
|
*
|
|
245
357
|
* @param {string} schemaName - Name of the schema.
|
|
358
|
+
* @param {object} options - Options.
|
|
359
|
+
* @param {boolean} [options.excludeLocal = false] - If set to true, exclude states
|
|
360
|
+
* created locallly, i.e. by the same node, from the collection.
|
|
246
361
|
* @returns {server.SharedStateCollection|client.SharedStateCollection}
|
|
247
362
|
*/
|
|
248
|
-
async getCollection(schemaName) {
|
|
249
|
-
const collection = new SharedStateCollection(this, schemaName);
|
|
250
|
-
|
|
363
|
+
async getCollection(schemaName, options) {
|
|
364
|
+
const collection = new SharedStateCollection(this, schemaName, options);
|
|
365
|
+
|
|
366
|
+
try {
|
|
367
|
+
await collection._init();
|
|
368
|
+
} catch (err) {
|
|
369
|
+
throw new Error(`Cannot create collection, schema "${schemaName}" does not exists`);
|
|
370
|
+
}
|
|
251
371
|
|
|
252
372
|
return collection;
|
|
253
373
|
}
|
|
@@ -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 = {
|
|
@@ -394,30 +394,34 @@ class ParameterBag {
|
|
|
394
394
|
getSchema(name = null) {
|
|
395
395
|
if (name === null) {
|
|
396
396
|
return this._schema;
|
|
397
|
-
}
|
|
398
|
-
if (!this.has(name)) {
|
|
399
|
-
throw new ReferenceError(`[SharedState] Cannot get schema description of undefined parameter "${name}"`);
|
|
400
|
-
}
|
|
397
|
+
}
|
|
401
398
|
|
|
402
|
-
|
|
399
|
+
if (!this.has(name)) {
|
|
400
|
+
throw new ReferenceError(`[SharedState] Cannot get schema description of undefined parameter "${name}"`);
|
|
403
401
|
}
|
|
402
|
+
|
|
403
|
+
return this._schema[name];
|
|
404
404
|
}
|
|
405
405
|
|
|
406
406
|
// return the default value, if initValue has been given, return init values
|
|
407
407
|
getInitValues() {
|
|
408
408
|
const initValues = {};
|
|
409
|
+
|
|
409
410
|
for (let [name, def] of Object.entries(this._schema)) {
|
|
410
411
|
initValues[name] = def.initValue;
|
|
411
412
|
}
|
|
413
|
+
|
|
412
414
|
return initValues;
|
|
413
415
|
}
|
|
414
416
|
|
|
415
417
|
// return the default value, if initValue has been given, return init values
|
|
416
418
|
getDefaults() {
|
|
417
419
|
const defaults = {};
|
|
420
|
+
|
|
418
421
|
for (let [name, def] of Object.entries(this._schema)) {
|
|
419
|
-
defaults[name] = def.
|
|
422
|
+
defaults[name] = def.default;
|
|
420
423
|
}
|
|
424
|
+
|
|
421
425
|
return defaults;
|
|
422
426
|
}
|
|
423
427
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { idGenerator } from '@ircam/sc-utils';
|
|
2
|
+
|
|
3
|
+
const generateRequestId = idGenerator();
|
|
4
|
+
|
|
5
|
+
export default class PromiseStore {
|
|
6
|
+
constructor(name) {
|
|
7
|
+
this.name = name;
|
|
8
|
+
this.store = new Map();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
add(resolve, reject, type) {
|
|
12
|
+
const reqId = generateRequestId.next().value;
|
|
13
|
+
this.store.set(reqId, { resolve, reject, type });
|
|
14
|
+
|
|
15
|
+
return reqId;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
resolve(reqId, data) {
|
|
19
|
+
const { resolve } = this.store.get(reqId);
|
|
20
|
+
this.store.delete(reqId);
|
|
21
|
+
|
|
22
|
+
resolve(data);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
reject(reqId, msg) {
|
|
26
|
+
const { reject } = this.store.get(reqId);
|
|
27
|
+
this.store.delete(reqId);
|
|
28
|
+
|
|
29
|
+
reject(new Error(msg));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// reject all pendeing request
|
|
33
|
+
flush() {
|
|
34
|
+
for (let [_reqId, entry] of this.store) {
|
|
35
|
+
const { reject, type } = entry;
|
|
36
|
+
reject(new Error(`[${this.name}] Discard promise "${type}", cannot resolve`));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
this.store.clear();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -89,49 +89,46 @@ class SharedStatePrivate {
|
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
if (hasUpdates) {
|
|
92
|
-
//
|
|
93
|
-
// client
|
|
94
|
-
|
|
95
|
-
//
|
|
96
|
-
// is synchronous it can break ordering if a subscription function makes
|
|
97
|
-
// itself an update in reaction to an update, therefore network messages
|
|
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:
|
|
92
|
+
// We need to handle cases where:
|
|
93
|
+
// - client state (client.id: 2) sends a request
|
|
94
|
+
// - server attached state (client.id: -1) spot a problem and overrides the value
|
|
95
|
+
// We want the remote client (id: 2) to receive in the right order:
|
|
104
96
|
// * 1. the value it requested,
|
|
105
97
|
// * 2. the value overriden by the server-side attached state (id: -1)
|
|
98
|
+
//
|
|
99
|
+
// such problem is now better solved with the the upateHook system, none
|
|
100
|
+
// nonetheway we don't want to introduce inconsistencies here
|
|
101
|
+
//
|
|
102
|
+
// Then we propagate server-side last, because as the server transport
|
|
103
|
+
// is synchronous it can break ordering if a subscription function makes
|
|
104
|
+
// itself an update in reaction to an update. Propagating to server last
|
|
105
|
+
// alllows to maintain network messages order consistent.
|
|
106
106
|
|
|
107
|
-
//
|
|
108
|
-
|
|
109
|
-
//
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
107
|
+
// @note - remoteId correspond to unique remote state id
|
|
108
|
+
|
|
109
|
+
// propagate RESPONSE to the client that originates the request if not the server
|
|
110
|
+
if (client.id !== -1) {
|
|
111
|
+
client.transport.emit(`${UPDATE_RESPONSE}-${this.id}-${remoteId}`, reqId, filteredUpdates, context);
|
|
112
|
+
}
|
|
113
113
|
|
|
114
|
-
|
|
115
|
-
|
|
114
|
+
// propagate NOTIFICATION to all other attached clients except server
|
|
115
|
+
for (let [peerRemoteId, peer] of this._attachedClients) {
|
|
116
116
|
if (remoteId !== peerRemoteId && peer.id !== -1) {
|
|
117
117
|
peer.transport.emit(`${UPDATE_NOTIFICATION}-${this.id}-${peerRemoteId}`, filteredUpdates, context);
|
|
118
118
|
}
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
if
|
|
121
|
+
// propagate RESPONSE to server if it is the requester
|
|
122
|
+
if (client.id === -1) {
|
|
122
123
|
client.transport.emit(`${UPDATE_RESPONSE}-${this.id}-${remoteId}`, reqId, filteredUpdates, context);
|
|
123
124
|
}
|
|
124
125
|
|
|
125
|
-
|
|
126
|
-
|
|
126
|
+
// propagate NOTIFICATION other attached state that belongs to server
|
|
127
|
+
for (let [peerRemoteId, peer] of this._attachedClients) {
|
|
127
128
|
if (remoteId !== peerRemoteId && peer.id === -1) {
|
|
128
129
|
peer.transport.emit(`${UPDATE_NOTIFICATION}-${this.id}-${peerRemoteId}`, filteredUpdates, context);
|
|
129
130
|
}
|
|
130
131
|
}
|
|
131
|
-
|
|
132
|
-
if (client.id === -1) {
|
|
133
|
-
client.transport.emit(`${UPDATE_RESPONSE}-${this.id}-${remoteId}`, reqId, filteredUpdates, context);
|
|
134
|
-
}
|
|
135
132
|
} else {
|
|
136
133
|
// propagate back to the requester that the update has been aborted
|
|
137
134
|
// ignore all other attached clients.
|
package/src/common/constants.js
CHANGED
|
@@ -21,6 +21,7 @@ export const DETACH_ERROR = 's:dt:err';
|
|
|
21
21
|
|
|
22
22
|
export const OBSERVE_REQUEST = 's:o:req';
|
|
23
23
|
export const OBSERVE_RESPONSE = 's:o:res';
|
|
24
|
+
export const OBSERVE_ERROR = 's:o:err';
|
|
24
25
|
export const OBSERVE_NOTIFICATION = 's:o:not';
|
|
25
26
|
|
|
26
27
|
export const UNOBSERVE_NOTIFICATION = 's:uo:not';
|
|
@@ -47,5 +48,5 @@ export const CLIENT_HANDSHAKE_RESPONSE = 'cl:h:res';
|
|
|
47
48
|
export const CLIENT_HANDSHAKE_ERROR = 'cl:h:err';
|
|
48
49
|
|
|
49
50
|
// audit state schema name
|
|
50
|
-
export const AUDIT_STATE_NAME = 's:
|
|
51
|
+
export const AUDIT_STATE_NAME = 'p:s:audit';
|
|
51
52
|
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/Server.js
CHANGED
|
@@ -5,14 +5,13 @@ import path from 'node:path';
|
|
|
5
5
|
import os from 'node:os';
|
|
6
6
|
import { X509Certificate, createPrivateKey } from 'node:crypto';
|
|
7
7
|
|
|
8
|
-
import { getTime } from '@ircam/sc-
|
|
9
|
-
import { isPlainObject, idGenerator } from '@ircam/sc-utils';
|
|
8
|
+
import { isPlainObject, idGenerator, getTime } from '@ircam/sc-utils';
|
|
10
9
|
import chalk from 'chalk';
|
|
11
10
|
import compression from 'compression';
|
|
12
11
|
import express from 'express';
|
|
13
12
|
import Keyv from 'keyv';
|
|
14
13
|
import { KeyvFile } from 'keyv-file';
|
|
15
|
-
import merge from 'lodash.
|
|
14
|
+
import merge from 'lodash/merge.js';
|
|
16
15
|
import pem from 'pem';
|
|
17
16
|
import compile from 'template-literal';
|
|
18
17
|
|
|
@@ -15,8 +15,11 @@ import BaseSharedStateCollection from '../common/BaseSharedStateCollection.js';
|
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* The `SharedStateCollection` interface represent a collection of all states
|
|
18
|
-
* created from a given schema name on the network
|
|
19
|
-
*
|
|
18
|
+
* created from a given schema name on the network.
|
|
19
|
+
*
|
|
20
|
+
* It can optionnaly exclude the states created by the current node.
|
|
21
|
+
*
|
|
22
|
+
* See {@link server.StateManager#getCollection} for factory method API
|
|
20
23
|
*
|
|
21
24
|
* ```
|
|
22
25
|
* const collection = await server.stateManager.getCollection('my-schema');
|
package/src/server/Socket.js
CHANGED