@soundworks/core 5.6.0 → 5.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/package.json +1 -1
- package/src/common/BaseStateManager.js +246 -219
- package/src/common/SharedState.js +395 -260
- package/src/common/SharedStateCollection.js +79 -29
- package/src/common/shared-state-utils.js +102 -0
- package/src/common/version.js +1 -1
- package/src/server/ServerStateManager.js +234 -223
- package/src/server/SharedStatePrivate.js +100 -82
- package/types/common/BaseStateManager.d.ts +34 -47
- package/types/common/BaseStateManager.d.ts.map +1 -1
- package/types/common/SharedState.d.ts +50 -29
- package/types/common/SharedState.d.ts.map +1 -1
- package/types/common/SharedStateCollection.d.ts +26 -8
- package/types/common/SharedStateCollection.d.ts.map +1 -1
- package/types/common/shared-state-utils.d.ts +12 -0
- package/types/common/shared-state-utils.d.ts.map +1 -0
- package/types/common/version.d.ts +1 -1
- package/types/server/ServerStateManager.d.ts +10 -10
- package/types/server/ServerStateManager.d.ts.map +1 -1
- package/types/server/SharedStatePrivate.d.ts +2 -3
- package/types/server/SharedStatePrivate.d.ts.map +1 -1
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
isPlainObject,
|
|
3
|
+
isString,
|
|
4
|
+
isFunction,
|
|
5
|
+
} from '@ircam/sc-utils';
|
|
2
6
|
|
|
3
7
|
import ParameterBag from './ParameterBag.js';
|
|
4
8
|
import PromiseStore from './PromiseStore.js';
|
|
@@ -20,8 +24,13 @@ import {
|
|
|
20
24
|
import {
|
|
21
25
|
kStateManagerClient,
|
|
22
26
|
kStateManagerDeleteState,
|
|
27
|
+
kPendingSharedStateConstructionData,
|
|
23
28
|
} from './BaseStateManager.js';
|
|
24
29
|
|
|
30
|
+
import {
|
|
31
|
+
sanitizeOnUpdateParams,
|
|
32
|
+
} from './shared-state-utils.js';
|
|
33
|
+
|
|
25
34
|
import warnings from './logs/warnings.js';
|
|
26
35
|
|
|
27
36
|
// for testing purposes
|
|
@@ -31,10 +40,14 @@ export const kSharedStatePromiseStore = Symbol('soundworks:shared-state-promise-
|
|
|
31
40
|
* Callback executed when updates are applied on a {@link SharedState}.
|
|
32
41
|
*
|
|
33
42
|
* @callback sharedStateOnUpdateCallback
|
|
34
|
-
* @param {Object} newValues - Key / value pairs of the updates that have been
|
|
35
|
-
* applied to the state
|
|
36
|
-
*
|
|
37
|
-
*
|
|
43
|
+
* @param {Object|any} newValues - Key / value pairs of the updates that have been
|
|
44
|
+
* applied to the state, or single value if the callback has been registered against
|
|
45
|
+
* a parameter name filter.
|
|
46
|
+
* @param {Object|any} oldValues - Key / value pairs of the updated params before
|
|
47
|
+
* the updates has been applied to the state, or single value if the callback has been
|
|
48
|
+
* registered against a parameter name filter.
|
|
49
|
+
* @param {sharedStateDeleteOnUpdateCallback} unsubscribe - Reference to unsubscribe
|
|
50
|
+
* function returned by the `SharedState#onUpdate` method.
|
|
38
51
|
*/
|
|
39
52
|
|
|
40
53
|
/**
|
|
@@ -107,243 +120,197 @@ class SharedState {
|
|
|
107
120
|
#client = null;
|
|
108
121
|
#manager = null;
|
|
109
122
|
#filter = null;
|
|
110
|
-
// true is the state has been detached or deleted
|
|
111
123
|
#detached = false;
|
|
112
|
-
// only valid for owners
|
|
113
124
|
#hasSiblings = false;
|
|
114
125
|
#parameters = null;
|
|
115
126
|
#onUpdateCallbacks = new Set();
|
|
116
127
|
#onDetachCallbacks = new Set();
|
|
117
128
|
#onDeleteCallbacks = new Set();
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
this.#
|
|
130
|
-
this.#client = manager[kStateManagerClient];
|
|
131
|
-
this.#className = className;
|
|
132
|
-
this.#id = stateId;
|
|
133
|
-
this.#instanceId = instanceId;
|
|
134
|
-
this.#isOwner = isOwner; // may be any node
|
|
135
|
-
this.#filter = filter;
|
|
129
|
+
#waitForConditions = new Set();
|
|
130
|
+
|
|
131
|
+
constructor() {
|
|
132
|
+
// cf. BaseStateManager#buildSharedState to get the whole pattern
|
|
133
|
+
// rationale is to have a clean constructor API for future derived classes
|
|
134
|
+
this.#manager = globalThis[kPendingSharedStateConstructionData].manager;
|
|
135
|
+
this.#client = globalThis[kPendingSharedStateConstructionData].manager[kStateManagerClient];
|
|
136
|
+
this.#className = globalThis[kPendingSharedStateConstructionData].className;
|
|
137
|
+
this.#id = globalThis[kPendingSharedStateConstructionData].stateId;
|
|
138
|
+
this.#instanceId =globalThis[kPendingSharedStateConstructionData]. instanceId;
|
|
139
|
+
this.#isOwner = globalThis[kPendingSharedStateConstructionData].isOwner; // may be any node
|
|
140
|
+
this.#filter = globalThis[kPendingSharedStateConstructionData].filter;
|
|
136
141
|
|
|
137
142
|
try {
|
|
138
|
-
this.#parameters = new ParameterBag(
|
|
143
|
+
this.#parameters = new ParameterBag(
|
|
144
|
+
globalThis[kPendingSharedStateConstructionData].classDescription,
|
|
145
|
+
globalThis[kPendingSharedStateConstructionData].initValues,
|
|
146
|
+
);
|
|
139
147
|
} catch (err) {
|
|
140
|
-
throw new Error(`Cannot construct 'SharedState': ${err.message}`);
|
|
148
|
+
throw new Error(`Cannot construct 'SharedState' (${this.#className}): ${err.message}`);
|
|
141
149
|
}
|
|
142
150
|
|
|
143
151
|
/** @private */
|
|
144
152
|
this[kSharedStatePromiseStore] = new PromiseStore(this.constructor.name);
|
|
145
153
|
|
|
146
|
-
|
|
147
|
-
this.#client.transport.addListener(`${
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
});
|
|
154
|
+
this.#client.transport.addListener(`${UPDATE_RESPONSE}-${this.#id}-${this.#instanceId}`, this.#onUpdateResponse);
|
|
155
|
+
this.#client.transport.addListener(`${UPDATE_ABORT}-${this.#id}-${this.#instanceId}`, this.#onUpdateAbort);
|
|
156
|
+
this.#client.transport.addListener(`${UPDATE_NOTIFICATION}-${this.#id}-${this.#instanceId}`, this.#onUpdateNotification);
|
|
157
|
+
this.#client.transport.addListener(`${DELETE_NOTIFICATION}-${this.#id}-${this.#instanceId}`, this.#onDeleteNotification);
|
|
151
158
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
this
|
|
156
|
-
}
|
|
159
|
+
if (this.#isOwner) {
|
|
160
|
+
this.#client.transport.addListener(`${DELETE_RESPONSE}-${this.#id}-${this.#instanceId}`, this.#onDeleteResponse);
|
|
161
|
+
this.#client.transport.addListener(`${DELETE_ERROR}-${this.#id}`, this.#onDeleteError);
|
|
162
|
+
this.#client.transport.addListener(`${HAS_SIBLINGS_NOTIFICATION}-${this.#id}-${this.#instanceId}`, this.#onHasSiblingsNotification);
|
|
163
|
+
} else {
|
|
164
|
+
this.#client.transport.addListener(`${DETACH_RESPONSE}-${this.#id}-${this.#instanceId}`, this.#onDetachResponse);
|
|
165
|
+
this.#client.transport.addListener(`${DETACH_ERROR}-${this.#id}`, this.#onDetachError);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
157
168
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
// # note: 2002-10-03
|
|
162
|
-
//
|
|
163
|
-
// `setTimeout(async () => this.#commit(updates, true, false));`
|
|
164
|
-
// appears to be the only way to push the update commit in the next event
|
|
165
|
-
// cycle so that `attach` can resolve before the update notification is
|
|
166
|
-
// actually dispatched. The alternative:
|
|
167
|
-
// `Promise.resolve().then(() => this.#commit(updates, true, false))``
|
|
168
|
-
// does not behave as expected...
|
|
169
|
-
//
|
|
170
|
-
// However this breaks the reliability of:
|
|
171
|
-
// ```
|
|
172
|
-
// /* given a0, a1 and a2 being 3 similar attached states */
|
|
173
|
-
// await state.set({ int: i });
|
|
174
|
-
//
|
|
175
|
-
// assert.equal(a0.get('int'), i);
|
|
176
|
-
// assert.equal(a1.get('int'), i);
|
|
177
|
-
// assert.equal(a2.get('int'), i);
|
|
178
|
-
// ```
|
|
179
|
-
// which is far more important than the edge case reported in the issue
|
|
180
|
-
// therefore this wont be fixed for now
|
|
181
|
-
this.#commit(updates, true, false);
|
|
182
|
-
});
|
|
169
|
+
#cleanup() {
|
|
170
|
+
this.#detached = true;
|
|
171
|
+
this.#manager[kStateManagerDeleteState](this.#id);
|
|
183
172
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
this.#client.transport.
|
|
188
|
-
this.#detached = true;
|
|
173
|
+
this.#client.transport.removeAllListeners(`${UPDATE_RESPONSE}-${this.#id}-${this.#instanceId}`);
|
|
174
|
+
this.#client.transport.removeAllListeners(`${UPDATE_NOTIFICATION}-${this.#id}-${this.#instanceId}`);
|
|
175
|
+
this.#client.transport.removeAllListeners(`${UPDATE_ABORT}-${this.#id}-${this.#instanceId}`);
|
|
176
|
+
this.#client.transport.removeAllListeners(`${DELETE_NOTIFICATION}-${this.#id}-${this.#instanceId}`);
|
|
189
177
|
|
|
190
|
-
|
|
191
|
-
this.#
|
|
178
|
+
if (this.#isOwner) {
|
|
179
|
+
this.#client.transport.removeAllListeners(`${DELETE_RESPONSE}-${this.#id}-${this.#instanceId}`);
|
|
180
|
+
this.#client.transport.removeAllListeners(`${DELETE_ERROR}-${this.#id}-${this.#instanceId}`);
|
|
181
|
+
this.#client.transport.removeAllListeners(`${HAS_SIBLINGS_NOTIFICATION}-${this.#id}-${this.#instanceId}`);
|
|
182
|
+
} else {
|
|
183
|
+
this.#client.transport.removeAllListeners(`${DETACH_RESPONSE}-${this.#id}-${this.#instanceId}`);
|
|
184
|
+
this.#client.transport.removeAllListeners(`${DETACH_ERROR}-${this.#id}-${this.#instanceId}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
192
187
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}
|
|
199
|
-
}
|
|
188
|
+
#onUpdateResponse = async (reqId, updates) => {
|
|
189
|
+
const updated = await this.#commit(updates, true, true);
|
|
190
|
+
this[kSharedStatePromiseStore].resolve(reqId, updated);
|
|
191
|
+
this.#checkPendingWaitForConditions();
|
|
192
|
+
};
|
|
200
193
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
194
|
+
#onUpdateAbort = async (reqId, updates) => {
|
|
195
|
+
const updated = await this.#commit(updates, false, true);
|
|
196
|
+
this[kSharedStatePromiseStore].resolve(reqId, updated);
|
|
197
|
+
this.#checkPendingWaitForConditions();
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
#onUpdateNotification = updates => {
|
|
201
|
+
this.#commit(updates, true, false);
|
|
202
|
+
this.#checkPendingWaitForConditions();
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
#onDetachResponse = async (reqId) => {
|
|
206
|
+
this.#cleanup();
|
|
207
|
+
|
|
208
|
+
for (let callback of this.#onDetachCallbacks) {
|
|
209
|
+
try {
|
|
210
|
+
await callback();
|
|
211
|
+
} catch (err) {
|
|
212
|
+
console.log(err.message);
|
|
205
213
|
}
|
|
214
|
+
}
|
|
206
215
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
216
|
+
this.#onDetachCallbacks.clear();
|
|
217
|
+
this.#onDeleteCallbacks.clear();
|
|
218
|
+
this[kSharedStatePromiseStore].resolve(reqId, this);
|
|
219
|
+
this[kSharedStatePromiseStore].flush();
|
|
220
|
+
};
|
|
211
221
|
|
|
222
|
+
#onDetachError = (reqId, msg) => {
|
|
223
|
+
this.#onDetachCallbacks.clear();
|
|
224
|
+
this.#onDeleteCallbacks.clear();
|
|
225
|
+
this[kSharedStatePromiseStore].reject(reqId, msg);
|
|
226
|
+
this[kSharedStatePromiseStore].flush();
|
|
227
|
+
};
|
|
212
228
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
// the creator has called `.delete()`
|
|
216
|
-
// ---------------------------------------------
|
|
217
|
-
this.#client.transport.addListener(`${DELETE_RESPONSE}-${this.#id}-${this.#instanceId}`, async (reqId) => {
|
|
218
|
-
this.#manager[kStateManagerDeleteState](this.#id);
|
|
219
|
-
this.#clearTransport();
|
|
220
|
-
|
|
221
|
-
for (let callback of this.#onDetachCallbacks) {
|
|
222
|
-
try {
|
|
223
|
-
await callback();
|
|
224
|
-
} catch (err) {
|
|
225
|
-
console.log(err.message);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
229
|
+
#onDeleteResponse = async (reqId) => {
|
|
230
|
+
this.#cleanup();
|
|
228
231
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
232
|
+
for (let callback of this.#onDetachCallbacks) {
|
|
233
|
+
try {
|
|
234
|
+
await callback();
|
|
235
|
+
} catch (err) {
|
|
236
|
+
console.log(err.message);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
236
239
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
240
|
+
for (let callback of this.#onDeleteCallbacks) {
|
|
241
|
+
try {
|
|
242
|
+
await callback();
|
|
243
|
+
} catch (err) {
|
|
244
|
+
console.log(err.message);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
242
247
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
248
|
+
this.#onDetachCallbacks.clear();
|
|
249
|
+
this.#onDeleteCallbacks.clear();
|
|
250
|
+
this[kSharedStatePromiseStore].resolve(reqId, this);
|
|
251
|
+
this[kSharedStatePromiseStore].flush();
|
|
252
|
+
};
|
|
246
253
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
254
|
+
#onDeleteError = (reqId, msg) => {
|
|
255
|
+
this[kSharedStatePromiseStore].reject(reqId, msg);
|
|
256
|
+
};
|
|
250
257
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
// the attached node has called `.detach()`
|
|
254
|
-
// ---------------------------------------------
|
|
255
|
-
this.#client.transport.addListener(`${DETACH_RESPONSE}-${this.#id}-${this.#instanceId}`, async (reqId) => {
|
|
256
|
-
this.#manager[kStateManagerDeleteState](this.#id);
|
|
257
|
-
this.#clearTransport();
|
|
258
|
-
|
|
259
|
-
for (let callback of this.#onDetachCallbacks) {
|
|
260
|
-
try {
|
|
261
|
-
await callback();
|
|
262
|
-
} catch (err) {
|
|
263
|
-
console.log(err.message);
|
|
264
|
-
}
|
|
265
|
-
}
|
|
258
|
+
#onDeleteNotification = async () => {
|
|
259
|
+
this.#cleanup();
|
|
266
260
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
261
|
+
for (let callback of this.#onDetachCallbacks) {
|
|
262
|
+
try {
|
|
263
|
+
await callback();
|
|
264
|
+
} catch (err) {
|
|
265
|
+
console.error(err.message);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
272
268
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
this[kSharedStatePromiseStore].reject(reqId, msg);
|
|
278
|
-
this[kSharedStatePromiseStore].flush();
|
|
279
|
-
});
|
|
269
|
+
if (this.#isOwner) {
|
|
270
|
+
for (let callback of this.#onDeleteCallbacks) {
|
|
271
|
+
await callback();
|
|
272
|
+
}
|
|
280
273
|
}
|
|
281
|
-
}
|
|
282
274
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
get id() {
|
|
288
|
-
return this.#id;
|
|
289
|
-
}
|
|
275
|
+
this.#onDetachCallbacks.clear();
|
|
276
|
+
this.#onDeleteCallbacks.clear();
|
|
277
|
+
this[kSharedStatePromiseStore].flush();
|
|
278
|
+
};
|
|
290
279
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
*/
|
|
295
|
-
get className() {
|
|
296
|
-
return this.#className;
|
|
297
|
-
}
|
|
280
|
+
#onHasSiblingsNotification = hasSiblings => {
|
|
281
|
+
this.#hasSiblings = hasSiblings;
|
|
282
|
+
};
|
|
298
283
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
warnings.deprecated('SharedState#schemaName', 'SharedState#className', '4.0.0-alpha.29');
|
|
304
|
-
return this.className;
|
|
305
|
-
}
|
|
284
|
+
#checkParamNameAgainstFilters(paramName) {
|
|
285
|
+
if (this.#filter === null) {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
306
288
|
|
|
307
|
-
|
|
308
|
-
* Indicates if the node is the owner of the state, i.e. if it created the state.
|
|
309
|
-
* @type {Boolean}
|
|
310
|
-
*/
|
|
311
|
-
get isOwner() {
|
|
312
|
-
return this.#isOwner;
|
|
313
|
-
}
|
|
289
|
+
const { whiteList, blackList } = this.#filter;
|
|
314
290
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
* @type {Boolean}
|
|
320
|
-
*/
|
|
321
|
-
get hasSiblings() {
|
|
322
|
-
if (this.#detached) {
|
|
323
|
-
return false;
|
|
291
|
+
if (whiteList && !whiteList.includes(paramName)) {
|
|
292
|
+
throw new Error(`Parameter '${paramName}' is not in white list`);
|
|
293
|
+
} else if (blackList && blackList.includes(paramName)) {
|
|
294
|
+
throw new Error(`Parameter '${paramName}' is in black list`);
|
|
324
295
|
}
|
|
296
|
+
}
|
|
325
297
|
|
|
326
|
-
|
|
327
|
-
|
|
298
|
+
#sanitizeValuesAgainstFilters(values) {
|
|
299
|
+
if (this.#filter === null) {
|
|
300
|
+
return values;
|
|
328
301
|
}
|
|
329
302
|
|
|
330
|
-
|
|
331
|
-
}
|
|
303
|
+
const { whiteList, blackList } = this.#filter;
|
|
332
304
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
if (this.#isOwner) {
|
|
341
|
-
this.#client.transport.removeAllListeners(`${DELETE_RESPONSE}-${this.#id}-${this.#instanceId}`);
|
|
342
|
-
this.#client.transport.removeAllListeners(`${DELETE_ERROR}-${this.#id}-${this.#instanceId}`);
|
|
343
|
-
} else {
|
|
344
|
-
this.#client.transport.removeAllListeners(`${DETACH_RESPONSE}-${this.#id}-${this.#instanceId}`);
|
|
345
|
-
this.#client.transport.removeAllListeners(`${DETACH_ERROR}-${this.#id}-${this.#instanceId}`);
|
|
305
|
+
for (let name in values) {
|
|
306
|
+
if (whiteList && !whiteList.includes(name)) {
|
|
307
|
+
delete values[name];
|
|
308
|
+
} else if (blackList && blackList.includes(name)) {
|
|
309
|
+
delete values[name];
|
|
310
|
+
}
|
|
346
311
|
}
|
|
312
|
+
|
|
313
|
+
return values;
|
|
347
314
|
}
|
|
348
315
|
|
|
349
316
|
async #commit(updates, propagate = true, initiator = false) {
|
|
@@ -375,19 +342,14 @@ class SharedState {
|
|
|
375
342
|
oldValues[name] = oldValue;
|
|
376
343
|
}
|
|
377
344
|
|
|
378
|
-
// if the `UPDATE_REQUEST` as been aborted by the server
|
|
379
|
-
let promises = [];
|
|
380
|
-
|
|
345
|
+
// do not propagate if the `UPDATE_REQUEST` as been aborted by the server.
|
|
381
346
|
if (propagate && Object.keys(newValues).length > 0) {
|
|
382
|
-
this.#
|
|
383
|
-
|
|
384
|
-
|
|
347
|
+
const callbackPromises = this.#executeUpdateCallbacks(newValues, oldValues);
|
|
348
|
+
// on a given client, `await state.set(update)` resolves after all
|
|
349
|
+
// update callbacks have themselves resolved
|
|
350
|
+
await Promise.all(callbackPromises);
|
|
385
351
|
}
|
|
386
352
|
|
|
387
|
-
// on a given client, `await state.set(update)` resolves after all
|
|
388
|
-
// update callbacks have themselves resolved
|
|
389
|
-
await Promise.all(promises);
|
|
390
|
-
|
|
391
353
|
// reset events to null after propagation of all listeners
|
|
392
354
|
for (let name in newValues) {
|
|
393
355
|
const { event } = this.#parameters.getDescription(name);
|
|
@@ -400,6 +362,104 @@ class SharedState {
|
|
|
400
362
|
return newValues;
|
|
401
363
|
}
|
|
402
364
|
|
|
365
|
+
#executeUpdateCallbacks(newValues, oldValues) {
|
|
366
|
+
let promises = [];
|
|
367
|
+
|
|
368
|
+
this.#onUpdateCallbacks.forEach(listenerPayload => {
|
|
369
|
+
let somePromise = this.#executeUpdateCallback(listenerPayload, newValues, oldValues);
|
|
370
|
+
promises.push(somePromise);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
return promises;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
#executeUpdateCallback(listenerPayload, newValues, oldValues) {
|
|
377
|
+
const { paramName, listener, unsubscribe } = listenerPayload;
|
|
378
|
+
|
|
379
|
+
if (paramName === null) {
|
|
380
|
+
return listener(newValues, oldValues, unsubscribe);
|
|
381
|
+
} else if (paramName in newValues) {
|
|
382
|
+
return listener(newValues[paramName], oldValues[paramName], unsubscribe);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
#checkPendingWaitForConditions() {
|
|
387
|
+
const values = this.getValues();
|
|
388
|
+
|
|
389
|
+
this.#waitForConditions.forEach(payload => {
|
|
390
|
+
const { resolve, condition, timeoutId } = payload;
|
|
391
|
+
let match;
|
|
392
|
+
|
|
393
|
+
if (isFunction(condition)) {
|
|
394
|
+
match = condition(values);
|
|
395
|
+
} else {
|
|
396
|
+
match = true;
|
|
397
|
+
|
|
398
|
+
for (let [key, value] of Object.entries(condition)) {
|
|
399
|
+
if (values[key] !== value) {
|
|
400
|
+
match = false;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (match === true) {
|
|
406
|
+
clearTimeout(timeoutId);
|
|
407
|
+
this.#waitForConditions.delete(payload);
|
|
408
|
+
resolve();
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Id of the state
|
|
415
|
+
* @type {Number}
|
|
416
|
+
*/
|
|
417
|
+
get id() {
|
|
418
|
+
return this.#id;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Name of the underlying {@link SharedState} class.
|
|
423
|
+
* @type {String}
|
|
424
|
+
*/
|
|
425
|
+
get className() {
|
|
426
|
+
return this.#className;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* @deprecated Use {@link SharedState#className} instead.
|
|
431
|
+
*/
|
|
432
|
+
get schemaName() {
|
|
433
|
+
warnings.deprecated('SharedState#schemaName', 'SharedState#className', '4.0.0-alpha.29');
|
|
434
|
+
return this.className;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Indicates if the node is the owner of the state, i.e. if it created the state.
|
|
439
|
+
* @type {Boolean}
|
|
440
|
+
*/
|
|
441
|
+
get isOwner() {
|
|
442
|
+
return this.#isOwner;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Indicates if the node as siblings, i.e. if several versions of the same state
|
|
447
|
+
* exists on the network
|
|
448
|
+
* Always return true if the state instance is attached
|
|
449
|
+
* @type {Boolean}
|
|
450
|
+
*/
|
|
451
|
+
get hasSiblings() {
|
|
452
|
+
if (this.#detached) {
|
|
453
|
+
return false;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (!this.isOwner) {
|
|
457
|
+
return true;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
return this.#hasSiblings;
|
|
461
|
+
}
|
|
462
|
+
|
|
403
463
|
/**
|
|
404
464
|
* Return the underlying {@link SharedStateClassDescription} or the
|
|
405
465
|
* {@link SharedStateParameterDescription} if `paramName` is given.
|
|
@@ -420,14 +480,6 @@ class SharedState {
|
|
|
420
480
|
}
|
|
421
481
|
}
|
|
422
482
|
|
|
423
|
-
/**
|
|
424
|
-
* @deprecated Use {@link SharedState#getDescription} instead.
|
|
425
|
-
*/
|
|
426
|
-
getSchema(paramName = null) {
|
|
427
|
-
warnings.deprecated('SharedState#getSchema', 'SharedState#getDescription', '4.0.0-alpha.29');
|
|
428
|
-
return this.getDescription(paramName);
|
|
429
|
-
}
|
|
430
|
-
|
|
431
483
|
/**
|
|
432
484
|
* Update the values of the state.
|
|
433
485
|
*
|
|
@@ -512,6 +564,12 @@ class SharedState {
|
|
|
512
564
|
let syncedOnUpdate = false;
|
|
513
565
|
|
|
514
566
|
for (let name in updates) {
|
|
567
|
+
// Throw early if some param is blacklisted or not whitelisted
|
|
568
|
+
try {
|
|
569
|
+
this.#checkParamNameAgainstFilters(name);
|
|
570
|
+
} catch (err) {
|
|
571
|
+
throw new DOMException(`Cannot execute 'set' on SharedState (${this.#className}): ${err.message}`, 'NotSupportedError');
|
|
572
|
+
}
|
|
515
573
|
// Try to coerce value now, so that eventual errors are triggered early
|
|
516
574
|
// on the node requesting the update and not only on the server side.
|
|
517
575
|
try {
|
|
@@ -521,13 +579,6 @@ class SharedState {
|
|
|
521
579
|
throw new TypeError(`Cannot execute 'set' on SharedState (${this.#className}): ${err.message}`);
|
|
522
580
|
}
|
|
523
581
|
|
|
524
|
-
// Make sure that given name is in filter white list (if any)
|
|
525
|
-
if (this.#filter !== null) {
|
|
526
|
-
if (!this.#filter.includes(name)) {
|
|
527
|
-
throw new DOMException(`Cannot execute 'set' on SharedState (${this.#className}): Parameter '${name}' is not in white list`, 'NotSupportedError');
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
|
|
531
582
|
// ### `immediate` modifier behavior
|
|
532
583
|
//
|
|
533
584
|
// If immediate=true
|
|
@@ -593,9 +644,9 @@ class SharedState {
|
|
|
593
644
|
}
|
|
594
645
|
}
|
|
595
646
|
|
|
596
|
-
// params that trigger a synced onUpdate call: immediate, local, non-acknowledged
|
|
647
|
+
// params that trigger a synced onUpdate call are: immediate, local, non-acknowledged
|
|
597
648
|
if (syncedOnUpdate) {
|
|
598
|
-
this.#
|
|
649
|
+
this.#executeUpdateCallbacks(newValues, oldValues);
|
|
599
650
|
}
|
|
600
651
|
|
|
601
652
|
// if we only have local params, we can resolve immediately
|
|
@@ -642,10 +693,11 @@ class SharedState {
|
|
|
642
693
|
throw new ReferenceError(`Cannot execute 'get' on SharedState (${this.#className}): Parameter '${name}' is not defined`);
|
|
643
694
|
}
|
|
644
695
|
|
|
645
|
-
if
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
696
|
+
// Throw early if some param is blacklisted or not whitelisted
|
|
697
|
+
try {
|
|
698
|
+
this.#checkParamNameAgainstFilters(name);
|
|
699
|
+
} catch (err) {
|
|
700
|
+
throw new DOMException(`Cannot execute 'get' on SharedState (${this.#className}): ${err.message}`, 'NotSupportedError');
|
|
649
701
|
}
|
|
650
702
|
|
|
651
703
|
return this.#parameters.get(name);
|
|
@@ -671,10 +723,11 @@ class SharedState {
|
|
|
671
723
|
throw new ReferenceError(`Cannot execute 'getUnsafe' on SharedState (${this.#className}): Parameter '${name}' is not defined`);
|
|
672
724
|
}
|
|
673
725
|
|
|
674
|
-
if
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
726
|
+
// Throw early if some param is blacklisted or not whitelisted
|
|
727
|
+
try {
|
|
728
|
+
this.#checkParamNameAgainstFilters(name);
|
|
729
|
+
} catch (err) {
|
|
730
|
+
throw new DOMException(`Cannot execute 'getUnsafe' on SharedState (${this.#className}): ${err.message}`, 'NotSupportedError');
|
|
678
731
|
}
|
|
679
732
|
|
|
680
733
|
return this.#parameters.getUnsafe(name);
|
|
@@ -691,16 +744,7 @@ class SharedState {
|
|
|
691
744
|
*/
|
|
692
745
|
getValues() {
|
|
693
746
|
const values = this.#parameters.getValues();
|
|
694
|
-
|
|
695
|
-
if (this.#filter !== null) {
|
|
696
|
-
for (let name in values) {
|
|
697
|
-
if (!this.#filter.includes(name)) {
|
|
698
|
-
delete values[name];
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
return values;
|
|
747
|
+
return this.#sanitizeValuesAgainstFilters(values);
|
|
704
748
|
}
|
|
705
749
|
|
|
706
750
|
/**
|
|
@@ -718,16 +762,7 @@ class SharedState {
|
|
|
718
762
|
*/
|
|
719
763
|
getValuesUnsafe() {
|
|
720
764
|
const values = this.#parameters.getValuesUnsafe();
|
|
721
|
-
|
|
722
|
-
if (this.#filter !== null) {
|
|
723
|
-
for (let name in values) {
|
|
724
|
-
if (!this.#filter.includes(name)) {
|
|
725
|
-
delete values[name];
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
return values;
|
|
765
|
+
return this.#sanitizeValuesAgainstFilters(values);
|
|
731
766
|
}
|
|
732
767
|
|
|
733
768
|
/**
|
|
@@ -808,9 +843,43 @@ class SharedState {
|
|
|
808
843
|
}
|
|
809
844
|
}
|
|
810
845
|
|
|
846
|
+
/**
|
|
847
|
+
* Subscribe to any updates in the state.
|
|
848
|
+
*
|
|
849
|
+
* @overload
|
|
850
|
+
* @param {sharedStateOnUpdateCallback} callback
|
|
851
|
+
* Callback to execute when an update is applied on the state.
|
|
852
|
+
* @param {Boolean} [executeListener=false] - Execute the callback immediately
|
|
853
|
+
* with current state values. Note that `oldValues` will be set to `{}`.
|
|
854
|
+
* @returns {sharedStateDeleteOnUpdateCallback}
|
|
855
|
+
* @example
|
|
856
|
+
* state.onUpdate(updates => {
|
|
857
|
+
* // ...
|
|
858
|
+
* });
|
|
859
|
+
*/
|
|
860
|
+
/**
|
|
861
|
+
* Subscribe to updates in the state filtered by a given parameter name.
|
|
862
|
+
*
|
|
863
|
+
* @overload
|
|
864
|
+
* @param {SharedStateParameterName} paramName
|
|
865
|
+
* @param {sharedStateOnUpdateCallback} callback
|
|
866
|
+
* Callback to execute when an update is applied on the state.
|
|
867
|
+
* @param {Boolean} [executeListener=false] - Execute the callback immediately
|
|
868
|
+
* with current state values. Note that `oldValues` will be set to `{}`.
|
|
869
|
+
* @returns {sharedStateDeleteOnUpdateCallback}
|
|
870
|
+
* @example
|
|
871
|
+
* state.onUpdate('my-param', myValue => {
|
|
872
|
+
* // ...
|
|
873
|
+
* });
|
|
874
|
+
*/
|
|
811
875
|
/**
|
|
812
876
|
* Subscribe to state updates.
|
|
813
877
|
*
|
|
878
|
+
* Alternative signatures:
|
|
879
|
+
* - `state.onUpdate(callback, executeListener)`
|
|
880
|
+
* - `state.onUpdate(paramName, callback, executeListener)`
|
|
881
|
+
*
|
|
882
|
+
* @param {SharedStateParameterName} paramName
|
|
814
883
|
* @param {sharedStateOnUpdateCallback} callback
|
|
815
884
|
* Callback to execute when an update is applied on the state.
|
|
816
885
|
* @param {Boolean} [executeListener=false] - Execute the callback immediately
|
|
@@ -828,8 +897,13 @@ class SharedState {
|
|
|
828
897
|
* // later
|
|
829
898
|
* unsubscribe();
|
|
830
899
|
*/
|
|
831
|
-
onUpdate(
|
|
832
|
-
this
|
|
900
|
+
onUpdate(...args) {
|
|
901
|
+
const { paramName, listener, executeListener } = sanitizeOnUpdateParams(this, ...args);
|
|
902
|
+
|
|
903
|
+
const listenerPayload = { paramName, listener };
|
|
904
|
+
this.#onUpdateCallbacks.add(listenerPayload);
|
|
905
|
+
// create unsubscribe and add to payload, to pass it back to the listeners as argument
|
|
906
|
+
listenerPayload.unsubscribe = () => this.#onUpdateCallbacks.delete(listenerPayload);
|
|
833
907
|
|
|
834
908
|
if (executeListener === true) {
|
|
835
909
|
const currentValues = this.getValues();
|
|
@@ -843,12 +917,10 @@ class SharedState {
|
|
|
843
917
|
}
|
|
844
918
|
}
|
|
845
919
|
|
|
846
|
-
|
|
920
|
+
this.#executeUpdateCallback(listenerPayload, currentValues, {});
|
|
847
921
|
}
|
|
848
922
|
|
|
849
|
-
return
|
|
850
|
-
this.#onUpdateCallbacks.delete(listener);
|
|
851
|
-
};
|
|
923
|
+
return listenerPayload.unsubscribe;
|
|
852
924
|
}
|
|
853
925
|
|
|
854
926
|
/**
|
|
@@ -879,6 +951,69 @@ class SharedState {
|
|
|
879
951
|
this.#onDeleteCallbacks.add(callback);
|
|
880
952
|
return () => this.#onDeleteCallbacks.delete(callback);
|
|
881
953
|
}
|
|
954
|
+
|
|
955
|
+
/**
|
|
956
|
+
* Wait for a given condition in the state of the SharedState instance.
|
|
957
|
+
* If the state watches the condition when `waitFor` is called, the promise is
|
|
958
|
+
* resolved immediately.
|
|
959
|
+
*
|
|
960
|
+
* @note - mark as private until tested in real-world
|
|
961
|
+
*
|
|
962
|
+
* @private
|
|
963
|
+
* @param {object} condition - Condition to be meet in the state for the
|
|
964
|
+
* returned promise to resolve. The condition is either a function which returns
|
|
965
|
+
* `true` when the condition is met or an object that describe a desired state
|
|
966
|
+
* of a subset the `SharedState` values.
|
|
967
|
+
* @param {object} timeout - Timeout (in milliseconds) that trigger the rejection
|
|
968
|
+
* of the returned promise.
|
|
969
|
+
* @return {Promise}
|
|
970
|
+
*/
|
|
971
|
+
waitFor(condition, timeout = null) {
|
|
972
|
+
if (!isPlainObject(condition) && !isFunction(condition)) {
|
|
973
|
+
throw new TypeError(`Cannot execute 'waitFor' on SharedState: argument 0 must be either a function or an object`);
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
if (timeout !== null && (!Number.isFinite(timeout) || timeout <= 0)) {
|
|
977
|
+
throw new TypeError(`Cannot execute 'waitFor' on SharedState: optional argument 1 must be a finite strictly positive number`);
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// @todo - check condition keys are not filtered, this could never resolve
|
|
981
|
+
const descriptionKeys = Object.keys(this.getDescription());
|
|
982
|
+
const conditionKeys = Object.keys(condition);
|
|
983
|
+
const diff = conditionKeys.filter(key => !descriptionKeys.includes(key));
|
|
984
|
+
|
|
985
|
+
if (diff.length > 0) {
|
|
986
|
+
throw new ReferenceError(`Cannot execute 'waitFor' on SharedState: condition contains keys (${diff.join(', ')}) that are not declared in class description`);
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
const { promise, resolve, reject } = Promise.withResolvers();
|
|
990
|
+
const payload = { resolve, condition };
|
|
991
|
+
|
|
992
|
+
if (timeout !== null) {
|
|
993
|
+
payload.timeoutId = setTimeout(() => {
|
|
994
|
+
this.#waitForConditions.delete(payload);
|
|
995
|
+
reject();
|
|
996
|
+
}, timeout);
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
this.#waitForConditions.add(payload);
|
|
1000
|
+
this.#checkPendingWaitForConditions();
|
|
1001
|
+
|
|
1002
|
+
return promise;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
// ---------------------------------------------------------------------------
|
|
1006
|
+
// DEPRECATED
|
|
1007
|
+
// ---------------------------------------------------------------------------
|
|
1008
|
+
|
|
1009
|
+
/**
|
|
1010
|
+
* @deprecated Use {@link SharedState#getDescription} instead.
|
|
1011
|
+
*/
|
|
1012
|
+
getSchema(paramName = null) {
|
|
1013
|
+
warnings.deprecated('SharedState#getSchema', 'SharedState#getDescription', '4.0.0-alpha.29');
|
|
1014
|
+
return this.getDescription(paramName);
|
|
1015
|
+
}
|
|
1016
|
+
|
|
882
1017
|
}
|
|
883
1018
|
|
|
884
1019
|
export default SharedState;
|