@soundworks/core 5.6.0 → 5.7.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 +13 -0
- package/package.json +1 -1
- package/src/common/BaseStateManager.js +246 -219
- package/src/common/SharedState.js +386 -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 +48 -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,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
isPlainObject,
|
|
3
|
+
isString,
|
|
4
|
+
} from '@ircam/sc-utils';
|
|
2
5
|
|
|
3
6
|
import ParameterBag from './ParameterBag.js';
|
|
4
7
|
import PromiseStore from './PromiseStore.js';
|
|
@@ -20,8 +23,13 @@ import {
|
|
|
20
23
|
import {
|
|
21
24
|
kStateManagerClient,
|
|
22
25
|
kStateManagerDeleteState,
|
|
26
|
+
kPendingSharedStateConstructionData,
|
|
23
27
|
} from './BaseStateManager.js';
|
|
24
28
|
|
|
29
|
+
import {
|
|
30
|
+
sanitizeOnUpdateParams,
|
|
31
|
+
} from './shared-state-utils.js';
|
|
32
|
+
|
|
25
33
|
import warnings from './logs/warnings.js';
|
|
26
34
|
|
|
27
35
|
// for testing purposes
|
|
@@ -31,10 +39,14 @@ export const kSharedStatePromiseStore = Symbol('soundworks:shared-state-promise-
|
|
|
31
39
|
* Callback executed when updates are applied on a {@link SharedState}.
|
|
32
40
|
*
|
|
33
41
|
* @callback sharedStateOnUpdateCallback
|
|
34
|
-
* @param {Object} newValues - Key / value pairs of the updates that have been
|
|
35
|
-
* applied to the state
|
|
36
|
-
*
|
|
37
|
-
*
|
|
42
|
+
* @param {Object|any} newValues - Key / value pairs of the updates that have been
|
|
43
|
+
* applied to the state, or single value if the callback has been registered against
|
|
44
|
+
* a parameter name filter.
|
|
45
|
+
* @param {Object|any} oldValues - Key / value pairs of the updated params before
|
|
46
|
+
* the updates has been applied to the state, or single value if the callback has been
|
|
47
|
+
* registered against a parameter name filter.
|
|
48
|
+
* @param {sharedStateDeleteOnUpdateCallback} unsubscribe - Reference to unsubscribe
|
|
49
|
+
* function returned by the `SharedState#onUpdate` method.
|
|
38
50
|
*/
|
|
39
51
|
|
|
40
52
|
/**
|
|
@@ -107,243 +119,197 @@ class SharedState {
|
|
|
107
119
|
#client = null;
|
|
108
120
|
#manager = null;
|
|
109
121
|
#filter = null;
|
|
110
|
-
// true is the state has been detached or deleted
|
|
111
122
|
#detached = false;
|
|
112
|
-
// only valid for owners
|
|
113
123
|
#hasSiblings = false;
|
|
114
124
|
#parameters = null;
|
|
115
125
|
#onUpdateCallbacks = new Set();
|
|
116
126
|
#onDetachCallbacks = new Set();
|
|
117
127
|
#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;
|
|
128
|
+
#waitForConditions = new Set();
|
|
129
|
+
|
|
130
|
+
constructor() {
|
|
131
|
+
// cf. BaseStateManager#buildSharedState to get the whole pattern
|
|
132
|
+
// rationale is to have a clean constructor API for future derived classes
|
|
133
|
+
this.#manager = globalThis[kPendingSharedStateConstructionData].manager;
|
|
134
|
+
this.#client = globalThis[kPendingSharedStateConstructionData].manager[kStateManagerClient];
|
|
135
|
+
this.#className = globalThis[kPendingSharedStateConstructionData].className;
|
|
136
|
+
this.#id = globalThis[kPendingSharedStateConstructionData].stateId;
|
|
137
|
+
this.#instanceId =globalThis[kPendingSharedStateConstructionData]. instanceId;
|
|
138
|
+
this.#isOwner = globalThis[kPendingSharedStateConstructionData].isOwner; // may be any node
|
|
139
|
+
this.#filter = globalThis[kPendingSharedStateConstructionData].filter;
|
|
136
140
|
|
|
137
141
|
try {
|
|
138
|
-
this.#parameters = new ParameterBag(
|
|
142
|
+
this.#parameters = new ParameterBag(
|
|
143
|
+
globalThis[kPendingSharedStateConstructionData].classDescription,
|
|
144
|
+
globalThis[kPendingSharedStateConstructionData].initValues,
|
|
145
|
+
);
|
|
139
146
|
} catch (err) {
|
|
140
|
-
throw new Error(`Cannot construct 'SharedState': ${err.message}`);
|
|
147
|
+
throw new Error(`Cannot construct 'SharedState' (${this.#className}): ${err.message}`);
|
|
141
148
|
}
|
|
142
149
|
|
|
143
150
|
/** @private */
|
|
144
151
|
this[kSharedStatePromiseStore] = new PromiseStore(this.constructor.name);
|
|
145
152
|
|
|
146
|
-
|
|
147
|
-
this.#client.transport.addListener(`${
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
});
|
|
153
|
+
this.#client.transport.addListener(`${UPDATE_RESPONSE}-${this.#id}-${this.#instanceId}`, this.#onUpdateResponse);
|
|
154
|
+
this.#client.transport.addListener(`${UPDATE_ABORT}-${this.#id}-${this.#instanceId}`, this.#onUpdateAbort);
|
|
155
|
+
this.#client.transport.addListener(`${UPDATE_NOTIFICATION}-${this.#id}-${this.#instanceId}`, this.#onUpdateNotification);
|
|
156
|
+
this.#client.transport.addListener(`${DELETE_NOTIFICATION}-${this.#id}-${this.#instanceId}`, this.#onDeleteNotification);
|
|
151
157
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
this
|
|
156
|
-
}
|
|
158
|
+
if (this.#isOwner) {
|
|
159
|
+
this.#client.transport.addListener(`${DELETE_RESPONSE}-${this.#id}-${this.#instanceId}`, this.#onDeleteResponse);
|
|
160
|
+
this.#client.transport.addListener(`${DELETE_ERROR}-${this.#id}`, this.#onDeleteError);
|
|
161
|
+
this.#client.transport.addListener(`${HAS_SIBLINGS_NOTIFICATION}-${this.#id}-${this.#instanceId}`, this.#onHasSiblingsNotification);
|
|
162
|
+
} else {
|
|
163
|
+
this.#client.transport.addListener(`${DETACH_RESPONSE}-${this.#id}-${this.#instanceId}`, this.#onDetachResponse);
|
|
164
|
+
this.#client.transport.addListener(`${DETACH_ERROR}-${this.#id}`, this.#onDetachError);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
157
167
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
});
|
|
168
|
+
#cleanup() {
|
|
169
|
+
this.#detached = true;
|
|
170
|
+
this.#manager[kStateManagerDeleteState](this.#id);
|
|
171
|
+
|
|
172
|
+
this.#client.transport.removeAllListeners(`${UPDATE_RESPONSE}-${this.#id}-${this.#instanceId}`);
|
|
173
|
+
this.#client.transport.removeAllListeners(`${UPDATE_NOTIFICATION}-${this.#id}-${this.#instanceId}`);
|
|
174
|
+
this.#client.transport.removeAllListeners(`${UPDATE_ABORT}-${this.#id}-${this.#instanceId}`);
|
|
175
|
+
this.#client.transport.removeAllListeners(`${DELETE_NOTIFICATION}-${this.#id}-${this.#instanceId}`);
|
|
183
176
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
177
|
+
if (this.#isOwner) {
|
|
178
|
+
this.#client.transport.removeAllListeners(`${DELETE_RESPONSE}-${this.#id}-${this.#instanceId}`);
|
|
179
|
+
this.#client.transport.removeAllListeners(`${DELETE_ERROR}-${this.#id}-${this.#instanceId}`);
|
|
180
|
+
this.#client.transport.removeAllListeners(`${HAS_SIBLINGS_NOTIFICATION}-${this.#id}-${this.#instanceId}`);
|
|
181
|
+
} else {
|
|
182
|
+
this.#client.transport.removeAllListeners(`${DETACH_RESPONSE}-${this.#id}-${this.#instanceId}`);
|
|
183
|
+
this.#client.transport.removeAllListeners(`${DETACH_ERROR}-${this.#id}-${this.#instanceId}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
189
186
|
|
|
190
|
-
|
|
191
|
-
|
|
187
|
+
#onUpdateResponse = async (reqId, updates) => {
|
|
188
|
+
const updated = await this.#commit(updates, true, true);
|
|
189
|
+
this[kSharedStatePromiseStore].resolve(reqId, updated);
|
|
190
|
+
this.#checkPendingWaitForConditions();
|
|
191
|
+
};
|
|
192
192
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}
|
|
199
|
-
}
|
|
193
|
+
#onUpdateAbort = async (reqId, updates) => {
|
|
194
|
+
const updated = await this.#commit(updates, false, true);
|
|
195
|
+
this[kSharedStatePromiseStore].resolve(reqId, updated);
|
|
196
|
+
this.#checkPendingWaitForConditions();
|
|
197
|
+
};
|
|
200
198
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
199
|
+
#onUpdateNotification = updates => {
|
|
200
|
+
this.#commit(updates, true, false);
|
|
201
|
+
this.#checkPendingWaitForConditions();
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
#onDetachResponse = async (reqId) => {
|
|
205
|
+
this.#cleanup();
|
|
206
|
+
|
|
207
|
+
for (let callback of this.#onDetachCallbacks) {
|
|
208
|
+
try {
|
|
209
|
+
await callback();
|
|
210
|
+
} catch (err) {
|
|
211
|
+
console.log(err.message);
|
|
205
212
|
}
|
|
213
|
+
}
|
|
206
214
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
215
|
+
this.#onDetachCallbacks.clear();
|
|
216
|
+
this.#onDeleteCallbacks.clear();
|
|
217
|
+
this[kSharedStatePromiseStore].resolve(reqId, this);
|
|
218
|
+
this[kSharedStatePromiseStore].flush();
|
|
219
|
+
};
|
|
211
220
|
|
|
221
|
+
#onDetachError = (reqId, msg) => {
|
|
222
|
+
this.#onDetachCallbacks.clear();
|
|
223
|
+
this.#onDeleteCallbacks.clear();
|
|
224
|
+
this[kSharedStatePromiseStore].reject(reqId, msg);
|
|
225
|
+
this[kSharedStatePromiseStore].flush();
|
|
226
|
+
};
|
|
212
227
|
|
|
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
|
-
}
|
|
228
|
+
#onDeleteResponse = async (reqId) => {
|
|
229
|
+
this.#cleanup();
|
|
228
230
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
231
|
+
for (let callback of this.#onDetachCallbacks) {
|
|
232
|
+
try {
|
|
233
|
+
await callback();
|
|
234
|
+
} catch (err) {
|
|
235
|
+
console.log(err.message);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
236
238
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
239
|
+
for (let callback of this.#onDeleteCallbacks) {
|
|
240
|
+
try {
|
|
241
|
+
await callback();
|
|
242
|
+
} catch (err) {
|
|
243
|
+
console.log(err.message);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
242
246
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
247
|
+
this.#onDetachCallbacks.clear();
|
|
248
|
+
this.#onDeleteCallbacks.clear();
|
|
249
|
+
this[kSharedStatePromiseStore].resolve(reqId, this);
|
|
250
|
+
this[kSharedStatePromiseStore].flush();
|
|
251
|
+
};
|
|
246
252
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
253
|
+
#onDeleteError = (reqId, msg) => {
|
|
254
|
+
this[kSharedStatePromiseStore].reject(reqId, msg);
|
|
255
|
+
};
|
|
250
256
|
|
|
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
|
-
}
|
|
257
|
+
#onDeleteNotification = async () => {
|
|
258
|
+
this.#cleanup();
|
|
266
259
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
260
|
+
for (let callback of this.#onDetachCallbacks) {
|
|
261
|
+
try {
|
|
262
|
+
await callback();
|
|
263
|
+
} catch (err) {
|
|
264
|
+
console.error(err.message);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
272
267
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
this[kSharedStatePromiseStore].reject(reqId, msg);
|
|
278
|
-
this[kSharedStatePromiseStore].flush();
|
|
279
|
-
});
|
|
268
|
+
if (this.#isOwner) {
|
|
269
|
+
for (let callback of this.#onDeleteCallbacks) {
|
|
270
|
+
await callback();
|
|
271
|
+
}
|
|
280
272
|
}
|
|
281
|
-
}
|
|
282
273
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
get id() {
|
|
288
|
-
return this.#id;
|
|
289
|
-
}
|
|
274
|
+
this.#onDetachCallbacks.clear();
|
|
275
|
+
this.#onDeleteCallbacks.clear();
|
|
276
|
+
this[kSharedStatePromiseStore].flush();
|
|
277
|
+
};
|
|
290
278
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
*/
|
|
295
|
-
get className() {
|
|
296
|
-
return this.#className;
|
|
297
|
-
}
|
|
279
|
+
#onHasSiblingsNotification = hasSiblings => {
|
|
280
|
+
this.#hasSiblings = hasSiblings;
|
|
281
|
+
};
|
|
298
282
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
warnings.deprecated('SharedState#schemaName', 'SharedState#className', '4.0.0-alpha.29');
|
|
304
|
-
return this.className;
|
|
305
|
-
}
|
|
283
|
+
#checkParamNameAgainstFilters(paramName) {
|
|
284
|
+
if (this.#filter === null) {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
306
287
|
|
|
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
|
-
}
|
|
288
|
+
const { whiteList, blackList } = this.#filter;
|
|
314
289
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
* @type {Boolean}
|
|
320
|
-
*/
|
|
321
|
-
get hasSiblings() {
|
|
322
|
-
if (this.#detached) {
|
|
323
|
-
return false;
|
|
290
|
+
if (whiteList && !whiteList.includes(paramName)) {
|
|
291
|
+
throw new Error(`Parameter '${paramName}' is not in white list`);
|
|
292
|
+
} else if (blackList && blackList.includes(paramName)) {
|
|
293
|
+
throw new Error(`Parameter '${paramName}' is in black list`);
|
|
324
294
|
}
|
|
295
|
+
}
|
|
325
296
|
|
|
326
|
-
|
|
327
|
-
|
|
297
|
+
#sanitizeValuesAgainstFilters(values) {
|
|
298
|
+
if (this.#filter === null) {
|
|
299
|
+
return values;
|
|
328
300
|
}
|
|
329
301
|
|
|
330
|
-
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
#clearTransport() {
|
|
334
|
-
// remove listeners
|
|
335
|
-
this.#client.transport.removeAllListeners(`${UPDATE_RESPONSE}-${this.#id}-${this.#instanceId}`);
|
|
336
|
-
this.#client.transport.removeAllListeners(`${UPDATE_NOTIFICATION}-${this.#id}-${this.#instanceId}`);
|
|
337
|
-
this.#client.transport.removeAllListeners(`${UPDATE_ABORT}-${this.#id}-${this.#instanceId}`);
|
|
338
|
-
this.#client.transport.removeAllListeners(`${DELETE_NOTIFICATION}-${this.#id}-${this.#instanceId}`);
|
|
302
|
+
const { whiteList, blackList } = this.#filter;
|
|
339
303
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
304
|
+
for (let name in values) {
|
|
305
|
+
if (whiteList && !whiteList.includes(name)) {
|
|
306
|
+
delete values[name];
|
|
307
|
+
} else if (blackList && blackList.includes(name)) {
|
|
308
|
+
delete values[name];
|
|
309
|
+
}
|
|
346
310
|
}
|
|
311
|
+
|
|
312
|
+
return values;
|
|
347
313
|
}
|
|
348
314
|
|
|
349
315
|
async #commit(updates, propagate = true, initiator = false) {
|
|
@@ -375,19 +341,14 @@ class SharedState {
|
|
|
375
341
|
oldValues[name] = oldValue;
|
|
376
342
|
}
|
|
377
343
|
|
|
378
|
-
// if the `UPDATE_REQUEST` as been aborted by the server
|
|
379
|
-
let promises = [];
|
|
380
|
-
|
|
344
|
+
// do not propagate if the `UPDATE_REQUEST` as been aborted by the server.
|
|
381
345
|
if (propagate && Object.keys(newValues).length > 0) {
|
|
382
|
-
this.#
|
|
383
|
-
|
|
384
|
-
|
|
346
|
+
const callbackPromises = this.#executeUpdateCallbacks(newValues, oldValues);
|
|
347
|
+
// on a given client, `await state.set(update)` resolves after all
|
|
348
|
+
// update callbacks have themselves resolved
|
|
349
|
+
await Promise.all(callbackPromises);
|
|
385
350
|
}
|
|
386
351
|
|
|
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
352
|
// reset events to null after propagation of all listeners
|
|
392
353
|
for (let name in newValues) {
|
|
393
354
|
const { event } = this.#parameters.getDescription(name);
|
|
@@ -400,6 +361,98 @@ class SharedState {
|
|
|
400
361
|
return newValues;
|
|
401
362
|
}
|
|
402
363
|
|
|
364
|
+
#executeUpdateCallbacks(newValues, oldValues) {
|
|
365
|
+
let promises = [];
|
|
366
|
+
|
|
367
|
+
this.#onUpdateCallbacks.forEach(listenerPayload => {
|
|
368
|
+
let somePromise = this.#executeUpdateCallback(listenerPayload, newValues, oldValues);
|
|
369
|
+
promises.push(somePromise);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
return promises;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
#executeUpdateCallback(listenerPayload, newValues, oldValues) {
|
|
376
|
+
const { paramName, listener, unsubscribe } = listenerPayload;
|
|
377
|
+
|
|
378
|
+
if (paramName === null) {
|
|
379
|
+
return listener(newValues, oldValues, unsubscribe);
|
|
380
|
+
} else if (paramName in newValues) {
|
|
381
|
+
return listener(newValues[paramName], oldValues[paramName], unsubscribe);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
#checkPendingWaitForConditions() {
|
|
386
|
+
const values = this.getValuesUnsafe();
|
|
387
|
+
|
|
388
|
+
this.#waitForConditions.forEach(payload => {
|
|
389
|
+
const { resolve, condition, timeoutId } = payload;
|
|
390
|
+
let match = true;
|
|
391
|
+
|
|
392
|
+
for (let [key, value] of Object.entries(condition)) {
|
|
393
|
+
if (values[key] !== value) {
|
|
394
|
+
match = false;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (match) {
|
|
399
|
+
clearTimeout(timeoutId);
|
|
400
|
+
this.#waitForConditions.delete(payload);
|
|
401
|
+
resolve();
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Id of the state
|
|
408
|
+
* @type {Number}
|
|
409
|
+
*/
|
|
410
|
+
get id() {
|
|
411
|
+
return this.#id;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Name of the underlying {@link SharedState} class.
|
|
416
|
+
* @type {String}
|
|
417
|
+
*/
|
|
418
|
+
get className() {
|
|
419
|
+
return this.#className;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* @deprecated Use {@link SharedState#className} instead.
|
|
424
|
+
*/
|
|
425
|
+
get schemaName() {
|
|
426
|
+
warnings.deprecated('SharedState#schemaName', 'SharedState#className', '4.0.0-alpha.29');
|
|
427
|
+
return this.className;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Indicates if the node is the owner of the state, i.e. if it created the state.
|
|
432
|
+
* @type {Boolean}
|
|
433
|
+
*/
|
|
434
|
+
get isOwner() {
|
|
435
|
+
return this.#isOwner;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Indicates if the node as siblings, i.e. if several versions of the same state
|
|
440
|
+
* exists on the network
|
|
441
|
+
* Always return true if the state instance is attached
|
|
442
|
+
* @type {Boolean}
|
|
443
|
+
*/
|
|
444
|
+
get hasSiblings() {
|
|
445
|
+
if (this.#detached) {
|
|
446
|
+
return false;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if (!this.isOwner) {
|
|
450
|
+
return true;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
return this.#hasSiblings;
|
|
454
|
+
}
|
|
455
|
+
|
|
403
456
|
/**
|
|
404
457
|
* Return the underlying {@link SharedStateClassDescription} or the
|
|
405
458
|
* {@link SharedStateParameterDescription} if `paramName` is given.
|
|
@@ -420,14 +473,6 @@ class SharedState {
|
|
|
420
473
|
}
|
|
421
474
|
}
|
|
422
475
|
|
|
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
476
|
/**
|
|
432
477
|
* Update the values of the state.
|
|
433
478
|
*
|
|
@@ -512,6 +557,12 @@ class SharedState {
|
|
|
512
557
|
let syncedOnUpdate = false;
|
|
513
558
|
|
|
514
559
|
for (let name in updates) {
|
|
560
|
+
// Throw early if some param is blacklisted or not whitelisted
|
|
561
|
+
try {
|
|
562
|
+
this.#checkParamNameAgainstFilters(name);
|
|
563
|
+
} catch (err) {
|
|
564
|
+
throw new DOMException(`Cannot execute 'set' on SharedState (${this.#className}): ${err.message}`, 'NotSupportedError');
|
|
565
|
+
}
|
|
515
566
|
// Try to coerce value now, so that eventual errors are triggered early
|
|
516
567
|
// on the node requesting the update and not only on the server side.
|
|
517
568
|
try {
|
|
@@ -521,13 +572,6 @@ class SharedState {
|
|
|
521
572
|
throw new TypeError(`Cannot execute 'set' on SharedState (${this.#className}): ${err.message}`);
|
|
522
573
|
}
|
|
523
574
|
|
|
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
575
|
// ### `immediate` modifier behavior
|
|
532
576
|
//
|
|
533
577
|
// If immediate=true
|
|
@@ -593,9 +637,9 @@ class SharedState {
|
|
|
593
637
|
}
|
|
594
638
|
}
|
|
595
639
|
|
|
596
|
-
// params that trigger a synced onUpdate call: immediate, local, non-acknowledged
|
|
640
|
+
// params that trigger a synced onUpdate call are: immediate, local, non-acknowledged
|
|
597
641
|
if (syncedOnUpdate) {
|
|
598
|
-
this.#
|
|
642
|
+
this.#executeUpdateCallbacks(newValues, oldValues);
|
|
599
643
|
}
|
|
600
644
|
|
|
601
645
|
// if we only have local params, we can resolve immediately
|
|
@@ -642,10 +686,11 @@ class SharedState {
|
|
|
642
686
|
throw new ReferenceError(`Cannot execute 'get' on SharedState (${this.#className}): Parameter '${name}' is not defined`);
|
|
643
687
|
}
|
|
644
688
|
|
|
645
|
-
if
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
689
|
+
// Throw early if some param is blacklisted or not whitelisted
|
|
690
|
+
try {
|
|
691
|
+
this.#checkParamNameAgainstFilters(name);
|
|
692
|
+
} catch (err) {
|
|
693
|
+
throw new DOMException(`Cannot execute 'get' on SharedState (${this.#className}): ${err.message}`, 'NotSupportedError');
|
|
649
694
|
}
|
|
650
695
|
|
|
651
696
|
return this.#parameters.get(name);
|
|
@@ -671,10 +716,11 @@ class SharedState {
|
|
|
671
716
|
throw new ReferenceError(`Cannot execute 'getUnsafe' on SharedState (${this.#className}): Parameter '${name}' is not defined`);
|
|
672
717
|
}
|
|
673
718
|
|
|
674
|
-
if
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
719
|
+
// Throw early if some param is blacklisted or not whitelisted
|
|
720
|
+
try {
|
|
721
|
+
this.#checkParamNameAgainstFilters(name);
|
|
722
|
+
} catch (err) {
|
|
723
|
+
throw new DOMException(`Cannot execute 'getUnsafe' on SharedState (${this.#className}): ${err.message}`, 'NotSupportedError');
|
|
678
724
|
}
|
|
679
725
|
|
|
680
726
|
return this.#parameters.getUnsafe(name);
|
|
@@ -691,16 +737,7 @@ class SharedState {
|
|
|
691
737
|
*/
|
|
692
738
|
getValues() {
|
|
693
739
|
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;
|
|
740
|
+
return this.#sanitizeValuesAgainstFilters(values);
|
|
704
741
|
}
|
|
705
742
|
|
|
706
743
|
/**
|
|
@@ -718,16 +755,7 @@ class SharedState {
|
|
|
718
755
|
*/
|
|
719
756
|
getValuesUnsafe() {
|
|
720
757
|
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;
|
|
758
|
+
return this.#sanitizeValuesAgainstFilters(values);
|
|
731
759
|
}
|
|
732
760
|
|
|
733
761
|
/**
|
|
@@ -808,9 +836,43 @@ class SharedState {
|
|
|
808
836
|
}
|
|
809
837
|
}
|
|
810
838
|
|
|
839
|
+
/**
|
|
840
|
+
* Subscribe to any updates in the state.
|
|
841
|
+
*
|
|
842
|
+
* @overload
|
|
843
|
+
* @param {sharedStateOnUpdateCallback} callback
|
|
844
|
+
* Callback to execute when an update is applied on the state.
|
|
845
|
+
* @param {Boolean} [executeListener=false] - Execute the callback immediately
|
|
846
|
+
* with current state values. Note that `oldValues` will be set to `{}`.
|
|
847
|
+
* @returns {sharedStateDeleteOnUpdateCallback}
|
|
848
|
+
* @example
|
|
849
|
+
* state.onUpdate(updates => {
|
|
850
|
+
* // ...
|
|
851
|
+
* });
|
|
852
|
+
*/
|
|
853
|
+
/**
|
|
854
|
+
* Subscribe to updates in the state filtered by a given parameter name.
|
|
855
|
+
*
|
|
856
|
+
* @overload
|
|
857
|
+
* @param {SharedStateParameterName} paramName
|
|
858
|
+
* @param {sharedStateOnUpdateCallback} callback
|
|
859
|
+
* Callback to execute when an update is applied on the state.
|
|
860
|
+
* @param {Boolean} [executeListener=false] - Execute the callback immediately
|
|
861
|
+
* with current state values. Note that `oldValues` will be set to `{}`.
|
|
862
|
+
* @returns {sharedStateDeleteOnUpdateCallback}
|
|
863
|
+
* @example
|
|
864
|
+
* state.onUpdate('my-param', myValue => {
|
|
865
|
+
* // ...
|
|
866
|
+
* });
|
|
867
|
+
*/
|
|
811
868
|
/**
|
|
812
869
|
* Subscribe to state updates.
|
|
813
870
|
*
|
|
871
|
+
* Alternative signatures:
|
|
872
|
+
* - `state.onUpdate(callback, executeListener)`
|
|
873
|
+
* - `state.onUpdate(paramName, callback, executeListener)`
|
|
874
|
+
*
|
|
875
|
+
* @param {SharedStateParameterName} paramName
|
|
814
876
|
* @param {sharedStateOnUpdateCallback} callback
|
|
815
877
|
* Callback to execute when an update is applied on the state.
|
|
816
878
|
* @param {Boolean} [executeListener=false] - Execute the callback immediately
|
|
@@ -828,8 +890,13 @@ class SharedState {
|
|
|
828
890
|
* // later
|
|
829
891
|
* unsubscribe();
|
|
830
892
|
*/
|
|
831
|
-
onUpdate(
|
|
832
|
-
this
|
|
893
|
+
onUpdate(...args) {
|
|
894
|
+
const { paramName, listener, executeListener } = sanitizeOnUpdateParams(this, ...args);
|
|
895
|
+
|
|
896
|
+
const listenerPayload = { paramName, listener };
|
|
897
|
+
this.#onUpdateCallbacks.add(listenerPayload);
|
|
898
|
+
// create unsubscribe and add to payload, to pass it back to the listeners as argument
|
|
899
|
+
listenerPayload.unsubscribe = () => this.#onUpdateCallbacks.delete(listenerPayload);
|
|
833
900
|
|
|
834
901
|
if (executeListener === true) {
|
|
835
902
|
const currentValues = this.getValues();
|
|
@@ -843,12 +910,10 @@ class SharedState {
|
|
|
843
910
|
}
|
|
844
911
|
}
|
|
845
912
|
|
|
846
|
-
|
|
913
|
+
this.#executeUpdateCallback(listenerPayload, currentValues, {});
|
|
847
914
|
}
|
|
848
915
|
|
|
849
|
-
return
|
|
850
|
-
this.#onUpdateCallbacks.delete(listener);
|
|
851
|
-
};
|
|
916
|
+
return listenerPayload.unsubscribe;
|
|
852
917
|
}
|
|
853
918
|
|
|
854
919
|
/**
|
|
@@ -879,6 +944,67 @@ class SharedState {
|
|
|
879
944
|
this.#onDeleteCallbacks.add(callback);
|
|
880
945
|
return () => this.#onDeleteCallbacks.delete(callback);
|
|
881
946
|
}
|
|
947
|
+
|
|
948
|
+
/**
|
|
949
|
+
* Wait for a given condition in the state of the SharedState instance.
|
|
950
|
+
* If the state watches the condition when `waitFor` is called, the promise is
|
|
951
|
+
* resolved immediately.
|
|
952
|
+
*
|
|
953
|
+
* @note - mark as private until tested in real-world
|
|
954
|
+
*
|
|
955
|
+
* @private
|
|
956
|
+
* @param {object} condition - Condition to be meet in the state for the
|
|
957
|
+
* returned promise to resolve.
|
|
958
|
+
* @param {object} condition - Timeout (in milliseconds) that trigger the rejection
|
|
959
|
+
* of the returned promise.
|
|
960
|
+
* @return {Promise}
|
|
961
|
+
*/
|
|
962
|
+
waitFor(condition, timeout = null) {
|
|
963
|
+
if (!isPlainObject(condition)) {
|
|
964
|
+
throw new TypeError(`Cannot execute 'waitFor' on SharedState: argument 0 must be an object`);
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
if (timeout !== null && (!Number.isFinite(timeout) || timeout <= 0)) {
|
|
968
|
+
throw new TypeError(`Cannot execute 'waitFor' on SharedState: optional argument 1 must be a finite strictly positive number`);
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
// @todo - check condition keys are not filtered, this could never resolve
|
|
972
|
+
const descriptionKeys = Object.keys(this.getDescription());
|
|
973
|
+
const conditionKeys = Object.keys(condition);
|
|
974
|
+
const diff = conditionKeys.filter(key => !descriptionKeys.includes(key));
|
|
975
|
+
|
|
976
|
+
if (diff.length > 0) {
|
|
977
|
+
throw new ReferenceError(`Cannot execute 'waitFor' on SharedState: condition contains keys (${diff.join(', ')}) that are not declared in class description`);
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
const { promise, resolve, reject } = Promise.withResolvers();
|
|
981
|
+
const payload = { resolve, condition };
|
|
982
|
+
|
|
983
|
+
if (timeout !== null) {
|
|
984
|
+
payload.timeoutId = setTimeout(() => {
|
|
985
|
+
this.#waitForConditions.delete(payload);
|
|
986
|
+
reject();
|
|
987
|
+
}, timeout);
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
this.#waitForConditions.add(payload);
|
|
991
|
+
this.#checkPendingWaitForConditions();
|
|
992
|
+
|
|
993
|
+
return promise;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
// ---------------------------------------------------------------------------
|
|
997
|
+
// DEPRECATED
|
|
998
|
+
// ---------------------------------------------------------------------------
|
|
999
|
+
|
|
1000
|
+
/**
|
|
1001
|
+
* @deprecated Use {@link SharedState#getDescription} instead.
|
|
1002
|
+
*/
|
|
1003
|
+
getSchema(paramName = null) {
|
|
1004
|
+
warnings.deprecated('SharedState#getSchema', 'SharedState#getDescription', '4.0.0-alpha.29');
|
|
1005
|
+
return this.getDescription(paramName);
|
|
1006
|
+
}
|
|
1007
|
+
|
|
882
1008
|
}
|
|
883
1009
|
|
|
884
1010
|
export default SharedState;
|