@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.
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 +63 -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
@@ -1,6 +1,11 @@
1
- import { isString, isFunction } from '@ircam/sc-utils';
2
- import SharedState from './BaseSharedState.js';
3
- import SharedStateCollection from './BaseSharedStateCollection.js';
1
+ import { isString, isFunction, isPlainObject } from '@ircam/sc-utils';
2
+ import SharedState from './SharedState.js';
3
+ import SharedStateCollection, {
4
+ kSharedStateCollectionInit,
5
+ } from './SharedStateCollection.js';
6
+ import BatchedTransport from './BatchedTransport.js';
7
+ import ParameterBag from './ParameterBag.js';
8
+ import PromiseStore from './PromiseStore.js';
4
9
  import {
5
10
  CREATE_REQUEST,
6
11
  CREATE_RESPONSE,
@@ -10,92 +15,175 @@ import {
10
15
  ATTACH_ERROR,
11
16
  OBSERVE_REQUEST,
12
17
  OBSERVE_RESPONSE,
18
+ OBSERVE_ERROR,
13
19
  OBSERVE_NOTIFICATION,
14
20
  UNOBSERVE_NOTIFICATION,
15
- DELETE_SCHEMA,
21
+ DELETE_SHARED_STATE_CLASS,
22
+ GET_CLASS_DESCRIPTION_REQUEST,
23
+ GET_CLASS_DESCRIPTION_RESPONSE,
24
+ GET_CLASS_DESCRIPTION_ERROR,
16
25
  } from './constants.js';
17
- import {
18
- storeRequestPromise,
19
- resolveRequest,
20
- rejectRequest,
21
- } from './promise-store.js';
26
+ import logger from './logger.js';
27
+
28
+ export const kStateManagerInit = Symbol('soundworks:state-manager-init');
29
+ export const kStateManagerDeleteState = Symbol('soundworks:state-manager-delete-state');
30
+ // for testing purposes
31
+ export const kStateManagerClient = Symbol('soundworks:state-manager-client');
22
32
 
23
33
  /**
24
- * @private
34
+ * Callback executed when a state is created on the network.
35
+ *
36
+ * @callback stateManagerObserveCallback
37
+ * @async
38
+ * @param {string} className - name of the class
39
+ * @param {number} stateId - id of the state
40
+ * @param {number} nodeId - id of the node that created the state
41
+ */
42
+ /**
43
+ * Callback to execute in order to remove a {@link stateManagerObserveCallback}
44
+ * from the list of observer.
45
+ *
46
+ * @callback stateManagerDeleteObserveCallback
25
47
  */
48
+
49
+ /** @private */
26
50
  class BaseStateManager {
51
+ #statesById = new Map();
52
+ #cachedClasses = new Map(); // <className, definition>
53
+ #observeListeners = new Set(); // Set <[observedClassName, callback, options]>
54
+ #observeRequestCallbacks = new Map(); // Map <reqId, [observedClassName, callback, options]>
55
+ #promiseStore = null;
56
+ #status = 'idle';
57
+
58
+ constructor() {
59
+ this.#promiseStore = new PromiseStore('BaseStateManager');
60
+ /** @private */
61
+ this[kStateManagerClient] = null;
62
+
63
+ }
64
+
65
+ /** @private */
66
+ #filterObserve(observedClassName, className, creatorId, options) {
67
+ let filter = true;
68
+
69
+ if (observedClassName === null || observedClassName === className) {
70
+ filter = false;
71
+ }
72
+ // filter state created by client if excludeLocal is true
73
+ if (options.excludeLocal === true && creatorId === this[kStateManagerClient].id) {
74
+ filter = true;
75
+ }
76
+
77
+ return filter;
78
+ }
79
+
80
+ /** @private */
81
+ [kStateManagerDeleteState](stateId) {
82
+ this.#statesById.delete(stateId);
83
+ }
84
+
27
85
  /**
86
+ * Executed on `client.init`
28
87
  * @param {Number} id - Id of the node.
29
- * @param {Object} transport - Transport to use for synchronizing the state.
88
+ * @param {Object} transport - Transport to use for synchronizing the states.
30
89
  * Must implement a basic EventEmitter API.
90
+ *
91
+ * @private
31
92
  */
32
- constructor(id, transport) {
33
- this.client = { id, transport };
34
-
35
- this._statesById = new Map();
36
- this._observeListeners = new Map(); // Map <callback, filterSchemaName>
37
- this._cachedSchemas = new Map();
38
- this._observeRequestCallbacks = new Map();
93
+ [kStateManagerInit](id, transport) {
94
+ const batchedTransport = new BatchedTransport(transport);
95
+ this[kStateManagerClient] = { id, transport: batchedTransport };
39
96
 
40
97
  // ---------------------------------------------
41
98
  // CREATE
42
99
  // ---------------------------------------------
100
+ this[kStateManagerClient].transport.addListener(
101
+ CREATE_RESPONSE,
102
+ (reqId, stateId, instanceId, className, classDescription, initValues) => {
103
+ // cache class description to save some bandwidth
104
+ // @note: when we make the class dynamic, we will need some mechanism to
105
+ // invalidate the cached description
106
+ if (!this.#cachedClasses.has(className)) {
107
+ this.#cachedClasses.set(className, classDescription);
108
+ }
43
109
 
44
- this.client.transport.addListener(CREATE_RESPONSE, (reqId, stateId, remoteId, schemaName, schema, initValues) => {
45
-
46
- // cache schema when first dealing it to save some bandwidth
47
- if (!this._cachedSchemas.has(schemaName)) {
48
- this._cachedSchemas.set(schemaName, schema);
49
- }
50
-
51
- schema = this._cachedSchemas.get(schemaName);
52
-
53
- const state = new SharedState(stateId, remoteId, schemaName, schema, this.client, true, this, initValues);
54
- this._statesById.set(state.id, state);
55
-
56
- resolveRequest(reqId, state);
57
- });
58
-
59
- this.client.transport.addListener(CREATE_ERROR, (reqId, msg) => {
60
- rejectRequest(reqId, msg);
110
+ classDescription = this.#cachedClasses.get(className);
111
+
112
+ const state = new SharedState({
113
+ manager: this,
114
+ className,
115
+ classDescription,
116
+ stateId,
117
+ instanceId,
118
+ isOwner: true,
119
+ initValues,
120
+ filter: null, // owner cannot filter parameters
121
+ });
122
+
123
+ this.#statesById.set(state.id, state);
124
+ this.#promiseStore.resolve(reqId, state);
125
+ },
126
+ );
127
+
128
+ this[kStateManagerClient].transport.addListener(CREATE_ERROR, (reqId, msg) => {
129
+ msg = `Cannot execute 'create' on BaseStateManager: ${msg}`;
130
+ this.#promiseStore.reject(reqId, msg);
61
131
  });
62
132
 
63
133
  // ---------------------------------------------
64
134
  // ATTACH (when creator, is attached by default)
65
135
  // ---------------------------------------------
66
- this.client.transport.addListener(ATTACH_RESPONSE, (reqId, stateId, remoteId, schemaName, schema, currentValues) => {
67
- // cache schema when first dealing with it to save some bandwidth
68
- // note: when we make the schemas dynamic at some point
69
- // the server should be able to invalidate the schema and send
70
- // it again despite the caching.
71
- if (!this._cachedSchemas.has(schemaName)) {
72
- this._cachedSchemas.set(schemaName, schema);
73
- }
74
-
75
- schema = this._cachedSchemas.get(schemaName);
76
-
77
- const state = new SharedState(stateId, remoteId, schemaName, schema, this.client, false, this, currentValues);
78
- this._statesById.set(state.id, state);
79
-
80
- resolveRequest(reqId, state);
81
- });
136
+ this[kStateManagerClient].transport.addListener(
137
+ ATTACH_RESPONSE,
138
+ (reqId, stateId, instanceId, className, classDescription, currentValues, filter) => {
139
+ // cache class description to save some bandwidth
140
+ // @note: when we make the class dynamic, we will need some mechanism to
141
+ // invalidate the cached description
142
+ if (!this.#cachedClasses.has(className)) {
143
+ this.#cachedClasses.set(className, classDescription);
144
+ }
82
145
 
83
- this.client.transport.addListener(ATTACH_ERROR, (reqId, msg) => {
84
- rejectRequest(reqId, msg);
146
+ classDescription = this.#cachedClasses.get(className);
147
+
148
+ const state = new SharedState({
149
+ manager: this,
150
+ className,
151
+ classDescription,
152
+ stateId,
153
+ instanceId,
154
+ isOwner: false,
155
+ initValues: currentValues,
156
+ filter,
157
+ });
158
+
159
+ this.#statesById.set(state.id, state);
160
+ this.#promiseStore.resolve(reqId, state);
161
+ },
162
+ );
163
+
164
+ this[kStateManagerClient].transport.addListener(ATTACH_ERROR, (reqId, msg) => {
165
+ msg = `Cannot execute 'attach' on BaseStateManager: ${msg}`;
166
+ this.#promiseStore.reject(reqId, msg);
85
167
  });
86
168
 
87
169
  // ---------------------------------------------
88
170
  // OBSERVE PEERS (be notified when a state is created, lazy)
89
171
  // ---------------------------------------------
90
- this.client.transport.addListener(OBSERVE_RESPONSE, async (reqId, ...list) => {
91
- // retrieve the callback that have been stored in observe to make sure
172
+ this[kStateManagerClient].transport.addListener(OBSERVE_RESPONSE, async (reqId, ...list) => {
173
+ // retrieve the callback that have been stored in `observe()` to make sure
92
174
  // we don't call another callback that may have been registered earlier.
93
- const [callback, filterSchemaName] = this._observeRequestCallbacks.get(reqId);
94
- this._observeRequestCallbacks.delete(reqId);
95
-
96
- const promises = list.map(([schemaName, stateId, nodeId]) => {
97
- if (filterSchemaName === '*' || filterSchemaName === schemaName) {
98
- return callback(schemaName, stateId, nodeId);
175
+ const observeInfos = this.#observeRequestCallbacks.get(reqId);
176
+ const [observedClassName, callback, options] = observeInfos;
177
+ // move observeInfos from `_observeRequestCallbacks` to `_observeListeners`
178
+ // to guarantee future order of execution
179
+ this.#observeRequestCallbacks.delete(reqId);
180
+ this.#observeListeners.add(observeInfos);
181
+
182
+ const promises = list.map(([className, stateId, nodeId]) => {
183
+ const filter = this.#filterObserve(observedClassName, className, nodeId, options);
184
+
185
+ if (!filter) {
186
+ return callback(className, stateId, nodeId);
99
187
  } else {
100
188
  return Promise.resolve();
101
189
  }
@@ -104,150 +192,552 @@ class BaseStateManager {
104
192
  await Promise.all(promises);
105
193
 
106
194
  const unsubscribe = () => {
107
- this._observeListeners.delete(callback);
108
- // no more listeners, we can stop receiving notification from the server
109
- if (this._observeListeners.size === 0) {
110
- this.client.transport.emit(UNOBSERVE_NOTIFICATION);
195
+ this.#observeListeners.delete(observeInfos);
196
+ // no more listeners, we can stop receiving notifications from the server
197
+ if (this.#observeListeners.size === 0) {
198
+ this[kStateManagerClient].transport.emit(UNOBSERVE_NOTIFICATION);
111
199
  }
112
200
  };
113
201
 
114
- resolveRequest(reqId, unsubscribe);
202
+ this.#promiseStore.resolve(reqId, unsubscribe);
115
203
  });
116
204
 
117
- this.client.transport.addListener(OBSERVE_NOTIFICATION, (schemaName, stateId, nodeId) => {
118
- this._observeListeners.forEach((filterSchemaName, callback) => {
119
- if (filterSchemaName === '*' || filterSchemaName === schemaName) {
120
- callback(schemaName, stateId, nodeId);
121
- }
122
- });
205
+ // Observe error occur if observed class name does not exists
206
+ this[kStateManagerClient].transport.addListener(OBSERVE_ERROR, (reqId, msg) => {
207
+ msg = `Cannot execute 'observe' on BaseStateManager: ${msg}`;
208
+ this.#observeRequestCallbacks.delete(reqId);
209
+ this.#promiseStore.reject(reqId, msg);
123
210
  });
124
211
 
212
+ this[kStateManagerClient].transport.addListener(
213
+ OBSERVE_NOTIFICATION,
214
+ (className, stateId, nodeId) => {
215
+ this.#observeListeners.forEach(observeInfos => {
216
+ const [observedClassName, callback, options] = observeInfos;
217
+ const filter = this.#filterObserve(observedClassName, className, nodeId, options);
218
+
219
+ if (!filter) {
220
+ callback(className, stateId, nodeId);
221
+ }
222
+ });
223
+ },
224
+ );
225
+
125
226
  // ---------------------------------------------
126
- // Clear cache when schema is deleted
227
+ // Clear cache when a shared state class is deleted
127
228
  // ---------------------------------------------
128
- this.client.transport.addListener(DELETE_SCHEMA, schemaName => {
129
- this._cachedSchemas.delete(schemaName);
229
+ this[kStateManagerClient].transport.addListener(DELETE_SHARED_STATE_CLASS, className => {
230
+ this.#cachedClasses.delete(className);
130
231
  });
232
+
233
+ // ---------------------------------------------
234
+ // Get class description
235
+ // ---------------------------------------------
236
+ this[kStateManagerClient].transport.addListener(
237
+ GET_CLASS_DESCRIPTION_RESPONSE,
238
+ (reqId, className, classDescription) => {
239
+ if (!this.#cachedClasses.has(className)) {
240
+ this.#cachedClasses.set(className, classDescription);
241
+ }
242
+ // This cannot throw as the SharedState class has already been registered
243
+ const parameterBag = new ParameterBag(classDescription);
244
+ this.#promiseStore.resolve(reqId, parameterBag.getDescription());
245
+ },
246
+ );
247
+
248
+ this[kStateManagerClient].transport.addListener(GET_CLASS_DESCRIPTION_ERROR, (reqId, msg) => {
249
+ msg = `Cannot execute 'getClassDescription' on BaseStateManager: ${msg}`;
250
+ this.#promiseStore.reject(reqId, msg);
251
+ });
252
+
253
+ this.#status = 'inited';
131
254
  }
132
255
 
133
256
  /**
134
- * Create a `SharedState` instance from a registered schema.
257
+ * Return a class description from a given class name
135
258
  *
136
- * @param {String} schemaName - Name of the schema as given on registration
259
+ * @param {SharedStateClassName} className - Name of the shared state class.
137
260
  * (cf. ServerStateManager)
138
- * @param {Object} [initValues={}] - Default values for the state.
139
- * @returns {client.SharedState|server.SharedState}
140
- * @see {@link client.SharedState}
141
- * @see {@link server.SharedState}
261
+ * @return {SharedStateClassDescription}
262
+ * @example
263
+ * const classDescription = await client.stateManager.getClassDescription('my-class');
264
+ */
265
+ async getClassDescription(className) {
266
+ if (this.#status !== 'inited') {
267
+ throw new DOMException(`Cannot execute 'getClassDescription' on BaseStateManager: BaseStateManager is not inited`, 'InvalidStateError');
268
+ }
269
+
270
+ if (this.#cachedClasses.has(className)) {
271
+ const classDescription = this.#cachedClasses.get(className);
272
+ // This cannot throw as the SharedState class has already been registered
273
+ const parameterBag = new ParameterBag(classDescription);
274
+ return parameterBag.getDescription();
275
+ }
276
+
277
+ return new Promise((resolve, reject) => {
278
+ const reqId = this.#promiseStore.add(resolve, reject, 'BaseStateManager#getClassDescription');
279
+ this[kStateManagerClient].transport.emit(GET_CLASS_DESCRIPTION_REQUEST, reqId, className);
280
+ });
281
+ }
282
+
283
+ /**
284
+ * @deprecated Use {@link BaseStateManager#getClassDescription} instead.
285
+ */
286
+ async getSchema(className) {
287
+ logger.deprecated('BaseStateManager#getSchema', 'BaseStateManager#getClassDescription', '4.0.0-alpha.29');
288
+ return this.getClassDescription(className);
289
+ }
290
+
291
+ /**
292
+ * Create a {@link SharedState} instance from a registered class.
293
+ *
294
+ * @param {SharedStateClassName} className - Name of the class.
295
+ * @param {Object.<string, any>} [initValues={}] - Default values of the created shared state.
296
+ * @returns {Promise<SharedState>}
142
297
  * @example
143
- * const state = await client.stateManager.create('my-schema');
298
+ * const state = await client.stateManager.create('my-class');
144
299
  */
145
- async create(schemaName, initValues = {}) {
300
+ async create(className, initValues = {}) {
301
+ if (this.#status !== 'inited') {
302
+ throw new DOMException(`Cannot execute 'create' on BaseStateManager: BaseStateManager is not inited`, 'InvalidStateError');
303
+ }
304
+
146
305
  return new Promise((resolve, reject) => {
147
- const reqId = storeRequestPromise(resolve, reject);
148
- const requireSchema = this._cachedSchemas.has(schemaName) ? false : true;
149
- this.client.transport.emit(CREATE_REQUEST, reqId, schemaName, requireSchema, initValues);
306
+ const reqId = this.#promiseStore.add(resolve, reject, 'BaseStateManager#create');
307
+ const requireDescription = this.#cachedClasses.has(className) ? false : true;
308
+ this[kStateManagerClient].transport.emit(CREATE_REQUEST, reqId, className, requireDescription, initValues);
150
309
  });
151
310
  }
152
311
 
153
312
  /**
154
- * Attach to an existing `SharedState` instance.
313
+ * Attach to an existing {@link SharedState} instance.
155
314
  *
156
- * @param {String} schemaName - Name of the schema as given on registration
157
- * (cf. ServerStateManager)
158
- * @param {Object} [stateId=null] - Id of the state to attach to. If `null`,
159
- * attach to the first state found with the given schema name (usefull for
315
+ * @overload
316
+ * @param {SharedStateClassName} className - Name of the class.
317
+ * @returns {Promise<SharedState>}
318
+ *
319
+ * @example
320
+ * const state = await client.stateManager.attach('my-class');
321
+ */
322
+ /**
323
+ * Attach to an existing {@link SharedState} instance.
324
+ *
325
+ * @overload
326
+ * @param {SharedStateClassName} className - Name of the class.
327
+ * @param {number} stateId - Id of the state
328
+ * @returns {Promise<SharedState>}
329
+ *
330
+ * @example
331
+ * const state = await client.stateManager.attach('my-class', stateId);
332
+ */
333
+ /**
334
+ * Attach to an existing {@link SharedState} instance.
335
+ *
336
+ * @overload
337
+ * @param {SharedStateClassName} className - Name of the class.
338
+ * @param {string[]} filter - List of parameters of interest
339
+ * @returns {Promise<SharedState>}
340
+ *
341
+ * @example
342
+ * const state = await client.stateManager.attach('my-class', ['some-param']);
343
+ */
344
+ /**
345
+ * Attach to an existing {@link SharedState} instance.
346
+ *
347
+ * @overload
348
+ * @param {SharedStateClassName} className - Name of the class.
349
+ * @param {number} stateId - Id of the state
350
+ * @param {string[]} filter - List of parameters of interest
351
+ * @returns {Promise<SharedState>}
352
+ *
353
+ * @example
354
+ * const state = await client.stateManager.attach('my-class', stateId, ['some-param']);
355
+ */
356
+ /**
357
+ * Attach to an existing {@link SharedState} instance.
358
+ *
359
+ * Alternative signatures:
360
+ * - `stateManager.attach(className)`
361
+ * - `stateManager.attach(className, stateId)`
362
+ * - `stateManager.attach(className, filter)`
363
+ * - `stateManager.attach(className, stateId, filter)`
364
+ *
365
+ * @param {SharedStateClassName} className - Name of the class.
366
+ * @param {number|string[]} [stateIdOrFilter] - Id of the state to attach to. If `null`,
367
+ * attach to the first state found with the given class name (useful for
160
368
  * globally shared states owned by the server).
161
- * @returns {client.SharedState|server.SharedState}
162
- * @see {@link client.SharedState}
163
- * @see {@link server.SharedState}
369
+ * @param {string[]} [filter] - List of parameters of interest in the
370
+ * returned state. If set to `null`, no filter is applied.
371
+ * @returns {Promise<SharedState>}
372
+ *
164
373
  * @example
165
- * const state = await client.stateManager.attach('my-schema');
374
+ * const state = await client.stateManager.attach('my-class');
166
375
  */
167
- async attach(schemaName, stateId = null) {
376
+ async attach(className, stateIdOrFilter = null, filter = null) {
377
+ if (this.#status !== 'inited') {
378
+ throw new DOMException(`Cannot execute 'attach' on BaseStateManager: BaseStateManager is not inited`, 'InvalidStateError');
379
+ }
380
+
381
+ let stateId = null;
382
+
383
+ if (!isString(className)) {
384
+ throw new TypeError(`Cannot execute 'attach' on BaseStateManager: argument 1 must be a string`);
385
+ }
386
+
387
+ if (arguments.length === 2) {
388
+ if (stateIdOrFilter === null) {
389
+ stateId = null;
390
+ filter = null;
391
+ } else if (Number.isFinite(stateIdOrFilter)) {
392
+ stateId = stateIdOrFilter;
393
+ filter = null;
394
+ } else if (Array.isArray(stateIdOrFilter)) {
395
+ stateId = null;
396
+ filter = stateIdOrFilter;
397
+ } else {
398
+ throw new TypeError(`Cannot execute 'attach' on BaseStateManager: argument 2 must be either null, a number or an array`);
399
+ }
400
+ }
401
+
402
+ if (arguments.length === 3) {
403
+ stateId = stateIdOrFilter;
404
+
405
+ if (stateId !== null && !Number.isFinite(stateId)) {
406
+ throw new TypeError(`Cannot execute 'attach' on BaseStateManager: argument 2 must be either null or a number`);
407
+ }
408
+
409
+ if (filter !== null && !Array.isArray(filter)) {
410
+ throw new TypeError(`Cannot execute 'attach' on BaseStateManager: argument 3 must be either null or an array`);
411
+ }
412
+ }
413
+
168
414
  return new Promise((resolve, reject) => {
169
- // @todo - add a timeout
170
- const reqId = storeRequestPromise(resolve, reject);
171
- const requireSchema = this._cachedSchemas.has(schemaName) ? false : true;
172
- this.client.transport.emit(ATTACH_REQUEST, reqId, schemaName, stateId, requireSchema);
415
+ const reqId = this.#promiseStore.add(resolve, reject, 'BaseStateManager#attach');
416
+ const requireDescription = this.#cachedClasses.has(className) ? false : true;
417
+ this[kStateManagerClient].transport.emit(ATTACH_REQUEST, reqId, className, stateId, requireDescription, filter);
173
418
  });
174
419
  }
175
420
 
176
421
  /**
177
- * Observe all the `SharedState` instances that are created on the network.
178
- * This can be usefull for clients with some controller role that might want to track
179
- * the state of all other clients of the application, to monitor them and/or take
180
- * control over them from a single point.
422
+ * Observe all the {@link SharedState} instances that are created on the network.
423
+ *
424
+ * @overload
425
+ * @param {stateManagerObserveCallback} callback - Function to execute when a
426
+ * new {@link SharedState} is created on the network.
427
+ * @example
428
+ * client.stateManager.observe(async (className, stateId) => {
429
+ * if (className === 'my-shared-state-class') {
430
+ * const attached = await client.stateManager.attach(className, stateId);
431
+ * }
432
+ * });
433
+ */
434
+ /**
435
+ * Observe all the {@link SharedState} instances of given {@link SharedStateClassName}
436
+ * that are created on the network.
437
+ *
438
+ * @overload
439
+ * @param {SharedStateClassName} className - Observe only ${@link SharedState}
440
+ * of given name.
441
+ * @param {stateManagerObserveCallback} callback - Function to execute when a
442
+ * new {@link SharedState} is created on the network.
443
+ * @example
444
+ * client.stateManager.observe('my-shared-state-class', async (className, stateId) => {
445
+ * const attached = await client.stateManager.attach(className, stateId);
446
+ * });
447
+ */
448
+ /**
449
+ * Observe all the {@link SharedState} instances of given excluding the ones
450
+ * created by the current node.
451
+ *
452
+ * @overload
453
+ * @param {stateManagerObserveCallback} callback - Function to execute when a
454
+ * new {@link SharedState} is created on the network.
455
+ * @param {object} options - Options.
456
+ * @param {boolean} options.excludeLocal=false - If set to true, exclude states
457
+ * created by the same node from the collection.
458
+ * @example
459
+ * client.stateManager.observe(async (className, stateId) => {
460
+ * if (className === 'my-shared-state-class') {
461
+ * const attached = await client.stateManager.attach(className, stateId);
462
+ * }
463
+ * }, { excludeLocal: true });
464
+ */
465
+ /**
466
+ * Observe all the {@link SharedState} instances of given {@link SharedStateClassName}
467
+ * that are created on the network, excluding the ones created by the current node.
468
+ *
469
+ * @overload
470
+ * @param {SharedStateClassName} className - Observe only ${@link SharedState}
471
+ * of given name.
472
+ * @param {stateManagerObserveCallback} callback - Function to execute when a
473
+ * new {@link SharedState} is created on the network.
474
+ * @param {object} options - Options.
475
+ * @param {boolean} options.excludeLocal=false - If set to true, exclude states
476
+ * created by the same node from the collection.
477
+ * @example
478
+ * client.stateManager.observe('my-shared-state-class', async (className, stateId) => {
479
+ * const attached = await client.stateManager.attach(className, stateId);
480
+ * }, { excludeLocal: true });
481
+ */
482
+ /**
483
+ * Observe all the {@link SharedState} instances that are created on the network.
181
484
  *
182
485
  * Notes:
183
- * - The states that are created by the same node are not propagated through
184
- * the observe callback.
185
- * - The order of execution is not guaranted, i.e. an state attached in the
186
- * `observe` callback could be created before the `async create` method resolves.
486
+ * - The order of execution is not guaranteed between nodes, i.e. a state attached
487
+ * in the `observe` callback can be instantiated before the `async create` method
488
+ * resolves on the creator node.
489
+ * - Filtering, i.e. `observedClassName` and `options.excludeLocal` are handled
490
+ * on the node side, the server just notify all state creation activity and
491
+ * the node executes the given callbacks according to the different filter rules.
492
+ * Such strategy allows to simply share the observe notifications between all observers.
493
+ *
494
+ * Alternative signatures:
495
+ * - `stateManager.observe(callback)`
496
+ * - `stateManager.observe(className, callback)`
497
+ * - `stateManager.observe(callback, options)`
498
+ * - `stateManager.observe(className, callback, options)`
187
499
  *
188
- * @param {String} [schemaName] - optionnal schema name to filter the observed
500
+ * @param {SharedStateClassName} [className] - Optional class name to filter the observed
189
501
  * states.
190
- * @param {server.StateManager~ObserveCallback|client.StateManager~ObserveCallback}
502
+ * @param {stateManagerObserveCallback}
191
503
  * callback - Function to be called when a new state is created on the network.
192
- * @returns {Promise<Function>} - Returns a Promise that resolves when the given
504
+ * @param {object} options - Options.
505
+ * @param {boolean} [options.excludeLocal = false] - If set to true, exclude states
506
+ * created locally, i.e. by the same node, from the collection.
507
+ * @returns {Promise<stateManagerDeleteObserveCallback>} - Returns a Promise that resolves when the given
193
508
  * callback as been executed on each existing states. The promise value is a
194
509
  * function which allows to stop observing the states on the network.
510
+ *
195
511
  * @example
196
- * client.stateManager.observe(async (schemaName, stateId, nodeId) => {
197
- * if (schemaName === 'something') {
198
- * const state = await this.client.stateManager.attach(schemaName, stateId);
199
- * console.log(state.getValues());
512
+ * client.stateManager.observe(async (className, stateId) => {
513
+ * if (className === 'my-shared-state-class') {
514
+ * const attached = await client.stateManager.attach(className, stateId);
200
515
  * }
201
516
  * });
202
517
  */
203
- // note: all filtering is done only on client-side as it is really more simple to
204
- // handle this way and the network overhead is very low for observe notifications:
205
- // i.e. schemaName, stateId, nodeId
206
518
  async observe(...args) {
207
- let filterSchemaName;
519
+ if (this.#status !== 'inited') {
520
+ throw new DOMException(`Cannot execute 'observe' on BaseStateManager: BaseStateManager is not inited`, 'InvalidStateError');
521
+ }
522
+
523
+ const defaultOptions = {
524
+ excludeLocal: false,
525
+ };
526
+
527
+ let observedClassName;
208
528
  let callback;
529
+ let options;
209
530
 
210
- if (args.length === 1) {
211
- filterSchemaName = '*';
212
- callback = args[0];
531
+ switch (args.length) {
532
+ case 1: {
533
+ // variation: .observe(callback)
534
+ if (!isFunction(args[0])) {
535
+ throw new TypeError(`Cannot execute 'observe' on BaseStateManager: Argument 1 should be a function`);
536
+ }
213
537
 
214
- if (!isFunction(callback)) {
215
- throw new Error(`[stateManager] Invalid arguments, valid signatures are "stateManager.observe(callback)" or "stateManager.observe(schemaName, callback)"`);
538
+ observedClassName = null;
539
+ callback = args[0];
540
+ options = defaultOptions;
541
+ break;
216
542
  }
217
- } else if (args.length === 2) {
218
- filterSchemaName = args[0];
219
- callback = args[1];
543
+ case 2: {
544
+ // variation: .observe(className, callback)
545
+ if (isString(args[0])) {
546
+ if (!isFunction(args[1])) {
547
+ throw new TypeError(`Cannot execute 'observe' on BaseStateManager: Argument 2 should be a function`);
548
+ }
549
+
550
+ observedClassName = args[0];
551
+ callback = args[1];
552
+ options = defaultOptions;
553
+
554
+ // variation: .observe(callback, options)
555
+ } else if (isFunction(args[0])) {
556
+ if (!isPlainObject(args[1])) {
557
+ throw new TypeError(`Cannot execute 'observe' on BaseStateManager: Argument 2 should be an object`);
558
+ }
559
+
560
+ observedClassName = null;
561
+ callback = args[0];
562
+ options = Object.assign(defaultOptions, args[1]);
563
+
564
+ } else {
565
+ throw new TypeError(`Cannot execute 'observe' on BaseStateManager: Invalid signature, refer to the documentation`);
566
+ }
220
567
 
221
- if (!isString(filterSchemaName) || !isFunction(callback)) {
222
- throw new Error(`[stateManager] Invalid arguments, valid signatures are "stateManager.observe(callback)" or "stateManager.observe(schemaName, callback)"`);
568
+ break;
569
+ }
570
+ case 3: {
571
+ // variation: .observe(className, callback, options)
572
+ if (!isString(args[0])) {
573
+ throw new TypeError(`Cannot execute 'observe' on BaseStateManager: Argument 1 should be a string`);
574
+ }
575
+
576
+ if (!isFunction(args[1])) {
577
+ throw new TypeError(`Cannot execute 'observe' on BaseStateManager: Argument 2 should be a function`);
578
+ }
579
+
580
+ if (!isPlainObject(args[2])) {
581
+ throw new TypeError(`Cannot execute 'observe' on BaseStateManager: Argument 3 should be an object`);
582
+ }
583
+
584
+ observedClassName = args[0];
585
+ callback = args[1];
586
+ options = Object.assign(defaultOptions, args[2]);
587
+
588
+ break;
589
+ }
590
+ // throw in all other cases
591
+ default: {
592
+ throw new TypeError(`Cannot execute 'observe' on BaseStateManager: Invalid signature, refer to the documentation`);
223
593
  }
224
- } else {
225
- throw new Error(`[stateManager] Invalid arguments, valid signatures are "stateManager.observe(callback)" or "stateManager.observe(schemaName, callback)"`);
226
594
  }
227
595
 
228
596
  // resend request to get updated list of states
229
597
  return new Promise((resolve, reject) => {
230
- const reqId = storeRequestPromise(resolve, reject);
598
+ const reqId = this.#promiseStore.add(resolve, reject, 'BaseStateManager#observe');
231
599
  // store the callback for execution on the response. the returned Promise
232
- // is fullfiled once callback has been executed with each existing states
233
- this._observeRequestCallbacks.set(reqId, [callback, filterSchemaName]);
234
- // store the callback for execution on subsequent notifications
235
- this._observeListeners.set(callback, filterSchemaName);
236
-
237
- this.client.transport.emit(OBSERVE_REQUEST, reqId);
600
+ // is fulfilled once callback has been executed with each existing states
601
+ const observeInfos = [observedClassName, callback, options];
602
+ this.#observeRequestCallbacks.set(reqId, observeInfos);
603
+
604
+ // NOTE: do not store in `_observeListeners` yet as it can produce race
605
+ // conditions, e.g.:
606
+ // ```
607
+ // await client.stateManager.observe(async (className, stateId, nodeId) => {});
608
+ // // client now receives OBSERVE_NOTIFICATIONS
609
+ // await otherClient.stateManager.create('a');
610
+ // // second observer added in between
611
+ // client.stateManager.observe(async (className, stateId, nodeId) => {});
612
+ // ````
613
+ // OBSERVE_NOTIFICATION is received before the OBSERVE_RESPONSE, then the
614
+ // second observer is called twice:
615
+ // - OBSERVE_RESPONSE 1 []
616
+ // - OBSERVE_NOTIFICATION [ 'a', 1, 0 ]
617
+ // - OBSERVE_NOTIFICATION [ 'a', 1, 0 ] // this should not happen
618
+ // - OBSERVE_RESPONSE 1 [ [ 'a', 1, 0 ] ]
619
+ //
620
+ // cf. unit test `observe should properly behave in race condition`
621
+
622
+ this[kStateManagerClient].transport.emit(OBSERVE_REQUEST, reqId, observedClassName);
238
623
  });
239
624
  }
240
625
 
241
626
  /**
242
- * Returns a collection of all the states created from the schema name. Except
243
- * the ones created by the current node.
627
+ * Returns a collection of all the states created from a given shared state class.
628
+ *
629
+ * @overload
630
+ * @param {SharedStateClassName} className - Name of the shared state class.
631
+ * @returns {Promise<SharedStateCollection>}
632
+ *
633
+ * @example
634
+ * const collection = await client.stateManager.getCollection(className);
635
+ */
636
+ /**
637
+ * Returns a collection of all the states created from a given shared state class.
638
+ *
639
+ * @overload
640
+ * @param {SharedStateClassName} className - Name of the shared state class.
641
+ * @param {SharedStateParameterName[]} filter - Filter parameter of interest for each
642
+ * state of the collection.
643
+ * @returns {Promise<SharedStateCollection>}
644
+ *
645
+ * @example
646
+ * const collection = await client.stateManager.getCollection(className, ['my-param']);
647
+ */
648
+ /**
649
+ * Returns a collection of all the states created from a given shared state class.
650
+ *
651
+ * @overload
652
+ * @param {SharedStateClassName} className - Name of the shared state class.
653
+ * @param {object} options - Options.
654
+ * @param {boolean} options.excludeLocal=false - If set to true, exclude states
655
+ * created by the same node from the collection.
656
+ * @returns {Promise<SharedStateCollection>}
244
657
  *
245
- * @param {string} schemaName - Name of the schema.
246
- * @returns {server.SharedStateCollection|client.SharedStateCollection}
658
+ * @example
659
+ * const collection = await client.stateManager.getCollection(className, { excludeLocal: true });
660
+ */
661
+ /**
662
+ * Returns a collection of all the states created from a given shared state class.
663
+ *
664
+ * @overload
665
+ * @param {SharedStateClassName} className - Name of the shared state class.
666
+ * @param {SharedStateParameterName[]} filter - Filter parameter of interest for each
667
+ * state of the collection.
668
+ * @param {object} options - Options.
669
+ * @param {boolean} options.excludeLocal=false - If set to true, exclude states
670
+ * created by the same node from the collection.
671
+ * @returns {Promise<SharedStateCollection>}
672
+ *
673
+ * @example
674
+ * const collection = await client.stateManager.getCollection(className, ['my-param'], { excludeLocal: true });
247
675
  */
248
- async getCollection(schemaName) {
249
- const collection = new SharedStateCollection(this, schemaName);
250
- await collection._init();
676
+ /**
677
+ * Returns a collection of all the states created from a given shared state class.
678
+ *
679
+ * Alternative signatures:
680
+ * - `stateManager.getCollection(className)`
681
+ * - `stateManager.getCollection(className, filter)`
682
+ * - `stateManager.getCollection(className, options)`
683
+ * - `stateManager.getCollection(className, filter, options)`
684
+ *
685
+ * @param {SharedStateClassName} className - Name of the shared state class.
686
+ * @param {array|null} [filter=null] - Filter parameter of interest for each
687
+ * state of the collection. If set to `null`, no filter applied.
688
+ * @param {object} [options={}] - Options.
689
+ * @param {boolean} [options.excludeLocal=false] - If set to true, exclude states
690
+ * created by the same node from the collection.
691
+ * @returns {Promise<SharedStateCollection>}
692
+ *
693
+ * @example
694
+ * const collection = await client.stateManager.getCollection(className);
695
+ */
696
+ async getCollection(className, filterOrOptions = null, options = {}) {
697
+ if (this.#status !== 'inited') {
698
+ throw new DOMException(`Cannot execute 'getCollection' on BaseStateManager: BaseStateManager is not inited`, 'InvalidStateError');
699
+ }
700
+
701
+ if (!isString(className)) {
702
+ throw new TypeError(`Cannot execute 'getCollection' on BaseStateManager: Argument 1 should be a string"`);
703
+ }
704
+
705
+ let filter;
706
+
707
+ if (arguments.length === 2) {
708
+ if (filterOrOptions === null) {
709
+ filter = null;
710
+ options = null;
711
+ } else if (Array.isArray(filterOrOptions)) {
712
+ filter = filterOrOptions;
713
+ options = {};
714
+ } else if (typeof filterOrOptions === 'object') {
715
+ filter = null;
716
+ options = filterOrOptions;
717
+ } else {
718
+ throw new TypeError(`Cannot execute 'getCollection' on BaseStateManager: Argument 2 should be either null, an array or an object"`);
719
+ }
720
+ }
721
+
722
+ if (arguments.length === 3) {
723
+ filter = filterOrOptions;
724
+
725
+ if (filter !== null && !Array.isArray(filter)) {
726
+ throw new TypeError(`Cannot execute 'getCollection' on BaseStateManager: Argument 2 should be either an array or null"`);
727
+ }
728
+
729
+ if (options === null || typeof options !== 'object') {
730
+ throw new TypeError(`Cannot execute 'getCollection' on BaseStateManager: Argument 3 should be either an object"`);
731
+ }
732
+ }
733
+
734
+ const collection = new SharedStateCollection(this, className, filter, options);
735
+
736
+ try {
737
+ await collection[kSharedStateCollectionInit]();
738
+ } catch (err) {
739
+ throw new ReferenceError(`Cannot execute 'getCollection' on BaseStateManager: ${err.message}`);
740
+ }
251
741
 
252
742
  return collection;
253
743
  }