@soundworks/core 5.5.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/README.md +6 -0
- package/package.json +17 -11
- package/src/client/Client.js +7 -4
- package/src/client/ClientSocket.js +2 -3
- package/src/common/BasePlugin.js +2 -2
- package/src/common/BasePluginManager.js +2 -2
- package/src/common/BaseStateManager.js +253 -221
- package/src/common/ParameterBag.js +14 -14
- package/src/common/SharedState.js +405 -247
- package/src/common/SharedStateCollection.js +82 -32
- package/src/common/constants.js +2 -0
- package/src/common/isomorphic/colored-logger.browser.js +17 -0
- package/src/common/isomorphic/colored-logger.node.js +19 -0
- package/src/common/isomorphic/web-socket.browser.js +1 -0
- package/src/common/isomorphic/web-socket.node.js +3 -0
- package/src/common/logs/gated-logger.js +30 -0
- package/src/common/logs/warnings.js +36 -0
- package/src/common/shared-state-utils.js +102 -0
- package/src/common/version.js +1 -1
- package/src/server/Server.js +23 -35
- package/src/server/ServerClient.js +7 -3
- package/src/server/ServerStateManager.js +242 -227
- package/src/server/SharedStatePrivate.js +105 -68
- package/src/server/create-http-server.js +81 -39
- package/src/server/crypto.js +2 -3
- package/types/client/Client.d.ts.map +1 -1
- package/types/client/ClientSocket.d.ts.map +1 -1
- package/types/common/BaseStateManager.d.ts +34 -47
- package/types/common/BaseStateManager.d.ts.map +1 -1
- package/types/common/ParameterBag.d.ts +28 -6
- package/types/common/ParameterBag.d.ts.map +1 -1
- package/types/common/SharedState.d.ts +55 -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/constants.d.ts +1 -0
- package/types/common/constants.d.ts.map +1 -1
- package/types/common/isomorphic/colored-logger.browser.d.ts +8 -0
- package/types/common/isomorphic/colored-logger.browser.d.ts.map +1 -0
- package/types/common/isomorphic/colored-logger.node.d.ts +8 -0
- package/types/common/isomorphic/colored-logger.node.d.ts.map +1 -0
- package/types/common/isomorphic/web-socket.browser.d.ts +10 -0
- package/types/common/isomorphic/web-socket.browser.d.ts.map +1 -0
- package/types/common/isomorphic/web-socket.node.d.ts +2 -0
- package/types/common/isomorphic/web-socket.node.d.ts.map +1 -0
- package/types/common/logs/gated-logger.d.ts +9 -0
- package/types/common/logs/gated-logger.d.ts.map +1 -0
- package/types/common/logs/warnings.d.ts +7 -0
- package/types/common/logs/warnings.d.ts.map +1 -0
- 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/Server.d.ts.map +1 -1
- package/types/server/ServerClient.d.ts.map +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
- package/types/server/create-http-server.d.ts +1 -0
- package/types/server/create-http-server.d.ts.map +1 -1
- package/types/server/crypto.d.ts.map +1 -1
- package/src/common/logger.js +0 -150
- package/types/common/logger.d.ts +0 -19
- package/types/common/logger.d.ts.map +0 -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';
|
|
@@ -14,14 +17,20 @@ import {
|
|
|
14
17
|
UPDATE_RESPONSE,
|
|
15
18
|
UPDATE_ABORT,
|
|
16
19
|
UPDATE_NOTIFICATION,
|
|
20
|
+
HAS_SIBLINGS_NOTIFICATION,
|
|
17
21
|
} from './constants.js';
|
|
18
22
|
|
|
19
23
|
import {
|
|
20
24
|
kStateManagerClient,
|
|
21
25
|
kStateManagerDeleteState,
|
|
26
|
+
kPendingSharedStateConstructionData,
|
|
22
27
|
} from './BaseStateManager.js';
|
|
23
28
|
|
|
24
|
-
import
|
|
29
|
+
import {
|
|
30
|
+
sanitizeOnUpdateParams,
|
|
31
|
+
} from './shared-state-utils.js';
|
|
32
|
+
|
|
33
|
+
import warnings from './logs/warnings.js';
|
|
25
34
|
|
|
26
35
|
// for testing purposes
|
|
27
36
|
export const kSharedStatePromiseStore = Symbol('soundworks:shared-state-promise-store');
|
|
@@ -30,10 +39,14 @@ export const kSharedStatePromiseStore = Symbol('soundworks:shared-state-promise-
|
|
|
30
39
|
* Callback executed when updates are applied on a {@link SharedState}.
|
|
31
40
|
*
|
|
32
41
|
* @callback sharedStateOnUpdateCallback
|
|
33
|
-
* @param {Object} newValues - Key / value pairs of the updates that have been
|
|
34
|
-
* applied to the state
|
|
35
|
-
*
|
|
36
|
-
*
|
|
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.
|
|
37
50
|
*/
|
|
38
51
|
|
|
39
52
|
/**
|
|
@@ -106,217 +119,197 @@ class SharedState {
|
|
|
106
119
|
#client = null;
|
|
107
120
|
#manager = null;
|
|
108
121
|
#filter = null;
|
|
109
|
-
// true is the state has been detached or deleted
|
|
110
122
|
#detached = false;
|
|
123
|
+
#hasSiblings = false;
|
|
111
124
|
#parameters = null;
|
|
112
125
|
#onUpdateCallbacks = new Set();
|
|
113
126
|
#onDetachCallbacks = new Set();
|
|
114
127
|
#onDeleteCallbacks = new Set();
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
this.#
|
|
127
|
-
this.#client = manager[kStateManagerClient];
|
|
128
|
-
this.#className = className;
|
|
129
|
-
this.#id = stateId;
|
|
130
|
-
this.#instanceId = instanceId;
|
|
131
|
-
this.#isOwner = isOwner; // may be any node
|
|
132
|
-
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;
|
|
133
140
|
|
|
134
141
|
try {
|
|
135
|
-
this.#parameters = new ParameterBag(
|
|
142
|
+
this.#parameters = new ParameterBag(
|
|
143
|
+
globalThis[kPendingSharedStateConstructionData].classDescription,
|
|
144
|
+
globalThis[kPendingSharedStateConstructionData].initValues,
|
|
145
|
+
);
|
|
136
146
|
} catch (err) {
|
|
137
|
-
throw new Error(`Cannot construct 'SharedState': ${err.message}`);
|
|
147
|
+
throw new Error(`Cannot construct 'SharedState' (${this.#className}): ${err.message}`);
|
|
138
148
|
}
|
|
139
149
|
|
|
140
150
|
/** @private */
|
|
141
151
|
this[kSharedStatePromiseStore] = new PromiseStore(this.constructor.name);
|
|
142
152
|
|
|
143
|
-
|
|
144
|
-
this.#client.transport.addListener(`${
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
});
|
|
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);
|
|
148
157
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
this
|
|
153
|
-
}
|
|
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
|
+
}
|
|
154
167
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
// # note: 2002-10-03
|
|
159
|
-
//
|
|
160
|
-
// `setTimeout(async () => this.#commit(updates, true, false));`
|
|
161
|
-
// appears to be the only way to push the update commit in the next event
|
|
162
|
-
// cycle so that `attach` can resolve before the update notification is
|
|
163
|
-
// actually dispatched. The alternative:
|
|
164
|
-
// `Promise.resolve().then(() => this.#commit(updates, true, false))``
|
|
165
|
-
// does not behave as expected...
|
|
166
|
-
//
|
|
167
|
-
// However this breaks the reliability of:
|
|
168
|
-
// ```
|
|
169
|
-
// /* given a0, a1 and a2 being 3 similar attached states */
|
|
170
|
-
// await state.set({ int: i });
|
|
171
|
-
//
|
|
172
|
-
// assert.equal(a0.get('int'), i);
|
|
173
|
-
// assert.equal(a1.get('int'), i);
|
|
174
|
-
// assert.equal(a2.get('int'), i);
|
|
175
|
-
// ```
|
|
176
|
-
// which is far more important than the edge case reported in the issue
|
|
177
|
-
// therefore this wont be fixed for now
|
|
178
|
-
this.#commit(updates, true, false);
|
|
179
|
-
});
|
|
168
|
+
#cleanup() {
|
|
169
|
+
this.#detached = true;
|
|
170
|
+
this.#manager[kStateManagerDeleteState](this.#id);
|
|
180
171
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
this.#client.transport.
|
|
185
|
-
this.#manager[kStateManagerDeleteState](this.#id);
|
|
186
|
-
this.#clearTransport();
|
|
187
|
-
|
|
188
|
-
for (let callback of this.#onDetachCallbacks) {
|
|
189
|
-
try {
|
|
190
|
-
await callback();
|
|
191
|
-
} catch (err) {
|
|
192
|
-
console.error(err.message);
|
|
193
|
-
}
|
|
194
|
-
}
|
|
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}`);
|
|
195
176
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
+
}
|
|
186
|
+
|
|
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
|
+
|
|
193
|
+
#onUpdateAbort = async (reqId, updates) => {
|
|
194
|
+
const updated = await this.#commit(updates, false, true);
|
|
195
|
+
this[kSharedStatePromiseStore].resolve(reqId, updated);
|
|
196
|
+
this.#checkPendingWaitForConditions();
|
|
197
|
+
};
|
|
198
|
+
|
|
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);
|
|
200
212
|
}
|
|
213
|
+
}
|
|
201
214
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
215
|
+
this.#onDetachCallbacks.clear();
|
|
216
|
+
this.#onDeleteCallbacks.clear();
|
|
217
|
+
this[kSharedStatePromiseStore].resolve(reqId, this);
|
|
218
|
+
this[kSharedStatePromiseStore].flush();
|
|
219
|
+
};
|
|
206
220
|
|
|
221
|
+
#onDetachError = (reqId, msg) => {
|
|
222
|
+
this.#onDetachCallbacks.clear();
|
|
223
|
+
this.#onDeleteCallbacks.clear();
|
|
224
|
+
this[kSharedStatePromiseStore].reject(reqId, msg);
|
|
225
|
+
this[kSharedStatePromiseStore].flush();
|
|
226
|
+
};
|
|
207
227
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
// the creator has called `.delete()`
|
|
211
|
-
// ---------------------------------------------
|
|
212
|
-
this.#client.transport.addListener(`${DELETE_RESPONSE}-${this.#id}-${this.#instanceId}`, async (reqId) => {
|
|
213
|
-
this.#manager[kStateManagerDeleteState](this.#id);
|
|
214
|
-
this.#clearTransport();
|
|
215
|
-
|
|
216
|
-
for (let callback of this.#onDetachCallbacks) {
|
|
217
|
-
try {
|
|
218
|
-
await callback();
|
|
219
|
-
} catch (err) {
|
|
220
|
-
console.log(err.message);
|
|
221
|
-
}
|
|
222
|
-
}
|
|
228
|
+
#onDeleteResponse = async (reqId) => {
|
|
229
|
+
this.#cleanup();
|
|
223
230
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
+
for (let callback of this.#onDetachCallbacks) {
|
|
232
|
+
try {
|
|
233
|
+
await callback();
|
|
234
|
+
} catch (err) {
|
|
235
|
+
console.log(err.message);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
231
238
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
239
|
+
for (let callback of this.#onDeleteCallbacks) {
|
|
240
|
+
try {
|
|
241
|
+
await callback();
|
|
242
|
+
} catch (err) {
|
|
243
|
+
console.log(err.message);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
237
246
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
247
|
+
this.#onDetachCallbacks.clear();
|
|
248
|
+
this.#onDeleteCallbacks.clear();
|
|
249
|
+
this[kSharedStatePromiseStore].resolve(reqId, this);
|
|
250
|
+
this[kSharedStatePromiseStore].flush();
|
|
251
|
+
};
|
|
241
252
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
// ---------------------------------------------
|
|
246
|
-
this.#client.transport.addListener(`${DETACH_RESPONSE}-${this.#id}-${this.#instanceId}`, async (reqId) => {
|
|
247
|
-
this.#manager[kStateManagerDeleteState](this.#id);
|
|
248
|
-
this.#clearTransport();
|
|
249
|
-
|
|
250
|
-
for (let callback of this.#onDetachCallbacks) {
|
|
251
|
-
try {
|
|
252
|
-
await callback();
|
|
253
|
-
} catch (err) {
|
|
254
|
-
console.log(err.message);
|
|
255
|
-
}
|
|
256
|
-
}
|
|
253
|
+
#onDeleteError = (reqId, msg) => {
|
|
254
|
+
this[kSharedStatePromiseStore].reject(reqId, msg);
|
|
255
|
+
};
|
|
257
256
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
this[kSharedStatePromiseStore].resolve(reqId, this);
|
|
261
|
-
this[kSharedStatePromiseStore].flush();
|
|
262
|
-
});
|
|
257
|
+
#onDeleteNotification = async () => {
|
|
258
|
+
this.#cleanup();
|
|
263
259
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
});
|
|
260
|
+
for (let callback of this.#onDetachCallbacks) {
|
|
261
|
+
try {
|
|
262
|
+
await callback();
|
|
263
|
+
} catch (err) {
|
|
264
|
+
console.error(err.message);
|
|
265
|
+
}
|
|
271
266
|
}
|
|
272
|
-
}
|
|
273
267
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
return this.#id;
|
|
280
|
-
}
|
|
268
|
+
if (this.#isOwner) {
|
|
269
|
+
for (let callback of this.#onDeleteCallbacks) {
|
|
270
|
+
await callback();
|
|
271
|
+
}
|
|
272
|
+
}
|
|
281
273
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
get className() {
|
|
287
|
-
return this.#className;
|
|
288
|
-
}
|
|
274
|
+
this.#onDetachCallbacks.clear();
|
|
275
|
+
this.#onDeleteCallbacks.clear();
|
|
276
|
+
this[kSharedStatePromiseStore].flush();
|
|
277
|
+
};
|
|
289
278
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
get schemaName() {
|
|
294
|
-
logger.deprecated('SharedState#schemaName', 'SharedState#className', '4.0.0-alpha.29');
|
|
295
|
-
return this.className;
|
|
296
|
-
}
|
|
279
|
+
#onHasSiblingsNotification = hasSiblings => {
|
|
280
|
+
this.#hasSiblings = hasSiblings;
|
|
281
|
+
};
|
|
297
282
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
283
|
+
#checkParamNameAgainstFilters(paramName) {
|
|
284
|
+
if (this.#filter === null) {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const { whiteList, blackList } = this.#filter;
|
|
289
|
+
|
|
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`);
|
|
294
|
+
}
|
|
304
295
|
}
|
|
305
296
|
|
|
306
|
-
#
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
this.#client.transport.removeAllListeners(`${UPDATE_ABORT}-${this.#id}-${this.#instanceId}`);
|
|
311
|
-
this.#client.transport.removeAllListeners(`${DELETE_NOTIFICATION}-${this.#id}-${this.#instanceId}`);
|
|
297
|
+
#sanitizeValuesAgainstFilters(values) {
|
|
298
|
+
if (this.#filter === null) {
|
|
299
|
+
return values;
|
|
300
|
+
}
|
|
312
301
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
302
|
+
const { whiteList, blackList } = this.#filter;
|
|
303
|
+
|
|
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
|
+
}
|
|
319
310
|
}
|
|
311
|
+
|
|
312
|
+
return values;
|
|
320
313
|
}
|
|
321
314
|
|
|
322
315
|
async #commit(updates, propagate = true, initiator = false) {
|
|
@@ -348,19 +341,14 @@ class SharedState {
|
|
|
348
341
|
oldValues[name] = oldValue;
|
|
349
342
|
}
|
|
350
343
|
|
|
351
|
-
// if the `UPDATE_REQUEST` as been aborted by the server
|
|
352
|
-
let promises = [];
|
|
353
|
-
|
|
344
|
+
// do not propagate if the `UPDATE_REQUEST` as been aborted by the server.
|
|
354
345
|
if (propagate && Object.keys(newValues).length > 0) {
|
|
355
|
-
this.#
|
|
356
|
-
|
|
357
|
-
|
|
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);
|
|
358
350
|
}
|
|
359
351
|
|
|
360
|
-
// on a given client, `await state.set(update)` resolves after all
|
|
361
|
-
// update callbacks have themselves resolved
|
|
362
|
-
await Promise.all(promises);
|
|
363
|
-
|
|
364
352
|
// reset events to null after propagation of all listeners
|
|
365
353
|
for (let name in newValues) {
|
|
366
354
|
const { event } = this.#parameters.getDescription(name);
|
|
@@ -373,6 +361,98 @@ class SharedState {
|
|
|
373
361
|
return newValues;
|
|
374
362
|
}
|
|
375
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
|
+
|
|
376
456
|
/**
|
|
377
457
|
* Return the underlying {@link SharedStateClassDescription} or the
|
|
378
458
|
* {@link SharedStateParameterDescription} if `paramName` is given.
|
|
@@ -393,14 +473,6 @@ class SharedState {
|
|
|
393
473
|
}
|
|
394
474
|
}
|
|
395
475
|
|
|
396
|
-
/**
|
|
397
|
-
* @deprecated Use {@link SharedState#getDescription} instead.
|
|
398
|
-
*/
|
|
399
|
-
getSchema(paramName = null) {
|
|
400
|
-
logger.deprecated('SharedState#getSchema', 'SharedState#getDescription', '4.0.0-alpha.29');
|
|
401
|
-
return this.getDescription(paramName);
|
|
402
|
-
}
|
|
403
|
-
|
|
404
476
|
/**
|
|
405
477
|
* Update the values of the state.
|
|
406
478
|
*
|
|
@@ -466,7 +538,7 @@ class SharedState {
|
|
|
466
538
|
}
|
|
467
539
|
|
|
468
540
|
if (isPlainObject(arguments[0]) && isPlainObject(arguments[1])) {
|
|
469
|
-
|
|
541
|
+
warnings.removed('`context` argument in SharedState.set(updates, context)', 'a regular parameter configured with `event=true`', '4.0.0-alpha.29');
|
|
470
542
|
}
|
|
471
543
|
|
|
472
544
|
if (arguments.length === 2 && isString(updates)) {
|
|
@@ -485,6 +557,12 @@ class SharedState {
|
|
|
485
557
|
let syncedOnUpdate = false;
|
|
486
558
|
|
|
487
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
|
+
}
|
|
488
566
|
// Try to coerce value now, so that eventual errors are triggered early
|
|
489
567
|
// on the node requesting the update and not only on the server side.
|
|
490
568
|
try {
|
|
@@ -494,13 +572,6 @@ class SharedState {
|
|
|
494
572
|
throw new TypeError(`Cannot execute 'set' on SharedState (${this.#className}): ${err.message}`);
|
|
495
573
|
}
|
|
496
574
|
|
|
497
|
-
// Make sure that given name is in filter white list (if any)
|
|
498
|
-
if (this.#filter !== null) {
|
|
499
|
-
if (!this.#filter.includes(name)) {
|
|
500
|
-
throw new DOMException(`Cannot execute 'set' on SharedState (${this.#className}): Parameter '${name}' is not in white list`, 'NotSupportedError');
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
|
|
504
575
|
// ### `immediate` modifier behavior
|
|
505
576
|
//
|
|
506
577
|
// If immediate=true
|
|
@@ -551,11 +622,14 @@ class SharedState {
|
|
|
551
622
|
hasLocalParam = true;
|
|
552
623
|
locallyResolvedParam[name] = this.#parameters.get(name);
|
|
553
624
|
} else if (acknowledge === false) {
|
|
554
|
-
//
|
|
555
|
-
//
|
|
625
|
+
// when acknowledge is false, e.g. streaming values, values are
|
|
626
|
+
// propagated on the network only if hasSiblings is true
|
|
556
627
|
hasNonAcknowledgedParams = true;
|
|
557
628
|
locallyResolvedParam[name] = this.#parameters.get(name);
|
|
558
|
-
|
|
629
|
+
// propagate only if there is an attached state somewhere
|
|
630
|
+
if (this.hasSiblings) {
|
|
631
|
+
networkPropagatedParams[name] = updates[name];
|
|
632
|
+
}
|
|
559
633
|
} else {
|
|
560
634
|
// note that immediate params are shared and acknowledged
|
|
561
635
|
hasRegularSharedParam = true;
|
|
@@ -563,9 +637,9 @@ class SharedState {
|
|
|
563
637
|
}
|
|
564
638
|
}
|
|
565
639
|
|
|
566
|
-
// params that trigger a synced onUpdate call: immediate, local, non-acknowledged
|
|
640
|
+
// params that trigger a synced onUpdate call are: immediate, local, non-acknowledged
|
|
567
641
|
if (syncedOnUpdate) {
|
|
568
|
-
this.#
|
|
642
|
+
this.#executeUpdateCallbacks(newValues, oldValues);
|
|
569
643
|
}
|
|
570
644
|
|
|
571
645
|
// if we only have local params, we can resolve immediately
|
|
@@ -580,7 +654,9 @@ class SharedState {
|
|
|
580
654
|
this[kSharedStatePromiseStore].associateResolveData(reqId, locallyResolvedParam);
|
|
581
655
|
}
|
|
582
656
|
|
|
583
|
-
|
|
657
|
+
if (Object.keys(networkPropagatedParams).length > 0) {
|
|
658
|
+
this.#client.transport.emit(`${UPDATE_REQUEST}-${this.#id}-${this.#instanceId}`, reqId, networkPropagatedParams);
|
|
659
|
+
}
|
|
584
660
|
|
|
585
661
|
// if we don't have regular shared params, we can resolve now
|
|
586
662
|
if (!hasRegularSharedParam) {
|
|
@@ -610,10 +686,11 @@ class SharedState {
|
|
|
610
686
|
throw new ReferenceError(`Cannot execute 'get' on SharedState (${this.#className}): Parameter '${name}' is not defined`);
|
|
611
687
|
}
|
|
612
688
|
|
|
613
|
-
if
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
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');
|
|
617
694
|
}
|
|
618
695
|
|
|
619
696
|
return this.#parameters.get(name);
|
|
@@ -639,10 +716,11 @@ class SharedState {
|
|
|
639
716
|
throw new ReferenceError(`Cannot execute 'getUnsafe' on SharedState (${this.#className}): Parameter '${name}' is not defined`);
|
|
640
717
|
}
|
|
641
718
|
|
|
642
|
-
if
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
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');
|
|
646
724
|
}
|
|
647
725
|
|
|
648
726
|
return this.#parameters.getUnsafe(name);
|
|
@@ -659,16 +737,7 @@ class SharedState {
|
|
|
659
737
|
*/
|
|
660
738
|
getValues() {
|
|
661
739
|
const values = this.#parameters.getValues();
|
|
662
|
-
|
|
663
|
-
if (this.#filter !== null) {
|
|
664
|
-
for (let name in values) {
|
|
665
|
-
if (!this.#filter.includes(name)) {
|
|
666
|
-
delete values[name];
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
return values;
|
|
740
|
+
return this.#sanitizeValuesAgainstFilters(values);
|
|
672
741
|
}
|
|
673
742
|
|
|
674
743
|
/**
|
|
@@ -686,16 +755,7 @@ class SharedState {
|
|
|
686
755
|
*/
|
|
687
756
|
getValuesUnsafe() {
|
|
688
757
|
const values = this.#parameters.getValuesUnsafe();
|
|
689
|
-
|
|
690
|
-
if (this.#filter !== null) {
|
|
691
|
-
for (let name in values) {
|
|
692
|
-
if (!this.#filter.includes(name)) {
|
|
693
|
-
delete values[name];
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
return values;
|
|
758
|
+
return this.#sanitizeValuesAgainstFilters(values);
|
|
699
759
|
}
|
|
700
760
|
|
|
701
761
|
/**
|
|
@@ -776,9 +836,43 @@ class SharedState {
|
|
|
776
836
|
}
|
|
777
837
|
}
|
|
778
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
|
+
*/
|
|
779
868
|
/**
|
|
780
869
|
* Subscribe to state updates.
|
|
781
870
|
*
|
|
871
|
+
* Alternative signatures:
|
|
872
|
+
* - `state.onUpdate(callback, executeListener)`
|
|
873
|
+
* - `state.onUpdate(paramName, callback, executeListener)`
|
|
874
|
+
*
|
|
875
|
+
* @param {SharedStateParameterName} paramName
|
|
782
876
|
* @param {sharedStateOnUpdateCallback} callback
|
|
783
877
|
* Callback to execute when an update is applied on the state.
|
|
784
878
|
* @param {Boolean} [executeListener=false] - Execute the callback immediately
|
|
@@ -796,8 +890,13 @@ class SharedState {
|
|
|
796
890
|
* // later
|
|
797
891
|
* unsubscribe();
|
|
798
892
|
*/
|
|
799
|
-
onUpdate(
|
|
800
|
-
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);
|
|
801
900
|
|
|
802
901
|
if (executeListener === true) {
|
|
803
902
|
const currentValues = this.getValues();
|
|
@@ -811,12 +910,10 @@ class SharedState {
|
|
|
811
910
|
}
|
|
812
911
|
}
|
|
813
912
|
|
|
814
|
-
|
|
913
|
+
this.#executeUpdateCallback(listenerPayload, currentValues, {});
|
|
815
914
|
}
|
|
816
915
|
|
|
817
|
-
return
|
|
818
|
-
this.#onUpdateCallbacks.delete(listener);
|
|
819
|
-
};
|
|
916
|
+
return listenerPayload.unsubscribe;
|
|
820
917
|
}
|
|
821
918
|
|
|
822
919
|
/**
|
|
@@ -847,6 +944,67 @@ class SharedState {
|
|
|
847
944
|
this.#onDeleteCallbacks.add(callback);
|
|
848
945
|
return () => this.#onDeleteCallbacks.delete(callback);
|
|
849
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
|
+
|
|
850
1008
|
}
|
|
851
1009
|
|
|
852
1010
|
export default SharedState;
|