@soundworks/core 4.0.0-alpha.2 → 4.0.0-alpha.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc +0 -1
- package/.jsdoc +57 -0
- package/CHANGELOG.md +33 -0
- package/README.md +17 -51
- package/package.json +24 -31
- package/src/client/Client.js +13 -7
- package/src/client/Context.js +12 -13
- package/src/client/ContextManager.js +1 -1
- package/src/client/PluginManager.js +1 -1
- package/src/client/SharedStateCollection.js +38 -0
- package/src/client/Socket.js +80 -44
- package/src/common/BasePlugin.js +31 -21
- package/src/common/BasePluginManager.js +34 -19
- package/src/common/BaseSharedState.js +304 -79
- package/src/common/BaseSharedStateCollection.js +324 -0
- package/src/common/BaseStateManager.js +353 -49
- package/src/common/BatchedTransport.js +73 -0
- package/src/common/ParameterBag.js +92 -30
- package/src/common/PromiseStore.js +53 -0
- package/src/common/SharedStatePrivate.js +87 -46
- package/src/common/constants.js +14 -1
- package/src/common/sockets-utils.js +3 -4
- package/src/server/Client.js +7 -2
- package/src/server/ContextManager.js +4 -4
- package/src/server/PluginManager.js +1 -1
- package/src/server/Server.js +263 -43
- package/src/server/SharedState.js +0 -1
- package/src/server/SharedStateCollection.js +39 -0
- package/src/server/Socket.js +55 -49
- package/src/server/Sockets.js +33 -11
- package/src/server/StateManager.js +187 -100
- package/src/server/audit-network-latency.worker.js +16 -4
- package/src/server/crypto.js +37 -0
- package/src/common/promise-store.js +0 -25
- package/src/common/utils.js +0 -22
- package/tsconfig.json +0 -21
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { isPlainObject } from '@ircam/sc-utils';
|
|
1
2
|
import ParameterBag from './ParameterBag.js';
|
|
3
|
+
import PromiseStore from './PromiseStore.js';
|
|
2
4
|
import {
|
|
3
5
|
DELETE_REQUEST,
|
|
4
6
|
DELETE_RESPONSE,
|
|
@@ -12,29 +14,44 @@ import {
|
|
|
12
14
|
UPDATE_ABORT,
|
|
13
15
|
UPDATE_NOTIFICATION,
|
|
14
16
|
} from './constants.js';
|
|
15
|
-
import {
|
|
16
|
-
storeRequestPromise,
|
|
17
|
-
resolveRequest,
|
|
18
|
-
rejectRequest,
|
|
19
|
-
} from './promise-store.js';
|
|
20
17
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
18
|
+
// @todo - replace isOwner by `type`:
|
|
19
|
+
// enum {
|
|
20
|
+
// 'owner'
|
|
21
|
+
// 'attached'
|
|
22
|
+
// 'collection-controlller'
|
|
23
|
+
// 'collection-attached'
|
|
24
|
+
// }
|
|
26
25
|
|
|
26
|
+
class BaseSharedState {
|
|
27
|
+
constructor(id, remoteId, schemaName, schema, client, isOwner, manager, initValues, filter) {
|
|
28
|
+
/** @private */
|
|
29
|
+
this._id = id;
|
|
30
|
+
/** @private */
|
|
31
|
+
this._remoteId = remoteId;
|
|
32
|
+
/** @private */
|
|
33
|
+
this._schemaName = schemaName;
|
|
27
34
|
/** @private */
|
|
28
35
|
this._isOwner = isOwner; // may be the server or any client
|
|
29
36
|
/** @private */
|
|
30
37
|
this._client = client;
|
|
31
38
|
/** @private */
|
|
32
39
|
this._manager = manager;
|
|
40
|
+
/** @private */
|
|
41
|
+
this._filter = filter;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* true is the state has been detached or deleted
|
|
45
|
+
* @private
|
|
46
|
+
*/
|
|
47
|
+
this._detached = false;
|
|
48
|
+
/* @private */
|
|
49
|
+
this._promiseStore = new PromiseStore(this.constructor.name);
|
|
33
50
|
|
|
34
51
|
try {
|
|
35
52
|
/** @private */
|
|
36
53
|
this._parameters = new ParameterBag(schema, initValues);
|
|
37
|
-
} catch(err) {
|
|
54
|
+
} catch (err) {
|
|
38
55
|
console.error(err.stack);
|
|
39
56
|
|
|
40
57
|
throw new Error(`Error creating or attaching state "${schemaName}" w/ values:\n
|
|
@@ -51,13 +68,13 @@ ${JSON.stringify(initValues, null, 2)}`);
|
|
|
51
68
|
// add listener for state updates
|
|
52
69
|
this._client.transport.addListener(`${UPDATE_RESPONSE}-${this.id}-${this.remoteId}`, async (reqId, updates, context) => {
|
|
53
70
|
const updated = await this._commit(updates, context, true, true);
|
|
54
|
-
|
|
71
|
+
this._promiseStore.resolve(reqId, updated);
|
|
55
72
|
});
|
|
56
73
|
|
|
57
74
|
// retrieve values but do not propagate to subscriptions
|
|
58
75
|
this._client.transport.addListener(`${UPDATE_ABORT}-${this.id}-${this.remoteId}`, async (reqId, updates, context) => {
|
|
59
76
|
const updated = await this._commit(updates, context, false, true);
|
|
60
|
-
|
|
77
|
+
this._promiseStore.resolve(reqId, updated);
|
|
61
78
|
});
|
|
62
79
|
|
|
63
80
|
this._client.transport.addListener(`${UPDATE_NOTIFICATION}-${this.id}-${this.remoteId}`, async (updates, context) => {
|
|
@@ -87,70 +104,121 @@ ${JSON.stringify(initValues, null, 2)}`);
|
|
|
87
104
|
});
|
|
88
105
|
|
|
89
106
|
// ---------------------------------------------
|
|
90
|
-
//
|
|
107
|
+
// state has been deleted by its creator or the schema has been deleted
|
|
91
108
|
// ---------------------------------------------
|
|
92
|
-
this._client.transport.addListener(`${DELETE_NOTIFICATION}-${this.id}-${this.remoteId}`, () => {
|
|
109
|
+
this._client.transport.addListener(`${DELETE_NOTIFICATION}-${this.id}-${this.remoteId}`, async () => {
|
|
93
110
|
this._manager._statesById.delete(this.id);
|
|
94
111
|
this._clearTransport();
|
|
95
112
|
|
|
96
|
-
|
|
97
|
-
|
|
113
|
+
for (let callback of this._onDetachCallbacks) {
|
|
114
|
+
try {
|
|
115
|
+
await callback();
|
|
116
|
+
} catch (err) {
|
|
117
|
+
console.error(err.message);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (this._isOwner) {
|
|
122
|
+
for (let callback of this._onDeleteCallbacks) {
|
|
123
|
+
await callback();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
98
126
|
|
|
99
|
-
this.
|
|
127
|
+
this._onDetachCallbacks.clear();
|
|
128
|
+
this._onDeleteCallbacks.clear();
|
|
129
|
+
this._promiseStore.flush();
|
|
100
130
|
});
|
|
101
131
|
|
|
102
132
|
|
|
103
133
|
if (this._isOwner) {
|
|
104
134
|
// ---------------------------------------------
|
|
105
|
-
//
|
|
135
|
+
// the creator has called `.delete()`
|
|
106
136
|
// ---------------------------------------------
|
|
107
|
-
this._client.transport.addListener(`${DELETE_RESPONSE}-${this.id}-${this.remoteId}`, (reqId) => {
|
|
137
|
+
this._client.transport.addListener(`${DELETE_RESPONSE}-${this.id}-${this.remoteId}`, async (reqId) => {
|
|
108
138
|
this._manager._statesById.delete(this.id);
|
|
109
139
|
this._clearTransport();
|
|
110
140
|
|
|
111
|
-
|
|
112
|
-
|
|
141
|
+
for (let callback of this._onDetachCallbacks) {
|
|
142
|
+
await callback();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
for (let callback of this._onDeleteCallbacks) {
|
|
146
|
+
await callback();
|
|
147
|
+
}
|
|
113
148
|
|
|
114
|
-
this.
|
|
115
|
-
|
|
149
|
+
this._onDetachCallbacks.clear();
|
|
150
|
+
this._onDeleteCallbacks.clear();
|
|
151
|
+
this._promiseStore.resolve(reqId, this);
|
|
152
|
+
this._promiseStore.flush();
|
|
116
153
|
});
|
|
117
154
|
|
|
118
155
|
this._client.transport.addListener(`${DELETE_ERROR}-${this.id}`, (reqId, msg) => {
|
|
119
|
-
|
|
156
|
+
this._promiseStore.reject(reqId, msg);
|
|
120
157
|
});
|
|
121
158
|
|
|
122
159
|
} else {
|
|
123
160
|
// ---------------------------------------------
|
|
124
|
-
//
|
|
161
|
+
// the attached node has called `.detach()`
|
|
125
162
|
// ---------------------------------------------
|
|
126
|
-
this._client.transport.addListener(`${DETACH_RESPONSE}-${this.id}-${this.remoteId}`, (reqId) => {
|
|
163
|
+
this._client.transport.addListener(`${DETACH_RESPONSE}-${this.id}-${this.remoteId}`, async (reqId) => {
|
|
127
164
|
this._manager._statesById.delete(this.id);
|
|
128
165
|
this._clearTransport();
|
|
129
166
|
|
|
130
|
-
this._onDetachCallbacks
|
|
167
|
+
for (let callback of this._onDetachCallbacks) {
|
|
168
|
+
await callback();
|
|
169
|
+
}
|
|
131
170
|
|
|
132
|
-
this.
|
|
133
|
-
|
|
171
|
+
this._onDetachCallbacks.clear();
|
|
172
|
+
this._onDeleteCallbacks.clear();
|
|
173
|
+
this._promiseStore.resolve(reqId, this);
|
|
174
|
+
this._promiseStore.flush();
|
|
134
175
|
});
|
|
135
176
|
|
|
136
177
|
// the state does not exists anymore in the server (should not happen)
|
|
137
178
|
this._client.transport.addListener(`${DETACH_ERROR}-${this.id}`, (reqId, msg) => {
|
|
138
|
-
|
|
179
|
+
this._onDetachCallbacks.clear();
|
|
180
|
+
this._onDeleteCallbacks.clear();
|
|
181
|
+
this._promiseStore.reject(reqId, msg);
|
|
182
|
+
this._promiseStore.flush();
|
|
139
183
|
});
|
|
140
|
-
|
|
141
184
|
}
|
|
142
185
|
}
|
|
143
186
|
|
|
144
|
-
/**
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
187
|
+
/**
|
|
188
|
+
* Id of the state
|
|
189
|
+
* @type {Number}
|
|
190
|
+
* @readonly
|
|
191
|
+
*/
|
|
192
|
+
get id() {
|
|
193
|
+
return this._id;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Unique id of the state for the current node
|
|
198
|
+
* @readonly
|
|
199
|
+
* @type {Number}
|
|
200
|
+
* @private
|
|
201
|
+
*/
|
|
202
|
+
get remoteId() {
|
|
203
|
+
return this._remoteId;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Name of the schema
|
|
208
|
+
* @type {String}
|
|
209
|
+
* @readonly
|
|
210
|
+
*/
|
|
211
|
+
get schemaName() {
|
|
212
|
+
return this._schemaName;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Indicates if the node is the owner of the state
|
|
217
|
+
* @type {Boolean}
|
|
218
|
+
* @readonly
|
|
219
|
+
*/
|
|
220
|
+
get isOwner() {
|
|
221
|
+
return this._isOwner;
|
|
154
222
|
}
|
|
155
223
|
|
|
156
224
|
/** @private */
|
|
@@ -158,9 +226,16 @@ ${JSON.stringify(initValues, null, 2)}`);
|
|
|
158
226
|
// remove listeners
|
|
159
227
|
this._client.transport.removeAllListeners(`${UPDATE_RESPONSE}-${this.id}-${this.remoteId}`);
|
|
160
228
|
this._client.transport.removeAllListeners(`${UPDATE_NOTIFICATION}-${this.id}-${this.remoteId}`);
|
|
161
|
-
this._client.transport.removeAllListeners(`${
|
|
229
|
+
this._client.transport.removeAllListeners(`${UPDATE_ABORT}-${this.id}-${this.remoteId}`);
|
|
162
230
|
this._client.transport.removeAllListeners(`${DELETE_NOTIFICATION}-${this.id}-${this.remoteId}`);
|
|
163
|
-
|
|
231
|
+
|
|
232
|
+
if (this._isOwner) {
|
|
233
|
+
this._client.transport.removeAllListeners(`${DELETE_RESPONSE}-${this.id}-${this.remoteId}`);
|
|
234
|
+
this._client.transport.removeAllListeners(`${DELETE_ERROR}-${this.id}-${this.remoteId}`);
|
|
235
|
+
} else {
|
|
236
|
+
this._client.transport.removeAllListeners(`${DETACH_RESPONSE}-${this.id}-${this.remoteId}`);
|
|
237
|
+
this._client.transport.removeAllListeners(`${DETACH_ERROR}-${this.id}-${this.remoteId}`);
|
|
238
|
+
}
|
|
164
239
|
}
|
|
165
240
|
|
|
166
241
|
/** @private */
|
|
@@ -255,85 +330,222 @@ ${JSON.stringify(initValues, null, 2)}`);
|
|
|
255
330
|
* const updates = await state.set({ myParam: Math.random() });
|
|
256
331
|
*/
|
|
257
332
|
async set(updates, context = null) {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
333
|
+
if (this._detached) {
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (!isPlainObject(updates)) {
|
|
338
|
+
throw new TypeError(`[SharedState] State "${this.schemaName}": state.set(updates[, context]) should receive an object as first parameter`);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (context !== null && !isPlainObject(context)) {
|
|
342
|
+
throw new TypeError(`[SharedState] State "${this.schemaName}": state.set(updates[, context]) should receive an object as second parameter`);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const newValues = {};
|
|
346
|
+
const oldValues = {};
|
|
347
|
+
const localParams = {};
|
|
348
|
+
const sharedParams = {};
|
|
349
|
+
let hasLocalParam = false;
|
|
350
|
+
let hasSharedParam = false;
|
|
351
|
+
let forwardParams = undefined;
|
|
261
352
|
let propagateNow = false;
|
|
262
353
|
|
|
263
354
|
for (let name in updates) {
|
|
264
|
-
//
|
|
265
|
-
|
|
266
|
-
|
|
355
|
+
// Try to coerce value early, so that eventual errors are triggered early
|
|
356
|
+
// on the node requesting the update, and not only on the server side
|
|
357
|
+
// This throws if name does not exists
|
|
358
|
+
this._parameters.coerceValue(name, updates[name]);
|
|
359
|
+
|
|
360
|
+
// Check that name is in filter list, if any
|
|
361
|
+
if (this._filter !== null) {
|
|
362
|
+
if (!this._filter.includes(name)) {
|
|
363
|
+
throw new DOMException(`[SharedState] State "${this.schemaName}": cannot set parameter '${name}', parameter is not in filter list`, 'NotSupportedError');
|
|
364
|
+
}
|
|
267
365
|
}
|
|
268
366
|
|
|
269
|
-
//
|
|
270
|
-
//
|
|
367
|
+
// `immediate` option behavior
|
|
368
|
+
//
|
|
369
|
+
// If immediate=true
|
|
271
370
|
// - call listeners if value changed
|
|
272
371
|
// - go through normal server path
|
|
273
372
|
// - retrigger only if response from server is different from current value
|
|
274
|
-
//
|
|
373
|
+
// If immediate=true && (filterChange=false || event=true)
|
|
275
374
|
// - call listeners with value regarless it changed
|
|
276
375
|
// - go through normal server path
|
|
277
376
|
// - if the node is initiator of the update (UPDATE_RESPONSE), (re-)check
|
|
278
377
|
// to prevent execute the listeners twice
|
|
279
378
|
|
|
280
|
-
|
|
379
|
+
// `local` option behavior
|
|
380
|
+
//
|
|
381
|
+
// - If the `updates` object only contains local variables, we can call the
|
|
382
|
+
// update listeners and return a fulfilled promise immediately
|
|
383
|
+
// - If parameters that require network communication are present, we call the
|
|
384
|
+
// update callback onces with the local payload, then we need to wait for the server
|
|
385
|
+
// response, call update listeners with server response and resolve promise with
|
|
386
|
+
// the full payload, i.e. reintegrating the local params in the resolve payload
|
|
387
|
+
|
|
388
|
+
const { local, immediate, filterChange, event } = this._parameters.getSchema(name);
|
|
281
389
|
|
|
282
|
-
if (immediate) {
|
|
390
|
+
if (immediate || local) {
|
|
283
391
|
const oldValue = this._parameters.get(name);
|
|
284
392
|
const [newValue, changed] = this._parameters.set(name, updates[name]);
|
|
285
393
|
|
|
394
|
+
// prepare data for immediate propagation of listeners
|
|
286
395
|
if (changed || filterChange === false || event) {
|
|
287
|
-
|
|
288
|
-
|
|
396
|
+
oldValues[name] = oldValue;
|
|
397
|
+
newValues[name] = newValue;
|
|
289
398
|
propagateNow = true;
|
|
290
399
|
}
|
|
291
400
|
}
|
|
401
|
+
|
|
402
|
+
// define params that must go through network or not
|
|
403
|
+
if (local) {
|
|
404
|
+
hasLocalParam = true;
|
|
405
|
+
// get sanitize value for fulfilling promise
|
|
406
|
+
localParams[name] = this._parameters.get(name);
|
|
407
|
+
} else {
|
|
408
|
+
// note that immediate are shared params too
|
|
409
|
+
hasSharedParam = true;
|
|
410
|
+
sharedParams[name] = updates[name];
|
|
411
|
+
}
|
|
292
412
|
}
|
|
293
413
|
|
|
414
|
+
// propagate immediate params if changed
|
|
294
415
|
if (propagateNow) {
|
|
295
|
-
this._onUpdateCallbacks.forEach(listener => listener(
|
|
416
|
+
this._onUpdateCallbacks.forEach(listener => listener(newValues, oldValues, context));
|
|
296
417
|
}
|
|
297
418
|
|
|
419
|
+
// check if we can resolve immediately or if we need to go through network
|
|
420
|
+
if (hasLocalParam) {
|
|
421
|
+
if (!hasSharedParam) {
|
|
422
|
+
return Promise.resolve(localParams);
|
|
423
|
+
} else {
|
|
424
|
+
// store local params to fulfill promise with all values, see PromiseStore
|
|
425
|
+
forwardParams = localParams;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// override updates to be shared on network without local params
|
|
430
|
+
updates = sharedParams;
|
|
431
|
+
|
|
298
432
|
// go through server-side normal behavior
|
|
299
433
|
return new Promise((resolve, reject) => {
|
|
300
|
-
const reqId =
|
|
434
|
+
const reqId = this._promiseStore.add(resolve, reject, 'update-request', forwardParams);
|
|
301
435
|
this._client.transport.emit(`${UPDATE_REQUEST}-${this.id}-${this.remoteId}`, reqId, updates, context);
|
|
302
436
|
});
|
|
303
437
|
}
|
|
304
438
|
|
|
305
439
|
/**
|
|
306
|
-
* Get the value of a
|
|
440
|
+
* Get the value of a parameter of the state. If the parameter is of `any` type,
|
|
441
|
+
* a deep copy is returned.
|
|
307
442
|
*
|
|
308
443
|
* @param {string} name - Name of the param.
|
|
309
444
|
* @throws Throws if `name` does not correspond to an existing field
|
|
310
445
|
* of the state.
|
|
311
446
|
* @return {mixed}
|
|
312
447
|
* @example
|
|
313
|
-
* const value = state.get('
|
|
448
|
+
* const value = state.get('paramName');
|
|
314
449
|
*/
|
|
315
450
|
get(name) {
|
|
451
|
+
if (!this._parameters.has(name)) {
|
|
452
|
+
throw new ReferenceError(`[SharedState] State "${this.schemaName}": Cannot get value of undefined parameter "${name}"`);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (this._filter !== null) {
|
|
456
|
+
if (!this._filter.includes(name)) {
|
|
457
|
+
throw new DOMException(`[SharedState] State "${this.schemaName}": cannot get parameter '${name}', parameter is not in filter list`, 'NotSupportedError');
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
316
461
|
return this._parameters.get(name);
|
|
317
462
|
}
|
|
318
463
|
|
|
464
|
+
/**
|
|
465
|
+
* Similar to `get` but returns a reference to the underlying value in case of
|
|
466
|
+
* `any` type. May be usefull if the underlying value is big (e.g. sensors
|
|
467
|
+
* recordings, etc.) and deep cloning expensive. Be aware that if changes are
|
|
468
|
+
* made on the returned object, the state of your application will become
|
|
469
|
+
* inconsistent.
|
|
470
|
+
*
|
|
471
|
+
* @param {string} name - Name of the param.
|
|
472
|
+
* @throws Throws if `name` does not correspond to an existing field
|
|
473
|
+
* of the state.
|
|
474
|
+
* @return {mixed}
|
|
475
|
+
* @example
|
|
476
|
+
* const value = state.getUnsafe('paramName');
|
|
477
|
+
*/
|
|
478
|
+
getUnsafe(name) {
|
|
479
|
+
if (!this._parameters.has(name)) {
|
|
480
|
+
throw new ReferenceError(`[SharedState] State "${this.schemaName}": Cannot get value of undefined parameter "${name}"`);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (this._filter !== null) {
|
|
484
|
+
if (!this._filter.includes(name)) {
|
|
485
|
+
throw new DOMException(`[SharedState] State "${this.schemaName}": cannot get parameter '${name}', parameter is not in filter list`, 'NotSupportedError');
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
return this._parameters.getUnsafe(name);
|
|
490
|
+
}
|
|
491
|
+
|
|
319
492
|
/**
|
|
320
493
|
* Get all the key / value pairs of the state.
|
|
321
494
|
*
|
|
495
|
+
* If a parameter is of `any` type, a deep copy is made.
|
|
496
|
+
*
|
|
322
497
|
* @return {object}
|
|
323
498
|
* @example
|
|
324
499
|
* const values = state.getValues();
|
|
325
500
|
*/
|
|
326
501
|
getValues() {
|
|
327
|
-
|
|
502
|
+
const values = this._parameters.getValues();
|
|
503
|
+
|
|
504
|
+
if (this._filter !== null) {
|
|
505
|
+
for (let name in values) {
|
|
506
|
+
if (!this._filter.includes(name)) {
|
|
507
|
+
delete values[name];
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
return values;
|
|
328
513
|
}
|
|
329
514
|
|
|
330
515
|
/**
|
|
331
|
-
* Get the
|
|
516
|
+
* Get all the key / value pairs of the state.
|
|
517
|
+
*
|
|
518
|
+
* Similar to `getValues` but returns a reference to the underlying value in
|
|
519
|
+
* case of `any` type. May be usefull if the underlying value is big (e.g.
|
|
520
|
+
* sensors recordings, etc.) and deep cloning expensive. Be aware that if
|
|
521
|
+
* changes are made on the returned object, the state of your application will
|
|
522
|
+
* become inconsistent.
|
|
332
523
|
*
|
|
333
|
-
* @
|
|
334
|
-
*
|
|
524
|
+
* @return {object}
|
|
525
|
+
* @example
|
|
526
|
+
* const values = state.getValues();
|
|
527
|
+
*/
|
|
528
|
+
getValuesUnsafe() {
|
|
529
|
+
const values = this._parameters.getValuesUnsafe();
|
|
530
|
+
|
|
531
|
+
if (this._filter !== null) {
|
|
532
|
+
for (let name in values) {
|
|
533
|
+
if (!this._filter.includes(name)) {
|
|
534
|
+
delete values[name];
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
return values;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Definition of schema from which the state has been created.
|
|
544
|
+
*
|
|
545
|
+
* @param {string} [name=null] - If given, returns only the definition
|
|
546
|
+
* corresponding to the given param name.
|
|
335
547
|
* @throws Throws if `name` does not correspond to an existing field
|
|
336
|
-
* of the
|
|
548
|
+
* of the schema.
|
|
337
549
|
* @return {object}
|
|
338
550
|
* @example
|
|
339
551
|
* const schema = state.getSchema();
|
|
@@ -367,7 +579,7 @@ ${JSON.stringify(initValues, null, 2)}`);
|
|
|
367
579
|
|
|
368
580
|
/**
|
|
369
581
|
* Detach from the state. If the client is the creator of the state, the state
|
|
370
|
-
* is deleted and all attached nodes get notified
|
|
582
|
+
* is deleted and all attached nodes get notified.
|
|
371
583
|
*
|
|
372
584
|
* @example
|
|
373
585
|
* const state = await client.state.attach('globals');
|
|
@@ -375,16 +587,21 @@ ${JSON.stringify(initValues, null, 2)}`);
|
|
|
375
587
|
* await state.detach();
|
|
376
588
|
*/
|
|
377
589
|
async detach() {
|
|
590
|
+
if (this._detached) {
|
|
591
|
+
throw new Error(`[SharedState] State "${this.schemaName} (${this.id})" already detached, cannot detach twice`);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
this._detached = true; // mark detached early
|
|
378
595
|
this._onUpdateCallbacks.clear();
|
|
379
596
|
|
|
380
597
|
if (this._isOwner) {
|
|
381
598
|
return new Promise((resolve, reject) => {
|
|
382
|
-
const reqId =
|
|
599
|
+
const reqId = this._promiseStore.add(resolve, reject, 'delete-request');
|
|
383
600
|
this._client.transport.emit(`${DELETE_REQUEST}-${this.id}-${this.remoteId}`, reqId);
|
|
384
601
|
});
|
|
385
602
|
} else {
|
|
386
603
|
return new Promise((resolve, reject) => {
|
|
387
|
-
const reqId =
|
|
604
|
+
const reqId = this._promiseStore.add(resolve, reject, 'detach-request');
|
|
388
605
|
this._client.transport.emit(`${DETACH_REQUEST}-${this.id}-${this.remoteId}`, reqId);
|
|
389
606
|
});
|
|
390
607
|
}
|
|
@@ -392,9 +609,11 @@ ${JSON.stringify(initValues, null, 2)}`);
|
|
|
392
609
|
|
|
393
610
|
/**
|
|
394
611
|
* Delete the state. Only the creator/owner of the state can use this method.
|
|
395
|
-
*
|
|
396
|
-
*
|
|
397
|
-
*
|
|
612
|
+
*
|
|
613
|
+
* All nodes attached to the state will be detached, triggering any registered
|
|
614
|
+
* `onDetach` callbacks. The creator of the state will also have its own `onDelete`
|
|
615
|
+
* callback triggered. The local `onDeatch` and `onDelete` callbacks will be
|
|
616
|
+
* executed *before* the returned Promise resolves
|
|
398
617
|
*
|
|
399
618
|
* @throws Throws if the method is called by a node which is not the owner of
|
|
400
619
|
* the state.
|
|
@@ -405,16 +624,20 @@ ${JSON.stringify(initValues, null, 2)}`);
|
|
|
405
624
|
*/
|
|
406
625
|
async delete() {
|
|
407
626
|
if (this._isOwner) {
|
|
627
|
+
if (this._detached) {
|
|
628
|
+
throw new Error(`[SharedState] State "${this.schemaName} (${this.id})" already deleted, cannot delete twice`);
|
|
629
|
+
}
|
|
630
|
+
|
|
408
631
|
return this.detach();
|
|
409
632
|
} else {
|
|
410
|
-
throw new Error(`[
|
|
633
|
+
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
634
|
}
|
|
412
635
|
}
|
|
413
636
|
|
|
414
637
|
/**
|
|
415
638
|
* Subscribe to state updates.
|
|
416
639
|
*
|
|
417
|
-
* @param {client.SharedState~onUpdateCallback|
|
|
640
|
+
* @param {client.SharedState~onUpdateCallback|server.SharedState~onUpdateCallback} callback
|
|
418
641
|
* Callback to execute when an update is applied on the state.
|
|
419
642
|
* @param {Boolean} [executeListener=false] - Execute the callback immediately
|
|
420
643
|
* with current state values. (`oldValues` will be set to `{}`, and `context` to `null`)
|
|
@@ -447,10 +670,11 @@ ${JSON.stringify(initValues, null, 2)}`);
|
|
|
447
670
|
}
|
|
448
671
|
|
|
449
672
|
/**
|
|
450
|
-
* Register a function to execute when detaching from the state
|
|
673
|
+
* Register a function to execute when detaching from the state. The function
|
|
674
|
+
* will be executed before the `detach` promise resolves.
|
|
451
675
|
*
|
|
452
|
-
* @param {Function} callback -
|
|
453
|
-
*
|
|
676
|
+
* @param {Function} callback - Callback to execute when detaching from the state.
|
|
677
|
+
* Whether the client as called `detach`, or the state has been deleted by its
|
|
454
678
|
* creator.
|
|
455
679
|
*/
|
|
456
680
|
onDetach(callback) {
|
|
@@ -460,9 +684,10 @@ ${JSON.stringify(initValues, null, 2)}`);
|
|
|
460
684
|
|
|
461
685
|
/**
|
|
462
686
|
* 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`
|
|
687
|
+
* node was the creator of the state. Is called after `onDetach` and executed
|
|
688
|
+
* before the `delete` Promise resolves.
|
|
464
689
|
*
|
|
465
|
-
* @param {Function} callback -
|
|
690
|
+
* @param {Function} callback - Callback to execute when the state is deleted.
|
|
466
691
|
*/
|
|
467
692
|
onDelete(callback) {
|
|
468
693
|
this._onDeleteCallbacks.add(callback);
|