@soundworks/core 4.0.0-alpha.14 → 4.0.0-alpha.16
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/.jsdoc +57 -0
- package/CHANGELOG.md +8 -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 +43 -31
- package/src/common/BaseSharedStateCollection.js +75 -16
- package/src/common/BaseStateManager.js +197 -54
- package/src/common/ParameterBag.js +11 -7
- package/src/common/PromiseStore.js +41 -0
- package/src/common/SharedStatePrivate.js +100 -42
- 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 +130 -88
- 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,14 @@ 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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
rejectRequest,
|
|
21
|
-
} from './promise-store.js';
|
|
19
|
+
|
|
20
|
+
export const kCreateCollectionController = Symbol('BaseStateManager::createCollectionController');
|
|
21
|
+
export const kAttachInCollection = Symbol('BaseStateManager::attachInCollection');
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* @private
|
|
@@ -32,10 +32,13 @@ class BaseStateManager {
|
|
|
32
32
|
constructor(id, transport) {
|
|
33
33
|
this.client = { id, transport };
|
|
34
34
|
|
|
35
|
-
this._statesById = new Map();
|
|
36
|
-
this.
|
|
37
|
-
|
|
38
|
-
this.
|
|
35
|
+
this._statesById = new Map(); // <id, state>
|
|
36
|
+
this._cachedSchemas = new Map(); // <shemaName, definition>
|
|
37
|
+
|
|
38
|
+
this._observeListeners = new Set(); // Set <[observedSchemaName, callback, options]>
|
|
39
|
+
this._observeRequestCallbacks = new Map(); // Map <reqId, [observedSchemaName, callback, options]>
|
|
40
|
+
|
|
41
|
+
this._promiseStore = new PromiseStore();
|
|
39
42
|
|
|
40
43
|
// ---------------------------------------------
|
|
41
44
|
// CREATE
|
|
@@ -53,11 +56,11 @@ class BaseStateManager {
|
|
|
53
56
|
const state = new SharedState(stateId, remoteId, schemaName, schema, this.client, true, this, initValues);
|
|
54
57
|
this._statesById.set(state.id, state);
|
|
55
58
|
|
|
56
|
-
|
|
59
|
+
this._promiseStore.resolve(reqId, state);
|
|
57
60
|
});
|
|
58
61
|
|
|
59
62
|
this.client.transport.addListener(CREATE_ERROR, (reqId, msg) => {
|
|
60
|
-
|
|
63
|
+
this._promiseStore.reject(reqId, msg);
|
|
61
64
|
});
|
|
62
65
|
|
|
63
66
|
// ---------------------------------------------
|
|
@@ -77,11 +80,11 @@ class BaseStateManager {
|
|
|
77
80
|
const state = new SharedState(stateId, remoteId, schemaName, schema, this.client, false, this, currentValues);
|
|
78
81
|
this._statesById.set(state.id, state);
|
|
79
82
|
|
|
80
|
-
|
|
83
|
+
this._promiseStore.resolve(reqId, state);
|
|
81
84
|
});
|
|
82
85
|
|
|
83
86
|
this.client.transport.addListener(ATTACH_ERROR, (reqId, msg) => {
|
|
84
|
-
|
|
87
|
+
this._promiseStore.reject(reqId, msg);
|
|
85
88
|
});
|
|
86
89
|
|
|
87
90
|
// ---------------------------------------------
|
|
@@ -90,11 +93,18 @@ class BaseStateManager {
|
|
|
90
93
|
this.client.transport.addListener(OBSERVE_RESPONSE, async (reqId, ...list) => {
|
|
91
94
|
// retrieve the callback that have been stored in observe to make sure
|
|
92
95
|
// we don't call another callback that may have been registered earlier.
|
|
93
|
-
const
|
|
96
|
+
const observeInfos = this._observeRequestCallbacks.get(reqId);
|
|
97
|
+
const [observedSchemaName, callback, options] = observeInfos;
|
|
98
|
+
|
|
99
|
+
// move observeInfos from `_observeRequestCallbacks` to `_observeListeners`
|
|
100
|
+
// to guarantee order of execution, @see not in `.observe`
|
|
94
101
|
this._observeRequestCallbacks.delete(reqId);
|
|
102
|
+
this._observeListeners.add(observeInfos);
|
|
95
103
|
|
|
96
104
|
const promises = list.map(([schemaName, stateId, nodeId]) => {
|
|
97
|
-
|
|
105
|
+
const filter = this._filterObserve(observedSchemaName, schemaName, nodeId, options);
|
|
106
|
+
|
|
107
|
+
if (!filter) {
|
|
98
108
|
return callback(schemaName, stateId, nodeId);
|
|
99
109
|
} else {
|
|
100
110
|
return Promise.resolve();
|
|
@@ -104,19 +114,29 @@ class BaseStateManager {
|
|
|
104
114
|
await Promise.all(promises);
|
|
105
115
|
|
|
106
116
|
const unsubscribe = () => {
|
|
107
|
-
this._observeListeners.delete(
|
|
108
|
-
|
|
117
|
+
this._observeListeners.delete(observeInfos);
|
|
118
|
+
|
|
119
|
+
// no more listeners, we can stop receiving notifications from the server
|
|
109
120
|
if (this._observeListeners.size === 0) {
|
|
110
121
|
this.client.transport.emit(UNOBSERVE_NOTIFICATION);
|
|
111
122
|
}
|
|
112
123
|
};
|
|
113
124
|
|
|
114
|
-
|
|
125
|
+
this._promiseStore.resolve(reqId, unsubscribe);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// Observe error can occur if observed schema name does not exists
|
|
129
|
+
this.client.transport.addListener(OBSERVE_ERROR, (reqId, msg) => {
|
|
130
|
+
this._observeRequestCallbacks.delete(reqId);
|
|
131
|
+
this._promiseStore.reject(reqId, msg);
|
|
115
132
|
});
|
|
116
133
|
|
|
117
134
|
this.client.transport.addListener(OBSERVE_NOTIFICATION, (schemaName, stateId, nodeId) => {
|
|
118
|
-
this._observeListeners.forEach(
|
|
119
|
-
|
|
135
|
+
this._observeListeners.forEach(observeInfos => {
|
|
136
|
+
const [observedSchemaName, callback, options] = observeInfos;
|
|
137
|
+
const filter = this._filterObserve(observedSchemaName, schemaName, nodeId, options);
|
|
138
|
+
|
|
139
|
+
if (!filter) {
|
|
120
140
|
callback(schemaName, stateId, nodeId);
|
|
121
141
|
}
|
|
122
142
|
});
|
|
@@ -130,6 +150,21 @@ class BaseStateManager {
|
|
|
130
150
|
});
|
|
131
151
|
}
|
|
132
152
|
|
|
153
|
+
/** @private */
|
|
154
|
+
_filterObserve(observedSchemaName, schemaName, creatorId, options) {
|
|
155
|
+
let filter = true;
|
|
156
|
+
// schema name filter filer
|
|
157
|
+
if (observedSchemaName === null || observedSchemaName === schemaName) {
|
|
158
|
+
filter = false;
|
|
159
|
+
}
|
|
160
|
+
// filter state created by client if excludeLocal is true
|
|
161
|
+
if (options.excludeLocal === true && creatorId === this.client.id) {
|
|
162
|
+
filter = true;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return filter;
|
|
166
|
+
}
|
|
167
|
+
|
|
133
168
|
/**
|
|
134
169
|
* Create a `SharedState` instance from a registered schema.
|
|
135
170
|
*
|
|
@@ -144,12 +179,24 @@ class BaseStateManager {
|
|
|
144
179
|
*/
|
|
145
180
|
async create(schemaName, initValues = {}) {
|
|
146
181
|
return new Promise((resolve, reject) => {
|
|
147
|
-
const reqId =
|
|
182
|
+
const reqId = this._promiseStore.add(resolve, reject, 'create-request');
|
|
148
183
|
const requireSchema = this._cachedSchemas.has(schemaName) ? false : true;
|
|
149
184
|
this.client.transport.emit(CREATE_REQUEST, reqId, schemaName, requireSchema, initValues);
|
|
150
185
|
});
|
|
151
186
|
}
|
|
152
187
|
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* @private
|
|
191
|
+
*/
|
|
192
|
+
async [kCreateCollectionController](schemaName) {
|
|
193
|
+
return new Promise((resolve, reject) => {
|
|
194
|
+
const reqId = this._promiseStore.add(resolve, reject, 'create-collection-controller-request');
|
|
195
|
+
const requireSchema = this._cachedSchemas.has(schemaName) ? false : true;
|
|
196
|
+
this.client.transport.emit(CREATE_REQUEST, reqId, schemaName, requireSchema, {}, true);
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
153
200
|
/**
|
|
154
201
|
* Attach to an existing `SharedState` instance.
|
|
155
202
|
*
|
|
@@ -166,13 +213,20 @@ class BaseStateManager {
|
|
|
166
213
|
*/
|
|
167
214
|
async attach(schemaName, stateId = null) {
|
|
168
215
|
return new Promise((resolve, reject) => {
|
|
169
|
-
|
|
170
|
-
const reqId = storeRequestPromise(resolve, reject);
|
|
216
|
+
const reqId = this._promiseStore.add(resolve, reject, 'attach-request');
|
|
171
217
|
const requireSchema = this._cachedSchemas.has(schemaName) ? false : true;
|
|
172
218
|
this.client.transport.emit(ATTACH_REQUEST, reqId, schemaName, stateId, requireSchema);
|
|
173
219
|
});
|
|
174
220
|
}
|
|
175
221
|
|
|
222
|
+
async [kAttachInCollection](schemaName, stateId) {
|
|
223
|
+
return new Promise((resolve, reject) => {
|
|
224
|
+
const reqId = this._promiseStore.add(resolve, reject, 'attach-in-collection-request');
|
|
225
|
+
const requireSchema = this._cachedSchemas.has(schemaName) ? false : true;
|
|
226
|
+
this.client.transport.emit(ATTACH_REQUEST, reqId, schemaName, stateId, requireSchema, true);
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
176
230
|
/**
|
|
177
231
|
* Observe all the `SharedState` instances that are created on the network.
|
|
178
232
|
* This can be usefull for clients with some controller role that might want to track
|
|
@@ -180,20 +234,31 @@ class BaseStateManager {
|
|
|
180
234
|
* control over them from a single point.
|
|
181
235
|
*
|
|
182
236
|
* Notes:
|
|
183
|
-
* - The
|
|
184
|
-
* the observe callback.
|
|
185
|
-
* -
|
|
186
|
-
*
|
|
237
|
+
* - The order of execution is not guaranted between nodes, i.e. an state attached
|
|
238
|
+
* in the `observe` callback could be created before the `async create` method resolves.
|
|
239
|
+
* - Filtering, i.e. `observedSchemaName` and `options.excludeLocal` are handled
|
|
240
|
+
* on the node side, the server just notify all state creation activity and
|
|
241
|
+
* the node executes the given callbacks according to the different filter rules.
|
|
242
|
+
* Such strategy allows to share the observe notifications between all observers.
|
|
243
|
+
*
|
|
244
|
+
* Alternative signatures:
|
|
245
|
+
* - .observe(callback)
|
|
246
|
+
* - .observe(schemaName, callback)
|
|
247
|
+
* - .observe(callback, options)
|
|
248
|
+
* - .observe(schemaName, callback, options)
|
|
187
249
|
*
|
|
188
|
-
* @param {
|
|
250
|
+
* @param {string} [schemaName] - optionnal schema name to filter the observed
|
|
189
251
|
* states.
|
|
190
252
|
* @param {server.StateManager~ObserveCallback|client.StateManager~ObserveCallback}
|
|
191
253
|
* callback - Function to be called when a new state is created on the network.
|
|
254
|
+
* @param {object} options - Options.
|
|
255
|
+
* @param {boolean} [options.excludeLocal = false] - If set to true, exclude states
|
|
256
|
+
* created locallly, i.e. by the same node, from the collection.
|
|
192
257
|
* @returns {Promise<Function>} - Returns a Promise that resolves when the given
|
|
193
258
|
* callback as been executed on each existing states. The promise value is a
|
|
194
259
|
* function which allows to stop observing the states on the network.
|
|
195
260
|
* @example
|
|
196
|
-
* client.stateManager.observe(async (schemaName, stateId
|
|
261
|
+
* client.stateManager.observe(async (schemaName, stateId) => {
|
|
197
262
|
* if (schemaName === 'something') {
|
|
198
263
|
* const state = await this.client.stateManager.attach(schemaName, stateId);
|
|
199
264
|
* console.log(state.getValues());
|
|
@@ -204,50 +269,128 @@ class BaseStateManager {
|
|
|
204
269
|
// handle this way and the network overhead is very low for observe notifications:
|
|
205
270
|
// i.e. schemaName, stateId, nodeId
|
|
206
271
|
async observe(...args) {
|
|
207
|
-
|
|
272
|
+
|
|
273
|
+
const defaultOptions = {
|
|
274
|
+
excludeLocal: false,
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
let observedSchemaName;
|
|
208
278
|
let callback;
|
|
279
|
+
let options;
|
|
209
280
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
281
|
+
switch (args.length) {
|
|
282
|
+
case 1: {
|
|
283
|
+
// variation: .observe(callback)
|
|
284
|
+
if (!isFunction(args[0])) {
|
|
285
|
+
throw new TypeError(`[stateManager] Invalid arguments, argument 1 should be a function"`);
|
|
286
|
+
}
|
|
213
287
|
|
|
214
|
-
|
|
215
|
-
|
|
288
|
+
observedSchemaName = null;
|
|
289
|
+
callback = args[0];
|
|
290
|
+
options = defaultOptions;
|
|
291
|
+
break;
|
|
216
292
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
293
|
+
case 2: {
|
|
294
|
+
// variation: .observe(schemaName, callback)
|
|
295
|
+
if (isString(args[0])) {
|
|
296
|
+
if (!isFunction(args[1])) {
|
|
297
|
+
throw new TypeError(`[stateManager] Invalid arguments, argument 2 should be a function"`);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
observedSchemaName = args[0];
|
|
301
|
+
callback = args[1];
|
|
302
|
+
options = defaultOptions;
|
|
303
|
+
|
|
304
|
+
// variation: .observe(callback, options) API
|
|
305
|
+
} else if (isFunction(args[0])) {
|
|
306
|
+
if (!isPlainObject(args[1])) {
|
|
307
|
+
throw new TypeError(`[stateManager] Invalid arguments, argument 2 should be an object"`);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
observedSchemaName = null;
|
|
311
|
+
callback = args[0];
|
|
312
|
+
options = Object.assign(defaultOptions, args[1]);
|
|
313
|
+
|
|
314
|
+
} else {
|
|
315
|
+
throw new Error(`[stateManager] Invalid signature, refer to the StateManager.observe documentation"`);
|
|
316
|
+
}
|
|
220
317
|
|
|
221
|
-
|
|
222
|
-
|
|
318
|
+
break;
|
|
319
|
+
}
|
|
320
|
+
case 3: {
|
|
321
|
+
// variation: .observe(schemaName, callback, options)
|
|
322
|
+
if (!isString(args[0])) {
|
|
323
|
+
throw new TypeError(`[stateManager] Invalid arguments, argument 1 should be a string"`);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (!isFunction(args[1])) {
|
|
327
|
+
throw new TypeError(`[stateManager] Invalid arguments, argument 2 should be a function"`);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (!isPlainObject(args[2])) {
|
|
331
|
+
throw new TypeError(`[stateManager] Invalid arguments, argument 2 should be an object"`);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
observedSchemaName = args[0];
|
|
335
|
+
callback = args[1];
|
|
336
|
+
options = Object.assign(defaultOptions, args[2]);
|
|
337
|
+
|
|
338
|
+
break;
|
|
339
|
+
}
|
|
340
|
+
// throw in all other cases
|
|
341
|
+
default: {
|
|
342
|
+
throw new Error(`[stateManager] Invalid signature, refer to the StateManager.observe documentation"`);
|
|
223
343
|
}
|
|
224
|
-
} else {
|
|
225
|
-
throw new Error(`[stateManager] Invalid arguments, valid signatures are "stateManager.observe(callback)" or "stateManager.observe(schemaName, callback)"`);
|
|
226
344
|
}
|
|
227
345
|
|
|
228
346
|
// resend request to get updated list of states
|
|
229
347
|
return new Promise((resolve, reject) => {
|
|
230
|
-
const reqId =
|
|
348
|
+
const reqId = this._promiseStore.add(resolve, reject, 'observe-request');
|
|
231
349
|
// store the callback for execution on the response. the returned Promise
|
|
232
350
|
// is fullfiled once callback has been executed with each existing states
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
351
|
+
const observeInfos = [observedSchemaName, callback, options];
|
|
352
|
+
this._observeRequestCallbacks.set(reqId, observeInfos);
|
|
353
|
+
|
|
354
|
+
// NOTE: do not store in `_observeListeners` yet as it can produce race
|
|
355
|
+
// conditions, e.g.:
|
|
356
|
+
// ```
|
|
357
|
+
// await client.stateManager.observe(async (schemaName, stateId, nodeId) => {});
|
|
358
|
+
// // client now receives OBSERVE_NOTIFICATIONS
|
|
359
|
+
// await otherClient.stateManager.create('a');
|
|
360
|
+
// // second observer added in between
|
|
361
|
+
// client.stateManager.observe(async (schemaName, stateId, nodeId) => {});
|
|
362
|
+
// ````
|
|
363
|
+
// OBSERVE_NOTIFICATION is received before the OBSERVE_RESPONSE, then the
|
|
364
|
+
// second observer is called twice:
|
|
365
|
+
// - OBSERVE_RESPONSE 1 []
|
|
366
|
+
// - OBSERVE_NOTIFICATION [ 'a', 1, 0 ]
|
|
367
|
+
// - OBSERVE_NOTIFICATION [ 'a', 1, 0 ] // this should not happen
|
|
368
|
+
// - OBSERVE_RESPONSE 1 [ [ 'a', 1, 0 ] ]
|
|
369
|
+
//
|
|
370
|
+
// cf. unit test `observe should properly behave in race condition`
|
|
371
|
+
|
|
372
|
+
this.client.transport.emit(OBSERVE_REQUEST, reqId, observedSchemaName);
|
|
238
373
|
});
|
|
239
374
|
}
|
|
240
375
|
|
|
241
376
|
/**
|
|
242
|
-
* Returns a collection of all the states created from the schema name.
|
|
243
|
-
* the ones created by the current node.
|
|
377
|
+
* Returns a collection of all the states created from the schema name.
|
|
244
378
|
*
|
|
245
379
|
* @param {string} schemaName - Name of the schema.
|
|
380
|
+
* @param {object} options - Options.
|
|
381
|
+
* @param {boolean} [options.excludeLocal = false] - If set to true, exclude states
|
|
382
|
+
* created locallly, i.e. by the same node, from the collection.
|
|
246
383
|
* @returns {server.SharedStateCollection|client.SharedStateCollection}
|
|
247
384
|
*/
|
|
248
|
-
async getCollection(schemaName) {
|
|
249
|
-
const collection = new SharedStateCollection(this, schemaName);
|
|
250
|
-
|
|
385
|
+
async getCollection(schemaName, options) {
|
|
386
|
+
const collection = new SharedStateCollection(this, schemaName, options);
|
|
387
|
+
|
|
388
|
+
try {
|
|
389
|
+
await collection._init();
|
|
390
|
+
} catch (err) {
|
|
391
|
+
console.log(err.message)
|
|
392
|
+
throw new Error(`Cannot create collection, schema "${schemaName}" does not exists`);
|
|
393
|
+
}
|
|
251
394
|
|
|
252
395
|
return collection;
|
|
253
396
|
}
|
|
@@ -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
|
+
}
|
|
@@ -13,13 +13,14 @@ 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
|
* @private
|
|
18
18
|
*/
|
|
19
19
|
class SharedStatePrivate {
|
|
20
20
|
constructor(id, schemaName, schema, manager, initValues = {}) {
|
|
21
21
|
this.id = id;
|
|
22
22
|
this.schemaName = schemaName;
|
|
23
|
+
this.isCollectionController = false;
|
|
23
24
|
|
|
24
25
|
this._manager = manager;
|
|
25
26
|
this._parameters = new ParameterBag(schema, initValues);
|
|
@@ -29,10 +30,16 @@ class SharedStatePrivate {
|
|
|
29
30
|
this._creatorId = null;
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
_attachClient(remoteId, client, isOwner = false) {
|
|
33
|
-
|
|
33
|
+
_attachClient(remoteId, client, isOwner = false, inCollection = false) {
|
|
34
|
+
|
|
35
|
+
// isOwner && inCollection -> collection controller
|
|
36
|
+
// !isOwner && inCollection -> attached in collection
|
|
37
|
+
|
|
38
|
+
const clientInfos = { client, isOwner, inCollection };
|
|
39
|
+
this._attachedClients.set(remoteId, clientInfos);
|
|
34
40
|
|
|
35
41
|
if (isOwner) {
|
|
42
|
+
this.isCollectionController = inCollection;
|
|
36
43
|
this._creatorRemoteId = remoteId;
|
|
37
44
|
this._creatorId = client.id;
|
|
38
45
|
}
|
|
@@ -89,48 +96,98 @@ class SharedStatePrivate {
|
|
|
89
96
|
}
|
|
90
97
|
|
|
91
98
|
if (hasUpdates) {
|
|
92
|
-
//
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
99
|
+
// Collection Controller logic
|
|
100
|
+
if (isOwner && inCollection) {
|
|
101
|
+
// notify the requester back
|
|
102
|
+
// no need to check for attached clients as it is not observable
|
|
103
|
+
client.transport.emit(
|
|
104
|
+
`${UPDATE_RESPONSE}-${this.id}-${remoteId}`,
|
|
105
|
+
reqId, filteredUpdates, context
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
// loop through all private states
|
|
109
|
+
for (let [stateId, state] of this._manager._sharedStatePrivateById) {
|
|
110
|
+
// pick all states with same schema name and not this
|
|
111
|
+
if (state !== this && this.schemaName === state.schemaName) {
|
|
112
|
+
// notify all attached clients except those who belong to a collection
|
|
113
|
+
// i.e. !isOwner && inCollection, they will be notified by their own
|
|
114
|
+
// collection controller
|
|
115
|
+
for (let [remoteId, clientInfos] of state._attachedClients) {
|
|
116
|
+
const { client, isOwner, inCollection } = clientInfos;
|
|
117
|
+
|
|
118
|
+
if (!clientInfos.isOwner && clientInfos.inCollection) {
|
|
119
|
+
continue;
|
|
120
|
+
} else {
|
|
121
|
+
const peer = clientInfos.client;
|
|
122
|
+
|
|
123
|
+
peer.transport.emit(
|
|
124
|
+
`${UPDATE_NOTIFICATION}-${state.id}-${remoteId}`,
|
|
125
|
+
filteredUpdates, context
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
118
130
|
}
|
|
119
|
-
}
|
|
131
|
+
} else {
|
|
120
132
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
133
|
+
// Normal case
|
|
134
|
+
|
|
135
|
+
// We need to handle cases where:
|
|
136
|
+
// - client state (client.id: 2) sends a request
|
|
137
|
+
// - server attached state (client.id: -1) spot a problem and overrides the value
|
|
138
|
+
// We want the remote client (id: 2) to receive in the right order:
|
|
139
|
+
// * 1. the value it requested,
|
|
140
|
+
// * 2. the value overriden by the server-side attached state (id: -1)
|
|
141
|
+
//
|
|
142
|
+
// such problem is now better solved with the the upateHook system, none
|
|
143
|
+
// nonetheway we don't want to introduce inconsistencies here
|
|
144
|
+
//
|
|
145
|
+
// Then we propagate server-side last, because as the server transport
|
|
146
|
+
// is synchronous it can break ordering if a subscription function makes
|
|
147
|
+
// itself an update in reaction to an update. Propagating to server last
|
|
148
|
+
// alllows to maintain network messages order consistent.
|
|
149
|
+
|
|
150
|
+
// @note - remoteId correspond to unique remote state id
|
|
151
|
+
|
|
152
|
+
// propagate RESPONSE to the client that originates the request if not the server
|
|
153
|
+
if (client.id !== -1) {
|
|
154
|
+
client.transport.emit(
|
|
155
|
+
`${UPDATE_RESPONSE}-${this.id}-${remoteId}`,
|
|
156
|
+
reqId, filteredUpdates, context
|
|
157
|
+
);
|
|
158
|
+
}
|
|
124
159
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
160
|
+
// propagate NOTIFICATION to all other attached clients except server
|
|
161
|
+
for (let [peerRemoteId, clientInfos] of this._attachedClients) {
|
|
162
|
+
const peer = clientInfos.client;
|
|
163
|
+
|
|
164
|
+
if (remoteId !== peerRemoteId && peer.id !== -1) {
|
|
165
|
+
peer.transport.emit(
|
|
166
|
+
`${UPDATE_NOTIFICATION}-${this.id}-${peerRemoteId}`,
|
|
167
|
+
filteredUpdates, context
|
|
168
|
+
);
|
|
169
|
+
}
|
|
129
170
|
}
|
|
130
|
-
}
|
|
131
171
|
|
|
132
|
-
|
|
133
|
-
client.
|
|
172
|
+
// propagate RESPONSE to server if it is the requester
|
|
173
|
+
if (client.id === -1) {
|
|
174
|
+
client.transport.emit(
|
|
175
|
+
`${UPDATE_RESPONSE}-${this.id}-${remoteId}`,
|
|
176
|
+
reqId, filteredUpdates, context
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// propagate NOTIFICATION other attached state that belongs to server
|
|
181
|
+
for (let [peerRemoteId, clientInfos] of this._attachedClients) {
|
|
182
|
+
const peer = clientInfos.client;
|
|
183
|
+
|
|
184
|
+
if (remoteId !== peerRemoteId && peer.id === -1) {
|
|
185
|
+
peer.transport.emit(
|
|
186
|
+
`${UPDATE_NOTIFICATION}-${this.id}-${peerRemoteId}`,
|
|
187
|
+
filteredUpdates, context
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
134
191
|
}
|
|
135
192
|
} else {
|
|
136
193
|
// propagate back to the requester that the update has been aborted
|
|
@@ -152,14 +209,15 @@ class SharedStatePrivate {
|
|
|
152
209
|
if (isOwner) {
|
|
153
210
|
// delete only if creator
|
|
154
211
|
client.transport.addListener(`${DELETE_REQUEST}-${this.id}-${remoteId}`, (reqId) => {
|
|
155
|
-
this._manager.
|
|
212
|
+
this._manager._sharedStatePrivateById.delete(this.id);
|
|
156
213
|
// --------------------------------------------------------------------
|
|
157
214
|
// WARNING - MAKE SURE WE DON'T HAVE PROBLEM W/ THAT
|
|
158
215
|
// --------------------------------------------------------------------
|
|
159
216
|
// @todo - propagate server-side last, because if a subscription function sends a
|
|
160
217
|
// message to a client, network messages order are kept coherent
|
|
161
218
|
// this._subscriptions.forEach(func => func(updated));
|
|
162
|
-
for (let [remoteId,
|
|
219
|
+
for (let [remoteId, clientInfos] of this._attachedClients) {
|
|
220
|
+
const attached = clientInfos.client;
|
|
163
221
|
this._detachClient(remoteId, attached);
|
|
164
222
|
|
|
165
223
|
if (remoteId === this._creatorRemoteId) {
|