@soundworks/core 4.0.0-alpha.1 → 4.0.0-alpha.11
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/README.md +17 -26
- package/package.json +20 -21
- package/src/client/Client.js +48 -8
- package/src/client/Context.js +39 -12
- package/src/client/ContextManager.js +1 -1
- package/src/client/Plugin.js +2 -1
- package/src/client/PluginManager.js +27 -18
- package/src/client/SharedStateCollection.js +35 -0
- package/src/client/Socket.js +61 -48
- package/src/client/index.js +0 -33
- package/src/common/BasePlugin.js +30 -20
- package/src/common/BasePluginManager.js +44 -20
- package/src/common/BaseSharedState.js +151 -41
- package/src/common/BaseSharedStateCollection.js +191 -0
- package/src/common/BaseStateManager.js +73 -16
- package/src/common/ParameterBag.js +87 -217
- package/src/common/promise-store.js +1 -1
- package/src/server/Client.js +7 -2
- package/src/server/Context.js +162 -18
- package/src/server/ContextManager.js +8 -9
- package/src/server/Plugin.js +2 -1
- package/src/server/PluginManager.js +27 -7
- package/src/server/Server.js +428 -101
- package/src/server/SharedState.js +0 -1
- package/src/server/SharedStateCollection.js +36 -0
- package/src/server/Socket.js +105 -62
- package/src/server/Sockets.js +52 -26
- package/src/server/StateManager.js +238 -28
- package/src/server/crypto.js +38 -0
- package/src/server/index.js +0 -20
- package/src/common/utils.js +0 -22
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isPlainObject } from '@ircam/sc-utils';
|
|
1
2
|
import ParameterBag from './ParameterBag.js';
|
|
2
3
|
import {
|
|
3
4
|
DELETE_REQUEST,
|
|
@@ -20,10 +21,12 @@ import {
|
|
|
20
21
|
|
|
21
22
|
class BaseSharedState {
|
|
22
23
|
constructor(id, remoteId, schemaName, schema, client, isOwner, manager, initValues = {}) {
|
|
23
|
-
|
|
24
|
-
this.
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
/** @private */
|
|
25
|
+
this._id = id;
|
|
26
|
+
/** @private */
|
|
27
|
+
this._remoteId = remoteId;
|
|
28
|
+
/** @private */
|
|
29
|
+
this._schemaName = schemaName;
|
|
27
30
|
/** @private */
|
|
28
31
|
this._isOwner = isOwner; // may be the server or any client
|
|
29
32
|
/** @private */
|
|
@@ -31,10 +34,12 @@ class BaseSharedState {
|
|
|
31
34
|
/** @private */
|
|
32
35
|
this._manager = manager;
|
|
33
36
|
|
|
37
|
+
this._detached = false;
|
|
38
|
+
|
|
34
39
|
try {
|
|
35
40
|
/** @private */
|
|
36
41
|
this._parameters = new ParameterBag(schema, initValues);
|
|
37
|
-
} catch(err) {
|
|
42
|
+
} catch (err) {
|
|
38
43
|
console.error(err.stack);
|
|
39
44
|
|
|
40
45
|
throw new Error(`Error creating or attaching state "${schemaName}" w/ values:\n
|
|
@@ -87,14 +92,25 @@ ${JSON.stringify(initValues, null, 2)}`);
|
|
|
87
92
|
});
|
|
88
93
|
|
|
89
94
|
// ---------------------------------------------
|
|
90
|
-
//
|
|
95
|
+
// state has been deleted by its creator or the schema has been deleted
|
|
91
96
|
// ---------------------------------------------
|
|
92
|
-
this._client.transport.addListener(`${DELETE_NOTIFICATION}-${this.id}-${this.remoteId}`, () => {
|
|
97
|
+
this._client.transport.addListener(`${DELETE_NOTIFICATION}-${this.id}-${this.remoteId}`, async () => {
|
|
93
98
|
this._manager._statesById.delete(this.id);
|
|
94
99
|
this._clearTransport();
|
|
95
100
|
|
|
96
|
-
|
|
97
|
-
|
|
101
|
+
for (let callback of this._onDetachCallbacks) {
|
|
102
|
+
try {
|
|
103
|
+
await callback();
|
|
104
|
+
} catch(err) {
|
|
105
|
+
console.error(err.message);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (this._isOwner) {
|
|
110
|
+
for (let callback of this._onDeleteCallbacks) {
|
|
111
|
+
await callback();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
98
114
|
|
|
99
115
|
this._clearDetach();
|
|
100
116
|
});
|
|
@@ -102,14 +118,19 @@ ${JSON.stringify(initValues, null, 2)}`);
|
|
|
102
118
|
|
|
103
119
|
if (this._isOwner) {
|
|
104
120
|
// ---------------------------------------------
|
|
105
|
-
//
|
|
121
|
+
// the creator has called `.delete()`
|
|
106
122
|
// ---------------------------------------------
|
|
107
|
-
this._client.transport.addListener(`${DELETE_RESPONSE}-${this.id}-${this.remoteId}`, (reqId) => {
|
|
123
|
+
this._client.transport.addListener(`${DELETE_RESPONSE}-${this.id}-${this.remoteId}`, async (reqId) => {
|
|
108
124
|
this._manager._statesById.delete(this.id);
|
|
109
125
|
this._clearTransport();
|
|
110
126
|
|
|
111
|
-
|
|
112
|
-
|
|
127
|
+
for (let callback of this._onDetachCallbacks) {
|
|
128
|
+
await callback();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
for (let callback of this._onDeleteCallbacks) {
|
|
132
|
+
await callback();
|
|
133
|
+
}
|
|
113
134
|
|
|
114
135
|
this._clearDetach();
|
|
115
136
|
resolveRequest(reqId, this);
|
|
@@ -121,13 +142,15 @@ ${JSON.stringify(initValues, null, 2)}`);
|
|
|
121
142
|
|
|
122
143
|
} else {
|
|
123
144
|
// ---------------------------------------------
|
|
124
|
-
//
|
|
145
|
+
// the attached node has called `.detach()`
|
|
125
146
|
// ---------------------------------------------
|
|
126
|
-
this._client.transport.addListener(`${DETACH_RESPONSE}-${this.id}-${this.remoteId}`, (reqId) => {
|
|
147
|
+
this._client.transport.addListener(`${DETACH_RESPONSE}-${this.id}-${this.remoteId}`, async (reqId) => {
|
|
127
148
|
this._manager._statesById.delete(this.id);
|
|
128
149
|
this._clearTransport();
|
|
129
150
|
|
|
130
|
-
this._onDetachCallbacks
|
|
151
|
+
for (let callback of this._onDetachCallbacks) {
|
|
152
|
+
await callback();
|
|
153
|
+
}
|
|
131
154
|
|
|
132
155
|
this._clearDetach();
|
|
133
156
|
resolveRequest(reqId, this);
|
|
@@ -137,20 +160,51 @@ ${JSON.stringify(initValues, null, 2)}`);
|
|
|
137
160
|
this._client.transport.addListener(`${DETACH_ERROR}-${this.id}`, (reqId, msg) => {
|
|
138
161
|
rejectRequest(reqId, msg);
|
|
139
162
|
});
|
|
140
|
-
|
|
141
163
|
}
|
|
142
164
|
}
|
|
143
165
|
|
|
166
|
+
/**
|
|
167
|
+
* Id of the state
|
|
168
|
+
* @type {Number}
|
|
169
|
+
* @readonly
|
|
170
|
+
*/
|
|
171
|
+
get id() {
|
|
172
|
+
return this._id;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Unique id of the state for the current node
|
|
177
|
+
* @readonly
|
|
178
|
+
* @type {Number}
|
|
179
|
+
* @private
|
|
180
|
+
*/
|
|
181
|
+
get remoteId() {
|
|
182
|
+
return this._remoteId;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Name of the schema
|
|
187
|
+
* @type {String}
|
|
188
|
+
* @readonly
|
|
189
|
+
*/
|
|
190
|
+
get schemaName() {
|
|
191
|
+
return this._schemaName;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Indicates if the node is the owner of the state
|
|
196
|
+
* @type {Boolean}
|
|
197
|
+
* @readonly
|
|
198
|
+
*/
|
|
199
|
+
get isOwner() {
|
|
200
|
+
return this._isOwner;
|
|
201
|
+
}
|
|
202
|
+
|
|
144
203
|
/** @private */
|
|
145
204
|
_clearDetach() {
|
|
146
205
|
this._onDetachCallbacks.clear();
|
|
147
206
|
this._onDeleteCallbacks.clear();
|
|
148
|
-
|
|
149
|
-
// Monkey patch detach so it throws if called twice. Doing nothing blocks
|
|
150
|
-
// the process on a second `detach` call as the Promise never resolves
|
|
151
|
-
this.detach = () => {
|
|
152
|
-
throw new Error(`[stateManager] State "${this.schemaName} (${this.id})" already detached, cannot detach twice`);
|
|
153
|
-
};
|
|
207
|
+
this._detached = true;
|
|
154
208
|
}
|
|
155
209
|
|
|
156
210
|
/** @private */
|
|
@@ -255,16 +309,23 @@ ${JSON.stringify(initValues, null, 2)}`);
|
|
|
255
309
|
* const updates = await state.set({ myParam: Math.random() });
|
|
256
310
|
*/
|
|
257
311
|
async set(updates, context = null) {
|
|
312
|
+
if (!isPlainObject(updates)) {
|
|
313
|
+
throw new ReferenceError(`[SharedState] State "${this.schemaName}": state.set(updates[, context]) should receive an object as first parameter`);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (context !== null && !isPlainObject(context)) {
|
|
317
|
+
throw new ReferenceError(`[SharedState] State "${this.schemaName}": state.set(updates[, context]) should receive an object as second parameter`);
|
|
318
|
+
}
|
|
319
|
+
|
|
258
320
|
// handle immediate option
|
|
259
321
|
const immediateNewValues = {};
|
|
260
322
|
const immediateOldValues = {};
|
|
261
323
|
let propagateNow = false;
|
|
262
324
|
|
|
263
325
|
for (let name in updates) {
|
|
264
|
-
//
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
}
|
|
326
|
+
// try to coerce value early, so that eventual errors are triggered early
|
|
327
|
+
// on the node requesting the update
|
|
328
|
+
const _ = this._parameters.coerceValue(name, updates[name]);
|
|
268
329
|
|
|
269
330
|
// @note: general idea...
|
|
270
331
|
// if immediate=true
|
|
@@ -303,21 +364,41 @@ ${JSON.stringify(initValues, null, 2)}`);
|
|
|
303
364
|
}
|
|
304
365
|
|
|
305
366
|
/**
|
|
306
|
-
* Get the value of a
|
|
367
|
+
* Get the value of a parameter of the state. If the parameter is of `any` type,
|
|
368
|
+
* a deep copy is returned.
|
|
307
369
|
*
|
|
308
370
|
* @param {string} name - Name of the param.
|
|
309
371
|
* @throws Throws if `name` does not correspond to an existing field
|
|
310
372
|
* of the state.
|
|
311
373
|
* @return {mixed}
|
|
312
374
|
* @example
|
|
313
|
-
* const value = state.get('
|
|
375
|
+
* const value = state.get('paramName');
|
|
314
376
|
*/
|
|
315
377
|
get(name) {
|
|
316
378
|
return this._parameters.get(name);
|
|
317
379
|
}
|
|
318
380
|
|
|
319
381
|
/**
|
|
320
|
-
*
|
|
382
|
+
* Similar to `get` but returns a reference to the underlying value in case of
|
|
383
|
+
* `any` type. May be usefull if the underlying value is big (e.g. sensors
|
|
384
|
+
* recordings, etc.) and deep cloning expensive. Be aware that if changes are
|
|
385
|
+
* made on the returned object, the state of your application will become
|
|
386
|
+
* inconsistent.
|
|
387
|
+
*
|
|
388
|
+
* @param {string} name - Name of the param.
|
|
389
|
+
* @throws Throws if `name` does not correspond to an existing field
|
|
390
|
+
* of the state.
|
|
391
|
+
* @return {mixed}
|
|
392
|
+
* @example
|
|
393
|
+
* const value = state.getUnsafe('paramName');
|
|
394
|
+
*/
|
|
395
|
+
getUnsafe(name) {
|
|
396
|
+
return this._parameters.getUnsafe(name);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Get all the key / value pairs of the state. If a parameter is of `any`
|
|
401
|
+
* type, a deep copy is made.
|
|
321
402
|
*
|
|
322
403
|
* @return {object}
|
|
323
404
|
* @example
|
|
@@ -327,6 +408,23 @@ ${JSON.stringify(initValues, null, 2)}`);
|
|
|
327
408
|
return this._parameters.getValues();
|
|
328
409
|
}
|
|
329
410
|
|
|
411
|
+
/**
|
|
412
|
+
* Get all the key / value pairs of the state. If a parameter is of `any`
|
|
413
|
+
* type, a deep copy is made.
|
|
414
|
+
* Similar to `getValues` but returns a reference to the underlying value in
|
|
415
|
+
* case of `any` type. May be usefull if the underlying value is big (e.g.
|
|
416
|
+
* sensors recordings, etc.) and deep cloning expensive. Be aware that if
|
|
417
|
+
* changes are made on the returned object, the state of your application will
|
|
418
|
+
* become inconsistent.
|
|
419
|
+
*
|
|
420
|
+
* @return {object}
|
|
421
|
+
* @example
|
|
422
|
+
* const values = state.getValues();
|
|
423
|
+
*/
|
|
424
|
+
getValuesUnsafe() {
|
|
425
|
+
return this._parameters.getValuesUnsafe();
|
|
426
|
+
}
|
|
427
|
+
|
|
330
428
|
/**
|
|
331
429
|
* Get the schema from which the state has been created.
|
|
332
430
|
*
|
|
@@ -367,7 +465,7 @@ ${JSON.stringify(initValues, null, 2)}`);
|
|
|
367
465
|
|
|
368
466
|
/**
|
|
369
467
|
* Detach from the state. If the client is the creator of the state, the state
|
|
370
|
-
* is deleted and all attached nodes get notified
|
|
468
|
+
* is deleted and all attached nodes get notified.
|
|
371
469
|
*
|
|
372
470
|
* @example
|
|
373
471
|
* const state = await client.state.attach('globals');
|
|
@@ -375,6 +473,10 @@ ${JSON.stringify(initValues, null, 2)}`);
|
|
|
375
473
|
* await state.detach();
|
|
376
474
|
*/
|
|
377
475
|
async detach() {
|
|
476
|
+
if (this._detached) {
|
|
477
|
+
throw new Error(`[SharedState] State "${this.schemaName} (${this.id})" already detached, cannot detach twice`);
|
|
478
|
+
}
|
|
479
|
+
|
|
378
480
|
this._onUpdateCallbacks.clear();
|
|
379
481
|
|
|
380
482
|
if (this._isOwner) {
|
|
@@ -392,9 +494,11 @@ ${JSON.stringify(initValues, null, 2)}`);
|
|
|
392
494
|
|
|
393
495
|
/**
|
|
394
496
|
* Delete the state. Only the creator/owner of the state can use this method.
|
|
395
|
-
*
|
|
396
|
-
*
|
|
397
|
-
*
|
|
497
|
+
*
|
|
498
|
+
* All nodes attached to the state will be detached, triggering any registered
|
|
499
|
+
* `onDetach` callbacks. The creator of the state will also have its own `onDelete`
|
|
500
|
+
* callback triggered. The local `onDeatch` and `onDelete` callbacks will be
|
|
501
|
+
* executed *before* the returned Promise resolves
|
|
398
502
|
*
|
|
399
503
|
* @throws Throws if the method is called by a node which is not the owner of
|
|
400
504
|
* the state.
|
|
@@ -405,16 +509,20 @@ ${JSON.stringify(initValues, null, 2)}`);
|
|
|
405
509
|
*/
|
|
406
510
|
async delete() {
|
|
407
511
|
if (this._isOwner) {
|
|
512
|
+
if (this._detached) {
|
|
513
|
+
throw new Error(`[SharedState] State "${this.schemaName} (${this.id})" already deleted, cannot delete twice`);
|
|
514
|
+
}
|
|
515
|
+
|
|
408
516
|
return this.detach();
|
|
409
517
|
} else {
|
|
410
|
-
throw new Error(`[
|
|
518
|
+
throw new Error(`[SharedState] Cannot delete state "${this.schemaName}", only the owner of the state (i.e. the node that created it) can delete the state. Use "detach" instead.`);
|
|
411
519
|
}
|
|
412
520
|
}
|
|
413
521
|
|
|
414
522
|
/**
|
|
415
523
|
* Subscribe to state updates.
|
|
416
524
|
*
|
|
417
|
-
* @param {client.SharedState~onUpdateCallback|
|
|
525
|
+
* @param {client.SharedState~onUpdateCallback|server.SharedState~onUpdateCallback} callback
|
|
418
526
|
* Callback to execute when an update is applied on the state.
|
|
419
527
|
* @param {Boolean} [executeListener=false] - Execute the callback immediately
|
|
420
528
|
* with current state values. (`oldValues` will be set to `{}`, and `context` to `null`)
|
|
@@ -447,10 +555,11 @@ ${JSON.stringify(initValues, null, 2)}`);
|
|
|
447
555
|
}
|
|
448
556
|
|
|
449
557
|
/**
|
|
450
|
-
* Register a function to execute when detaching from the state
|
|
558
|
+
* Register a function to execute when detaching from the state. The function
|
|
559
|
+
* will be executed before the `detach` promise resolves.
|
|
451
560
|
*
|
|
452
|
-
* @param {Function} callback -
|
|
453
|
-
*
|
|
561
|
+
* @param {Function} callback - Callback to execute when detaching from the state.
|
|
562
|
+
* Whether the client as called `detach`, or the state has been deleted by its
|
|
454
563
|
* creator.
|
|
455
564
|
*/
|
|
456
565
|
onDetach(callback) {
|
|
@@ -460,9 +569,10 @@ ${JSON.stringify(initValues, null, 2)}`);
|
|
|
460
569
|
|
|
461
570
|
/**
|
|
462
571
|
* Register a function to execute when the state is deleted. Only called if the
|
|
463
|
-
* node was the creator of the state. Is called after `onDetach`
|
|
572
|
+
* node was the creator of the state. Is called after `onDetach` and executed
|
|
573
|
+
* before the `delete` Promise resolves.
|
|
464
574
|
*
|
|
465
|
-
* @param {Function} callback -
|
|
575
|
+
* @param {Function} callback - Callback to execute when the state is deleted.
|
|
466
576
|
*/
|
|
467
577
|
onDelete(callback) {
|
|
468
578
|
this._onDeleteCallbacks.add(callback);
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @private
|
|
3
|
+
*/
|
|
4
|
+
class BaseSharedStateCollection {
|
|
5
|
+
constructor(stateManager, schemaName) {
|
|
6
|
+
this._stateManager = stateManager;
|
|
7
|
+
this._schemaName = schemaName;
|
|
8
|
+
this._states = [];
|
|
9
|
+
this._onUpdateCallbacks = new Set();
|
|
10
|
+
this._onAttachCallbacks = new Set();
|
|
11
|
+
this._onDetachCallbacks = new Set();
|
|
12
|
+
this._unobserve = null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async _init() {
|
|
16
|
+
this._unobserve = await this._stateManager.observe(this._schemaName, async (schemaName, stateId, nodeId) => {
|
|
17
|
+
const state = await this._stateManager.attach(schemaName, stateId);
|
|
18
|
+
|
|
19
|
+
this._states.push(state);
|
|
20
|
+
|
|
21
|
+
state.onDetach(() => {
|
|
22
|
+
const index = this._states.indexOf(state);
|
|
23
|
+
this._states.splice(index, 1);
|
|
24
|
+
|
|
25
|
+
this._onDetachCallbacks.forEach(callback => callback(state));
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
state.onUpdate((newValues, oldValues, context) => {
|
|
29
|
+
Array.from(this._onUpdateCallbacks).forEach(callback => {
|
|
30
|
+
callback(state, newValues, oldValues, context);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
this._onAttachCallbacks.forEach(callback => callback(state));
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Size of the collection
|
|
40
|
+
* @type {number}
|
|
41
|
+
*/
|
|
42
|
+
get length() {
|
|
43
|
+
return this._states.length;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Detach from the collection, i.e. detach all underlying shared states.
|
|
48
|
+
* @type {number}
|
|
49
|
+
*/
|
|
50
|
+
async detach() {
|
|
51
|
+
this._unobserve();
|
|
52
|
+
|
|
53
|
+
const promises = Array.from(this._states).map(state => state.detach());
|
|
54
|
+
await Promise.all(promises);
|
|
55
|
+
|
|
56
|
+
this._onUpdateCallbacks.clear();
|
|
57
|
+
this._onDetachCallbacks.clear();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Return the current values of all the states in the collection.
|
|
62
|
+
* @return {Object[]}
|
|
63
|
+
*/
|
|
64
|
+
getValues() {
|
|
65
|
+
return this._states.map(state => state.getValues());
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Return the current param value of all the states in the collection.
|
|
70
|
+
*
|
|
71
|
+
* @param {String} name - Name of the parameter
|
|
72
|
+
* @return {any[]}
|
|
73
|
+
*/
|
|
74
|
+
get(name) {
|
|
75
|
+
return this._states.map(state => state.get(name));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Update all states of the collection with given values.
|
|
80
|
+
* @param {object} updates - key / value pairs of updates to apply to the state.
|
|
81
|
+
* @param {mixed} [context=null] - optionnal contextual object that will be propagated
|
|
82
|
+
* alongside the updates of the state. The context is valid only for the
|
|
83
|
+
* current call and will be passed as third argument to all update listeners.
|
|
84
|
+
*/
|
|
85
|
+
async set(updates, context = null) {
|
|
86
|
+
const promises = this._states.map(state => state.set(updates, context));
|
|
87
|
+
return Promise.all(promises);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Subscribe to any state update of the collection.
|
|
92
|
+
*
|
|
93
|
+
* @param {server.SharedStateCollection~onUpdateCallback|client.SharedStateCollection~onUpdateCallback}
|
|
94
|
+
* callback - Callback to execute when an update is applied on a state.
|
|
95
|
+
* @param {Boolean} [executeListener=false] - Execute the callback immediately
|
|
96
|
+
* for all underlying states with current state values. (`oldValues` will be
|
|
97
|
+
* set to `{}`, and `context` to `null`)
|
|
98
|
+
* @returns {Function} - Function that delete the registered listener.
|
|
99
|
+
*/
|
|
100
|
+
onUpdate(callback, executeListener = false) {
|
|
101
|
+
this._onUpdateCallbacks.add(callback);
|
|
102
|
+
|
|
103
|
+
if (executeListener === true) {
|
|
104
|
+
this._states.forEach(state => {
|
|
105
|
+
const currentValues = state.getValues();
|
|
106
|
+
const oldValues = {};
|
|
107
|
+
const context = null;
|
|
108
|
+
|
|
109
|
+
callback(state, currentValues, oldValues, context);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return () => this._onUpdateCallbacks.delete(callback);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Register a function to execute when a state is added to the collection.
|
|
118
|
+
*
|
|
119
|
+
* @param {Function} callback - callback to execute when a state is added to
|
|
120
|
+
* the collection.
|
|
121
|
+
*/
|
|
122
|
+
onAttach(callback) {
|
|
123
|
+
this._onAttachCallbacks.add(callback);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Register a function to execute when a state is removed from the collection.
|
|
128
|
+
*
|
|
129
|
+
* @param {Function} callback - callback to execute when a state is removed
|
|
130
|
+
* from the collection.
|
|
131
|
+
*/
|
|
132
|
+
onDetach(callback) {
|
|
133
|
+
this._onDetachCallbacks.add(callback);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Execute the given function once for each states of the collection (see `Array.forEach`).
|
|
138
|
+
*
|
|
139
|
+
* @param {Function} func - A function to execute for each element in the array.
|
|
140
|
+
* Its return value is discarded.
|
|
141
|
+
*/
|
|
142
|
+
forEach(func) {
|
|
143
|
+
this._states.forEach(func);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Creates a new array populated with the results of calling a provided function
|
|
148
|
+
* on every state of the collection (see `Array.map`).
|
|
149
|
+
*
|
|
150
|
+
* @param {Function} func - A function to execute for each element in the array.
|
|
151
|
+
* Its return value is added as a single element in the new array.
|
|
152
|
+
*/
|
|
153
|
+
map(func) {
|
|
154
|
+
return this._states.map(func);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Creates a shallow copy of a portion of the collection, filtered down to just
|
|
159
|
+
* the estates that pass the test implemented by the provided function (see `Array.filter`).
|
|
160
|
+
*
|
|
161
|
+
* @param {Function} func - A function to execute for each element in the array.
|
|
162
|
+
* It should return a truthy to keep the element in the resulting array, and a f
|
|
163
|
+
* alsy value otherwise.
|
|
164
|
+
*/
|
|
165
|
+
filter(func) {
|
|
166
|
+
return this._states.filter(func);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Sort the elements of the collection in place (see `Array.sort`).
|
|
171
|
+
*
|
|
172
|
+
* @param {Function} func - Function that defines the sort order.
|
|
173
|
+
*/
|
|
174
|
+
sort(func) {
|
|
175
|
+
this._states.sort(func);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Returns the first element of the collection that satisfies the provided testing
|
|
180
|
+
* function. If no values satisfy the testing function, undefined is returned.
|
|
181
|
+
*
|
|
182
|
+
* @param {Function} func - Function to execute for each element in the array.
|
|
183
|
+
* It should return a truthy value to indicate a matching element has been found.
|
|
184
|
+
* @return {}
|
|
185
|
+
*/
|
|
186
|
+
find(func) {
|
|
187
|
+
return this._states.find(func);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export default BaseSharedStateCollection;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { isString, isFunction } from '@ircam/sc-utils';
|
|
1
2
|
import SharedState from './BaseSharedState.js';
|
|
3
|
+
import SharedStateCollection from './BaseSharedStateCollection.js';
|
|
2
4
|
import {
|
|
3
5
|
CREATE_REQUEST,
|
|
4
6
|
CREATE_RESPONSE,
|
|
@@ -31,7 +33,7 @@ class BaseStateManager {
|
|
|
31
33
|
this.client = { id, transport };
|
|
32
34
|
|
|
33
35
|
this._statesById = new Map();
|
|
34
|
-
this._observeListeners = new
|
|
36
|
+
this._observeListeners = new Map(); // Map <callback, filterSchemaName>
|
|
35
37
|
this._cachedSchemas = new Map();
|
|
36
38
|
this._observeRequestCallbacks = new Map();
|
|
37
39
|
|
|
@@ -88,28 +90,35 @@ class BaseStateManager {
|
|
|
88
90
|
this.client.transport.addListener(OBSERVE_RESPONSE, async (reqId, ...list) => {
|
|
89
91
|
// retrieve the callback that have been stored in observe to make sure
|
|
90
92
|
// we don't call another callback that may have been registered earlier.
|
|
91
|
-
const callback = this._observeRequestCallbacks.get(reqId);
|
|
93
|
+
const [callback, filterSchemaName] = this._observeRequestCallbacks.get(reqId);
|
|
92
94
|
this._observeRequestCallbacks.delete(reqId);
|
|
93
95
|
|
|
94
96
|
const promises = list.map(([schemaName, stateId, nodeId]) => {
|
|
95
|
-
|
|
97
|
+
if (filterSchemaName === '*' || filterSchemaName === schemaName) {
|
|
98
|
+
return callback(schemaName, stateId, nodeId);
|
|
99
|
+
} else {
|
|
100
|
+
return Promise.resolve();
|
|
101
|
+
}
|
|
96
102
|
});
|
|
97
103
|
|
|
98
104
|
await Promise.all(promises);
|
|
99
105
|
|
|
100
106
|
const unsubscribe = () => {
|
|
101
107
|
this._observeListeners.delete(callback);
|
|
102
|
-
|
|
108
|
+
// no more listeners, we can stop receiving notification from the server
|
|
103
109
|
if (this._observeListeners.size === 0) {
|
|
104
110
|
this.client.transport.emit(UNOBSERVE_NOTIFICATION);
|
|
105
111
|
}
|
|
106
112
|
};
|
|
113
|
+
|
|
107
114
|
resolveRequest(reqId, unsubscribe);
|
|
108
115
|
});
|
|
109
116
|
|
|
110
|
-
this.client.transport.addListener(OBSERVE_NOTIFICATION, (
|
|
111
|
-
|
|
112
|
-
|
|
117
|
+
this.client.transport.addListener(OBSERVE_NOTIFICATION, (schemaName, stateId, nodeId) => {
|
|
118
|
+
this._observeListeners.forEach((filterSchemaName, callback) => {
|
|
119
|
+
if (filterSchemaName === '*' || filterSchemaName === schemaName) {
|
|
120
|
+
callback(schemaName, stateId, nodeId);
|
|
121
|
+
}
|
|
113
122
|
});
|
|
114
123
|
});
|
|
115
124
|
|
|
@@ -170,12 +179,19 @@ class BaseStateManager {
|
|
|
170
179
|
* the state of all other clients of the application, to monitor them and/or take
|
|
171
180
|
* control over them from a single point.
|
|
172
181
|
*
|
|
173
|
-
*
|
|
182
|
+
* Notes:
|
|
183
|
+
* - The states that are created by the same node are not propagated through
|
|
184
|
+
* the observe callback.
|
|
185
|
+
* - The order of execution is not guaranted, i.e. an state attached in the
|
|
186
|
+
* `observe` callback could be created before the `async create` method resolves.
|
|
187
|
+
*
|
|
188
|
+
* @param {String} [schemaName] - optionnal schema name to filter the observed
|
|
189
|
+
* states.
|
|
174
190
|
* @param {server.StateManager~ObserveCallback|client.StateManager~ObserveCallback}
|
|
175
191
|
* callback - Function to be called when a new state is created on the network.
|
|
176
|
-
* @returns {Promise<Function>} -
|
|
177
|
-
* as been executed
|
|
178
|
-
* allows to stop observing the network.
|
|
192
|
+
* @returns {Promise<Function>} - Returns a Promise that resolves when the given
|
|
193
|
+
* callback as been executed on each existing states. The promise value is a
|
|
194
|
+
* function which allows to stop observing the states on the network.
|
|
179
195
|
* @example
|
|
180
196
|
* client.stateManager.observe(async (schemaName, stateId, nodeId) => {
|
|
181
197
|
* if (schemaName === 'something') {
|
|
@@ -184,16 +200,57 @@ class BaseStateManager {
|
|
|
184
200
|
* }
|
|
185
201
|
* });
|
|
186
202
|
*/
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
203
|
+
// note: all filtering is done only on client-side as it is really more simple to
|
|
204
|
+
// handle this way and the network overhead is very low for observe notifications:
|
|
205
|
+
// i.e. schemaName, stateId, nodeId
|
|
206
|
+
async observe(...args) {
|
|
207
|
+
let filterSchemaName;
|
|
208
|
+
let callback;
|
|
209
|
+
|
|
210
|
+
if (args.length === 1) {
|
|
211
|
+
filterSchemaName = '*';
|
|
212
|
+
callback = args[0];
|
|
213
|
+
|
|
214
|
+
if (!isFunction(callback)) {
|
|
215
|
+
throw new Error(`[stateManager] Invalid arguments, valid signatures are "stateManager.observe(callback)" or "stateManager.observe(schemaName, callback)"`);
|
|
216
|
+
}
|
|
217
|
+
} else if (args.length === 2) {
|
|
218
|
+
filterSchemaName = args[0];
|
|
219
|
+
callback = args[1];
|
|
220
|
+
|
|
221
|
+
if (!isString(filterSchemaName) || !isFunction(callback)) {
|
|
222
|
+
throw new Error(`[stateManager] Invalid arguments, valid signatures are "stateManager.observe(callback)" or "stateManager.observe(schemaName, callback)"`);
|
|
223
|
+
}
|
|
224
|
+
} else {
|
|
225
|
+
throw new Error(`[stateManager] Invalid arguments, valid signatures are "stateManager.observe(callback)" or "stateManager.observe(schemaName, callback)"`);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// resend request to get updated list of states
|
|
190
229
|
return new Promise((resolve, reject) => {
|
|
191
230
|
const reqId = storeRequestPromise(resolve, reject);
|
|
192
|
-
// store the callback
|
|
193
|
-
|
|
231
|
+
// store the callback for execution on the response. the returned Promise
|
|
232
|
+
// is fullfiled once callback has been executed with each existing states
|
|
233
|
+
this._observeRequestCallbacks.set(reqId, [callback, filterSchemaName]);
|
|
234
|
+
// store the callback for execution on subsequent notifications
|
|
235
|
+
this._observeListeners.set(callback, filterSchemaName);
|
|
236
|
+
|
|
194
237
|
this.client.transport.emit(OBSERVE_REQUEST, reqId);
|
|
195
238
|
});
|
|
196
239
|
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Returns a collection of all the states created from the schema name. Except
|
|
243
|
+
* the ones created by the current node.
|
|
244
|
+
*
|
|
245
|
+
* @param {string} schemaName - Name of the schema.
|
|
246
|
+
* @returns {server.SharedStateCollection|client.SharedStateCollection}
|
|
247
|
+
*/
|
|
248
|
+
async getCollection(schemaName) {
|
|
249
|
+
const collection = new SharedStateCollection(this, schemaName);
|
|
250
|
+
await collection._init();
|
|
251
|
+
|
|
252
|
+
return collection;
|
|
253
|
+
}
|
|
197
254
|
}
|
|
198
255
|
|
|
199
256
|
export default BaseStateManager;
|