@soundworks/core 4.0.0-alpha.9 → 5.0.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 +102 -0
- package/README.md +14 -29
- package/package.json +24 -42
- package/src/client/Client.js +306 -256
- package/src/client/{Context.js → ClientContext.js} +63 -54
- package/src/client/ClientContextManager.js +115 -0
- package/src/client/{Plugin.js → ClientPlugin.js} +23 -19
- package/src/client/{PluginManager.js → ClientPluginManager.js} +43 -40
- package/src/client/ClientSocket.js +270 -0
- package/src/client/{StateManager.js → ClientStateManager.js} +13 -13
- package/src/client/index.js +36 -39
- package/src/common/BasePlugin.js +63 -58
- package/src/common/BasePluginManager.js +191 -142
- package/src/common/BaseStateManager.js +635 -145
- package/src/common/BatchedTransport.js +73 -0
- package/src/common/ParameterBag.js +90 -93
- package/src/common/PromiseStore.js +54 -0
- package/src/common/SharedState.js +821 -0
- package/src/common/SharedStateCollection.js +492 -0
- package/src/common/constants.js +14 -2
- package/src/common/logger.js +64 -68
- package/src/common/shared-state-types.js +82 -0
- package/src/common/sockets-utils.js +0 -64
- package/src/common/version.js +2 -0
- package/src/server/Server.js +552 -824
- package/src/server/ServerClient.js +74 -0
- package/src/server/{Context.js → ServerContext.js} +96 -77
- package/src/server/{ContextManager.js → ServerContextManager.js} +107 -87
- package/src/server/{Plugin.js → ServerPlugin.js} +30 -32
- package/src/server/ServerPluginManager.js +177 -0
- package/src/server/ServerSocket.js +256 -0
- package/src/server/ServerSockets.js +150 -0
- package/src/server/ServerStateManager.js +706 -0
- package/src/server/SharedStatePrivate.js +277 -0
- package/src/server/audit-network-latency.worker.js +19 -4
- package/src/server/{audit-schema.js → audit-state-class-description.js} +3 -2
- package/src/server/create-http-server.js +127 -0
- package/src/server/crypto.js +8 -7
- package/src/server/index.js +60 -17
- package/types/client/Client.d.ts +95 -70
- package/types/client/{Context.d.ts → ClientContext.d.ts} +59 -34
- package/types/client/{ContextManager.d.ts → ClientContextManager.d.ts} +19 -22
- package/types/client/ClientPlugin.d.ts +51 -0
- package/types/client/ClientPluginManager.d.ts +112 -0
- package/types/client/ClientSocket.d.ts +76 -0
- package/types/client/{StateManager.d.ts → ClientStateManager.d.ts} +18 -21
- package/types/client/index.d.ts +33 -6
- package/types/common/BasePlugin.d.ts +64 -53
- package/types/common/BasePluginManager.d.ts +84 -39
- package/types/common/BaseStateManager.d.ts +227 -49
- package/types/common/BatchedTransport.d.ts +17 -0
- package/types/common/ParameterBag.d.ts +73 -76
- package/types/common/PromiseStore.d.ts +11 -0
- package/types/common/SharedState.d.ts +291 -0
- package/types/common/SharedStateCollection.d.ts +311 -0
- package/types/common/constants.d.ts +10 -3
- package/types/common/logger.d.ts +8 -6
- package/types/common/shared-state-types.d.ts +95 -0
- package/types/common/sockets-utils.d.ts +0 -5
- package/types/common/version.d.ts +2 -0
- package/types/server/Server.d.ts +215 -136
- package/types/server/ServerClient.d.ts +45 -0
- package/types/server/ServerContext.d.ts +209 -0
- package/types/server/ServerContextManager.d.ts +75 -0
- package/types/server/ServerPlugin.d.ts +57 -0
- package/types/server/ServerPluginManager.d.ts +115 -0
- package/types/server/ServerSocket.d.ts +69 -0
- package/types/server/ServerSockets.d.ts +42 -0
- package/types/server/ServerStateManager.d.ts +284 -0
- package/types/server/SharedStatePrivate.d.ts +23 -0
- package/types/server/audit-network-latency.worker.d.ts +2 -2
- package/types/server/{audit-schema.d.ts → audit-state-class-description.d.ts} +8 -9
- package/types/server/create-http-server.d.ts +5 -0
- package/types/server/crypto.d.ts +4 -0
- package/types/server/index.d.ts +51 -3
- package/.eslintrc +0 -15
- package/.github/workflows/gh-pages.yml +0 -31
- package/.github/workflows/unit-tests.yml +0 -18
- package/src/client/ContextManager.js +0 -111
- package/src/client/SharedState.js +0 -84
- package/src/client/SharedStateCollection.js +0 -35
- package/src/client/Socket.js +0 -402
- package/src/common/BaseSharedState.js +0 -583
- package/src/common/BaseSharedStateCollection.js +0 -179
- package/src/common/SharedStatePrivate.js +0 -192
- package/src/common/promise-store.js +0 -25
- package/src/server/Client.js +0 -55
- package/src/server/PluginManager.js +0 -170
- package/src/server/SharedState.js +0 -85
- package/src/server/SharedStateCollection.js +0 -36
- package/src/server/Socket.js +0 -396
- package/src/server/Sockets.js +0 -224
- package/src/server/StateManager.js +0 -620
- package/tsconfig.json +0 -21
- package/types/client/Client.d.ts.map +0 -1
- package/types/client/Context.d.ts.map +0 -1
- package/types/client/ContextManager.d.ts.map +0 -1
- package/types/client/Plugin.d.ts +0 -53
- package/types/client/Plugin.d.ts.map +0 -1
- package/types/client/PluginManager.d.ts +0 -96
- package/types/client/PluginManager.d.ts.map +0 -1
- package/types/client/SharedState.d.ts +0 -85
- package/types/client/SharedState.d.ts.map +0 -1
- package/types/client/Socket.d.ts +0 -138
- package/types/client/Socket.d.ts.map +0 -1
- package/types/client/StateManager.d.ts.map +0 -1
- package/types/client/index.d.ts.map +0 -1
- package/types/common/BasePlugin.d.ts.map +0 -1
- package/types/common/BasePluginManager.d.ts.map +0 -1
- package/types/common/BaseSharedState.d.ts +0 -173
- package/types/common/BaseSharedState.d.ts.map +0 -1
- package/types/common/BaseStateManager.d.ts.map +0 -1
- package/types/common/ParameterBag.d.ts.map +0 -1
- package/types/common/SharedStatePrivate.d.ts +0 -20
- package/types/common/SharedStatePrivate.d.ts.map +0 -1
- package/types/common/constants.d.ts.map +0 -1
- package/types/common/logger.d.ts.map +0 -1
- package/types/common/promise-store.d.ts +0 -4
- package/types/common/promise-store.d.ts.map +0 -1
- package/types/common/sockets-utils.d.ts.map +0 -1
- package/types/common/utils.d.ts +0 -5
- package/types/common/utils.d.ts.map +0 -1
- package/types/server/Client.d.ts +0 -40
- package/types/server/Client.d.ts.map +0 -1
- package/types/server/Context.d.ts +0 -59
- package/types/server/Context.d.ts.map +0 -1
- package/types/server/ContextManager.d.ts +0 -69
- package/types/server/ContextManager.d.ts.map +0 -1
- package/types/server/Plugin.d.ts +0 -70
- package/types/server/Plugin.d.ts.map +0 -1
- package/types/server/PluginManager.d.ts +0 -91
- package/types/server/PluginManager.d.ts.map +0 -1
- package/types/server/Server.d.ts.map +0 -1
- package/types/server/SharedState.d.ts +0 -86
- package/types/server/SharedState.d.ts.map +0 -1
- package/types/server/Socket.d.ts +0 -128
- package/types/server/Socket.d.ts.map +0 -1
- package/types/server/Sockets.d.ts +0 -73
- package/types/server/Sockets.d.ts.map +0 -1
- package/types/server/StateManager.d.ts +0 -190
- package/types/server/StateManager.d.ts.map +0 -1
- package/types/server/audit-network-latency.worker.d.ts.map +0 -1
- package/types/server/audit-schema.d.ts.map +0 -1
- package/types/server/index.d.ts.map +0 -1
|
@@ -0,0 +1,821 @@
|
|
|
1
|
+
import { isPlainObject, isString } from '@ircam/sc-utils';
|
|
2
|
+
|
|
3
|
+
import ParameterBag from './ParameterBag.js';
|
|
4
|
+
import PromiseStore from './PromiseStore.js';
|
|
5
|
+
import {
|
|
6
|
+
DELETE_REQUEST,
|
|
7
|
+
DELETE_RESPONSE,
|
|
8
|
+
DELETE_ERROR,
|
|
9
|
+
DELETE_NOTIFICATION,
|
|
10
|
+
DETACH_REQUEST,
|
|
11
|
+
DETACH_RESPONSE,
|
|
12
|
+
DETACH_ERROR,
|
|
13
|
+
UPDATE_REQUEST,
|
|
14
|
+
UPDATE_RESPONSE,
|
|
15
|
+
UPDATE_ABORT,
|
|
16
|
+
UPDATE_NOTIFICATION,
|
|
17
|
+
} from './constants.js';
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
kStateManagerClient,
|
|
21
|
+
kStateManagerDeleteState,
|
|
22
|
+
} from './BaseStateManager.js';
|
|
23
|
+
|
|
24
|
+
import logger from './logger.js';
|
|
25
|
+
|
|
26
|
+
// for testing purposes
|
|
27
|
+
export const kSharedStatePromiseStore = Symbol('soundworks:shared-state-promise-store');
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Callback executed when updates are applied on a {@link SharedState}.
|
|
31
|
+
*
|
|
32
|
+
* @callback sharedStateOnUpdateCallback
|
|
33
|
+
* @param {Object} newValues - Key / value pairs of the updates that have been
|
|
34
|
+
* applied to the state.
|
|
35
|
+
* @param {Object} oldValues - Key / value pairs of the updated params before
|
|
36
|
+
* the updates has been applied to the state.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Delete the registered {@link sharedStateOnUpdateCallback}.
|
|
41
|
+
*
|
|
42
|
+
* @callback sharedStateDeleteOnUpdateCallback
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The `SharedState` is one of the most important and versatile abstraction provided
|
|
47
|
+
* by `soundworks`. It represents a set of parameters that are synchronized across
|
|
48
|
+
* every nodes of the application (clients and server) that declared some interest
|
|
49
|
+
* to the shared state.
|
|
50
|
+
*
|
|
51
|
+
* A `SharedState` instance is created according to a shared state class definition
|
|
52
|
+
* which is composed of a {@link SharedStateClassName} and of a {@link SharedStateClassDescription}
|
|
53
|
+
* registered in the {@link ServerStateManager}. Any number of `SharedState`s
|
|
54
|
+
* can be created from a single class definition.
|
|
55
|
+
*
|
|
56
|
+
* A shared state can be created both by the clients or by the server, in which case
|
|
57
|
+
* it is generally considered as a global state of the application. Similarly any
|
|
58
|
+
* node of the application (clients or server) can declare interest and "attach" to
|
|
59
|
+
* a state created by another node. All node attached to a state can modify its values
|
|
60
|
+
* and/or react to the modifications applied by other nodes.
|
|
61
|
+
*
|
|
62
|
+
* Tutorial: {@link https://soundworks.dev/guide/state-manager.html}
|
|
63
|
+
*
|
|
64
|
+
* ```
|
|
65
|
+
* // server-side
|
|
66
|
+
* import { Server } from '@soundworks/server/index.js';
|
|
67
|
+
*
|
|
68
|
+
* const server = new Server(config);
|
|
69
|
+
* // define a shared state class
|
|
70
|
+
* server.stateManager.defineClass('some-global-state', {
|
|
71
|
+
* myRandom: {
|
|
72
|
+
* type: 'float',
|
|
73
|
+
* default: 0,
|
|
74
|
+
* }
|
|
75
|
+
* });
|
|
76
|
+
*
|
|
77
|
+
* await server.start();
|
|
78
|
+
*
|
|
79
|
+
* // create a global state server-side
|
|
80
|
+
* const globalState = await server.stateManager.create('some-global-state');
|
|
81
|
+
* // listen and react to the changes made by the clients
|
|
82
|
+
* globalState.onUpdate(updates => console.log(updates));
|
|
83
|
+
* ```
|
|
84
|
+
*
|
|
85
|
+
* ```
|
|
86
|
+
* // client-side
|
|
87
|
+
* import { Client } from '@soundworks/client.index.js';
|
|
88
|
+
*
|
|
89
|
+
* const client = new Client(config);
|
|
90
|
+
* await client.start();
|
|
91
|
+
*
|
|
92
|
+
* // attach to the global state created by the server
|
|
93
|
+
* const globalState = await client.stateManager.attach('some-global-state');
|
|
94
|
+
*
|
|
95
|
+
* // update the value of a `myRandom` parameter every seconds
|
|
96
|
+
* setInterval(() => {
|
|
97
|
+
* globalState.set({ myRandom: Math.random() });
|
|
98
|
+
* }, 1000);
|
|
99
|
+
* ```
|
|
100
|
+
*/
|
|
101
|
+
class SharedState {
|
|
102
|
+
#id = null;
|
|
103
|
+
#instanceId = null;
|
|
104
|
+
#className = null;
|
|
105
|
+
#isOwner = null;
|
|
106
|
+
#client = null;
|
|
107
|
+
#manager = null;
|
|
108
|
+
#filter = null;
|
|
109
|
+
// true is the state has been detached or deleted
|
|
110
|
+
#detached = false;
|
|
111
|
+
#parameters = null;
|
|
112
|
+
#onUpdateCallbacks = new Set();
|
|
113
|
+
#onDetachCallbacks = new Set();
|
|
114
|
+
#onDeleteCallbacks = new Set();
|
|
115
|
+
|
|
116
|
+
constructor({
|
|
117
|
+
stateId,
|
|
118
|
+
instanceId,
|
|
119
|
+
className,
|
|
120
|
+
classDescription,
|
|
121
|
+
isOwner,
|
|
122
|
+
manager,
|
|
123
|
+
initValues,
|
|
124
|
+
filter,
|
|
125
|
+
}) {
|
|
126
|
+
this.#manager = manager;
|
|
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;
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
this.#parameters = new ParameterBag(classDescription, initValues);
|
|
136
|
+
} catch (err) {
|
|
137
|
+
throw new Error(`Cannot construct 'SharedState': ${err.message}`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** @private */
|
|
141
|
+
this[kSharedStatePromiseStore] = new PromiseStore(this.constructor.name);
|
|
142
|
+
|
|
143
|
+
// add listener for state updates
|
|
144
|
+
this.#client.transport.addListener(`${UPDATE_RESPONSE}-${this.#id}-${this.#instanceId}`, async (reqId, updates) => {
|
|
145
|
+
const updated = await this.#commit(updates, true, true);
|
|
146
|
+
this[kSharedStatePromiseStore].resolve(reqId, updated);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// retrieve values but do not propagate to subscriptions
|
|
150
|
+
this.#client.transport.addListener(`${UPDATE_ABORT}-${this.#id}-${this.#instanceId}`, async (reqId, updates) => {
|
|
151
|
+
const updated = await this.#commit(updates, false, true);
|
|
152
|
+
this[kSharedStatePromiseStore].resolve(reqId, updated);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
this.#client.transport.addListener(`${UPDATE_NOTIFICATION}-${this.#id}-${this.#instanceId}`, async (updates) => {
|
|
156
|
+
// https://github.com/collective-soundworks/soundworks/issues/18
|
|
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
|
+
});
|
|
180
|
+
|
|
181
|
+
// ---------------------------------------------
|
|
182
|
+
// state has been deleted by its creator or the class has been deleted
|
|
183
|
+
// ---------------------------------------------
|
|
184
|
+
this.#client.transport.addListener(`${DELETE_NOTIFICATION}-${this.#id}-${this.#instanceId}`, async () => {
|
|
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
|
+
}
|
|
195
|
+
|
|
196
|
+
if (this.#isOwner) {
|
|
197
|
+
for (let callback of this.#onDeleteCallbacks) {
|
|
198
|
+
await callback();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
this.#onDetachCallbacks.clear();
|
|
203
|
+
this.#onDeleteCallbacks.clear();
|
|
204
|
+
this[kSharedStatePromiseStore].flush();
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
if (this.#isOwner) {
|
|
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
|
+
await callback();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
for (let callback of this.#onDeleteCallbacks) {
|
|
221
|
+
await callback();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
this.#onDetachCallbacks.clear();
|
|
225
|
+
this.#onDeleteCallbacks.clear();
|
|
226
|
+
this[kSharedStatePromiseStore].resolve(reqId, this);
|
|
227
|
+
this[kSharedStatePromiseStore].flush();
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
this.#client.transport.addListener(`${DELETE_ERROR}-${this.#id}`, (reqId, msg) => {
|
|
231
|
+
this[kSharedStatePromiseStore].reject(reqId, msg);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
} else {
|
|
235
|
+
// ---------------------------------------------
|
|
236
|
+
// the attached node has called `.detach()`
|
|
237
|
+
// ---------------------------------------------
|
|
238
|
+
this.#client.transport.addListener(`${DETACH_RESPONSE}-${this.#id}-${this.#instanceId}`, async (reqId) => {
|
|
239
|
+
this.#manager[kStateManagerDeleteState](this.#id);
|
|
240
|
+
this.#clearTransport();
|
|
241
|
+
|
|
242
|
+
for (let callback of this.#onDetachCallbacks) {
|
|
243
|
+
await callback();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
this.#onDetachCallbacks.clear();
|
|
247
|
+
this.#onDeleteCallbacks.clear();
|
|
248
|
+
this[kSharedStatePromiseStore].resolve(reqId, this);
|
|
249
|
+
this[kSharedStatePromiseStore].flush();
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// the state does not exists anymore in the server (should not happen)
|
|
253
|
+
this.#client.transport.addListener(`${DETACH_ERROR}-${this.#id}`, (reqId, msg) => {
|
|
254
|
+
this.#onDetachCallbacks.clear();
|
|
255
|
+
this.#onDeleteCallbacks.clear();
|
|
256
|
+
this[kSharedStatePromiseStore].reject(reqId, msg);
|
|
257
|
+
this[kSharedStatePromiseStore].flush();
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Id of the state
|
|
264
|
+
* @type {Number}
|
|
265
|
+
*/
|
|
266
|
+
get id() {
|
|
267
|
+
return this.#id;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Name of the underlying {@link SharedState} class.
|
|
272
|
+
* @type {String}
|
|
273
|
+
*/
|
|
274
|
+
get className() {
|
|
275
|
+
return this.#className;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* @deprecated Use {@link SharedState#className} instead.
|
|
280
|
+
*/
|
|
281
|
+
get schemaName() {
|
|
282
|
+
logger.deprecated('SharedState#schemaName', 'SharedState#className', '4.0.0-alpha.29');
|
|
283
|
+
return this.className;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Indicates if the node is the owner of the state, i.e. if it created the state.
|
|
288
|
+
* @type {Boolean}
|
|
289
|
+
*/
|
|
290
|
+
get isOwner() {
|
|
291
|
+
return this.#isOwner;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
#clearTransport() {
|
|
295
|
+
// remove listeners
|
|
296
|
+
this.#client.transport.removeAllListeners(`${UPDATE_RESPONSE}-${this.#id}-${this.#instanceId}`);
|
|
297
|
+
this.#client.transport.removeAllListeners(`${UPDATE_NOTIFICATION}-${this.#id}-${this.#instanceId}`);
|
|
298
|
+
this.#client.transport.removeAllListeners(`${UPDATE_ABORT}-${this.#id}-${this.#instanceId}`);
|
|
299
|
+
this.#client.transport.removeAllListeners(`${DELETE_NOTIFICATION}-${this.#id}-${this.#instanceId}`);
|
|
300
|
+
|
|
301
|
+
if (this.#isOwner) {
|
|
302
|
+
this.#client.transport.removeAllListeners(`${DELETE_RESPONSE}-${this.#id}-${this.#instanceId}`);
|
|
303
|
+
this.#client.transport.removeAllListeners(`${DELETE_ERROR}-${this.#id}-${this.#instanceId}`);
|
|
304
|
+
} else {
|
|
305
|
+
this.#client.transport.removeAllListeners(`${DETACH_RESPONSE}-${this.#id}-${this.#instanceId}`);
|
|
306
|
+
this.#client.transport.removeAllListeners(`${DETACH_ERROR}-${this.#id}-${this.#instanceId}`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async #commit(updates, propagate = true, initiator = false) {
|
|
311
|
+
const newValues = {};
|
|
312
|
+
const oldValues = {};
|
|
313
|
+
|
|
314
|
+
for (let name in updates) {
|
|
315
|
+
const { immediate, event } = this.#parameters.getDescription(name);
|
|
316
|
+
// @note 20211209 - we had an issue here server-side, because if the value
|
|
317
|
+
// is an object or an array, the reference is shared by everybody, therefore
|
|
318
|
+
// `changed` is always false and the new value is never propagated...
|
|
319
|
+
// FIXED - `state.get` now returns a deep copy when `type` is `any`
|
|
320
|
+
const oldValue = this.#parameters.get(name);
|
|
321
|
+
const [newValue, changed] = this.#parameters.set(name, updates[name]);
|
|
322
|
+
|
|
323
|
+
// handle immediate stuff
|
|
324
|
+
if (initiator && immediate) {
|
|
325
|
+
// @note - we don't need to check filterChange here because the value
|
|
326
|
+
// has been updated in parameters on the `set` side so can rely on `changed`
|
|
327
|
+
// to avoid retrigger listeners.
|
|
328
|
+
// If the value has been overridden by the server, `changed` will true
|
|
329
|
+
// anyway so it should behave correctly.
|
|
330
|
+
if (!changed || event) {
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
newValues[name] = newValue;
|
|
336
|
+
oldValues[name] = oldValue;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// if the `UPDATE_REQUEST` as been aborted by the server, do not propagate
|
|
340
|
+
let promises = [];
|
|
341
|
+
|
|
342
|
+
if (propagate && Object.keys(newValues).length > 0) {
|
|
343
|
+
this.#onUpdateCallbacks.forEach(listener => {
|
|
344
|
+
promises.push(listener(newValues, oldValues));
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// on a given client, `await state.set(update)` resolves after all
|
|
349
|
+
// update callbacks have themselves resolved
|
|
350
|
+
await Promise.all(promises);
|
|
351
|
+
|
|
352
|
+
// reset events to null after propagation of all listeners
|
|
353
|
+
for (let name in newValues) {
|
|
354
|
+
const { event } = this.#parameters.getDescription(name);
|
|
355
|
+
|
|
356
|
+
if (event) {
|
|
357
|
+
this.#parameters.set(name, null);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return newValues;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Return the underlying {@link SharedStateClassDescription} or the
|
|
366
|
+
* {@link SharedStateParameterDescription} if `paramName` is given.
|
|
367
|
+
*
|
|
368
|
+
* @param {string} [paramName=null] - If defined, returns the parameter
|
|
369
|
+
* description of the given parameter name rather than the full class description.
|
|
370
|
+
* @return {SharedStateClassDescription|SharedStateParameterDescription}
|
|
371
|
+
* @throws Throws if `paramName` is not null and does not exists.
|
|
372
|
+
* @example
|
|
373
|
+
* const classDescription = state.getDescription();
|
|
374
|
+
* const paramDescription = state.getDescription('my-param');
|
|
375
|
+
*/
|
|
376
|
+
getDescription(paramName = null) {
|
|
377
|
+
try {
|
|
378
|
+
return this.#parameters.getDescription(paramName);
|
|
379
|
+
} catch (err) {
|
|
380
|
+
throw new ReferenceError(`Cannot execute 'getDescription' on SharedState: ${err.message}`);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* @deprecated Use {@link SharedState#getDescription} instead.
|
|
386
|
+
*/
|
|
387
|
+
getSchema(paramName = null) {
|
|
388
|
+
logger.deprecated('SharedState#getSchema', 'SharedState#getDescription', '4.0.0-alpha.29');
|
|
389
|
+
return this.getDescription(paramName);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Update the values of the state.
|
|
394
|
+
*
|
|
395
|
+
* The returned `Promise` resolves on an object that contains the applied updates,
|
|
396
|
+
* and resolves after all the `onUpdate` callbacks have resolved themselves
|
|
397
|
+
*
|
|
398
|
+
* @overload
|
|
399
|
+
* @param {object} updates - Key / value pairs of updates to apply to the state.
|
|
400
|
+
* @returns {Promise<Object>} - Promise to the (coerced) updates.
|
|
401
|
+
*/
|
|
402
|
+
/**
|
|
403
|
+
* Update the values of the state.
|
|
404
|
+
*
|
|
405
|
+
* The returned `Promise` resolves on an object that contains the applied updates,
|
|
406
|
+
* and resolves after all the `onUpdate` callbacks have resolved themselves
|
|
407
|
+
*
|
|
408
|
+
* @overload
|
|
409
|
+
* @param {SharedStateParameterName} name - Name of the parameter.
|
|
410
|
+
* @param {*} value - Value of the parameter.
|
|
411
|
+
* @returns {Promise<Object>} - Promise to the (coerced) updates.
|
|
412
|
+
*/
|
|
413
|
+
/**
|
|
414
|
+
* Update the values of the state.
|
|
415
|
+
*
|
|
416
|
+
* The returned `Promise` resolves on an object that contains the applied updates,
|
|
417
|
+
* and resolves after all the `onUpdate` callbacks have resolved themselves, i.e.:
|
|
418
|
+
* ```js
|
|
419
|
+
* server.stateManager.defineClass('test', {
|
|
420
|
+
* myBool: { type: 'boolean', default: false },
|
|
421
|
+
* });
|
|
422
|
+
* const a = await server.stateManager.create('a');
|
|
423
|
+
*
|
|
424
|
+
* let asyncCallbackCalled = false;
|
|
425
|
+
*
|
|
426
|
+
* a.onUpdate(updates => {
|
|
427
|
+
* return new Promise(resolve => {
|
|
428
|
+
* setTimeout(() => {
|
|
429
|
+
* asyncCallbackCalled = true;
|
|
430
|
+
* resolve();
|
|
431
|
+
* }, 100);
|
|
432
|
+
* });
|
|
433
|
+
* });
|
|
434
|
+
*
|
|
435
|
+
* const updates = await a.set({ myBool: true });
|
|
436
|
+
* assert.equal(asyncCallbackCalled, true);
|
|
437
|
+
* assert.deepEqual(updates, { myBool: true });
|
|
438
|
+
* ```
|
|
439
|
+
*
|
|
440
|
+
* Alternative signatures:
|
|
441
|
+
* - `await state.set(updates)`
|
|
442
|
+
* - `await state.set(name, value)`
|
|
443
|
+
*
|
|
444
|
+
* @param {object} updates - Key / value pairs of updates to apply to the state.
|
|
445
|
+
* @returns {Promise<Object>} - Promise to the (coerced) updates.
|
|
446
|
+
*
|
|
447
|
+
* @example
|
|
448
|
+
* const state = await client.stateManager.attach('globals');
|
|
449
|
+
* const updates = await state.set({ myParam: Math.random() });
|
|
450
|
+
*/
|
|
451
|
+
async set(updates) {
|
|
452
|
+
if (this.#detached) {
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (isPlainObject(arguments[0]) && isPlainObject(arguments[1])) {
|
|
457
|
+
logger.removed('`context` argument in SharedState.set(updates, context)', 'a regular parameter configured with `event=true`', '4.0.0-alpha.29');
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (arguments.length === 2 && isString(updates)) {
|
|
461
|
+
updates = { [updates]: arguments[1] };
|
|
462
|
+
} else if (!isPlainObject(updates)) {
|
|
463
|
+
throw new TypeError(`Cannot execute 'set' on SharedState (${this.#className}): 'updates' argument should be an object`);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const newValues = {};
|
|
467
|
+
const oldValues = {};
|
|
468
|
+
const localParams = {};
|
|
469
|
+
const sharedParams = {};
|
|
470
|
+
let hasLocalParam = false;
|
|
471
|
+
let hasSharedParam = false;
|
|
472
|
+
let forwardParams = undefined;
|
|
473
|
+
let propagateNow = false;
|
|
474
|
+
|
|
475
|
+
for (let name in updates) {
|
|
476
|
+
// Try to coerce value early, so that eventual errors are triggered early
|
|
477
|
+
// on the node requesting the update, and not only on the server side
|
|
478
|
+
// This throws if name does not exists
|
|
479
|
+
try {
|
|
480
|
+
this.#parameters.coerceValue(name, updates[name]);
|
|
481
|
+
} catch (err) {
|
|
482
|
+
throw new TypeError(`Cannot execute 'set' on SharedState (${this.#className}): ${err.message}`);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Check that name is in filter list, if any
|
|
486
|
+
if (this.#filter !== null) {
|
|
487
|
+
if (!this.#filter.includes(name)) {
|
|
488
|
+
throw new DOMException(`Cannot execute 'set' on SharedState (${this.#className}): Parameter '${name}' is not in white list`, 'NotSupportedError');
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// `immediate` option behavior
|
|
493
|
+
//
|
|
494
|
+
// If immediate=true
|
|
495
|
+
// - call listeners if value changed
|
|
496
|
+
// - go through normal server path
|
|
497
|
+
// - retrigger only if response from server is different from current value
|
|
498
|
+
// If immediate=true && (filterChange=false || event=true)
|
|
499
|
+
// - call listeners with value regardless it changed
|
|
500
|
+
// - go through normal server path
|
|
501
|
+
// - if the node is initiator of the update (UPDATE_RESPONSE), (re-)check
|
|
502
|
+
// to prevent execute the listeners twice
|
|
503
|
+
|
|
504
|
+
// `local` option behavior
|
|
505
|
+
//
|
|
506
|
+
// - If the `updates` object only contains local variables, we can call the
|
|
507
|
+
// update listeners and return a fulfilled promise immediately
|
|
508
|
+
// - If parameters that require network communication are present, we call the
|
|
509
|
+
// update callback onces with the local payload, then we need to wait for the server
|
|
510
|
+
// response, call update listeners with server response and resolve promise with
|
|
511
|
+
// the full payload, i.e. reintegrating the local params in the resolve payload
|
|
512
|
+
|
|
513
|
+
const { local, immediate, filterChange, event } = this.#parameters.getDescription(name);
|
|
514
|
+
|
|
515
|
+
if (immediate || local) {
|
|
516
|
+
const oldValue = this.#parameters.get(name);
|
|
517
|
+
const [newValue, changed] = this.#parameters.set(name, updates[name]);
|
|
518
|
+
|
|
519
|
+
// prepare data for immediate propagation of listeners
|
|
520
|
+
if (changed || filterChange === false || event) {
|
|
521
|
+
oldValues[name] = oldValue;
|
|
522
|
+
newValues[name] = newValue;
|
|
523
|
+
propagateNow = true;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// define params that must go through network or not
|
|
528
|
+
if (local) {
|
|
529
|
+
hasLocalParam = true;
|
|
530
|
+
// get sanitize value for fulfilling promise
|
|
531
|
+
localParams[name] = this.#parameters.get(name);
|
|
532
|
+
} else {
|
|
533
|
+
// note that immediate are shared params too
|
|
534
|
+
hasSharedParam = true;
|
|
535
|
+
sharedParams[name] = updates[name];
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// propagate immediate params if changed
|
|
540
|
+
if (propagateNow) {
|
|
541
|
+
this.#onUpdateCallbacks.forEach(listener => listener(newValues, oldValues));
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// check if we can resolve immediately or if we need to go through network
|
|
545
|
+
if (hasLocalParam) {
|
|
546
|
+
if (!hasSharedParam) {
|
|
547
|
+
return Promise.resolve(localParams);
|
|
548
|
+
} else {
|
|
549
|
+
// store local params to fulfill promise with all values, see PromiseStore
|
|
550
|
+
forwardParams = localParams;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// override updates to be shared on network without local params
|
|
555
|
+
updates = sharedParams;
|
|
556
|
+
|
|
557
|
+
// go through server-side normal behavior
|
|
558
|
+
return new Promise((resolve, reject) => {
|
|
559
|
+
const reqId = this[kSharedStatePromiseStore].add(resolve, reject, 'SharedState#set', forwardParams);
|
|
560
|
+
this.#client.transport.emit(`${UPDATE_REQUEST}-${this.#id}-${this.#instanceId}`, reqId, updates);
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Get the value of a parameter of the state.
|
|
566
|
+
*
|
|
567
|
+
* Be aware that in case of 'any' type, the returned value is deeply copied.
|
|
568
|
+
* While this prevents from pollution of the state by mutating the reference,
|
|
569
|
+
* this can also lead to performance issues when the parameter contains large
|
|
570
|
+
* data. In such cases you should use the {@link SharedState#getUnsafe} method
|
|
571
|
+
* and make sure to treat the returned object as readonly.
|
|
572
|
+
*
|
|
573
|
+
* @param {SharedStateParameterName} name - Name of the param.
|
|
574
|
+
* @return {any}
|
|
575
|
+
* @throws Throws if `name` does not exists.
|
|
576
|
+
* @example
|
|
577
|
+
* const value = state.get('paramName');
|
|
578
|
+
*/
|
|
579
|
+
get(name) {
|
|
580
|
+
if (!this.#parameters.has(name)) {
|
|
581
|
+
throw new ReferenceError(`Cannot execute 'get' on SharedState (${this.#className}): Parameter '${name}' is not defined`);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
if (this.#filter !== null) {
|
|
585
|
+
if (!this.#filter.includes(name)) {
|
|
586
|
+
throw new DOMException(`Cannot execute 'get' on SharedState (${this.#className}): Parameter '${name}' is not in white list`, 'NotSupportedError');
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
return this.#parameters.get(name);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Get an unsafe reference to the value of a parameter of the state.
|
|
595
|
+
*
|
|
596
|
+
* Similar to `get` but returns a reference to the underlying value in case of
|
|
597
|
+
* `any` type. Can be useful if the underlying value is large (e.g. sensors
|
|
598
|
+
* recordings, etc.) and deep cloning expensive. Be aware that if changes are
|
|
599
|
+
* made on the returned object, the state of your application will become
|
|
600
|
+
* inconsistent.
|
|
601
|
+
*
|
|
602
|
+
* @param {SharedStateParameterName} name - Name of the param.
|
|
603
|
+
* @return {any}
|
|
604
|
+
* @throws Throws if `name` does not exists.
|
|
605
|
+
* @example
|
|
606
|
+
* const value = state.getUnsafe('paramName');
|
|
607
|
+
*/
|
|
608
|
+
getUnsafe(name) {
|
|
609
|
+
if (!this.#parameters.has(name)) {
|
|
610
|
+
throw new ReferenceError(`Cannot execute 'getUnsafe' on SharedState (${this.#className}): Parameter '${name}' is not defined`);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
if (this.#filter !== null) {
|
|
614
|
+
if (!this.#filter.includes(name)) {
|
|
615
|
+
throw new DOMException(`Cannot execute 'getUnsafe' on SharedState (${this.#className}): Parameter '${name}' is not in white list`, 'NotSupportedError');
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
return this.#parameters.getUnsafe(name);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Get all the key / value pairs of the state.
|
|
624
|
+
*
|
|
625
|
+
* If a parameter is of `any` type, a deep copy is made.
|
|
626
|
+
*
|
|
627
|
+
* @return {object}
|
|
628
|
+
* @example
|
|
629
|
+
* const values = state.getValues();
|
|
630
|
+
*/
|
|
631
|
+
getValues() {
|
|
632
|
+
const values = this.#parameters.getValues();
|
|
633
|
+
|
|
634
|
+
if (this.#filter !== null) {
|
|
635
|
+
for (let name in values) {
|
|
636
|
+
if (!this.#filter.includes(name)) {
|
|
637
|
+
delete values[name];
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
return values;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Get all the key / value pairs of the state.
|
|
647
|
+
*
|
|
648
|
+
* Similar to `getValues` but returns a reference to the underlying value in
|
|
649
|
+
* case of `any` type. Can be useful if the underlying value is big (e.g.
|
|
650
|
+
* sensors recordings, etc.) and deep cloning expensive. Be aware that if
|
|
651
|
+
* changes are made on the returned object, the state of your application will
|
|
652
|
+
* become inconsistent.
|
|
653
|
+
*
|
|
654
|
+
* @return {object}
|
|
655
|
+
* @example
|
|
656
|
+
* const values = state.getValues();
|
|
657
|
+
*/
|
|
658
|
+
getValuesUnsafe() {
|
|
659
|
+
const values = this.#parameters.getValuesUnsafe();
|
|
660
|
+
|
|
661
|
+
if (this.#filter !== null) {
|
|
662
|
+
for (let name in values) {
|
|
663
|
+
if (!this.#filter.includes(name)) {
|
|
664
|
+
delete values[name];
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
return values;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* Get the values with which the state has been created. May defer from the
|
|
674
|
+
* default values declared in the class description.
|
|
675
|
+
*
|
|
676
|
+
* @return {object}
|
|
677
|
+
* @example
|
|
678
|
+
* const initValues = state.getInitValues();
|
|
679
|
+
*/
|
|
680
|
+
getInitValues() {
|
|
681
|
+
return this.#parameters.getInitValues();
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/**
|
|
685
|
+
* Get the default values as declared in the class description.
|
|
686
|
+
*
|
|
687
|
+
* @return {object}
|
|
688
|
+
* @example
|
|
689
|
+
* const defaults = state.getDefaults();
|
|
690
|
+
*/
|
|
691
|
+
getDefaults() {
|
|
692
|
+
return this.#parameters.getDefaults();
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* Detach from the state. If the client is the creator of the state, the state
|
|
697
|
+
* is deleted and all attached nodes get notified.
|
|
698
|
+
*
|
|
699
|
+
* @example
|
|
700
|
+
* const state = await client.state.attach('globals');
|
|
701
|
+
* // later
|
|
702
|
+
* await state.detach();
|
|
703
|
+
*/
|
|
704
|
+
async detach() {
|
|
705
|
+
if (this.#detached) {
|
|
706
|
+
throw new DOMException(`Cannot execute 'detach' on SharedState (${this.#className}): SharedState (${this.#id}) already detached`, 'NotSupportedError');
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
this.#detached = true; // mark detached early
|
|
710
|
+
this.#onUpdateCallbacks.clear();
|
|
711
|
+
|
|
712
|
+
if (this.#isOwner) {
|
|
713
|
+
return new Promise((resolve, reject) => {
|
|
714
|
+
const reqId = this[kSharedStatePromiseStore].add(resolve, reject, 'SharedState#delete');
|
|
715
|
+
this.#client.transport.emit(`${DELETE_REQUEST}-${this.#id}-${this.#instanceId}`, reqId);
|
|
716
|
+
});
|
|
717
|
+
} else {
|
|
718
|
+
return new Promise((resolve, reject) => {
|
|
719
|
+
const reqId = this[kSharedStatePromiseStore].add(resolve, reject, 'SharedState#detach');
|
|
720
|
+
this.#client.transport.emit(`${DETACH_REQUEST}-${this.#id}-${this.#instanceId}`, reqId);
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/**
|
|
726
|
+
* Delete the state. Only the creator/owner of the state can use this method.
|
|
727
|
+
*
|
|
728
|
+
* All nodes attached to the state will be detached, triggering any registered
|
|
729
|
+
* `onDetach` callbacks. The creator of the state will also have its own `onDelete`
|
|
730
|
+
* callback triggered. The local `onDetach` and `onDelete` callbacks will be
|
|
731
|
+
* executed *before* the returned Promise resolves
|
|
732
|
+
*
|
|
733
|
+
* @throws Throws if the method is called by a node which is not the owner of
|
|
734
|
+
* the state.
|
|
735
|
+
* @example
|
|
736
|
+
* const state = await client.stateManager.create('my-class-name');
|
|
737
|
+
* // later
|
|
738
|
+
* await state.delete();
|
|
739
|
+
*/
|
|
740
|
+
async delete() {
|
|
741
|
+
if (this.#isOwner) {
|
|
742
|
+
if (this.#detached) {
|
|
743
|
+
throw new DOMException(`Cannot execute 'delete' on SharedState (${this.#className}): SharedState (${this.#id}) already deleted`, 'NotSupportedError');
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
return this.detach();
|
|
747
|
+
} else {
|
|
748
|
+
throw new DOMException(`Cannot execute 'delete' on SharedState (${this.#className}): SharedState (${this.#id}) is not owned by this node. Use 'SharedState#detach' instead`, 'NotSupportedError');
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* Subscribe to state updates.
|
|
754
|
+
*
|
|
755
|
+
* @param {sharedStateOnUpdateCallback} callback
|
|
756
|
+
* Callback to execute when an update is applied on the state.
|
|
757
|
+
* @param {Boolean} [executeListener=false] - Execute the callback immediately
|
|
758
|
+
* with current state values. Note that `oldValues` will be set to `{}`.
|
|
759
|
+
* @returns {sharedStateDeleteOnUpdateCallback}
|
|
760
|
+
* @example
|
|
761
|
+
* const unsubscribe = state.onUpdate(async (newValues, oldValues) => {
|
|
762
|
+
* for (let [key, value] of Object.entries(newValues)) {
|
|
763
|
+
* switch (key) {
|
|
764
|
+
* // do something
|
|
765
|
+
* }
|
|
766
|
+
* }
|
|
767
|
+
* });
|
|
768
|
+
*
|
|
769
|
+
* // later
|
|
770
|
+
* unsubscribe();
|
|
771
|
+
*/
|
|
772
|
+
onUpdate(listener, executeListener = false) {
|
|
773
|
+
this.#onUpdateCallbacks.add(listener);
|
|
774
|
+
|
|
775
|
+
if (executeListener === true) {
|
|
776
|
+
const currentValues = this.getValues();
|
|
777
|
+
// filter `event: true` parameters from currentValues, having them here is
|
|
778
|
+
// misleading as we are in the context of a callback, not from an active read
|
|
779
|
+
const classDescription = this.getDescription();
|
|
780
|
+
|
|
781
|
+
for (let name in classDescription) {
|
|
782
|
+
if (classDescription[name].event === true) {
|
|
783
|
+
delete currentValues[name];
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
listener(currentValues, {});
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
return () => {
|
|
791
|
+
this.#onUpdateCallbacks.delete(listener);
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
/**
|
|
796
|
+
* Register a function to execute when detaching from the state. The function
|
|
797
|
+
* will be executed before the `detach` promise resolves.
|
|
798
|
+
*
|
|
799
|
+
* @param {Function} callback - Callback to execute when detaching from the state.
|
|
800
|
+
* Whether the client as called `detach`, or the state has been deleted by its
|
|
801
|
+
* creator.
|
|
802
|
+
*/
|
|
803
|
+
onDetach(callback) {
|
|
804
|
+
this.#onDetachCallbacks.add(callback);
|
|
805
|
+
return () => this.#onDetachCallbacks.delete(callback);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/**
|
|
809
|
+
* Register a function to execute when the state is deleted. Only called if the
|
|
810
|
+
* node was the creator of the state. Is called after `onDetach` and executed
|
|
811
|
+
* before the `delete` Promise resolves.
|
|
812
|
+
*
|
|
813
|
+
* @param {Function} callback - Callback to execute when the state is deleted.
|
|
814
|
+
*/
|
|
815
|
+
onDelete(callback) {
|
|
816
|
+
this.#onDeleteCallbacks.add(callback);
|
|
817
|
+
return () => this.#onDeleteCallbacks.delete(callback);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
export default SharedState;
|