@soundworks/core 4.0.0-alpha.9 → 5.0.1

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.
Files changed (144) hide show
  1. package/CHANGELOG.md +102 -0
  2. package/README.md +14 -29
  3. package/package.json +24 -42
  4. package/src/client/Client.js +306 -256
  5. package/src/client/{Context.js → ClientContext.js} +63 -54
  6. package/src/client/ClientContextManager.js +115 -0
  7. package/src/client/{Plugin.js → ClientPlugin.js} +23 -19
  8. package/src/client/{PluginManager.js → ClientPluginManager.js} +43 -40
  9. package/src/client/ClientSocket.js +270 -0
  10. package/src/client/{StateManager.js → ClientStateManager.js} +13 -13
  11. package/src/client/index.js +36 -39
  12. package/src/common/BasePlugin.js +64 -58
  13. package/src/common/BasePluginManager.js +191 -142
  14. package/src/common/BaseStateManager.js +635 -145
  15. package/src/common/BatchedTransport.js +73 -0
  16. package/src/common/ParameterBag.js +90 -93
  17. package/src/common/PromiseStore.js +54 -0
  18. package/src/common/SharedState.js +821 -0
  19. package/src/common/SharedStateCollection.js +492 -0
  20. package/src/common/constants.js +14 -2
  21. package/src/common/logger.js +64 -68
  22. package/src/common/shared-state-types.js +82 -0
  23. package/src/common/sockets-utils.js +0 -64
  24. package/src/common/version.js +2 -0
  25. package/src/server/Server.js +552 -824
  26. package/src/server/ServerClient.js +74 -0
  27. package/src/server/{Context.js → ServerContext.js} +96 -77
  28. package/src/server/{ContextManager.js → ServerContextManager.js} +107 -87
  29. package/src/server/{Plugin.js → ServerPlugin.js} +30 -32
  30. package/src/server/ServerPluginManager.js +177 -0
  31. package/src/server/ServerSocket.js +256 -0
  32. package/src/server/ServerSockets.js +150 -0
  33. package/src/server/ServerStateManager.js +706 -0
  34. package/src/server/SharedStatePrivate.js +277 -0
  35. package/src/server/audit-network-latency.worker.js +19 -4
  36. package/src/server/{audit-schema.js → audit-state-class-description.js} +3 -2
  37. package/src/server/create-http-server.js +127 -0
  38. package/src/server/crypto.js +8 -7
  39. package/src/server/index.js +60 -17
  40. package/types/client/Client.d.ts +95 -70
  41. package/types/client/{Context.d.ts → ClientContext.d.ts} +59 -34
  42. package/types/client/{ContextManager.d.ts → ClientContextManager.d.ts} +19 -22
  43. package/types/client/ClientPlugin.d.ts +51 -0
  44. package/types/client/ClientPluginManager.d.ts +112 -0
  45. package/types/client/ClientSocket.d.ts +76 -0
  46. package/types/client/{StateManager.d.ts → ClientStateManager.d.ts} +18 -21
  47. package/types/client/index.d.ts +33 -6
  48. package/types/common/BasePlugin.d.ts +64 -53
  49. package/types/common/BasePluginManager.d.ts +84 -39
  50. package/types/common/BaseStateManager.d.ts +227 -49
  51. package/types/common/BatchedTransport.d.ts +17 -0
  52. package/types/common/ParameterBag.d.ts +73 -76
  53. package/types/common/PromiseStore.d.ts +11 -0
  54. package/types/common/SharedState.d.ts +291 -0
  55. package/types/common/SharedStateCollection.d.ts +311 -0
  56. package/types/common/constants.d.ts +10 -3
  57. package/types/common/logger.d.ts +8 -6
  58. package/types/common/shared-state-types.d.ts +95 -0
  59. package/types/common/sockets-utils.d.ts +0 -5
  60. package/types/common/version.d.ts +2 -0
  61. package/types/server/Server.d.ts +215 -136
  62. package/types/server/ServerClient.d.ts +45 -0
  63. package/types/server/ServerContext.d.ts +209 -0
  64. package/types/server/ServerContextManager.d.ts +75 -0
  65. package/types/server/ServerPlugin.d.ts +57 -0
  66. package/types/server/ServerPluginManager.d.ts +115 -0
  67. package/types/server/ServerSocket.d.ts +69 -0
  68. package/types/server/ServerSockets.d.ts +42 -0
  69. package/types/server/ServerStateManager.d.ts +284 -0
  70. package/types/server/SharedStatePrivate.d.ts +23 -0
  71. package/types/server/audit-network-latency.worker.d.ts +2 -2
  72. package/types/server/{audit-schema.d.ts → audit-state-class-description.d.ts} +8 -9
  73. package/types/server/create-http-server.d.ts +5 -0
  74. package/types/server/crypto.d.ts +4 -0
  75. package/types/server/index.d.ts +51 -3
  76. package/.eslintrc +0 -15
  77. package/.github/workflows/gh-pages.yml +0 -31
  78. package/.github/workflows/unit-tests.yml +0 -18
  79. package/src/client/ContextManager.js +0 -111
  80. package/src/client/SharedState.js +0 -84
  81. package/src/client/SharedStateCollection.js +0 -35
  82. package/src/client/Socket.js +0 -402
  83. package/src/common/BaseSharedState.js +0 -583
  84. package/src/common/BaseSharedStateCollection.js +0 -179
  85. package/src/common/SharedStatePrivate.js +0 -192
  86. package/src/common/promise-store.js +0 -25
  87. package/src/server/Client.js +0 -55
  88. package/src/server/PluginManager.js +0 -170
  89. package/src/server/SharedState.js +0 -85
  90. package/src/server/SharedStateCollection.js +0 -36
  91. package/src/server/Socket.js +0 -396
  92. package/src/server/Sockets.js +0 -224
  93. package/src/server/StateManager.js +0 -620
  94. package/tsconfig.json +0 -21
  95. package/types/client/Client.d.ts.map +0 -1
  96. package/types/client/Context.d.ts.map +0 -1
  97. package/types/client/ContextManager.d.ts.map +0 -1
  98. package/types/client/Plugin.d.ts +0 -53
  99. package/types/client/Plugin.d.ts.map +0 -1
  100. package/types/client/PluginManager.d.ts +0 -96
  101. package/types/client/PluginManager.d.ts.map +0 -1
  102. package/types/client/SharedState.d.ts +0 -85
  103. package/types/client/SharedState.d.ts.map +0 -1
  104. package/types/client/Socket.d.ts +0 -138
  105. package/types/client/Socket.d.ts.map +0 -1
  106. package/types/client/StateManager.d.ts.map +0 -1
  107. package/types/client/index.d.ts.map +0 -1
  108. package/types/common/BasePlugin.d.ts.map +0 -1
  109. package/types/common/BasePluginManager.d.ts.map +0 -1
  110. package/types/common/BaseSharedState.d.ts +0 -173
  111. package/types/common/BaseSharedState.d.ts.map +0 -1
  112. package/types/common/BaseStateManager.d.ts.map +0 -1
  113. package/types/common/ParameterBag.d.ts.map +0 -1
  114. package/types/common/SharedStatePrivate.d.ts +0 -20
  115. package/types/common/SharedStatePrivate.d.ts.map +0 -1
  116. package/types/common/constants.d.ts.map +0 -1
  117. package/types/common/logger.d.ts.map +0 -1
  118. package/types/common/promise-store.d.ts +0 -4
  119. package/types/common/promise-store.d.ts.map +0 -1
  120. package/types/common/sockets-utils.d.ts.map +0 -1
  121. package/types/common/utils.d.ts +0 -5
  122. package/types/common/utils.d.ts.map +0 -1
  123. package/types/server/Client.d.ts +0 -40
  124. package/types/server/Client.d.ts.map +0 -1
  125. package/types/server/Context.d.ts +0 -59
  126. package/types/server/Context.d.ts.map +0 -1
  127. package/types/server/ContextManager.d.ts +0 -69
  128. package/types/server/ContextManager.d.ts.map +0 -1
  129. package/types/server/Plugin.d.ts +0 -70
  130. package/types/server/Plugin.d.ts.map +0 -1
  131. package/types/server/PluginManager.d.ts +0 -91
  132. package/types/server/PluginManager.d.ts.map +0 -1
  133. package/types/server/Server.d.ts.map +0 -1
  134. package/types/server/SharedState.d.ts +0 -86
  135. package/types/server/SharedState.d.ts.map +0 -1
  136. package/types/server/Socket.d.ts +0 -128
  137. package/types/server/Socket.d.ts.map +0 -1
  138. package/types/server/Sockets.d.ts +0 -73
  139. package/types/server/Sockets.d.ts.map +0 -1
  140. package/types/server/StateManager.d.ts +0 -190
  141. package/types/server/StateManager.d.ts.map +0 -1
  142. package/types/server/audit-network-latency.worker.d.ts.map +0 -1
  143. package/types/server/audit-schema.d.ts.map +0 -1
  144. package/types/server/index.d.ts.map +0 -1
@@ -0,0 +1,706 @@
1
+ import { counter, isString, isPlainObject, isFunction } from '@ircam/sc-utils';
2
+ import clonedeep from 'lodash/cloneDeep.js';
3
+
4
+ import BaseStateManager, {
5
+ kStateManagerInit,
6
+ } from '../common/BaseStateManager.js';
7
+ import BatchedTransport from '../common/BatchedTransport.js';
8
+ import ParameterBag from '../common/ParameterBag.js';
9
+ import {
10
+ CREATE_REQUEST,
11
+ CREATE_RESPONSE,
12
+ CREATE_ERROR,
13
+ DELETE_NOTIFICATION,
14
+ ATTACH_REQUEST,
15
+ ATTACH_RESPONSE,
16
+ ATTACH_ERROR,
17
+ OBSERVE_REQUEST,
18
+ OBSERVE_RESPONSE,
19
+ OBSERVE_ERROR,
20
+ OBSERVE_NOTIFICATION,
21
+ UNOBSERVE_NOTIFICATION,
22
+ DELETE_SHARED_STATE_CLASS,
23
+ GET_CLASS_DESCRIPTION_REQUEST,
24
+ GET_CLASS_DESCRIPTION_RESPONSE,
25
+ GET_CLASS_DESCRIPTION_ERROR,
26
+ PRIVATE_STATES,
27
+ } from '../common/constants.js';
28
+
29
+ import SharedStatePrivate, {
30
+ kSharedStatePrivateAttachClient,
31
+ kSharedStatePrivateDetachClient,
32
+ kSharedStatePrivateGetValues,
33
+ } from './SharedStatePrivate.js';
34
+
35
+ import logger from '../common/logger.js';
36
+
37
+
38
+ const generateStateId = counter();
39
+ const instanceIdGenerator = counter();
40
+
41
+ export const kServerStateManagerAddClient = Symbol('soundworks:server-state-manager-add-client');
42
+ export const kServerStateManagerRemoveClient = Symbol('soundworks:server-state-manager-remove-client');
43
+ export const kServerStateManagerHasClient = Symbol('soundworks:server-state-manager-has-client');
44
+ export const kServerStateManagerDeletePrivateState = Symbol('soundworks:server-state-manager-delete-private-state');
45
+ export const kServerStateManagerGetUpdateHooks = Symbol('soundworks:server-state-manager-get-update-hooks');
46
+ // for testing purposes
47
+ export const kStateManagerClientsByNodeId = Symbol('soundworks:server-state-clients-by-node-id');
48
+
49
+
50
+ /**
51
+ * @callback serverStateManagerCreateHook
52
+ * @async
53
+ *
54
+ * @param {object} initValues - Initialization values object as given when the
55
+ * shared state is created
56
+ */
57
+
58
+ /**
59
+ * @callback serverStateManagerUpdateHook
60
+ * @async
61
+ *
62
+ * @param {object} updates - Update object as given on a `set` callback, or
63
+ * result of the previous hook.
64
+ * @param {object} currentValues - Current values of the state.
65
+ * @returns {object} The "real" updates to be applied on the state.
66
+ */
67
+
68
+ /**
69
+ * @callback serverStateManagerDeleteHook
70
+ * @async
71
+ *
72
+ * @param {object} currentValues - Update object as given on a `set` callback, or
73
+ * result of the previous hook.
74
+ */
75
+
76
+ /**
77
+ * The `StateManager` allows to create new {@link SharedState}s, or attach
78
+ * to {@link SharedState}s created by other nodes (clients or server). It
79
+ * can also track all the {@link SharedState}s created by other nodes.
80
+ *
81
+ * An instance of `StateManager` is automatically created by the `soundworks.Server`
82
+ * at initialization (cf. {@link Server#stateManager}).
83
+ *
84
+ * Compared to the {@link ClientStateManager}, the `ServerStateManager` can also
85
+ * define and delete shared state classes, as well as register hooks executed at
86
+ * lifecycle phases of a shared state
87
+ *
88
+ * See {@link Server#stateManager}
89
+ *
90
+ * Tutorial: {@link https://soundworks.dev/guide/state-manager.html}
91
+ *
92
+ * ```
93
+ * // server-side
94
+ * import { Server } from '@soundworks/server/index.js';
95
+ *
96
+ * const server = new Server(config);
97
+ * // declare and register the class of a shared state.
98
+ * server.stateManager.defineClass('some-global-state', {
99
+ * myRandom: {
100
+ * type: 'float',
101
+ * default: 0,
102
+ * }
103
+ * });
104
+ *
105
+ * await server.start();
106
+ *
107
+ * // create a global state server-side
108
+ * const globalState = await server.stateManager.create('some-global-state');
109
+ * // listen and react to the changes made by the clients
110
+ * globalState.onUpdate(updates => console.log(updates));
111
+ * ```
112
+ *
113
+ * ```
114
+ * // client-side
115
+ * import { Client } from '@soundworks/client.index.js';
116
+ *
117
+ * const client = new Client(config);
118
+ * await client.start();
119
+ *
120
+ * // attach to the global state created by the server
121
+ * const globalState = await client.stateManager.attach('some-global-state');
122
+ *
123
+ * // update the value of a `myRandom` parameter every seconds
124
+ * setInterval(() => {
125
+ * globalState.set({ myRandom: Math.random() });
126
+ * }, 1000);
127
+ * ```
128
+ *
129
+ * @extends BaseStateManager
130
+ * @inheritdoc
131
+ * @hideconstructor
132
+ */
133
+ class ServerStateManager extends BaseStateManager {
134
+ #sharedStatePrivateById = new Map();
135
+ #classes = new Map();
136
+ #observers = new Set();
137
+ #createHooksByClassName = new Map();
138
+ #updateHooksByClassName = new Map();
139
+ #deleteHooksByClassName = new Map();
140
+
141
+ constructor() {
142
+ super();
143
+ /** @private */
144
+ this[kStateManagerClientsByNodeId] = new Map();
145
+ }
146
+
147
+ /** @private */
148
+ [kStateManagerInit](id, transport) {
149
+ super[kStateManagerInit](id, transport);
150
+ // add itself as client of the state manager server
151
+ this[kServerStateManagerAddClient](id, transport);
152
+ }
153
+
154
+ /** @private */
155
+ async [kServerStateManagerDeletePrivateState](state) {
156
+ this.#sharedStatePrivateById.delete(state.id);
157
+ // @todo - could use getValuesUnsafe instead
158
+ let currentValues = state[kSharedStatePrivateGetValues]();
159
+ const hooks = this.#deleteHooksByClassName.get(state.className);
160
+
161
+ for (let hook of hooks.values()) {
162
+ const result = await hook(currentValues);
163
+
164
+ if (result === null) { // explicit abort
165
+ break;
166
+ } else if (result === undefined) { // implicit continue
167
+ continue;
168
+ } else {
169
+ currentValues = result;
170
+ }
171
+ }
172
+ }
173
+
174
+ /** @private */
175
+ [kServerStateManagerGetUpdateHooks](className) {
176
+ return this.#updateHooksByClassName.get(className);
177
+ }
178
+
179
+ /** @private */
180
+ [kServerStateManagerHasClient](nodeId) {
181
+ return this[kStateManagerClientsByNodeId].has(nodeId);
182
+ }
183
+
184
+ /**
185
+ * Add a client to the manager.
186
+ *
187
+ * This is automatically handled by the {@link Server} when a client connects.
188
+ *
189
+ * @param {number} nodeId - Unique id of the client node
190
+ * @param {object} transport - Transport mechanism to communicate with the
191
+ * client. Must implement a basic EventEmitter API.
192
+ *
193
+ * @private
194
+ */
195
+ [kServerStateManagerAddClient](nodeId, transport) {
196
+ const batchedTransport = new BatchedTransport(transport);
197
+ const client = {
198
+ id: nodeId,
199
+ transport: batchedTransport,
200
+ };
201
+
202
+ this[kStateManagerClientsByNodeId].set(nodeId, client);
203
+
204
+ // ---------------------------------------------
205
+ // CREATE
206
+ // ---------------------------------------------
207
+ client.transport.addListener(
208
+ CREATE_REQUEST,
209
+ async (reqId, className, requireDescription, initValues = {}) => {
210
+ if (this.#classes.has(className)) {
211
+ try {
212
+ const classDescription = this.#classes.get(className);
213
+ const stateId = generateStateId();
214
+ const instanceId = instanceIdGenerator();
215
+
216
+ // apply create hooks on init values
217
+ const hooks = this.#createHooksByClassName.get(className);
218
+ let hookAborted = false;
219
+
220
+ for (let hook of hooks.values()) {
221
+ const result = await hook(initValues);
222
+
223
+ if (result === null) { // explicit abort
224
+ hookAborted = true;
225
+ break;
226
+ } else if (result === undefined) { // implicit continue
227
+ continue;
228
+ } else {
229
+ initValues = result;
230
+ }
231
+ }
232
+
233
+ if (hookAborted) {
234
+ throw new Error(`A 'serverStateManagerCreateHook' explicitly aborted state creation of class '${className}' by returning 'null'`);
235
+ }
236
+
237
+ const state = new SharedStatePrivate(this, className, classDescription, stateId, initValues);
238
+ // attach client to the state as owner
239
+ const isOwner = true;
240
+ const filter = null;
241
+ state[kSharedStatePrivateAttachClient](instanceId, client, isOwner, filter);
242
+
243
+ this.#sharedStatePrivateById.set(stateId, state);
244
+
245
+ const currentValues = state.parameters.getValues();
246
+ const classDescriptionOption = requireDescription ? classDescription : null;
247
+
248
+ client.transport.emit(
249
+ CREATE_RESPONSE,
250
+ reqId,
251
+ stateId,
252
+ instanceId,
253
+ className,
254
+ classDescriptionOption,
255
+ currentValues,
256
+ );
257
+
258
+ const isObservable = this.#isObservableState(state);
259
+
260
+ if (isObservable) {
261
+ this.#observers.forEach(observer => {
262
+ observer.transport.emit(OBSERVE_NOTIFICATION, className, stateId, nodeId);
263
+ });
264
+ }
265
+ } catch (err) {
266
+ const msg = `${err.message}`;
267
+ client.transport.emit(CREATE_ERROR, reqId, msg);
268
+ }
269
+ } else {
270
+ const msg = `Undefined SharedStateClassName '${className}'`;
271
+ client.transport.emit(CREATE_ERROR, reqId, msg);
272
+ }
273
+ },
274
+ );
275
+
276
+ // ---------------------------------------------
277
+ // ATTACH (when creator, is attached by default)
278
+ // ---------------------------------------------
279
+ client.transport.addListener(
280
+ ATTACH_REQUEST,
281
+ (reqId, className, stateId = null, requireDescription = true, filter = null) => {
282
+ if (this.#classes.has(className)) {
283
+ let state = null;
284
+
285
+ if (stateId !== null && this.#sharedStatePrivateById.has(stateId)) {
286
+ state = this.#sharedStatePrivateById.get(stateId);
287
+ } else if (stateId === null) {
288
+ // if no `stateId` given, we try to find the first state with the given
289
+ // `className` in the list, this allow a client to attach to a global
290
+ // state created by the server (or some persistent client) without
291
+ // having to know the `stateId` (e.g. some global state...)
292
+ for (let existingState of this.#sharedStatePrivateById.values()) {
293
+ if (existingState.className === className) {
294
+ state = existingState;
295
+ break;
296
+ }
297
+ }
298
+ }
299
+
300
+ if (state !== null) {
301
+ // @note - we use a unique remote id to allow a client to attach
302
+ // several times to the same state.
303
+ // i.e. same state -> several remote attach on the same node
304
+ const instanceId = instanceIdGenerator();
305
+ const isOwner = false;
306
+ const currentValues = state.parameters.getValues();
307
+ const classDescription = this.#classes.get(className);
308
+ const classDescriptionOption = requireDescription ? classDescription : null;
309
+
310
+ // if filter given, check that all filter entries are valid class keys
311
+ // @todo - improve error reporting: report invalid filters
312
+ if (filter !== null) {
313
+ const keys = Object.keys(classDescription);
314
+ const isValid = filter.reduce((acc, key) => acc && keys.includes(key), true);
315
+
316
+ if (!isValid) {
317
+ const msg = `Invalid filter (${filter.join(', ')}) for class '${className}'`;
318
+ return client.transport.emit(ATTACH_ERROR, reqId, msg);
319
+ }
320
+ }
321
+
322
+ state[kSharedStatePrivateAttachClient](instanceId, client, isOwner, filter);
323
+
324
+ client.transport.emit(
325
+ ATTACH_RESPONSE,
326
+ reqId,
327
+ state.id,
328
+ instanceId,
329
+ className,
330
+ classDescriptionOption,
331
+ currentValues,
332
+ filter,
333
+ );
334
+
335
+ } else {
336
+ const msg = `No existing state for class "${className}" with stateId: "${stateId}"`;
337
+ client.transport.emit(ATTACH_ERROR, reqId, msg);
338
+ }
339
+ } else {
340
+ const msg = `Undefined SharedStateClassName '${className}'`;
341
+ client.transport.emit(ATTACH_ERROR, reqId, msg);
342
+ }
343
+ },
344
+ );
345
+
346
+ // ---------------------------------------------
347
+ // OBSERVE PEERS (be notified when a state is created, lazy)
348
+ // ---------------------------------------------
349
+ client.transport.addListener(OBSERVE_REQUEST, (reqId, observedClassName) => {
350
+ if (observedClassName === null || this.#classes.has(observedClassName)) {
351
+ const list = [];
352
+
353
+ this.#sharedStatePrivateById.forEach(state => {
354
+ const isObservable = this.#isObservableState(state);
355
+
356
+ if (isObservable) {
357
+ const { className, id, creatorId } = state;
358
+ list.push([className, id, creatorId]);
359
+ }
360
+ });
361
+
362
+ // add client to observers first because if some synchronous server side
363
+ // callback throws, the client would never be added to the list
364
+ this.#observers.add(client);
365
+
366
+ client.transport.emit(OBSERVE_RESPONSE, reqId, ...list);
367
+ } else {
368
+ const msg = `Undefined SharedStateClassName '${observedClassName}'`;
369
+ client.transport.emit(OBSERVE_ERROR, reqId, msg);
370
+ }
371
+ });
372
+
373
+ client.transport.addListener(UNOBSERVE_NOTIFICATION, () => {
374
+ this.#observers.delete(client);
375
+ });
376
+
377
+ // ---------------------------------------------
378
+ // GET CLASS DESCRIPTION
379
+ // ---------------------------------------------
380
+ client.transport.addListener(GET_CLASS_DESCRIPTION_REQUEST, (reqId, className) => {
381
+ if (this.#classes.has(className)) {
382
+ const classDescription = this.#classes.get(className);
383
+ client.transport.emit(
384
+ GET_CLASS_DESCRIPTION_RESPONSE,
385
+ reqId,
386
+ className,
387
+ classDescription,
388
+ );
389
+ } else {
390
+ const msg = `Undefined SharedStateClassName '${className}'`;
391
+ client.transport.emit(GET_CLASS_DESCRIPTION_ERROR, reqId, msg);
392
+ }
393
+ });
394
+ }
395
+
396
+ /**
397
+ * Remove a client from the manager. Clean all created or attached states.
398
+ *
399
+ * This is automatically handled by the {@link Server} when a client disconnects.
400
+ *
401
+ * @param {number} nodeId - Id of the client node, as given in
402
+ * {@link client.StateManager}
403
+ *
404
+ * @private
405
+ */
406
+ [kServerStateManagerRemoveClient](nodeId) {
407
+ for (let [_id, state] of this.#sharedStatePrivateById.entries()) {
408
+ let deleteState = false;
409
+
410
+ // define if the client is the creator of the state, in which case
411
+ // everybody must delete it
412
+ for (let [instanceId, clientInfos] of state.attachedClients) {
413
+ const attachedClient = clientInfos.client;
414
+
415
+ if (nodeId === attachedClient.id && instanceId === state.creatorInstanceId) {
416
+ deleteState = true;
417
+ }
418
+ }
419
+
420
+ for (let [instanceId, clientInfos] of state.attachedClients) {
421
+ const attachedClient = clientInfos.client;
422
+
423
+ if (nodeId === attachedClient.id) {
424
+ state[kSharedStatePrivateDetachClient](instanceId, attachedClient);
425
+ }
426
+
427
+ if (deleteState) {
428
+ if (instanceId !== state.creatorInstanceId) {
429
+ // send notification to other attached nodes
430
+ attachedClient.transport.emit(`${DELETE_NOTIFICATION}-${state.id}-${instanceId}`);
431
+ }
432
+
433
+ this[kServerStateManagerDeletePrivateState](state);
434
+ }
435
+ }
436
+ }
437
+
438
+ // if is an observer, delete it
439
+ const client = this[kStateManagerClientsByNodeId].get(nodeId);
440
+ this.#observers.delete(client);
441
+ this[kStateManagerClientsByNodeId].delete(nodeId);
442
+ }
443
+
444
+ #isObservableState(state) {
445
+ // is observable if not in private states list
446
+ return !PRIVATE_STATES.includes(state.className);
447
+ }
448
+
449
+ /**
450
+ * Define a generic class from which {@link SharedState}s can be created.
451
+ *
452
+ * @param {SharedStateClassName} className - Name of the class.
453
+ * @param {SharedStateClassDescription} classDescription - Description of the class.
454
+ *
455
+ * @see {@link ServerStateManager#create}
456
+ * @see {@link ClientStateManager#create}
457
+ *
458
+ * @example
459
+ * server.stateManager.defineClass('my-class', {
460
+ * myBoolean: {
461
+ * type: 'boolean'
462
+ * default: false,
463
+ * },
464
+ * myFloat: {
465
+ * type: 'float'
466
+ * default: 0.1,
467
+ * min: -1,
468
+ * max: 1
469
+ * }
470
+ * });
471
+ */
472
+ defineClass(className, classDescription) {
473
+ if (!isString(className)) {
474
+ throw new TypeError(`Cannot execute 'defineClass' on ServerStateManager: argument 1 must be of type SharedStateClassName`);
475
+ }
476
+
477
+ if (!isPlainObject(classDescription)) {
478
+ throw new TypeError(`Cannot execute 'defineClass' on ServerStateManager: argument 2 must be of type SharedStateClassDescription`);
479
+ }
480
+
481
+ if (this.#classes.has(className)) {
482
+ throw new DOMException(`Cannot execute 'defineClass' on ServerStateManager: SharedState class '${className}' is already defined`, 'NotSupportedError');
483
+ }
484
+
485
+ try {
486
+ ParameterBag.validateDescription(classDescription);
487
+ } catch (err) {
488
+ throw new TypeError(`Cannot execute 'defineClass' on ServerStateManager: ${err.message}`);
489
+ }
490
+
491
+ this.#classes.set(className, clonedeep(classDescription));
492
+ // create hooks list
493
+ this.#createHooksByClassName.set(className, new Set());
494
+ this.#updateHooksByClassName.set(className, new Set());
495
+ this.#deleteHooksByClassName.set(className, new Set());
496
+ }
497
+
498
+ /**
499
+ * @deprecated Use {@link ServerStateManager#defineClass} instead.
500
+ */
501
+ registerSchema(className, classDescription) {
502
+ logger.deprecated('ServerStateManager#registerSchema', 'ServerStateManager#defineClass', '4.0.0-alpha.29');
503
+ this.defineClass(className, classDescription);
504
+ }
505
+
506
+ /**
507
+ * Delete a whole class of {@link SharedState}.
508
+ *
509
+ * All {@link SharedState} instances created from this class will be deleted
510
+ * as well, triggering their eventual `onDetach` and `onDelete` callbacks.
511
+ *
512
+ * @param {SharedStateClassName} className - Name of the shared state class to delete.
513
+ */
514
+ deleteClass(className) {
515
+ // @note: deleting schema
516
+ for (let [_, state] of this.#sharedStatePrivateById) {
517
+ if (state.className === className) {
518
+ for (let [instanceId, clientInfos] of state.attachedClients) {
519
+ const attached = clientInfos.client;
520
+ state[kSharedStatePrivateDetachClient](instanceId, attached);
521
+ attached.transport.emit(`${DELETE_NOTIFICATION}-${state.id}-${instanceId}`);
522
+ }
523
+
524
+ this[kServerStateManagerDeletePrivateState](state);
525
+ }
526
+ }
527
+
528
+ // clear class cache of all connected clients
529
+ for (let client of this[kStateManagerClientsByNodeId].values()) {
530
+ client.transport.emit(`${DELETE_SHARED_STATE_CLASS}`, className);
531
+ }
532
+
533
+ this.#classes.delete(className);
534
+ // delete registered hooks
535
+ this.#createHooksByClassName.delete(className);
536
+ this.#updateHooksByClassName.delete(className);
537
+ this.#deleteHooksByClassName.delete(className);
538
+ }
539
+
540
+ /**
541
+ * @deprecated Use {@link ServerStateManager#defineClass} instead.
542
+ */
543
+ deleteSchema(className) {
544
+ logger.deprecated('ServerStateManager#deleteSchema', 'ServerStateManager#deleteClass', '4.0.0-alpha.29');
545
+ this.deleteClass(className);
546
+ }
547
+
548
+ /**
549
+ * Register a function for a given class of shared state class to be executed
550
+ * when a state is created.
551
+ *
552
+ * For example, this can be useful to retrieve some initialization values stored
553
+ * in the filesystem, given the value (e.g. a hostname) of one the parameters.
554
+ *
555
+ * The hook is associated to each states created from the given class name.
556
+ * Note that the hooks are executed server-side regardless the node on which
557
+ * `create` has been called.
558
+ *
559
+ * Multiple hook can be added to the same `className`, they will be executed in
560
+ * order of registration.
561
+ *
562
+ * @param {string} className - Kind of states on which applying the hook.
563
+ * @param {serverStateManagerUpdateHook} createHook - Function called on when
564
+ * a state of `className` is created on the network.
565
+ *
566
+ * @returns {function} deleteHook - Handler that deletes the hook when executed.
567
+ *
568
+ * @example
569
+ * server.stateManager.defineClass('hooked', {
570
+ * name: { type: 'string', required: true },
571
+ * hookTriggered: { type: 'boolean', default: false },
572
+ * });
573
+ * server.stateManager.registerCreateHook('hooked', initValues => {
574
+ * return {
575
+ * ...initValues
576
+ * hookTriggered: true,
577
+ * };
578
+ * });
579
+ *
580
+ * const state = await server.stateManager.create('hooked', {
581
+ * name: 'coucou',
582
+ * });
583
+ *
584
+ * const values = state.getValues();
585
+ * assert.deepEqual(result, { value: 'coucou', hookTriggered: true });
586
+ */
587
+ registerCreateHook(className, createHook) {
588
+ if (!this.#classes.has(className)) {
589
+ throw new TypeError(`Cannot execute 'registerCreateHook' on BaseStateManager: SharedState class '${className}' is not defined`);
590
+ }
591
+
592
+ if (!isFunction(createHook)) {
593
+ throw new TypeError(`Cannot execute 'registerCreateHook' on BaseStateManager: argument 2 must be a function`);
594
+ }
595
+
596
+ const hooks = this.#createHooksByClassName.get(className);
597
+ hooks.add(createHook);
598
+
599
+ return () => hooks.delete(createHook);
600
+ }
601
+
602
+ /**
603
+ * Register a function for a given class of shared state class to be executed
604
+ * when a state is deleted.
605
+ *
606
+ * For example, this can be useful to store the values of a given shared state
607
+ * in the filesystem.
608
+ *
609
+ * The hook is associated to each states created from the given class name.
610
+ * Note that the hooks are executed server-side regardless the node on which
611
+ * `delete` has been called.
612
+ *
613
+ * Multiple hook can be added to the same `className`, they will be executed in
614
+ * order of registration.
615
+ *
616
+ * @param {string} className - Kind of states on which applying the hook.
617
+ * @param {serverStateManagerUpdateHook} createHook - Function called on when
618
+ * a state of `className` is created on the network.
619
+ *
620
+ * @returns {function} deleteHook - Handler that deletes the hook when executed.
621
+ *
622
+ * @example
623
+ * server.stateManager.defineClass('hooked', {
624
+ * name: { type: 'string', required: true },
625
+ * hookTriggered: { type: 'boolean', default: false },
626
+ * });
627
+ * server.stateManager.registerDeleteHook('hooked', async currentValues => {
628
+ * await doSomethingWithValues(currentValues)
629
+ * });
630
+ *
631
+ * const state = await server.stateManager.create('hooked');
632
+ * // later
633
+ * await state.delete();
634
+ */
635
+ registerDeleteHook(className, deleteHook) {
636
+ if (!this.#classes.has(className)) {
637
+ throw new TypeError(`Cannot execute 'registerDeleteHook' on BaseStateManager: SharedState class '${className}' is not defined`);
638
+ }
639
+
640
+ if (!isFunction(deleteHook)) {
641
+ throw new TypeError(`Cannot execute 'registerDeleteHook' on BaseStateManager: argument 2 must be a function`);
642
+ }
643
+
644
+ const hooks = this.#deleteHooksByClassName.get(className);
645
+ hooks.add(deleteHook);
646
+
647
+ return () => hooks.delete(deleteHook);
648
+ }
649
+
650
+ /**
651
+ * Register a function for a given class of shared state to be executed between
652
+ * `set` instructions and `onUpdate` callback(s).
653
+ *
654
+ * For example, this can be used to implement a preset system where all the values
655
+ * of the state are updated from e.g. some data stored in filesystem while the
656
+ * consumer of the state only want to update the preset name.
657
+ *
658
+ * The hook is associated to each states created from the given class name and
659
+ * executed on each update (i.e. `state.set(updates)`). Note that the hooks are
660
+ * executed server-side regardless the node on which `set` has been called and
661
+ * before the call of the `onUpdate` callback of the shared state.
662
+ *
663
+ * Multiple hook can be added to the same `className`, they will be executed in
664
+ * order of registration.
665
+ *
666
+ * @param {string} className - Kind of states on which applying the hook.
667
+ * @param {serverStateManagerUpdateHook} updateHook - Function called on each update,
668
+ * to eventually modify the updates before they are actually applied.
669
+ *
670
+ * @returns {function} deleteHook - Handler that deletes the hook when executed.
671
+ *
672
+ * @example
673
+ * server.stateManager.defineClass('hooked', {
674
+ * value: { type: 'string', default: null, nullable: true },
675
+ * numUpdates: { type: 'integer', default: 0 },
676
+ * });
677
+ * server.stateManager.registerUpdateHook('hooked', updates => {
678
+ * return {
679
+ * ...updates
680
+ * numUpdates: currentValues.numUpdates + 1,
681
+ * };
682
+ * });
683
+ *
684
+ * const state = await server.stateManager.create('hooked');
685
+ *
686
+ * await state.set({ value: 'test' });
687
+ * const values = state.getValues();
688
+ * assert.deepEqual(result, { value: 'test', numUpdates: 1 });
689
+ */
690
+ registerUpdateHook(className, updateHook) {
691
+ if (!this.#classes.has(className)) {
692
+ throw new TypeError(`Cannot execute 'registerUpdateHook' on BaseStateManager: SharedState class '${className}' is not defined`);
693
+ }
694
+
695
+ if (!isFunction(updateHook)) {
696
+ throw new TypeError(`Cannot execute 'registerUpdateHook' on BaseStateManager: argument 2 must be a function`);
697
+ }
698
+
699
+ const hooks = this.#updateHooksByClassName.get(className);
700
+ hooks.add(updateHook);
701
+
702
+ return () => hooks.delete(updateHook);
703
+ }
704
+ }
705
+
706
+ export default ServerStateManager;