@soundworks/core 3.0.4 → 3.1.0-beta.3
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/README.md +1 -0
- package/client/Client.js +1 -1
- package/client/PluginManager.js +4 -3
- package/client/Socket.js +70 -39
- package/common/ParameterBag.js +561 -0
- package/common/SharedState.js +218 -50
- package/common/SharedStateManagerClient.js +7 -1
- package/common/SharedStateManagerServer.js +100 -17
- package/common/SharedStatePrivate.js +96 -55
- package/common/shared-state-utils.js +6 -5
- package/common/sockets-encoder-decoder.js +1 -1
- package/package.json +15 -14
- package/server/Client.js +2 -4
- package/server/PluginManager.js +4 -3
- package/server/Server.js +51 -7
- package/server/utils/Db.js +0 -67
package/common/SharedState.js
CHANGED
|
@@ -5,9 +5,7 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
5
5
|
});
|
|
6
6
|
exports.default = void 0;
|
|
7
7
|
|
|
8
|
-
var
|
|
9
|
-
|
|
10
|
-
var _lodash = _interopRequireDefault(require("lodash.clonedeep"));
|
|
8
|
+
var _ParameterBag = _interopRequireDefault(require("./ParameterBag.js"));
|
|
11
9
|
|
|
12
10
|
var _sharedStateUtils = require("./shared-state-utils.js");
|
|
13
11
|
|
|
@@ -27,18 +25,17 @@ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { de
|
|
|
27
25
|
* @see {server.SharedStateManagerServer}
|
|
28
26
|
*/
|
|
29
27
|
class SharedState {
|
|
30
|
-
constructor(id, remoteId, schemaName, schema, client,
|
|
28
|
+
constructor(id, remoteId, schemaName, schema, client, isOwner, manager, initValues = {}) {
|
|
31
29
|
this.id = id;
|
|
32
30
|
this.remoteId = remoteId;
|
|
33
31
|
this.schemaName = schemaName;
|
|
34
|
-
this.
|
|
35
|
-
this._isCreator = isCreator; // may be the server or any client
|
|
32
|
+
this._isOwner = isOwner; // may be the server or any client
|
|
36
33
|
|
|
37
34
|
this._client = client;
|
|
38
35
|
this._manager = manager;
|
|
39
36
|
|
|
40
37
|
try {
|
|
41
|
-
this._parameters =
|
|
38
|
+
this._parameters = new _ParameterBag.default(schema, initValues);
|
|
42
39
|
} catch (err) {
|
|
43
40
|
console.error(err.stack);
|
|
44
41
|
throw new Error(`Error creating or attaching state "${schemaName}" w/ values:\n
|
|
@@ -49,20 +46,18 @@ ${JSON.stringify(initValues, null, 2)}`);
|
|
|
49
46
|
this._onDetachCallbacks = new Set();
|
|
50
47
|
this._onDeleteCallbacks = new Set(); // add listener for state updates
|
|
51
48
|
|
|
52
|
-
client.transport.addListener(`${_sharedStateUtils.UPDATE_RESPONSE}-${id}-${this.remoteId}`, (reqId, updates) => {
|
|
53
|
-
const updated = this._commit(updates);
|
|
54
|
-
|
|
49
|
+
client.transport.addListener(`${_sharedStateUtils.UPDATE_RESPONSE}-${id}-${this.remoteId}`, async (reqId, updates, context) => {
|
|
50
|
+
const updated = await this._commit(updates, context, true, true);
|
|
55
51
|
(0, _sharedStateUtils.resolveRequest)(reqId, updated);
|
|
56
52
|
}); // retrieve values but do not propagate to subscriptions
|
|
57
53
|
|
|
58
|
-
client.transport.addListener(`${_sharedStateUtils.UPDATE_ABORT}-${id}-${this.remoteId}`, (reqId, updates) => {
|
|
59
|
-
const updated = this._commit(updates, false);
|
|
60
|
-
|
|
54
|
+
client.transport.addListener(`${_sharedStateUtils.UPDATE_ABORT}-${id}-${this.remoteId}`, async (reqId, updates, context) => {
|
|
55
|
+
const updated = await this._commit(updates, context, false, true);
|
|
61
56
|
(0, _sharedStateUtils.resolveRequest)(reqId, updated);
|
|
62
57
|
});
|
|
63
|
-
client.transport.addListener(`${_sharedStateUtils.UPDATE_NOTIFICATION}-${id}-${this.remoteId}`, updates => {
|
|
58
|
+
client.transport.addListener(`${_sharedStateUtils.UPDATE_NOTIFICATION}-${id}-${this.remoteId}`, async (updates, context) => {
|
|
64
59
|
// cf. https://github.com/collective-soundworks/soundworks/issues/18
|
|
65
|
-
this._commit(updates);
|
|
60
|
+
await this._commit(updates, context, true, false);
|
|
66
61
|
}); // ---------------------------------------------
|
|
67
62
|
// DELETE initiated by creator, or schema deleted
|
|
68
63
|
// ---------------------------------------------
|
|
@@ -79,7 +74,7 @@ ${JSON.stringify(initValues, null, 2)}`);
|
|
|
79
74
|
this._clearDetach();
|
|
80
75
|
});
|
|
81
76
|
|
|
82
|
-
if (this.
|
|
77
|
+
if (this._isOwner) {
|
|
83
78
|
// ---------------------------------------------
|
|
84
79
|
// DELETE (can only delete if creator)
|
|
85
80
|
// ---------------------------------------------
|
|
@@ -124,12 +119,12 @@ ${JSON.stringify(initValues, null, 2)}`);
|
|
|
124
119
|
_clearDetach() {
|
|
125
120
|
this._onDetachCallbacks.clear();
|
|
126
121
|
|
|
127
|
-
this._onDeleteCallbacks.clear(); // Monkey patch detach so it throws
|
|
128
|
-
// the process on a second `detach` call as the Promise
|
|
122
|
+
this._onDeleteCallbacks.clear(); // Monkey patch detach so it throws if called twice. Doing nothing blocks
|
|
123
|
+
// the process on a second `detach` call as the Promise never resolves
|
|
129
124
|
|
|
130
125
|
|
|
131
126
|
this.detach = () => {
|
|
132
|
-
throw new Error(`State "${this.schemaName} (${this.id})" already detached, cannot detach twice`);
|
|
127
|
+
throw new Error(`[stateManager] State "${this.schemaName} (${this.id})" already detached, cannot detach twice`);
|
|
133
128
|
};
|
|
134
129
|
}
|
|
135
130
|
|
|
@@ -148,50 +143,141 @@ ${JSON.stringify(initValues, null, 2)}`);
|
|
|
148
143
|
/** @private */
|
|
149
144
|
|
|
150
145
|
|
|
151
|
-
_commit(
|
|
152
|
-
const
|
|
146
|
+
async _commit(updates, context, propagate = true, initiator = false) {
|
|
147
|
+
const newValues = {};
|
|
148
|
+
const oldValues = {};
|
|
149
|
+
|
|
150
|
+
for (let name in updates) {
|
|
151
|
+
const {
|
|
152
|
+
immediate,
|
|
153
|
+
filterChange,
|
|
154
|
+
event
|
|
155
|
+
} = this._parameters.getSchema(name); // @note 20211209 - we have an issue here server-side, because if the value
|
|
156
|
+
// is an object or an array, the reference is shared by everybody, therefore
|
|
157
|
+
// `changed` is always false and the new value is never propagated...
|
|
158
|
+
// FIXED - `state.get` now returns a deep copy when `type` is `any`
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
const oldValue = this._parameters.get(name);
|
|
162
|
+
|
|
163
|
+
const [newValue, changed] = this._parameters.set(name, updates[name]); // handle immediate stuff
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
if (initiator && immediate) {
|
|
167
|
+
// @note - we don't need to check filterChange here because the value
|
|
168
|
+
// has been updated in parameters on the `set` side so can rely on `changed`
|
|
169
|
+
// to avoid retrigger listeners.
|
|
170
|
+
// If the value has been overriden by the server, `changed` will true
|
|
171
|
+
// anyway so it should behave correctly.
|
|
172
|
+
if (!changed || event) {
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
153
176
|
|
|
154
|
-
|
|
155
|
-
|
|
177
|
+
newValues[name] = newValue;
|
|
178
|
+
oldValues[name] = oldValue;
|
|
156
179
|
} // if the `UPDATE_REQUEST` as been aborted by the server, do not propagate
|
|
157
180
|
|
|
158
181
|
|
|
159
|
-
|
|
160
|
-
|
|
182
|
+
let promises = [];
|
|
183
|
+
|
|
184
|
+
if (propagate && Object.keys(newValues).length > 0) {
|
|
185
|
+
this._subscriptions.forEach(listener => {
|
|
186
|
+
promises.push(listener(newValues, oldValues, context));
|
|
187
|
+
});
|
|
161
188
|
}
|
|
162
189
|
|
|
163
|
-
|
|
190
|
+
await Promise.all(promises);
|
|
191
|
+
return newValues;
|
|
164
192
|
}
|
|
165
193
|
/**
|
|
166
|
-
*
|
|
194
|
+
* Updates values of the state. Wait for all subscriptions to be resolved
|
|
195
|
+
* before resolving itself, i.e.:
|
|
167
196
|
*
|
|
168
|
-
*
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
*
|
|
197
|
+
* ```js
|
|
198
|
+
* const a = await server.stateManager.create('a');
|
|
199
|
+
* let asyncCallbackCalled = false;
|
|
200
|
+
*
|
|
201
|
+
* a.subscribe(updates => {
|
|
202
|
+
* return new Promise(resolve => {
|
|
203
|
+
* setTimeout(() => {
|
|
204
|
+
* asyncCallbackCalled = true;
|
|
205
|
+
* resolve();
|
|
206
|
+
* }, 100);
|
|
207
|
+
* });
|
|
208
|
+
* });
|
|
209
|
+
*
|
|
210
|
+
* await a.set({ bool: true });
|
|
211
|
+
* assert.equal(asyncCallbackCalled, true);
|
|
212
|
+
* ```
|
|
177
213
|
*
|
|
178
214
|
* @async
|
|
179
|
-
* @param {Object} updates - key / value pairs of updates to apply to the state
|
|
180
|
-
* @
|
|
215
|
+
* @param {Object} updates - key / value pairs of updates to apply to the state.
|
|
216
|
+
* @param {Mixed} [context=null] - optionnal context that will be propagated
|
|
217
|
+
* alongside the updates of the state. The context is valid only for the
|
|
218
|
+
* current call and will be passed as third argument to any subscribe listeners.
|
|
219
|
+
* @return {Promise<Object>} A promise to the (coerced) updates.
|
|
220
|
+
*
|
|
221
|
+
* @see {common.SharedState~subscribeCallback}
|
|
181
222
|
*/
|
|
182
223
|
|
|
183
224
|
|
|
184
|
-
async set(updates) {
|
|
225
|
+
async set(updates, context = null) {
|
|
226
|
+
// handle immediate option
|
|
227
|
+
const immediateNewValues = {};
|
|
228
|
+
const immediateOldValues = {};
|
|
229
|
+
let propagateNow = false;
|
|
230
|
+
|
|
231
|
+
for (let name in updates) {
|
|
232
|
+
// throw early (client-side and not only server-side) if parameter is undefined
|
|
233
|
+
if (!this._parameters.has(name)) {
|
|
234
|
+
throw new ReferenceError(`[stateManager] Cannot set value of undefined parameter "${name}"`);
|
|
235
|
+
} // @note: general idea...
|
|
236
|
+
// if immediate=true
|
|
237
|
+
// - call listeners if value changed
|
|
238
|
+
// - go through normal server path
|
|
239
|
+
// - retrigger only if response from server is different from current value
|
|
240
|
+
// if immediate=true && (filterChange=false || event=true)
|
|
241
|
+
// - call listeners with value regarless it changed
|
|
242
|
+
// - go through normal server path
|
|
243
|
+
// - if the node is initiator of the update (UPDATE_RESPONSE), (re-)check
|
|
244
|
+
// to prevent execute the listeners twice
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
const {
|
|
248
|
+
immediate,
|
|
249
|
+
filterChange,
|
|
250
|
+
event
|
|
251
|
+
} = this._parameters.getSchema(name);
|
|
252
|
+
|
|
253
|
+
if (immediate) {
|
|
254
|
+
const oldValue = this._parameters.get(name);
|
|
255
|
+
|
|
256
|
+
const [newValue, changed] = this._parameters.set(name, updates[name]);
|
|
257
|
+
|
|
258
|
+
if (changed || filterChange === false) {
|
|
259
|
+
immediateOldValues[name] = oldValue;
|
|
260
|
+
immediateNewValues[name] = newValue;
|
|
261
|
+
propagateNow = true;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (propagateNow) {
|
|
267
|
+
this._subscriptions.forEach(listener => listener(immediateNewValues, immediateOldValues, context));
|
|
268
|
+
} // go through server-side normal behavior
|
|
269
|
+
|
|
270
|
+
|
|
185
271
|
return new Promise((resolve, reject) => {
|
|
186
272
|
const reqId = (0, _sharedStateUtils.storeRequestPromise)(resolve, reject);
|
|
187
273
|
|
|
188
|
-
this._client.transport.emit(`${_sharedStateUtils.UPDATE_REQUEST}-${this.id}-${this.remoteId}`, reqId, updates);
|
|
274
|
+
this._client.transport.emit(`${_sharedStateUtils.UPDATE_REQUEST}-${this.id}-${this.remoteId}`, reqId, updates, context);
|
|
189
275
|
});
|
|
190
276
|
}
|
|
191
277
|
/**
|
|
192
278
|
* Get a value of the state by its name
|
|
193
279
|
*
|
|
194
|
-
* @param {String} name - Name of the param
|
|
280
|
+
* @param {String} name - Name of the param. Throws an error if the name is invalid.
|
|
195
281
|
* @return {Mixed}
|
|
196
282
|
*/
|
|
197
283
|
|
|
@@ -209,29 +295,89 @@ ${JSON.stringify(initValues, null, 2)}`);
|
|
|
209
295
|
getValues() {
|
|
210
296
|
return this._parameters.getValues();
|
|
211
297
|
}
|
|
298
|
+
/**
|
|
299
|
+
* Get the schema that describes the state.
|
|
300
|
+
*
|
|
301
|
+
* @param {String} [name=null] - if given, returns only the definition
|
|
302
|
+
* of the given param name. Throws an error if the name is invalid.
|
|
303
|
+
* @return {Object}
|
|
304
|
+
*/
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
getSchema(name = null) {
|
|
308
|
+
return this._parameters.getSchema(name);
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Get the values with which the state has been initialized.
|
|
312
|
+
*
|
|
313
|
+
* @return {Object}
|
|
314
|
+
*/
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
getInitValues() {
|
|
318
|
+
return this._parameters.getInitValues();
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Get the default values that has been declared in the schema.
|
|
322
|
+
*
|
|
323
|
+
* @return {Object}
|
|
324
|
+
*/
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
getDefaults() {
|
|
328
|
+
return this._parameters.getDefaults();
|
|
329
|
+
}
|
|
212
330
|
/**
|
|
213
331
|
* @callback common.SharedState~subscribeCallback
|
|
214
|
-
* @param {Object}
|
|
215
|
-
* applied to the state
|
|
332
|
+
* @param {Object} newValues - key / value pairs of the updates that have been
|
|
333
|
+
* applied to the state.
|
|
334
|
+
* @param {Object} oldValues - key / value pairs of the related params before
|
|
335
|
+
* the updates has been applied to the state.
|
|
336
|
+
* @param {Mixed} [context=null] - Optionnal context data that has been passed
|
|
337
|
+
* with the updates in the `set` call.
|
|
338
|
+
*
|
|
339
|
+
* @example
|
|
340
|
+
* state.subscribe(async (newValues, oldValues[, context=null]) => {
|
|
341
|
+
* for (let [key, value] of Object.entries(newValues)) {
|
|
342
|
+
* switch (key) {
|
|
343
|
+
* // do something
|
|
344
|
+
* }
|
|
345
|
+
* }
|
|
346
|
+
* });
|
|
347
|
+
*
|
|
348
|
+
* @see {common.SharedState#set}
|
|
349
|
+
* @see {common.SharedState#subscribe}
|
|
216
350
|
*/
|
|
217
351
|
|
|
218
352
|
/**
|
|
219
353
|
* Subscribe to state updates
|
|
220
354
|
*
|
|
221
355
|
* @param {common.SharedState~subscribeCallback} callback - callback to execute
|
|
222
|
-
* when an update is
|
|
356
|
+
* when an update is applied on the state.
|
|
357
|
+
* @param {Boolean} [executeListener=false] - execute the given listener with
|
|
358
|
+
* current state values. (`oldValues` will be set to `{}`, and `context` to `null`)
|
|
359
|
+
*
|
|
223
360
|
* @example
|
|
224
|
-
* state.subscribe(async (
|
|
225
|
-
* for (let [key, value] of Object.entries(
|
|
226
|
-
*
|
|
361
|
+
* state.subscribe(async (newValues, oldValues) => {
|
|
362
|
+
* for (let [key, value] of Object.entries(newValues)) {
|
|
363
|
+
* switch (key) {
|
|
364
|
+
* // do something
|
|
365
|
+
* }
|
|
227
366
|
* }
|
|
228
|
-
* }
|
|
367
|
+
* });
|
|
229
368
|
*/
|
|
230
369
|
|
|
231
370
|
|
|
232
|
-
subscribe(listener) {
|
|
371
|
+
subscribe(listener, executeListener = false) {
|
|
233
372
|
this._subscriptions.add(listener);
|
|
234
373
|
|
|
374
|
+
if (executeListener === true) {
|
|
375
|
+
const currentValues = this.getValues();
|
|
376
|
+
const oldValues = {};
|
|
377
|
+
const context = null;
|
|
378
|
+
listener(currentValues, oldValues, context);
|
|
379
|
+
}
|
|
380
|
+
|
|
235
381
|
return () => {
|
|
236
382
|
this._subscriptions.delete(listener);
|
|
237
383
|
};
|
|
@@ -253,7 +399,7 @@ ${JSON.stringify(initValues, null, 2)}`);
|
|
|
253
399
|
async detach() {
|
|
254
400
|
this._subscriptions.clear();
|
|
255
401
|
|
|
256
|
-
if (this.
|
|
402
|
+
if (this._isOwner) {
|
|
257
403
|
return new Promise((resolve, reject) => {
|
|
258
404
|
const reqId = (0, _sharedStateUtils.storeRequestPromise)(resolve, reject);
|
|
259
405
|
|
|
@@ -267,6 +413,28 @@ ${JSON.stringify(initValues, null, 2)}`);
|
|
|
267
413
|
});
|
|
268
414
|
}
|
|
269
415
|
}
|
|
416
|
+
/**
|
|
417
|
+
* Delete the state. Only the creator/owner of the state (i.e. a state created using
|
|
418
|
+
* `create`) can use this method. If a non-owner call this method (i.e. a
|
|
419
|
+
* state created using `attach`), an error will be thrown.
|
|
420
|
+
*
|
|
421
|
+
* @async
|
|
422
|
+
* @see {common.SharedState#onDetach}
|
|
423
|
+
* @see {common.SharedState#onDelete}
|
|
424
|
+
* @see {client.SharedStateManagerClient#create}
|
|
425
|
+
* @see {server.SharedStateManagerClient#create}
|
|
426
|
+
* @see {client.SharedStateManagerClient#attach}
|
|
427
|
+
* @see {server.SharedStateManagerClient#attach}
|
|
428
|
+
*/
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
async delete() {
|
|
432
|
+
if (this._isOwner) {
|
|
433
|
+
return this.detach();
|
|
434
|
+
} else {
|
|
435
|
+
throw new Error(`[stateManager] can delete state "${this.schemaName}", only owner of the state (i.e. the node that "create[d]" it) can delete it`);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
270
438
|
/**
|
|
271
439
|
* Register a function to execute when detaching from the state
|
|
272
440
|
*
|
|
@@ -15,7 +15,7 @@ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { de
|
|
|
15
15
|
* Component dedicated at managing distributed states, accessible through {@link common.SharedState} instances, among the application.
|
|
16
16
|
*
|
|
17
17
|
* An instance of `SharedStateManagerClient` is automatically created by the
|
|
18
|
-
* `soundworks.Client` (cf. {@link client.Client#stateManager}).
|
|
18
|
+
* `soundworks.Client` at initialization (cf. {@link client.Client#stateManager}).
|
|
19
19
|
*
|
|
20
20
|
* Tutorial: [https://collective-soundworks.github.io/tutorials/state-manager.html](https://collective-soundworks.github.io/tutorials/state-manager.html)
|
|
21
21
|
*
|
|
@@ -99,6 +99,12 @@ class SharedStateManagerClient {
|
|
|
99
99
|
list.forEach(([schemaName, stateId, nodeId]) => {
|
|
100
100
|
this._observeListeners.forEach(callback => callback(schemaName, stateId, nodeId));
|
|
101
101
|
});
|
|
102
|
+
}); // ---------------------------------------------
|
|
103
|
+
// Clear cache when schema is deleted
|
|
104
|
+
// ---------------------------------------------
|
|
105
|
+
|
|
106
|
+
this.client.transport.addListener(_sharedStateUtils.DELETE_SCHEMA, schemaName => {
|
|
107
|
+
this._cachedSchemas.delete(schemaName);
|
|
102
108
|
});
|
|
103
109
|
}
|
|
104
110
|
/**
|
|
@@ -7,7 +7,7 @@ exports.default = void 0;
|
|
|
7
7
|
|
|
8
8
|
var _events = _interopRequireDefault(require("events"));
|
|
9
9
|
|
|
10
|
-
var
|
|
10
|
+
var _ParameterBag = _interopRequireDefault(require("./ParameterBag.js"));
|
|
11
11
|
|
|
12
12
|
var _lodash = _interopRequireDefault(require("lodash.clonedeep"));
|
|
13
13
|
|
|
@@ -26,8 +26,8 @@ const generateRemoteId = (0, _sharedStateUtils.idGenerator)();
|
|
|
26
26
|
* The `SharedStateManagerServer` extends the `SharedStateManagerClient` with
|
|
27
27
|
* additionnal functionnalities to register schemas and handle clients.
|
|
28
28
|
*
|
|
29
|
-
* An instance of `
|
|
30
|
-
* `soundworks.Server` (cf. {@link server.Server#stateManager}).
|
|
29
|
+
* An instance of `SharedStateManagerServer` is automatically created by the
|
|
30
|
+
* `soundworks.Server` at initialization (cf. {@link server.Server#stateManager}).
|
|
31
31
|
*
|
|
32
32
|
* Tutorial: [https://collective-soundworks.github.io/tutorials/state-manager.html](https://collective-soundworks.github.io/tutorials/state-manager.html)
|
|
33
33
|
*
|
|
@@ -49,6 +49,8 @@ class SharedStateManagerServer extends _SharedStateManagerClient.default {
|
|
|
49
49
|
this._serverStatesById = new Map();
|
|
50
50
|
this._schemas = new Map();
|
|
51
51
|
this._observers = new Set();
|
|
52
|
+
this._hooksBySchemaName = new Map(); // protected
|
|
53
|
+
|
|
52
54
|
this.addClient(localClientId, localTransport);
|
|
53
55
|
}
|
|
54
56
|
/**
|
|
@@ -222,38 +224,56 @@ class SharedStateManagerServer extends _SharedStateManagerClient.default {
|
|
|
222
224
|
this._clientByNodeId.delete(nodeId);
|
|
223
225
|
}
|
|
224
226
|
/**
|
|
225
|
-
* Register a schema
|
|
226
|
-
*
|
|
227
|
+
* Register a schema from which shared states (cf. {@link common.SharedState})
|
|
228
|
+
* can be instanciated.
|
|
227
229
|
*
|
|
228
230
|
* @param {String} schemaName - Name of the schema.
|
|
229
|
-
* @param {
|
|
231
|
+
* @param {server.SharedStateManagerServer~schema} schema - Data structure
|
|
232
|
+
* describing the states that will be created from this schema.
|
|
233
|
+
*
|
|
234
|
+
* @see {@link server.SharedStateManagerServer#create}
|
|
235
|
+
* @see {@link client.SharedStateManagerClient#create}
|
|
236
|
+
*
|
|
237
|
+
* @example
|
|
238
|
+
* server.stateManager.registerSchema('my-schema', {
|
|
239
|
+
* myBoolean: {
|
|
240
|
+
* type: 'boolean'
|
|
241
|
+
* default: false,
|
|
242
|
+
* },
|
|
243
|
+
* myFloat: {
|
|
244
|
+
* type: 'float'
|
|
245
|
+
* default: 0.1,
|
|
246
|
+
* min: -1,
|
|
247
|
+
* max: 1
|
|
248
|
+
* }
|
|
249
|
+
* })
|
|
230
250
|
*/
|
|
231
251
|
|
|
232
252
|
|
|
233
253
|
registerSchema(schemaName, schema) {
|
|
234
254
|
if (this._schemas.has(schemaName)) {
|
|
235
|
-
throw new Error(`schema "${schemaName}" already
|
|
236
|
-
}
|
|
255
|
+
throw new Error(`[stateManager.registerSchema] cannot register schema with name: "${schemaName}", schema name already exists`);
|
|
256
|
+
}
|
|
237
257
|
|
|
258
|
+
_ParameterBag.default.validateSchema(schema);
|
|
259
|
+
|
|
260
|
+
this._schemas.set(schemaName, (0, _lodash.default)(schema)); // create hooks list
|
|
238
261
|
|
|
239
|
-
try {
|
|
240
|
-
(0, _parameters.default)(schema, {});
|
|
241
|
-
} catch (err) {
|
|
242
|
-
throw new Error(`Invalid schema "${schemaName}": ${err.message}`);
|
|
243
|
-
}
|
|
244
262
|
|
|
245
|
-
this.
|
|
263
|
+
this._hooksBySchemaName.set(schemaName, new Set());
|
|
246
264
|
}
|
|
247
265
|
/**
|
|
248
266
|
* Delete a schema and all associated states.
|
|
249
|
-
* When a schema is deleted, all
|
|
250
|
-
*
|
|
267
|
+
* When a schema is deleted, all states created from this schema are deleted
|
|
268
|
+
* as well, therefore all attached clients are detached and the `onDetach`
|
|
269
|
+
* and `onDelete` callbacks are called on the related states.
|
|
251
270
|
*
|
|
252
271
|
* @param {String} schemaName - Name of the schema.
|
|
253
272
|
*/
|
|
254
273
|
|
|
255
274
|
|
|
256
275
|
deleteSchema(schemaName) {
|
|
276
|
+
// @note: deleting schema
|
|
257
277
|
for (let [id, state] of this._serverStatesById.entries()) {
|
|
258
278
|
if (state.schemaName === schemaName) {
|
|
259
279
|
for (let [remoteId, attached] of state._attachedClients.entries()) {
|
|
@@ -264,9 +284,72 @@ class SharedStateManagerServer extends _SharedStateManagerClient.default {
|
|
|
264
284
|
|
|
265
285
|
this._serverStatesById.delete(this.id);
|
|
266
286
|
}
|
|
287
|
+
} // clear schema cache of all connected clients
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
for (let client of this._clientByNodeId.values()) {
|
|
291
|
+
client.transport.emit(`${_sharedStateUtils.DELETE_SCHEMA}`, schemaName);
|
|
267
292
|
}
|
|
268
293
|
|
|
269
|
-
this._schemas.delete(schemaName);
|
|
294
|
+
this._schemas.delete(schemaName); // delete registered hooks
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
this._hooksBySchemaName.delete(schemaName);
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* @callback server.SharedStateManagerServer~updateHook
|
|
301
|
+
*
|
|
302
|
+
* @param {Object} updates - Update object as given on a set callback, or
|
|
303
|
+
* result of the previous hook
|
|
304
|
+
* @param {Object} currentValues - Current values of the state.
|
|
305
|
+
* @param {Object} [context=null] - Optionnal context passed by the creator
|
|
306
|
+
* of the update.
|
|
307
|
+
* @return {Object} The "real" updates to be applied on the state.
|
|
308
|
+
*/
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Register a function for a given schema (e.g. will be applied on all states
|
|
312
|
+
* created from this schema) that will be executed before the update values
|
|
313
|
+
* are propagated. For example, this could be used to implement a preset system
|
|
314
|
+
* where all the values of the state are updated from e.g. some data stored in
|
|
315
|
+
* filesystem while the consumer of the state only want to update the preset name.
|
|
316
|
+
*
|
|
317
|
+
* The hook is associated to every state of its kind (i.e. schemaName) and
|
|
318
|
+
* executed on every update (call of `set`). Note that the hooks are executed
|
|
319
|
+
* server-side regarless the node on which `set` has been called and before
|
|
320
|
+
* the "actual" update of the state (e.g. before the call of `subscribe`).
|
|
321
|
+
*
|
|
322
|
+
* @example
|
|
323
|
+
* server.stateManager.registerSchema('hooked', schema);
|
|
324
|
+
* server.stateManager.registerUpdateHook('hooked', (updates, currentValues) => {
|
|
325
|
+
* return {
|
|
326
|
+
* ...updates
|
|
327
|
+
* numUpdates: currentValues.numUpdates + 1,
|
|
328
|
+
* };
|
|
329
|
+
* });
|
|
330
|
+
*
|
|
331
|
+
* const state = await server.stateManager.create('hooked');
|
|
332
|
+
*
|
|
333
|
+
* await state.set({ name: 'test' });
|
|
334
|
+
* state.getValues();
|
|
335
|
+
* // > { name: 'test', numUpdates: 1 };
|
|
336
|
+
*
|
|
337
|
+
* @param {String} schemaName - Kind of states on which applying the hook.
|
|
338
|
+
* @param {server.SharedStateManagerServer~updateHook} updateHook - Function
|
|
339
|
+
* called between the `set` call and the actual update.
|
|
340
|
+
*/
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
registerUpdateHook(schemaName, updateHook) {
|
|
344
|
+
// throw error if schemaName has not been registered
|
|
345
|
+
if (!this._schemas.has(schemaName)) {
|
|
346
|
+
throw new Error(`[stateManager.registerUpdateHook] cannot register update hook for schema name "${schemaName}", schema name does not exists`);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const hooks = this._hooksBySchemaName.get(schemaName);
|
|
350
|
+
|
|
351
|
+
hooks.add(updateHook);
|
|
352
|
+
return () => hooks.delete(updateHook);
|
|
270
353
|
}
|
|
271
354
|
|
|
272
355
|
}
|