@soundworks/core 4.0.0-alpha.7 → 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 +16 -50
  3. package/package.json +26 -44
  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 +636 -143
  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 +559 -821
  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 -517
  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,9 +1,22 @@
1
- import { isBrowser, isPlainObject } from '@ircam/sc-utils';
1
+ import { isBrowser, isPlainObject, isString } from '@ircam/sc-utils';
2
+
3
+ import ClientContextManager, {
4
+ kClientContextManagerStart,
5
+ kClientContextManagerStop,
6
+ } from './ClientContextManager.js';
7
+ import ClientPluginManager from './ClientPluginManager.js';
8
+ import {
9
+ kPluginManagerStart,
10
+ kPluginManagerStop,
11
+ } from '../common/BasePluginManager.js';
12
+ import ClientSocket, {
13
+ kSocketTerminate,
14
+ } from './ClientSocket.js';
15
+ import ClientStateManager from './ClientStateManager.js';
16
+ import {
17
+ kStateManagerInit,
18
+ } from '../common/BaseStateManager.js';
2
19
 
3
- import ContextManager from './ContextManager.js';
4
- import PluginManager from './PluginManager.js';
5
- import Socket from './Socket.js';
6
- import StateManager from './StateManager.js';
7
20
  import {
8
21
  CLIENT_HANDSHAKE_REQUEST,
9
22
  CLIENT_HANDSHAKE_RESPONSE,
@@ -11,78 +24,68 @@ import {
11
24
  AUDIT_STATE_NAME,
12
25
  } from '../common/constants.js';
13
26
  import logger from '../common/logger.js';
27
+ import VERSION from '../common/version.js';
14
28
 
15
- /**
16
- * Configuration object for a client running in a browser runtime.
17
- *
18
- * @typedef BrowserClientConfig
19
- * @memberof client
20
- * @type {object}
21
- * @property {string} role - Role of the client in the application (e.g. 'player', 'controller').
22
- * @property {object} [app] - Application configration object.
23
- * @property {string} [app.name=''] - Name of the application.
24
- * @property {string} [app.author=''] - Name of the author.
25
- * @property {object} [env] - Environment configration object.
26
- * @property {string} [env.websockets={}] - Configuration options for websockets.
27
- * @property {string} [env.subpath=''] - If running behind a proxy, path to the application.
28
- */
29
-
30
- /**
31
- * Configuration object for a client running in a node runtime.
32
- *
33
- * @typedef NodeClientConfig
34
- * @memberof client
35
- * @type {object}
36
- * @property {string} role - Role of the client in the application (e.g. 'player', 'controller').
37
- * @property {object} [app] - Application configration object.
38
- * @property {string} [app.name=''] - Name of the application.
39
- * @property {string} [app.author=''] - Name of the author.
40
- * @property {object} env - Environment configration object.
41
- * @property {boolean} env.serverAddress - Domain name or IP of the server.
42
- * @property {boolean} env.useHttps - Define is the server run in http or in https.
43
- * @property {boolean} env.port - Port on which the server is listening.
44
- * @property {string} [env.websockets={}] - Configuration options for websockets.
45
- * @property {string} [env.subpath=''] - If running behind a proxy, path to the application.
46
- */
29
+ // for testing purposes
30
+ export const kClientVersionTest = Symbol('soundworks:client-version-test');
31
+ export const kClientOnStatusChangeCallbacks = Symbol('soundworks:client-on-status-change-callbacks');
47
32
 
48
33
  /**
49
34
  * The `Client` class is the main entry point for the client-side of a soundworks
50
35
  * application.
51
36
  *
52
- * A `Client` instance allows to access soundworks components such as {@link client.StateManager},
53
- * {@link client.PluginManager},{@link client.Socket} or {@link client.ContextManager}.
54
- * Its is also responsible for handling the initialization lifecycles of the different
55
- * soundworks components.
37
+ * A `soundworks` client can run seamlessly in a browser or in a Node.js runtime.
38
+ *
39
+ * It provides an access to the different soundworks components such as the {@link ClientStateManager},
40
+ * {@link ClientPluginManager}, {@link ClientSocket} and the {@link ClientContextManager}.
56
41
  *
57
42
  * ```
58
43
  * import { Client } from '@soundworks/core/client.js';
59
- * // create a new soundworks `Client` instance
60
- * const client = new Client({ role: 'player' });
61
- * // init and start the client
44
+ * // create a `Client` instance
45
+ * const client = new Client({
46
+ * role: 'player',
47
+ * env: {
48
+ * useHttps: false,
49
+ * serverAddress: 'localhost',
50
+ * port: 8000,
51
+ * },
52
+ * });
53
+ * // start the client
62
54
  * await client.start();
63
55
  * ```
64
- *
65
- * @memberof client
66
56
  */
67
57
  class Client {
58
+ #config = null;
59
+ #version = null;
60
+ #role = null;
61
+ #id = null;
62
+ #uuid = null;
63
+ #token = null;
64
+ #runtime = null;
65
+ #socket = null;
66
+ #contextManager = null;
67
+ #pluginManager = null;
68
+ #stateManager = null;
69
+ #status = 'idle';
70
+ #auditState = null;
71
+
68
72
  /**
69
- * @param {client.BrowserClientConfig|client.NodeClientConfig} config -
70
- * Configuration of the soundworks client.
73
+ * @param {ClientConfig} config - Configuration of the soundworks client.
71
74
  * @throws Will throw if the given config object is invalid.
72
75
  */
73
76
  constructor(config) {
74
77
  if (!isPlainObject(config)) {
75
- throw new Error(`[soundworks:Client] Invalid argument for Client constructor, config should be an object`);
78
+ throw new Error(`Cannot construct Client: argument 1 must be an object`);
76
79
  }
77
80
 
78
- if (!('role' in config)) {
79
- throw new Error('[soundworks:Client] Invalid config object, "config.role" should be defined');
81
+ if (!('role' in config) || !isString(config.role)) {
82
+ throw new Error('Cannot construct Client: Invalid ClientConfig object: Property "role" must be a string');
80
83
  }
81
84
 
82
- // for node clients env.https is requires to open the websocket
85
+ // for node clients env.https is required to open the websocket
83
86
  if (!isBrowser()) {
84
87
  if (!('env' in config)) {
85
- throw new Error('[soundworks:Client] Invalid config object, "config.env" { useHttps, serverAddress, port } should be defined');
88
+ throw new Error(`Cannot construct Client: The 'env' property with type 'ClientEnvConfig' must be defined`);
86
89
  }
87
90
 
88
91
  let missing = [];
@@ -100,125 +103,197 @@ class Client {
100
103
  }
101
104
 
102
105
  if (missing.length) {
103
- throw new Error(`[soundworks:Client] Invalid config object, "config.env" is missing: ${missing.join(', ')}`);
106
+ throw new Error(`Cannot construct Client: Invalid 'env' property of type 'ClientEnvConfig': Fields ${missing.join(', ')} are missing`);
104
107
  }
105
108
  }
106
109
 
107
- /**
108
- * Role of the client in the application.
109
- *
110
- * @type {string}
111
- */
112
- this.role = config.role;
113
-
114
- /**
115
- * Configuration object.
116
- *
117
- * @type {client.BrowserClientConfig|client.NodeClientConfig}
118
- */
119
- this.config = config;
120
-
121
- if (!this.config.env) {
122
- this.config.env = {};
110
+ this.#config = config;
111
+
112
+ if (!this.#config.env) {
113
+ this.#config.env = {};
114
+ }
115
+
116
+ this.#version = VERSION;
117
+ // allow override though config for testing
118
+ if (config[kClientVersionTest]) {
119
+ this.#version = config[kClientVersionTest];
123
120
  }
124
121
 
125
- // minimal configuration for websockets
126
- this.config.env.websockets = Object.assign({
122
+ this.#role = config.role;
123
+ this.#runtime = isBrowser() ? 'browser' : 'node';
124
+ this.#socket = new ClientSocket(this.#role, this.#config, {
127
125
  path: 'socket',
128
- pingInterval: 5000,
129
- }, config.env.websockets);
130
-
131
- /**
132
- * Session id of the client (incremeted positive number), generated and
133
- * retrieved by the server during `client.init`. The counter is reset when
134
- * the server restarts.
135
- *
136
- * @type {number}
137
- */
138
- this.id = null;
139
-
140
- /**
141
- * Unique session uuid of the client (uuidv4), generated and retrieved by
142
- * the server during {@link client.Client#init}.
143
- *
144
- * @type {string}
145
- */
146
- this.uuid = null;
147
-
148
- /**
149
- * Instance of the {@link client.Socket} class that handle websockets communications with
150
- * the server.
151
- *
152
- * @see {@link client.Socket}
153
- * @type {client.Socket}
154
- */
155
- this.socket = new Socket();
156
-
157
- /**
158
- * Runtime platform on which the client is executed, i.e. 'browser' or 'node'.
159
- *
160
- * @type {string}
161
- */
162
- this.target = isBrowser() ? 'browser' : 'node';
163
-
164
- /**
165
- * Instance of the {@link client.ContextManager} class.
166
- *
167
- * The context manager can be safely used after `client.init()` has been fulfilled.
168
- *
169
- * @see {@link client.ContextManager}
170
- * @type {client.ContextManager}
171
- */
172
- this.contextManager = new ContextManager();
173
-
174
- /**
175
- * Instance of the {@link client.PluginManager} class.
176
- *
177
- * The context manager can be safely used after `client.init()` has been fulfilled.
178
- *
179
- * @see {@link client.PluginManager}
180
- * @type {client.PluginManager}
181
- */
182
- this.pluginManager = new PluginManager(this);
183
-
184
- /**
185
- * Instance of the {@link client.StateManager} class.
186
- *
187
- * The context manager can be safely used after `client.init()` has been fulfilled.
188
- *
189
- * @see {@link client.StateManager}
190
- * @type {client.StateManager}
191
- */
192
- this.stateManager = null;
193
-
194
- /**
195
- * Status of the client, 'idle', 'inited', 'started' or 'errored'.
196
- *
197
- * @type {string}
198
- */
199
- this.status = 'idle';
200
-
201
- /**
202
- * Token of the client if connected through HTTP authentication.
203
- * @private
204
- */
205
- this.token = null;
206
-
207
- /** @private */
208
- this._onStatusChangeCallbacks = new Set();
209
- /** @private */
210
- this._auditState = null;
126
+ retryConnectionRefusedTimeout: 1000,
127
+ });
128
+ this.#contextManager = new ClientContextManager();
129
+ this.#pluginManager = new ClientPluginManager(this);
130
+ this.#stateManager = new ClientStateManager();
131
+ this.#status = 'idle';
132
+
133
+ this[kClientOnStatusChangeCallbacks] = new Set();
211
134
 
212
135
  logger.configure(!!config.env.verbose);
213
136
  }
214
137
 
138
+ /**
139
+ * Package version.
140
+ *
141
+ * @type {string}
142
+ */
143
+ get version() {
144
+ return this.#version;
145
+ }
146
+
147
+ /**
148
+ * Role of the client in the application.
149
+ *
150
+ * @type {string}
151
+ */
152
+ get role() {
153
+ return this.#role;
154
+ }
155
+
156
+ /**
157
+ * Configuration object.
158
+ *
159
+ * @type {ClientConfig}
160
+ */
161
+ get config() {
162
+ return this.#config;
163
+ }
164
+
165
+ /**
166
+ * Session id of the client.
167
+ *
168
+ * Incremented positive integer generated and retrieved by the server during
169
+ * {@link Client#init}. The counter is reset when the server restarts.
170
+ *
171
+ * @type {number}
172
+ */
173
+ get id() {
174
+ return this.#id;
175
+ }
176
+
177
+ /**
178
+ * Unique session uuid of the client (uuidv4).
179
+ *
180
+ * Generated and retrieved by the server during {@link Client#init}.
181
+ * @type {string}
182
+ */
183
+ get uuid() {
184
+ return this.#uuid;
185
+ }
186
+
187
+ /**
188
+ * Authentication token generated by the server.
189
+ *
190
+ * Only set if client has gone through authentication process and is considered trusted.
191
+ *
192
+ * Generated and retrieved by the server during {@link Client#init}.
193
+ * @type {string}
194
+ */
195
+ get token() {
196
+ return this.#token;
197
+ }
198
+
199
+ /**
200
+ * Runtime platform on which the client is executed, i.e. 'browser' or 'node'.
201
+ *
202
+ * @type {'node'|'browser'}
203
+ */
204
+ get runtime() {
205
+ return this.#runtime;
206
+ }
207
+
208
+ /**
209
+ * @deprecated Use {@link Client#runtime} instead.
210
+ */
211
+ get target() {
212
+ logger.deprecated('Client#target', 'Client#runtime', '4.0.0-alpha.29');
213
+ return this.runtime;
214
+ }
215
+
216
+ /**
217
+ * Instance of the {@link client.Socket} class.
218
+ *
219
+ * @see {@link ClientSocket}
220
+ * @type {ClientSocket}
221
+ */
222
+ get socket() {
223
+ return this.#socket;
224
+ }
225
+
226
+ /**
227
+ * Instance of the {@link ClientContextManager} class.
228
+ *
229
+ * The context manager can be safely used after `client.init()` has been fulfilled.
230
+ *
231
+ * @see {@link ClientContextManager}
232
+ * @type {ClientContextManager}
233
+ */
234
+ get contextManager() {
235
+ return this.#contextManager;
236
+ }
237
+
238
+ /**
239
+ * Instance of the {@link ClientPluginManager} class.
240
+ *
241
+ * The context manager can be safely used after `client.init()` has been fulfilled.
242
+ *
243
+ * @see {@link ClientPluginManager}
244
+ * @type {ClientPluginManager}
245
+ */
246
+ get pluginManager() {
247
+ return this.#pluginManager;
248
+ }
249
+
250
+ /**
251
+ * Instance of the {@link ClientStateManager} class.
252
+ *
253
+ * The context manager can be safely used after `client.init()` has been fulfilled.
254
+ *
255
+ * @see {@link ClientStateManager}
256
+ * @type {ClientStateManager}
257
+ */
258
+ get stateManager() {
259
+ return this.#stateManager;
260
+ }
261
+
262
+ /**
263
+ * Status of the client.
264
+ *
265
+ * @type {'idle'|'inited'|'started'|'errored'}
266
+ */
267
+ get status() {
268
+ return this.#status;
269
+ }
270
+
271
+ /** @private */
272
+ async #dispatchStatus(status) {
273
+ this.#status = status;
274
+
275
+ // if node target and launched in a child process, forward status to parent process
276
+ if (this.#runtime === 'node' && process.send !== undefined) {
277
+ process.send(`soundworks:client:${status}`);
278
+ }
279
+
280
+ // execute all callbacks in parallel
281
+ const promises = [];
282
+
283
+ for (let callback of this[kClientOnStatusChangeCallbacks]) {
284
+ promises.push(callback(status));
285
+ }
286
+
287
+ await Promise.all(promises);
288
+ }
289
+
215
290
  /**
216
291
  * The `init` method is part of the initialization lifecycle of the `soundworks`
217
- * client. Most of the time, the `init` method will be implicitly called by the
218
- * {@link client.Client#start} method.
292
+ * client. Most of the time, this method will be implicitly executed by the
293
+ * {@link Client#start} method.
219
294
  *
220
295
  * In some situations you might want to call this method manually, in such cases
221
- * the method should be called before the {@link client.Client#start} method.
296
+ * the method should be called before the {@link Client#start} method.
222
297
  *
223
298
  * What it does:
224
299
  * - connect the sockets to be server
@@ -226,89 +301,81 @@ class Client {
226
301
  * - launch the state manager
227
302
  * - initialize all registered plugin
228
303
  *
229
- * After `await client.init()` is fulfilled, the {@link client.Client#stateManager},
230
- * the {@link client.Client#pluginManager} and the {@link client.Client#socket}
304
+ * After `await client.init()` is fulfilled, the {@link Client#stateManager},
305
+ * the {@link Client#pluginManager} and the {@link Client#socket}
231
306
  * can be safely used.
232
307
  *
233
308
  * @example
234
309
  * import { Client } from '@soundworks/core/client.js'
235
310
  *
236
311
  * const client = new Client(config);
237
- * // optionnal explicit call of `init` before `start`
312
+ * // optional explicit call of `init` before `start`
238
313
  * await client.init();
239
314
  * await client.start();
240
315
  */
241
316
  async init() {
242
- // init socket communications (string and binary)
243
- await this.socket.init(this.role, this.config);
317
+ if (this.#status !== 'idle') {
318
+ throw new DOMException(`Cannot execute 'init' on Client: Lifecycle methods must be called in following order: init, start, stop`, 'InvalidAccessError');
319
+ }
320
+
321
+ // init socket communications
322
+ await this.#socket.init();
244
323
 
245
324
  // we need the try/catch block to change the promise rejection into proper error
246
325
  try {
247
326
  await new Promise((resolve, reject) => {
248
327
  // wait for handshake response before starting stateManager and pluginManager
249
- this.socket.addListener(CLIENT_HANDSHAKE_RESPONSE, async ({ id, uuid, token }) => {
250
- this.id = id;
251
- this.uuid = uuid;
252
- this.token = token;
328
+ this.#socket.addListener(CLIENT_HANDSHAKE_RESPONSE, async ({ id, uuid, token, version }) => {
329
+ this.#id = id;
330
+ this.#uuid = uuid;
331
+ this.#token = token;
332
+
333
+ if (version !== this.#version) {
334
+ logger.warnVersionDiscrepancies(this.#role, this.#version, version);
335
+ }
253
336
 
254
337
  resolve();
255
338
  });
256
339
 
257
- this.socket.addListener(CLIENT_HANDSHAKE_ERROR, (err) => {
258
- let msg = ``;
259
-
260
- switch (err.type) {
261
- case 'invalid-client-type':
262
- msg = `[soundworks:Client] ${err.message}`;
263
- break;
264
- case 'invalid-plugin-list':
265
- msg = `[soundworks:Client] ${err.message}`;
266
- break;
267
- default:
268
- msg = `Undefined error`;
269
- break;
270
- }
271
-
272
- this.socket.terminate();
340
+ this.#socket.addListener(CLIENT_HANDSHAKE_ERROR, (err) => {
341
+ let msg = `Cannot execute 'init' on Client: ${err.message}`;
342
+ // These are development errors, we can just hang. If we terminate the
343
+ // socket, a reload is triggered by the launcher which is not good DX.
273
344
  reject(msg);
274
345
  });
275
346
 
276
347
  // send handshake request
277
348
  const payload = {
278
- role: this.role,
279
- registeredPlugins: this.pluginManager.getRegisteredPlugins(),
349
+ role: this.#role,
350
+ version: this.#version,
351
+ registeredPlugins: this.#pluginManager.getRegisteredPlugins(),
280
352
  };
281
353
 
282
- this.socket.send(CLIENT_HANDSHAKE_REQUEST, payload);
354
+ this.#socket.send(CLIENT_HANDSHAKE_REQUEST, payload);
283
355
  });
284
- } catch (err) {
285
- throw new Error(err);
356
+ } catch (msg) {
357
+ throw new Error(msg);
286
358
  }
287
359
 
288
- // ------------------------------------------------------------
289
- // CREATE STATE MANAGER
290
- // ------------------------------------------------------------
291
- this.stateManager = new StateManager(this.id, {
292
- emit: this.socket.send.bind(this.socket), // need to alias this
293
- addListener: this.socket.addListener.bind(this.socket),
294
- removeAllListeners: this.socket.removeAllListeners.bind(this.socket),
360
+ // init state manager
361
+ this.#stateManager[kStateManagerInit](this.id, {
362
+ emit: this.#socket.send.bind(this.#socket), // need to alias this
363
+ addListener: this.#socket.addListener.bind(this.#socket),
364
+ removeAllListeners: this.#socket.removeAllListeners.bind(this.#socket),
295
365
  });
296
366
 
297
- // ------------------------------------------------------------
298
- // INIT PLUGIN MANAGER
299
- // ------------------------------------------------------------
300
- await this.pluginManager.start();
367
+ await this.#pluginManager[kPluginManagerStart]();
301
368
 
302
- await this._dispatchStatus('inited');
369
+ await this.#dispatchStatus('inited');
303
370
  }
304
371
 
305
372
  /**
306
373
  * The `start` method is part of the initialization lifecycle of the `soundworks`
307
- * client. The `start` method will implicitly call the {@link client.Client#init}
308
- * method if it has not been called manually.
374
+ * client. This method will implicitly execute {@link Client#init} method if it
375
+ * has not been called manually.
309
376
  *
310
377
  * What it does:
311
- * - implicitly call {@link client.Client#init} if not done manually
378
+ * - implicitly call {@link Client#init} if not done manually
312
379
  * - start all created contexts. For that to happen, you will have to call `client.init`
313
380
  * manually and instantiate the contexts between `client.init()` and `client.start()`
314
381
  *
@@ -319,31 +386,24 @@ class Client {
319
386
  * await client.start();
320
387
  */
321
388
  async start() {
322
- if (this.status === 'idle') {
389
+ // lazily call init for convenience
390
+ if (this.#status === 'idle') {
323
391
  await this.init();
324
392
  }
325
393
 
326
- if (this.status === 'started') {
327
- throw new Error(`[soundworks:Server] Cannot call "client.start()" twice`);
394
+ if (this.#status !== 'inited') {
395
+ throw new DOMException(`Cannot execute 'start' on Client: Lifecycle methods must be called in following order: init, start, stop`, 'InvalidAccessError');
328
396
  }
329
397
 
330
- if (this.status !== 'inited') {
331
- throw new Error(`[soundworks:Server] Cannot "client.start()" before "client.init()"`);
332
- }
333
-
334
- // ------------------------------------------------------------
335
- // START CONTEXT MANAGER
336
- // ------------------------------------------------------------
337
- await this.contextManager.start();
338
-
339
- await this._dispatchStatus('started');
398
+ await this.#contextManager[kClientContextManagerStart]();
399
+ await this.#dispatchStatus('started');
340
400
  }
341
401
 
342
402
  /**
343
403
  * Stops all started contexts, plugins and terminates the socket connections.
344
404
  *
345
405
  * In most situations, you might not need to call this method. However, it can
346
- * be usefull for unit testing or similar situations where you want to create
406
+ * be useful for unit testing or similar situations where you want to create
347
407
  * and delete several clients in the same process.
348
408
  *
349
409
  * @example
@@ -356,76 +416,66 @@ class Client {
356
416
  * await client.stop();
357
417
  */
358
418
  async stop() {
359
- if (this.status !== 'started') {
360
- throw new Error(`[soundworks:Client] Cannot "client.stop()" before "client.start()"`);
419
+ if (this.#status !== 'started') {
420
+ throw new DOMException(`Cannot execute 'stop' on Client: Lifecycle methods must be called in following order: init, start, stop`, 'InvalidAccessError');
361
421
  }
362
422
 
363
- await this.contextManager.stop();
364
- await this.pluginManager.stop();
365
- await this.socket.terminate();
423
+ await this.#contextManager[kClientContextManagerStop]();
424
+ await this.#pluginManager[kPluginManagerStop]();
425
+ await this.#socket[kSocketTerminate]();
366
426
 
367
- await this._dispatchStatus('stopped');
427
+ await this.#dispatchStatus('stopped');
368
428
  }
369
429
 
370
430
  /**
371
431
  * Attach and retrieve the global audit state of the application.
372
432
  *
373
- * The audit state is a {@link client.SharedState} instance that keeps track of
374
- * global informations about the application such as, the number of connected
375
- * clients, network latency estimation, etc. It is usefull for controller client
433
+ * The audit state is a {@link SharedState} instance that keeps track of
434
+ * global information about the application such as, the number of connected
435
+ * clients, network latency estimation, etc. It is useful for controller client
376
436
  * roles to give the user an overview about the state of the application.
377
437
  *
378
438
  * The audit state is lazily attached to the client only if this method is called.
379
439
  *
380
- * @returns {Promise<client.SharedState>}
440
+ * @returns {Promise<SharedState>}
381
441
  * @throws Will throw if called before `client.init()`
382
- * @see {@link client.SharedState}
442
+ * @see {@link SharedState}
383
443
  * @example
384
444
  * const auditState = await client.getAuditState();
385
445
  * auditState.onUpdate(() => console.log(auditState.getValues()), true);
386
446
  */
387
447
  async getAuditState() {
388
- if (this.status === 'idle') {
389
- throw new Error(`[soundworks.Client] Cannot access audit state before "client.init()"`);
448
+ if (this.#status === 'idle') {
449
+ throw new DOMException(`Cannot execute 'getAuditState' on Server: 'init' must be called first`, 'InvalidAccessError');
390
450
  }
391
451
 
392
- if (this._auditState === null) {
393
- this._auditState = await this.stateManager.attach(AUDIT_STATE_NAME);
452
+ if (this.#auditState === null) {
453
+ this.#auditState = await this.#stateManager.attach(AUDIT_STATE_NAME);
394
454
  }
395
455
 
396
- return this._auditState;
456
+ return this.#auditState;
397
457
  }
398
458
 
399
459
  /**
400
460
  * Listen for the status change ('inited', 'started', 'stopped') of the client.
401
461
  *
402
462
  * @param {Function} callback - Listener to the status change.
403
- * @returns {Function} Delete the listener.
463
+ * @returns {Function} Function that delete the listener when executed.
404
464
  */
405
465
  onStatusChange(callback) {
406
- this._onStatusChangeCallbacks.add(callback);
407
-
408
- return () => this._onStatusChangeCallbacks.delete(callback);
409
- }
410
-
411
- /** @private */
412
- async _dispatchStatus(status) {
413
- this.status = status;
414
-
415
- // if node target and launched in a child process, forward status to parent process
416
- if (this.target === 'node' && process.send !== undefined) {
417
- process.send(`soundworks:client:${status}`);
418
- }
419
-
420
- // execute all callbacks in parallel
421
- const promises = [];
422
-
423
- for (let callback of this._onStatusChangeCallbacks) {
424
- promises.push(callback(status));
425
- }
426
-
427
- await Promise.all(promises);
466
+ this[kClientOnStatusChangeCallbacks].add(callback);
467
+ return () => this[kClientOnStatusChangeCallbacks].delete(callback);
428
468
  }
429
469
  }
430
470
 
471
+ Object.defineProperties(Client.prototype, {
472
+ [Symbol.toStringTag]: {
473
+ __proto__: null,
474
+ writable: false,
475
+ enumerable: false,
476
+ configurable: true,
477
+ value: 'Client',
478
+ },
479
+ });
480
+
431
481
  export default Client;