@soundworks/core 4.0.0-alpha.27 → 4.0.0-alpha.29

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 (128) hide show
  1. package/.jsdoc +3 -1
  2. package/CHANGELOG.md +15 -5
  3. package/package.json +5 -5
  4. package/src/client/Client.js +78 -83
  5. package/src/client/{Context.js → ClientContext.js} +48 -38
  6. package/src/client/ClientContextManager.js +115 -0
  7. package/src/client/{Plugin.js → ClientPlugin.js} +22 -18
  8. package/src/client/{PluginManager.js → ClientPluginManager.js} +39 -35
  9. package/src/client/{Socket.js → ClientSocket.js} +54 -65
  10. package/src/client/{StateManager.js → ClientStateManager.js} +7 -8
  11. package/src/client/index.js +17 -41
  12. package/src/common/BasePlugin.js +56 -51
  13. package/src/common/BasePluginManager.js +175 -139
  14. package/src/common/BaseStateManager.js +316 -120
  15. package/src/common/BatchedTransport.js +24 -24
  16. package/src/common/ParameterBag.js +18 -6
  17. package/src/common/PromiseStore.js +1 -0
  18. package/src/common/{BaseSharedState.js → SharedState.js} +271 -199
  19. package/src/common/{BaseSharedStateCollection.js → SharedStateCollection.js} +92 -61
  20. package/src/common/logger.js +5 -0
  21. package/src/common/shared-state-types.js +76 -0
  22. package/src/common/version.js +1 -1
  23. package/src/server/Server.js +391 -354
  24. package/src/server/ServerClient.js +73 -0
  25. package/src/server/{Context.js → ServerContext.js} +85 -66
  26. package/src/server/{ContextManager.js → ServerContextManager.js} +98 -85
  27. package/src/server/{Plugin.js → ServerPlugin.js} +10 -24
  28. package/src/server/{PluginManager.js → ServerPluginManager.js} +76 -69
  29. package/src/server/{Socket.js → ServerSocket.js} +19 -18
  30. package/src/server/{Sockets.js → ServerSockets.js} +36 -27
  31. package/src/server/ServerStateManager.js +515 -0
  32. package/src/{common → server}/SharedStatePrivate.js +61 -26
  33. package/src/server/audit-network-latency.worker.js +9 -7
  34. package/src/server/audit-schema.js +1 -1
  35. package/src/server/crypto.js +2 -0
  36. package/src/server/index.js +18 -21
  37. package/types/client/Client.d.ts +74 -95
  38. package/types/client/{Context.d.ts → ClientContext.d.ts} +25 -26
  39. package/types/client/{ContextManager.d.ts → ClientContextManager.d.ts} +19 -22
  40. package/types/client/{Plugin.d.ts → ClientPlugin.d.ts} +17 -20
  41. package/types/client/{PluginManager.d.ts → ClientPluginManager.d.ts} +13 -36
  42. package/types/client/ClientSocket.d.ts +74 -0
  43. package/types/client/{StateManager.d.ts → ClientStateManager.d.ts} +11 -15
  44. package/types/client/index.d.ts +26 -2
  45. package/types/common/BasePlugin.d.ts +52 -46
  46. package/types/common/BasePluginManager.d.ts +68 -44
  47. package/types/common/BaseStateManager.d.ts +207 -97
  48. package/types/common/BatchedTransport.d.ts +1 -5
  49. package/types/common/ParameterBag.d.ts +16 -18
  50. package/types/common/PromiseStore.d.ts +1 -1
  51. package/types/common/SharedState.d.ts +297 -0
  52. package/types/common/{BaseSharedStateCollection.d.ts → SharedStateCollection.d.ts} +34 -18
  53. package/types/common/constants.d.ts +0 -2
  54. package/types/common/logger.d.ts +2 -1
  55. package/types/common/shared-state-types.d.ts +83 -0
  56. package/types/common/sockets-utils.d.ts +0 -5
  57. package/types/common/version.d.ts +1 -2
  58. package/types/server/Server.d.ts +78 -132
  59. package/types/server/ServerClient.d.ts +45 -0
  60. package/types/server/{Context.d.ts → ServerContext.d.ts} +53 -43
  61. package/types/server/ServerContextManager.d.ts +80 -0
  62. package/types/server/{Plugin.d.ts → ServerPlugin.d.ts} +19 -33
  63. package/types/server/{PluginManager.d.ts → ServerPluginManager.d.ts} +39 -37
  64. package/types/server/ServerSocket.d.ts +85 -0
  65. package/types/server/ServerSockets.d.ts +50 -0
  66. package/types/server/ServerStateManager.d.ts +189 -0
  67. package/types/server/SharedStatePrivate.d.ts +21 -0
  68. package/types/server/audit-network-latency.worker.d.ts +1 -2
  69. package/types/server/audit-schema.d.ts +0 -1
  70. package/types/server/crypto.d.ts +2 -1
  71. package/types/server/index.d.ts +24 -2
  72. package/src/client/ContextManager.js +0 -111
  73. package/src/client/SharedState.js +0 -84
  74. package/src/client/SharedStateCollection.js +0 -38
  75. package/src/server/Client.js +0 -55
  76. package/src/server/SharedState.js +0 -85
  77. package/src/server/SharedStateCollection.js +0 -39
  78. package/src/server/StateManager.js +0 -696
  79. package/types/client/Client.d.ts.map +0 -1
  80. package/types/client/Context.d.ts.map +0 -1
  81. package/types/client/ContextManager.d.ts.map +0 -1
  82. package/types/client/Plugin.d.ts.map +0 -1
  83. package/types/client/PluginManager.d.ts.map +0 -1
  84. package/types/client/SharedState.d.ts +0 -85
  85. package/types/client/SharedState.d.ts.map +0 -1
  86. package/types/client/SharedStateCollection.d.ts +0 -41
  87. package/types/client/SharedStateCollection.d.ts.map +0 -1
  88. package/types/client/Socket.d.ts +0 -149
  89. package/types/client/Socket.d.ts.map +0 -1
  90. package/types/client/StateManager.d.ts.map +0 -1
  91. package/types/client/index.d.ts.map +0 -1
  92. package/types/common/BasePlugin.d.ts.map +0 -1
  93. package/types/common/BasePluginManager.d.ts.map +0 -1
  94. package/types/common/BaseSharedState.d.ts +0 -244
  95. package/types/common/BaseSharedState.d.ts.map +0 -1
  96. package/types/common/BaseSharedStateCollection.d.ts.map +0 -1
  97. package/types/common/BaseStateManager.d.ts.map +0 -1
  98. package/types/common/BatchedTransport.d.ts.map +0 -1
  99. package/types/common/ParameterBag.d.ts.map +0 -1
  100. package/types/common/PromiseStore.d.ts.map +0 -1
  101. package/types/common/SharedStatePrivate.d.ts +0 -21
  102. package/types/common/SharedStatePrivate.d.ts.map +0 -1
  103. package/types/common/constants.d.ts.map +0 -1
  104. package/types/common/logger.d.ts.map +0 -1
  105. package/types/common/sockets-utils.d.ts.map +0 -1
  106. package/types/common/version.d.ts.map +0 -1
  107. package/types/server/Client.d.ts +0 -45
  108. package/types/server/Client.d.ts.map +0 -1
  109. package/types/server/Context.d.ts.map +0 -1
  110. package/types/server/ContextManager.d.ts +0 -69
  111. package/types/server/ContextManager.d.ts.map +0 -1
  112. package/types/server/Plugin.d.ts.map +0 -1
  113. package/types/server/PluginManager.d.ts.map +0 -1
  114. package/types/server/Server.d.ts.map +0 -1
  115. package/types/server/SharedState.d.ts +0 -86
  116. package/types/server/SharedState.d.ts.map +0 -1
  117. package/types/server/SharedStateCollection.d.ts +0 -41
  118. package/types/server/SharedStateCollection.d.ts.map +0 -1
  119. package/types/server/Socket.d.ts +0 -164
  120. package/types/server/Socket.d.ts.map +0 -1
  121. package/types/server/Sockets.d.ts +0 -80
  122. package/types/server/Sockets.d.ts.map +0 -1
  123. package/types/server/StateManager.d.ts +0 -405
  124. package/types/server/StateManager.d.ts.map +0 -1
  125. package/types/server/audit-network-latency.worker.d.ts.map +0 -1
  126. package/types/server/audit-schema.d.ts.map +0 -1
  127. package/types/server/crypto.d.ts.map +0 -1
  128. package/types/server/index.d.ts.map +0 -1
@@ -29,15 +29,39 @@ import {
29
29
  encryptData,
30
30
  decryptData,
31
31
  } from './crypto.js';
32
- import Client from './Client.js';
33
- import ContextManager from './ContextManager.js';
34
- import PluginManager from './PluginManager.js';
35
- import StateManager from './StateManager.js';
32
+ import ServerClient, {
33
+ kServerClientToken,
34
+ } from './ServerClient.js';
35
+ import ServerContextManager, {
36
+ kServerContextManagerStart,
37
+ kServerContextManagerStop,
38
+ kServerContextManagerAddClient,
39
+ kServerContextManagerRemoveClient,
40
+ } from './ServerContextManager.js';
41
+ import ServerPluginManager, {
42
+ kServerPluginManagerCheckRegisteredPlugins,
43
+ kServerPluginManagerAddClient,
44
+ kServerPluginManagerRemoveClient,
45
+ } from './ServerPluginManager.js';
46
+ import {
47
+ kPluginManagerStart,
48
+ kPluginManagerStop,
49
+ } from '../common/BasePluginManager.js';
50
+ import ServerStateManager, {
51
+ kServerStateManagerAddClient,
52
+ kServerStateManagerRemoveClient,
53
+ } from './ServerStateManager.js';
54
+ import {
55
+ kStateManagerInit,
56
+ } from '../common/BaseStateManager.js';
36
57
  import {
37
58
  kSocketClientId,
38
59
  kSocketTerminate,
39
- } from './Socket.js';
40
- import Sockets from './Sockets.js';
60
+ } from './ServerSocket.js';
61
+ import ServerSockets, {
62
+ kSocketsStart,
63
+ kSocketsStop,
64
+ } from './ServerSockets.js';
41
65
  import logger from '../common/logger.js';
42
66
  import {
43
67
  SERVER_ID,
@@ -46,30 +70,10 @@ import {
46
70
  CLIENT_HANDSHAKE_ERROR,
47
71
  AUDIT_STATE_NAME,
48
72
  } from '../common/constants.js';
49
- import version from '../common/version.js';
73
+ import VERSION from '../common/version.js';
50
74
 
51
75
  let _dbNamespaces = new Set();
52
76
 
53
- /**
54
- * Configuration object for the server.
55
- *
56
- * @typedef ServerConfig
57
- * @memberof server
58
- * @type {object}
59
- * @property {object} [app] - Application configration object.
60
- * @property {object} app.clients - Definition of the application clients.
61
- * @property {string} [app.name=''] - Name of the application.
62
- * @property {string} [app.author=''] - Name of the author.
63
- * @property {object} [env] - Environment configration object.
64
- * @property {boolean} env.port - Port on which the server is listening.
65
- * @property {boolean} env.useHttps - Define is the server run in http or in https.
66
- * @property {boolean} [env.httpsInfos={}] - Path to cert files for https.
67
- * @property {boolean} env.serverAddress - Domain name or IP of the server.
68
- * Mandatory if node clients are defined
69
- * @property {string} [env.websockets={}] - Configuration options for websockets.
70
- * @property {string} [env.subpath=''] - If running behind a proxy, path to the application.
71
- */
72
-
73
77
  /** @private */
74
78
  const DEFAULT_CONFIG = {
75
79
  env: {
@@ -77,10 +81,6 @@ const DEFAULT_CONFIG = {
77
81
  port: 8000,
78
82
  serverAddress: '',
79
83
  subpath: '',
80
- websockets: {
81
- path: 'socket',
82
- pingInterval: 5000,
83
- },
84
84
  useHttps: false,
85
85
  httpsInfos: null,
86
86
  crossOriginIsolated: true,
@@ -94,12 +94,19 @@ const DEFAULT_CONFIG = {
94
94
 
95
95
  const TOKEN_VALID_DURATION = 20; // sec
96
96
 
97
+ export const kServerOnSocketConnection = Symbol('soundworks:server-on-socket-connection');
98
+ export const kServerIsProtectedRole = Symbol('soundworks:server-is-protected-role');
99
+ export const kServerIsValidConnectionToken = Symbol('soundworks:server-is-valid-connection-token');
100
+ // for testing purposes
101
+ export const kServerOnStatusChangeCallbacks = Symbol('soundworks:server-on-status-change-callbacks');
102
+ export const kServerApplicationTemplateOptions = Symbol('soundworks:server-application-template-options');
103
+
97
104
  /**
98
105
  * The `Server` class is the main entry point for the server-side of a soundworks
99
106
  * application.
100
107
  *
101
- * The `Server` instance allows to access soundworks components such as {@link server.StateManager},
102
- * {@link server.PluginManager},{@link server.Socket} or {@link server.ContextManager}.
108
+ * The `Server` instance allows to access soundworks components such as {@link ServerStateManager},
109
+ * {@link ServerPluginManager}, {@link ServerSocket} or {@link ServerContextManager}.
103
110
  * Its is also responsible for handling the initialization lifecycles of the different
104
111
  * soundworks components.
105
112
  *
@@ -138,12 +145,31 @@ const TOKEN_VALID_DURATION = 20; // sec
138
145
  * The server will listen to the following URLs:
139
146
  * - `http://127.0.0.1:8000/` for the `player` role, which is defined as the default client.
140
147
  * - `http://127.0.0.1:8000/controller` for the `controller` role.
141
- *
142
- * @memberof server
143
148
  */
144
149
  class Server {
150
+ #config = null;
151
+ #version = null;
152
+ #status = null;
153
+ #router = null;
154
+ #httpServer = null;
155
+ #db = null;
156
+
157
+ #sockets = null;
158
+ #pluginManager = null;
159
+ #stateManager = null;
160
+ #contextManager = null;
161
+
162
+ // If `https` is required, hold informations about the certificates, e.g. if
163
+ // self-signed, the dates of validity of the certificates, etc.
164
+ #httpsInfos = null;
165
+ #onClientConnectCallbacks = new Set();
166
+ #onClientDisconnectCallbacks = new Set();
167
+ #auditState = null;
168
+ #pendingConnectionTokens = new Set();
169
+ #trustedClients = new Set();
170
+
145
171
  /**
146
- * @param {server.ServerConfig} config - Configuration object for the server.
172
+ * @param {ServerConfig} config - Configuration object for the server.
147
173
  * @throws
148
174
  * - If `config.app.clients` is empty.
149
175
  * - If a `node` client is defined but `config.env.serverAddress` is not defined.
@@ -156,38 +182,14 @@ class Server {
156
182
  throw new Error(`[soundworks:Server] Invalid argument for Server constructor, config should be an object`);
157
183
  }
158
184
 
159
- /**
160
- * @description Given config object merged with the following defaults:
161
- * @example
162
- * {
163
- * env: {
164
- * type: 'development',
165
- * port: 8000,
166
- * serverAddress: '',
167
- * subpath: '',
168
- * websockets: {
169
- * path: 'socket',
170
- * pingInterval: 5000,
171
- * },
172
- * useHttps: false,
173
- * httpsInfos: null,
174
- * crossOriginIsolated: true,
175
- * verbose: true,
176
- * },
177
- * app: {
178
- * name: 'soundworks',
179
- * clients: {},
180
- * }
181
- * }
182
- */
183
- this.config = merge({}, DEFAULT_CONFIG, config);
185
+ this.#config = merge({}, DEFAULT_CONFIG, config);
184
186
 
185
187
  // parse config
186
- if (Object.keys(this.config.app.clients).length === 0) {
188
+ if (Object.keys(this.#config.app.clients).length === 0) {
187
189
  throw new Error(`[soundworks:Server] Invalid "app.clients" config, at least one client should be declared`);
188
190
  }
189
191
 
190
- // @peeka - remove
192
+ // @peeka - remove this check
191
193
  // [2024-05-29] Override default `config.env.serverAddress`` provided from
192
194
  // template `loadConfig` to '' so that browser clients can default to
193
195
  // window.location.hostname and node clients to `127.0.0.1`
@@ -195,10 +197,10 @@ class Server {
195
197
  this.config.env.serverAddress = '';
196
198
  }
197
199
 
198
- if (this.config.env.useHttps && this.config.env.httpsInfos !== null) {
199
- const httpsInfos = this.config.env.httpsInfos;
200
+ if (this.#config.env.useHttps && this.#config.env.httpsInfos !== null) {
201
+ const httpsInfos = this.#config.env.httpsInfos;
200
202
 
201
- if (!isPlainObject(this.config.env.httpsInfos)) {
203
+ if (!isPlainObject(this.#config.env.httpsInfos)) {
202
204
  throw new Error(`[soundworks:Server] Invalid "env.httpsInfos" config, should be null or object { cert, key }`);
203
205
  }
204
206
 
@@ -215,135 +217,221 @@ class Server {
215
217
  }
216
218
  }
217
219
 
218
- this.version = version;
219
-
220
- /**
221
- * Instance of the express router.
222
- *
223
- * The router can be used to open new route, for example to expose a directory
224
- * of static assets (in default soundworks applications only the `public` is exposed).
225
- *
226
- * @see {@link https://github.com/expressjs/express}
227
- * @example
228
- * import { Server } from '@soundworks/core/server.js';
229
- * import express from 'express';
230
- *
231
- * // create the soundworks server instance
232
- * const server = new Server(config);
233
- *
234
- * // expose assets located in the `soundfiles` directory on the network
235
- * server.router.use('/soundfiles', express.static('soundfiles')));
236
- */
220
+ this.#version = VERSION;
237
221
  // @note: we use express() instead of express.Router() because all 404 and
238
222
  // error stuff is handled by default
239
- this.router = express();
240
- // compression (must be set before express.static())
241
- this.router.use(compression());
242
-
243
- /**
244
- * Raw Node.js `http` or `https` instance
245
- *
246
- * @see {@link https://nodejs.org/api/http.html}
247
- * @see {@link https://nodejs.org/api/https.html}
248
- */
249
- this.httpServer = null;
250
-
251
- /**
252
- * Instance of the {@link server.Sockets} class.
253
- *
254
- * @see {@link server.Sockets}
255
- * @type {server.Sockets}
256
- */
257
- this.sockets = new Sockets();
258
-
259
- /**
260
- * Instance of the {@link server.PluginManager} class.
261
- *
262
- * @see {@link server.PluginManager}
263
- * @type {server.PluginManager}
264
- */
265
- this.pluginManager = new PluginManager(this);
266
-
267
- /**
268
- * Instance of the {@link server.StateManager} class.
269
- *
270
- * @see {@link server.StateManager}
271
- * @type {server.StateManager}
272
- */
273
- this.stateManager = new StateManager();
274
-
275
- /**
276
- * Instance of the {@link server.ContextManager} class.
277
- *
278
- * @see {@link server.ContextManager}
279
- * @type {server.ContextManager}
280
- */
281
- this.contextManager = new ContextManager(this);
282
-
283
- /**
284
- * If `https` is required, hold informations about the certificates, e.g. if
285
- * self-signed, the dates of validity of the certificates, etc.
286
- */
287
- this.httpsInfos = null;
288
-
289
- /**
290
- * Status of the server, 'idle', 'inited', 'started' or 'errored'.
291
- *
292
- * @type {string}
293
- */
294
- this.status = 'idle';
295
-
296
- /**
297
- * Simple key / value database with Promise based Map API store on filesystem,
298
- * basically a tiny wrapper around the `kvey` package.
299
- *
300
- * @private
301
- * @see {@link https://github.com/lukechilds/keyv}
302
- */
303
- this.db = this.createNamespacedDb('core');
304
-
305
- /** @private */
306
- this._applicationTemplateOptions = {
223
+ // compression must be set before `express.static()`
224
+ this.#router = express();
225
+ this.#router.use(compression());
226
+ this.#sockets = new ServerSockets(this, { path: 'socket' });
227
+ this.#pluginManager = new ServerPluginManager(this);
228
+ this.#stateManager = new ServerStateManager();
229
+ this.#contextManager = new ServerContextManager(this);
230
+ this.#status = 'idle';
231
+ this.#db = this.createNamespacedDb('core');
232
+
233
+ this[kServerOnStatusChangeCallbacks] = new Set();
234
+ this[kServerApplicationTemplateOptions] = {
307
235
  templateEngine: null,
308
236
  templatePath: null,
309
237
  clientConfigFunction: null,
310
238
  };
311
239
 
312
- /** @private */
313
- this._onStatusChangeCallbacks = new Set();
314
- /** @private */
315
- this._onClientConnectCallbacks = new Set();
316
- /** @private */
317
- this._onClientDisconnectCallbacks = new Set();
318
- /** @private */
319
- this._auditState = null;
320
- /** @private */
321
- this._pendingConnectionTokens = new Set();
322
- /** @private */
323
- this._trustedClients = new Set();
324
-
325
240
  // register audit state schema
326
- this.stateManager.registerSchema(AUDIT_STATE_NAME, auditSchema);
241
+ this.#stateManager.registerSchema(AUDIT_STATE_NAME, auditSchema);
242
+
243
+ logger.configure(this.#config.env.verbose);
244
+ }
327
245
 
328
- logger.configure(this.config.env.verbose);
246
+ /**
247
+ * Given config object merged with the following defaults:
248
+ * @example
249
+ * {
250
+ * env: {
251
+ * type: 'development',
252
+ * port: 8000,
253
+ * serverAddress: null,
254
+ * subpath: '',
255
+ * useHttps: false,
256
+ * httpsInfos: null,
257
+ * crossOriginIsolated: true,
258
+ * verbose: true,
259
+ * },
260
+ * app: {
261
+ * name: 'soundworks',
262
+ * clients: {},
263
+ * }
264
+ * }
265
+ * @type {ServerConfig}
266
+ */
267
+ get config() {
268
+ return this.#config;
269
+ }
270
+
271
+ /**
272
+ * Package version.
273
+ *
274
+ * @type {string}
275
+ */
276
+ get version() {
277
+ return this.#version;
329
278
  }
330
279
 
331
280
  /**
332
281
  * Id of the server, a constant set to -1
333
- * @type {Number}
282
+ * @type {number}
334
283
  * @readonly
335
284
  */
336
285
  get id() {
337
286
  return SERVER_ID;
338
287
  }
339
288
 
289
+ /**
290
+ * Status of the server.
291
+ *
292
+ * @type {'idle'|'inited'|'started'|'errored'}
293
+ */
294
+ get status() {
295
+ return this.#status;
296
+ }
297
+
298
+ /**
299
+ * Instance of the express router.
300
+ *
301
+ * The router can be used to open new route, for example to expose a directory
302
+ * of static assets (in default soundworks applications only the `public` is exposed).
303
+ *
304
+ * @see {@link https://github.com/expressjs/express}
305
+ * @example
306
+ * import { Server } from '@soundworks/core/server.js';
307
+ * import express from 'express';
308
+ *
309
+ * // create the soundworks server instance
310
+ * const server = new Server(config);
311
+ *
312
+ * // expose assets located in the `soundfiles` directory on the network
313
+ * server.router.use('/soundfiles', express.static('soundfiles')));
314
+ */
315
+ get router() {
316
+ return this.#router;
317
+ }
318
+
319
+ /**
320
+ * Raw Node.js `http` or `https` instance
321
+ *
322
+ * @see {@link https://nodejs.org/api/http.html}
323
+ * @see {@link https://nodejs.org/api/https.html}
324
+ */
325
+ get httpServer() {
326
+ return this.#httpServer;
327
+ }
328
+
329
+
330
+ /**
331
+ * Simple key / value filesystem database with Promise based Map API.
332
+ *
333
+ * Basically a tiny wrapper around the {@link https://github.com/lukechilds/keyv} package.
334
+ */
335
+ get db() {
336
+ return this.#db;
337
+ }
338
+
339
+ /**
340
+ * Instance of the {@link ServerSockets} class.
341
+ *
342
+ * @type {ServerSockets}
343
+ */
344
+ get sockets() {
345
+ return this.#sockets;
346
+ }
347
+
348
+ /**
349
+ * Instance of the {@link ServerPluginManager} class.
350
+ *
351
+ * @type {ServerPluginManager}
352
+ */
353
+ get pluginManager() {
354
+ return this.#pluginManager;
355
+ }
356
+
357
+ /**
358
+ * Instance of the {@link ServerStateManager} class.
359
+ *
360
+ * @type {ServerStateManager}
361
+ */
362
+ get stateManager() {
363
+ return this.#stateManager;
364
+ }
365
+
366
+ /**
367
+ * Instance of the {@link ServerContextManager} class.
368
+ *
369
+ * @type {ServerContextManager}
370
+ */
371
+ get contextManager() {
372
+ return this.#contextManager;
373
+ }
374
+
375
+ /** @private */
376
+ async #dispatchStatus(status) {
377
+ this.#status = status;
378
+
379
+ // if launched in a child process, forward status to parent process
380
+ if (process.send !== undefined) {
381
+ process.send(`soundworks:server:${status}`);
382
+ }
383
+
384
+ // execute all callbacks in parallel
385
+ const promises = [];
386
+
387
+ for (let callback of this[kServerOnStatusChangeCallbacks]) {
388
+ promises.push(callback(status));
389
+ }
390
+
391
+ await Promise.all(promises);
392
+ }
393
+
394
+ /**
395
+ * Register a callback to execute when status change
396
+ *
397
+ * @param {function} callback
398
+ */
399
+ onStatusChange(callback) {
400
+ this[kServerOnStatusChangeCallbacks].add(callback);
401
+ return () => this[kServerOnStatusChangeCallbacks].delete(callback);
402
+ }
403
+
404
+ /**
405
+ * Attach and retrieve the global audit state of the application.
406
+ *
407
+ * The audit state is a {@link SharedState} instance that keeps track of
408
+ * global informations about the application such as, the number of connected
409
+ * clients, network latency estimation, etc.
410
+ *
411
+ * The audit state is created by the server on start up.
412
+ *
413
+ * @returns {Promise<SharedState>}
414
+ * @throws Will throw if called before `server.init()`
415
+ *
416
+ * @example
417
+ * const auditState = await server.getAuditState();
418
+ * auditState.onUpdate(() => console.log(auditState.getValues()), true);
419
+ */
420
+ async getAuditState() {
421
+ if (this.#status === 'idle') {
422
+ throw new Error(`[soundworks.Server] Cannot access audit state before init`);
423
+ }
424
+
425
+ return this.#auditState;
426
+ }
427
+
340
428
  /**
341
429
  * The `init` method is part of the initialization lifecycle of the `soundworks`
342
430
  * server. Most of the time, the `init` method will be implicitly called by the
343
- * {@link server.Server#start} method.
431
+ * {@link Server#start} method.
344
432
  *
345
433
  * In some situations you might want to call this method manually, in such cases
346
- * the method should be called before the {@link server.Server#start} method.
434
+ * the method should be called before the {@link Server#start} method.
347
435
  *
348
436
  * What it does:
349
437
  * - create the audit state
@@ -351,7 +439,7 @@ class Server {
351
439
  * declared in `config.app.clients`
352
440
  * - initialize all registered plugins
353
441
  *
354
- * After `await server.init()` is fulfilled, the {@link server.Server#stateManager}
442
+ * After `await server.init()` is fulfilled, the {@link Server#stateManager}
355
443
  * and all registered plugins can be safely used.
356
444
  *
357
445
  * @example
@@ -363,26 +451,23 @@ class Server {
363
451
  * await server.start(); // init is called implicitely
364
452
  */
365
453
  async init() {
366
- // ------------------------------------------------------------
367
- // INIT STATE MANAGER
368
- // ------------------------------------------------------------
369
- this.stateManager.init(SERVER_ID, new EventEmitter());
454
+ this.#stateManager[kStateManagerInit](SERVER_ID, new EventEmitter());
370
455
 
371
456
  const numClients = {};
372
- for (let name in this.config.app.clients) {
457
+ for (let name in this.#config.app.clients) {
373
458
  numClients[name] = 0;
374
459
  }
375
460
  /** @private */
376
- this._auditState = await this.stateManager.create(AUDIT_STATE_NAME, { numClients });
461
+ this.#auditState = await this.#stateManager.create(AUDIT_STATE_NAME, { numClients });
377
462
 
378
463
  // basic http authentication
379
- if (this.config.env.auth) {
464
+ if (this.#config.env.auth) {
380
465
  const ids = idGenerator();
381
466
 
382
467
  const soundworksAuth = (req, res, next) => {
383
468
  let role = null;
384
469
 
385
- for (let [_role, config] of Object.entries(this.config.app.clients)) {
470
+ for (let [_role, config] of Object.entries(this.#config.app.clients)) {
386
471
  if (req.path === config.route) {
387
472
  role = _role;
388
473
  }
@@ -394,11 +479,11 @@ class Server {
394
479
  return;
395
480
  }
396
481
 
397
- const isProtected = this.isProtected(role);
482
+ const isProtected = this[kServerIsProtectedRole](role);
398
483
 
399
484
  if (isProtected) {
400
485
  // authentication middleware
401
- const auth = this.config.env.auth;
486
+ const auth = this.#config.env.auth;
402
487
  // parse login and password from headers
403
488
  const b64auth = (req.headers.authorization || '').split(' ')[1] || '';
404
489
  const [login, password] = Buffer.from(b64auth, 'base64').toString().split(':');
@@ -413,10 +498,10 @@ class Server {
413
498
  const token = { id, ip, time };
414
499
  const encryptedToken = encryptData(token);
415
500
 
416
- this._pendingConnectionTokens.add(encryptedToken);
501
+ this.#pendingConnectionTokens.add(encryptedToken);
417
502
 
418
503
  setTimeout(() => {
419
- this._pendingConnectionTokens.delete(encryptedToken);
504
+ this.#pendingConnectionTokens.delete(encryptedToken);
420
505
  }, TOKEN_VALID_DURATION * 1000);
421
506
 
422
507
  // pass to the response object to be send to the client
@@ -438,18 +523,18 @@ class Server {
438
523
  }
439
524
  };
440
525
 
441
- this.router.use(soundworksAuth);
526
+ this.#router.use(soundworksAuth);
442
527
  }
443
528
 
444
529
  // ------------------------------------------------------------
445
530
  // create HTTP(S) SERVER
446
531
  // ------------------------------------------------------------
447
- const useHttps = this.config.env.useHttps || false;
532
+ const useHttps = this.#config.env.useHttps || false;
448
533
 
449
534
  if (!useHttps) {
450
- this.httpServer = http.createServer(this.router);
535
+ this.#httpServer = http.createServer(this.#router);
451
536
  } else {
452
- const httpsInfos = this.config.env.httpsInfos;
537
+ const httpsInfos = this.#config.env.httpsInfos;
453
538
  let useSelfSigned = false;
454
539
 
455
540
  if (!httpsInfos || equal(httpsInfos, { cert: null, key: null })) {
@@ -467,7 +552,7 @@ class Server {
467
552
  try {
468
553
  x509 = new X509Certificate(cert);
469
554
  } catch (err) {
470
- this._dispatchStatus('errored');
555
+ this.#dispatchStatus('errored');
471
556
  throw new Error(`[soundworks:Server] Invalid https cert file`);
472
557
  }
473
558
 
@@ -475,11 +560,11 @@ class Server {
475
560
  const keyObj = createPrivateKey(key);
476
561
 
477
562
  if (!x509.checkPrivateKey(keyObj)) {
478
- this._dispatchStatus('errored');
563
+ this.#dispatchStatus('errored');
479
564
  throw new Error(`[soundworks:Server] Invalid https key file`);
480
565
  }
481
566
  } catch (err) {
482
- this._dispatchStatus('errored');
567
+ this.#dispatchStatus('errored');
483
568
  throw new Error(`[soundworks:Server] Invalid https key file`);
484
569
  }
485
570
 
@@ -491,7 +576,7 @@ class Server {
491
576
  const diff = certExpire - now;
492
577
  const daysRemaining = Math.round(diff / 1000 / 60 / 60 / 24);
493
578
 
494
- this.httpsInfos = {
579
+ this.#httpsInfos = {
495
580
  selfSigned: false,
496
581
  CN: x509.subject.split('=')[1],
497
582
  altNames: x509.subjectAltName.split(',').map(e => e.trim().split(':')[1]),
@@ -501,7 +586,7 @@ class Server {
501
586
  daysRemaining: daysRemaining,
502
587
  };
503
588
 
504
- this.httpServer = https.createServer({ key, cert }, this.router);
589
+ this.#httpServer = https.createServer({ key, cert }, this.#router);
505
590
  } catch (err) {
506
591
  logger.error(`
507
592
  Invalid certificate files, please check your:
@@ -509,25 +594,25 @@ Invalid certificate files, please check your:
509
594
  - cert file: ${httpsInfos.cert}
510
595
  `);
511
596
 
512
- this._dispatchStatus('errored');
597
+ this.#dispatchStatus('errored');
513
598
  throw err;
514
599
  }
515
600
  } else {
516
601
  // generate certs
517
602
  // --------------------------------------------------------
518
- const cert = await this.db.get('httpsCert');
519
- const key = await this.db.get('httpsKey');
603
+ const cert = await this.#db.get('httpsCert');
604
+ const key = await this.#db.get('httpsKey');
520
605
 
521
606
  if (key && cert) {
522
- this.httpsInfos = { selfSigned: true };
523
- this.httpServer = https.createServer({ cert, key }, this.router);
607
+ this.#httpsInfos = { selfSigned: true };
608
+ this.#httpServer = https.createServer({ cert, key }, this.#router);
524
609
  } else {
525
- this.httpServer = await new Promise((resolve, reject) => {
610
+ this.#httpServer = await new Promise((resolve, reject) => {
526
611
  // generate certificate on the fly (for development purposes)
527
612
  pem.createCertificate({ days: 1, selfSigned: true }, async (err, keys) => {
528
613
  if (err) {
529
614
  logger.error(err.stack);
530
- this._dispatchStatus('errored');
615
+ this.#dispatchStatus('errored');
531
616
 
532
617
  reject(err);
533
618
  return;
@@ -536,13 +621,13 @@ Invalid certificate files, please check your:
536
621
  const cert = keys.certificate;
537
622
  const key = keys.serviceKey;
538
623
 
539
- this.httpsInfos = { selfSigned: true };
624
+ this.#httpsInfos = { selfSigned: true };
540
625
  // we store the generated cert so that we don't have to re-accept
541
626
  // the cert each time the server restarts in development
542
- await this.db.set('httpsCert', cert);
543
- await this.db.set('httpsKey', key);
627
+ await this.#db.set('httpsCert', cert);
628
+ await this.#db.set('httpsKey', key);
544
629
 
545
- const httpsServer = https.createServer({ cert, key }, this.router);
630
+ const httpsServer = https.createServer({ cert, key }, this.#router);
546
631
 
547
632
  resolve(httpsServer);
548
633
  });
@@ -554,18 +639,18 @@ Invalid certificate files, please check your:
554
639
  let nodeOnly = true;
555
640
  // do not throw if no browser clients are defined, very usefull for
556
641
  // cleaning tests in particular
557
- for (let role in this.config.app.clients) {
558
- if (this.config.app.clients[role].target === 'browser') {
642
+ for (let role in this.#config.app.clients) {
643
+ if (this.#config.app.clients[role].target === 'browser') {
559
644
  nodeOnly = false;
560
645
  }
561
646
  }
562
647
 
563
648
  if (!nodeOnly) {
564
- if (this._applicationTemplateOptions.templateEngine === null
565
- || this._applicationTemplateOptions.templatePath === null
566
- || this._applicationTemplateOptions.clientConfigFunction === null
649
+ if (this[kServerApplicationTemplateOptions].templateEngine === null
650
+ || this[kServerApplicationTemplateOptions].templatePath === null
651
+ || this[kServerApplicationTemplateOptions].clientConfigFunction === null
567
652
  ) {
568
- throw new Error('[soundworks:Server] A browser client has been found in "config.app.clients" but configuration for html templating is missing. You should probably call `server.setDefaultTemplateConfig()` if you use the soundworks-template and/or refer (at your own risks) to the documentation of `setCustomTemplateConfig()`');
653
+ throw new Error('[soundworks:Server] A browser client has been found in "config.app.clients" but configuration for html templating is missing. You should probably call `server.useDefaultApplicationTemplate()` if you use the soundworks-template and/or refer (at your own risks) to the documentation of `setCustomTemplateConfig()`');
569
654
  }
570
655
  }
571
656
 
@@ -577,8 +662,8 @@ Invalid certificate files, please check your:
577
662
  const routes = [];
578
663
  const clientsConfig = [];
579
664
 
580
- for (let role in this.config.app.clients) {
581
- const config = Object.assign({}, this.config.app.clients[role]);
665
+ for (let role in this.#config.app.clients) {
666
+ const config = Object.assign({}, this.#config.app.clients[role]);
582
667
  config.role = role;
583
668
  clientsConfig.push(config);
584
669
  }
@@ -587,29 +672,29 @@ Invalid certificate files, please check your:
587
672
  clientsConfig
588
673
  .sort(a => a.default === true ? 1 : -1)
589
674
  .forEach(config => {
590
- const path = this._openClientRoute(this.router, config);
675
+ const path = this.#openClientRoute(this.#router, config);
591
676
  routes.push({ role: config.role, path });
592
677
  });
593
678
 
594
- logger.clientConfigAndRouting(routes, this.config);
679
+ logger.clientConfigAndRouting(routes, this.#config);
595
680
 
596
681
  // ------------------------------------------------------------
597
682
  // START PLUGIN MANAGER
598
683
  // ------------------------------------------------------------
599
- await this.pluginManager.start();
684
+ await this.#pluginManager[kPluginManagerStart]();
600
685
 
601
- await this._dispatchStatus('inited');
686
+ await this.#dispatchStatus('inited');
602
687
 
603
688
  return Promise.resolve();
604
689
  }
605
690
 
606
691
  /**
607
692
  * The `start` method is part of the initialization lifecycle of the `soundworks`
608
- * server. The `start` method will implicitly call the {@link server.Server#init}
693
+ * server. The `start` method will implicitly call the {@link Server#init}
609
694
  * method if it has not been called manually.
610
695
  *
611
696
  * What it does:
612
- * - implicitely call {@link server.Server#init} if not done manually
697
+ * - implicitely call {@link Server#init} if not done manually
613
698
  * - launch the HTTP and WebSocket servers
614
699
  * - start all created contexts. To this end, you will have to call `server.init`
615
700
  * manually and instantiate the contexts between `server.init()` and `server.start()`
@@ -623,42 +708,38 @@ Invalid certificate files, please check your:
623
708
  * await server.start();
624
709
  */
625
710
  async start() {
626
- if (this.status === 'idle') {
711
+ if (this.#status === 'idle') {
627
712
  await this.init();
628
713
  }
629
714
 
630
- if (this.status === 'started') {
715
+ if (this.#status === 'started') {
631
716
  throw new Error(`[soundworks:Server] Cannot call "server.start()" twice`);
632
717
  }
633
718
 
634
- if (this.status !== 'inited') {
719
+ if (this.#status !== 'inited') {
635
720
  throw new Error(`[soundworks:Server] Cannot "server.start()" before "server.init()"`);
636
721
  }
637
722
 
638
723
  // ------------------------------------------------------------
639
724
  // START CONTEXT MANAGER
640
725
  // ------------------------------------------------------------
641
- await this.contextManager.start();
726
+ await this.#contextManager[kServerContextManagerStart]();
642
727
 
643
728
  // ------------------------------------------------------------
644
729
  // START SOCKET SERVER
645
730
  // ------------------------------------------------------------
646
- await this.sockets.start(
647
- this,
648
- this.config.env.websockets,
649
- (...args) => this._onSocketConnection(...args),
650
- );
731
+ await this.#sockets[kSocketsStart]();
651
732
 
652
733
  // ------------------------------------------------------------
653
734
  // START HTTP SERVER
654
735
  // ------------------------------------------------------------
655
736
  return new Promise(resolve => {
656
- const port = this.config.env.port;
657
- const useHttps = this.config.env.useHttps || false;
737
+ const port = this.#config.env.port;
738
+ const useHttps = this.#config.env.useHttps || false;
658
739
  const protocol = useHttps ? 'https' : 'http';
659
740
  const ifaces = os.networkInterfaces();
660
741
 
661
- this.httpServer.listen(port, async () => {
742
+ this.#httpServer.listen(port, async () => {
662
743
  logger.title(`${protocol} server listening on`);
663
744
 
664
745
  Object.keys(ifaces).forEach(dev => {
@@ -669,41 +750,41 @@ Invalid certificate files, please check your:
669
750
  });
670
751
  });
671
752
 
672
- if (this.httpsInfos !== null) {
753
+ if (this.#httpsInfos !== null) {
673
754
  logger.title(`https certificates infos`);
674
755
 
675
- // this.httpsInfos.selfSigned = true;
676
- if (this.httpsInfos.selfSigned) {
677
- logger.log(` self-signed: ${this.httpsInfos.selfSigned ? 'true' : 'false'}`);
756
+ // this.#httpsInfos.selfSigned = true;
757
+ if (this.#httpsInfos.selfSigned) {
758
+ logger.log(` self-signed: ${this.#httpsInfos.selfSigned ? 'true' : 'false'}`);
678
759
  logger.log(chalk.yellow` > INVALID CERTIFICATE (self-signed)`);
679
760
 
680
761
  } else {
681
- logger.log(` valid from: ${this.httpsInfos.validFrom}`);
682
- logger.log(` valid to: ${this.httpsInfos.validTo}`);
762
+ logger.log(` valid from: ${this.#httpsInfos.validFrom}`);
763
+ logger.log(` valid to: ${this.#httpsInfos.validTo}`);
683
764
 
684
- // this.httpsInfos.isValid = false; // for testing
685
- if (!this.httpsInfos.isValid) {
765
+ // this.#httpsInfos.isValid = false; // for testing
766
+ if (!this.#httpsInfos.isValid) {
686
767
  logger.error(chalk.red` -------------------------------------------`);
687
768
  logger.error(chalk.red` > INVALID CERTIFICATE `);
688
769
  logger.error(chalk.red` i.e. you pretend to be safe but you are not`);
689
770
  logger.error(chalk.red` -------------------------------------------`);
690
771
  } else {
691
- // this.httpsInfos.daysRemaining = 2; // for testing
692
- if (this.httpsInfos.daysRemaining < 5) {
693
- logger.log(chalk.red` > CERTIFICATE IS VALID... BUT ONLY ${this.httpsInfos.daysRemaining} DAYS LEFT, PLEASE CONSIDER UPDATING YOUR CERTS!`);
694
- } else if (this.httpsInfos.daysRemaining < 15) {
695
- logger.log(chalk.yellow` > CERTIFICATE IS VALID - only ${this.httpsInfos.daysRemaining} days left, be careful...`);
772
+ // this.#httpsInfos.daysRemaining = 2; // for testing
773
+ if (this.#httpsInfos.daysRemaining < 5) {
774
+ logger.log(chalk.red` > CERTIFICATE IS VALID... BUT ONLY ${this.#httpsInfos.daysRemaining} DAYS LEFT, PLEASE CONSIDER UPDATING YOUR CERTS!`);
775
+ } else if (this.#httpsInfos.daysRemaining < 15) {
776
+ logger.log(chalk.yellow` > CERTIFICATE IS VALID - only ${this.#httpsInfos.daysRemaining} days left, be careful...`);
696
777
  } else {
697
- logger.log(chalk.green` > CERTIFICATE IS VALID (${this.httpsInfos.daysRemaining} days left)`);
778
+ logger.log(chalk.green` > CERTIFICATE IS VALID (${this.#httpsInfos.daysRemaining} days left)`);
698
779
  }
699
780
  }
700
781
 
701
782
  }
702
783
  }
703
784
 
704
- await this._dispatchStatus('started');
785
+ await this.#dispatchStatus('started');
705
786
 
706
- if (this.config.env.type === 'development') {
787
+ if (this.#config.env.type === 'development') {
707
788
  logger.log(`\n> press "${chalk.bold('Ctrl + C')}" to exit`);
708
789
  }
709
790
 
@@ -730,28 +811,29 @@ Invalid certificate files, please check your:
730
811
  * await server.stop();
731
812
  */
732
813
  async stop() {
733
- if (this.status !== 'started') {
814
+ if (this.#status !== 'started') {
734
815
  throw new Error(`[soundworks:Server] Cannot stop() before start()`);
735
816
  }
736
817
 
737
- await this.contextManager.stop();
738
- await this.pluginManager.stop();
818
+ await this.#contextManager[kServerContextManagerStop]();
819
+ await this.#pluginManager[kPluginManagerStop]();
739
820
 
740
- this.sockets.terminate();
741
- this.httpServer.close(err => {
821
+ this.#sockets[kSocketsStop]();
822
+
823
+ this.#httpServer.close(err => {
742
824
  if (err) {
743
825
  throw new Error(err.message);
744
826
  }
745
827
  });
746
828
 
747
- await this._dispatchStatus('stopped');
829
+ await this.#dispatchStatus('stopped');
748
830
  }
749
831
 
750
832
  /**
751
833
  * Open the route for a given client.
752
834
  * @private
753
835
  */
754
- _openClientRoute(router, config) {
836
+ #openClientRoute(router, config) {
755
837
  const { role, target } = config;
756
838
  const isDefault = (config.default === true);
757
839
  // only browser targets need a route
@@ -765,14 +847,14 @@ Invalid certificate files, please check your:
765
847
  route += `${role}`;
766
848
  }
767
849
 
768
- this.config.app.clients[role].route = route;
850
+ this.#config.app.clients[role].route = route;
769
851
 
770
852
  // define template filename: `${role}.html` or `default.html`
771
853
  const {
772
854
  templatePath,
773
855
  templateEngine,
774
856
  clientConfigFunction,
775
- } = this._applicationTemplateOptions;
857
+ } = this[kServerApplicationTemplateOptions];
776
858
 
777
859
  const clientTmpl = path.join(templatePath, `${role}.tmpl`);
778
860
  const defaultTmpl = path.join(templatePath, `default.tmpl`);
@@ -798,7 +880,7 @@ Invalid certificate files, please check your:
798
880
  const tmpl = templateEngine.compile(tmplString);
799
881
 
800
882
  const soundworksClientHandler = (req, res) => {
801
- const data = clientConfigFunction(role, this.config, req);
883
+ const data = clientConfigFunction(role, this.#config, req);
802
884
 
803
885
  // if the client has gone through the connection middleware (add succedeed),
804
886
  // add the token to the data object
@@ -809,7 +891,7 @@ Invalid certificate files, please check your:
809
891
  // CORS / COOP / COEP headers for `crossOriginIsolated pages,
810
892
  // enables `sharedArrayBuffers` and high precision timers
811
893
  // cf. https://web.dev/why-coop-coep/
812
- if (this.config.env.crossOriginIsolated) {
894
+ if (this.#config.env.crossOriginIsolated) {
813
895
  res.writeHead(200, {
814
896
  'Cross-Origin-Resource-Policy': 'same-origin',
815
897
  'Cross-Origin-Embedder-Policy': 'require-corp',
@@ -829,62 +911,62 @@ Invalid certificate files, please check your:
829
911
  }
830
912
 
831
913
  onClientConnect(callback) {
832
- this._onClientConnectCallbacks.add(callback);
914
+ this.#onClientConnectCallbacks.add(callback);
833
915
 
834
- return () => this._onClientConnectCallbacks.delete(callback);
916
+ return () => this.#onClientConnectCallbacks.delete(callback);
835
917
  }
836
918
 
837
919
  onClientDisconnect(callback) {
838
- this._onClientDisconnectCallbacks.add(callback);
920
+ this.#onClientDisconnectCallbacks.add(callback);
839
921
 
840
- return () => this._onClientDisconnectCallbacks.delete(callback);
922
+ return () => this.#onClientDisconnectCallbacks.delete(callback);
841
923
  }
842
924
 
843
925
  /**
844
926
  * Socket connection callback.
845
927
  * @private
846
928
  */
847
- _onSocketConnection(role, socket, connectionToken) {
848
- const client = new Client(role, socket);
929
+ [kServerOnSocketConnection](role, socket, connectionToken) {
930
+ const client = new ServerClient(role, socket);
849
931
  socket[kSocketClientId] = client.id;
850
- const roles = Object.keys(this.config.app.clients);
932
+ const roles = Object.keys(this.#config.app.clients);
851
933
 
852
934
  // this has been validated
853
- if (this.isProtected(role) && this.isValidConnectionToken(connectionToken)) {
935
+ if (this[kServerIsProtectedRole](role) && this[kServerIsValidConnectionToken](connectionToken)) {
854
936
  const { ip } = decryptData(connectionToken);
855
937
  const newData = { ip, id: client.id };
856
938
  const newToken = encryptData(newData);
857
939
 
858
- client.token = newToken;
940
+ client[kServerClientToken] = newToken;
859
941
 
860
- this._pendingConnectionTokens.delete(connectionToken);
861
- this._trustedClients.add(client);
942
+ this.#pendingConnectionTokens.delete(connectionToken);
943
+ this.#trustedClients.add(client);
862
944
  }
863
945
 
864
946
  socket.addListener('close', async () => {
865
947
  // do nothing if client role is invalid
866
948
  if (roles.includes(role)) {
867
949
  // decrement audit state counter
868
- const numClients = this._auditState.get('numClients');
950
+ const numClients = this.#auditState.get('numClients');
869
951
  numClients[role] -= 1;
870
- this._auditState.set({ numClients });
952
+ this.#auditState.set({ numClients });
871
953
 
872
954
  // delete token
873
- if (this._trustedClients.has(client)) {
874
- this._trustedClients.delete(client);
955
+ if (this.#trustedClients.has(client)) {
956
+ this.#trustedClients.delete(client);
875
957
  }
876
958
 
877
959
  // if something goes wrong here, the 'close' event is called again and
878
960
  // again and again... let's just log the error and terminate the socket
879
961
  try {
880
962
  // clean context manager, await before cleaning state manager
881
- await this.contextManager.removeClient(client);
963
+ await this.#contextManager[kServerContextManagerRemoveClient](client);
882
964
  // remove client from pluginManager
883
- await this.pluginManager.removeClient(client);
965
+ await this.#pluginManager[kServerPluginManagerRemoveClient](client);
884
966
  // clean state manager
885
- await this.stateManager.removeClient(client.id);
967
+ await this.#stateManager[kServerStateManagerRemoveClient](client.id);
886
968
 
887
- this._onClientDisconnectCallbacks.forEach(callback => callback(client));
969
+ this.#onClientDisconnectCallbacks.forEach(callback => callback(client));
888
970
  } catch (err) {
889
971
  console.error(err);
890
972
  }
@@ -907,12 +989,12 @@ Invalid certificate files, please check your:
907
989
  return;
908
990
  }
909
991
 
910
- if (version !== this.version) {
911
- logger.warnVersionDiscepancies(role, version, this.version);
992
+ if (version !== this.#version) {
993
+ logger.warnVersionDiscepancies(role, version, this.#version);
912
994
  }
913
995
 
914
996
  try {
915
- this.pluginManager.checkRegisteredPlugins(registeredPlugins);
997
+ this.#pluginManager[kServerPluginManagerCheckRegisteredPlugins](registeredPlugins);
916
998
  } catch (err) {
917
999
  socket.send(CLIENT_HANDSHAKE_ERROR, {
918
1000
  type: 'invalid-plugin-list',
@@ -922,26 +1004,27 @@ Invalid certificate files, please check your:
922
1004
  }
923
1005
 
924
1006
  // increment audit state
925
- const numClients = this._auditState.get('numClients');
1007
+ const numClients = this.#auditState.get('numClients');
926
1008
  numClients[role] += 1;
927
- this._auditState.set({ numClients });
1009
+ this.#auditState.set({ numClients });
928
1010
 
929
- // add client to state manager
930
- await this.stateManager.addClient(client.id, {
1011
+ const transport = {
931
1012
  emit: client.socket.send.bind(client.socket),
932
1013
  addListener: client.socket.addListener.bind(client.socket),
933
1014
  removeAllListeners: client.socket.removeAllListeners.bind(client.socket),
934
- });
1015
+ };
1016
+ // add client to state manager
1017
+ await this.#stateManager[kServerStateManagerAddClient](client.id, transport);
935
1018
  // add client to plugin manager
936
1019
  // server-side, all plugins are active for the lifetime of the client
937
- await this.pluginManager.addClient(client, registeredPlugins);
1020
+ await this.#pluginManager[kServerPluginManagerAddClient](client, registeredPlugins);
938
1021
  // add client to context manager
939
- await this.contextManager.addClient(client);
1022
+ await this.#contextManager[kServerContextManagerAddClient](client);
940
1023
 
941
- this._onClientConnectCallbacks.forEach(callback => callback(client));
1024
+ this.#onClientConnectCallbacks.forEach(callback => callback(client));
942
1025
 
943
1026
  const { id, uuid, token } = client;
944
- socket.send(CLIENT_HANDSHAKE_RESPONSE, { id, uuid, token, version: this.version });
1027
+ socket.send(CLIENT_HANDSHAKE_RESPONSE, { id, uuid, token, version: this.#version });
945
1028
  });
946
1029
  }
947
1030
 
@@ -974,31 +1057,6 @@ Invalid certificate files, please check your:
974
1057
  return db;
975
1058
  }
976
1059
 
977
- onStatusChange(callback) {
978
- this._onStatusChangeCallbacks.add(callback);
979
-
980
- return () => this._onStatusChangeCallbacks.delete(callback);
981
- }
982
-
983
- /** @private */
984
- async _dispatchStatus(status) {
985
- this.status = status;
986
-
987
- // if launched in a child process, forward status to parent process
988
- if (process.send !== undefined) {
989
- process.send(`soundworks:server:${status}`);
990
- }
991
-
992
- // execute all callbacks in parallel
993
- const promises = [];
994
-
995
- for (let callback of this._onStatusChangeCallbacks) {
996
- promises.push(callback(status));
997
- }
998
-
999
- await Promise.all(promises);
1000
- }
1001
-
1002
1060
  /**
1003
1061
  * Configure the server to work _out-of-the-box_ within the soundworks application
1004
1062
  * template provided by `@soundworks/create.
@@ -1021,10 +1079,10 @@ Invalid certificate files, please check your:
1021
1079
  const buildDirectory = path.join('.build', 'public');
1022
1080
 
1023
1081
  const useMinifiedFile = {};
1024
- const roles = Object.keys(this.config.app.clients);
1082
+ const roles = Object.keys(this.#config.app.clients);
1025
1083
 
1026
1084
  roles.forEach(role => {
1027
- if (this.config.env.type === 'production') {
1085
+ if (this.#config.env.type === 'production') {
1028
1086
  // check if minified file exists
1029
1087
  const minifiedFilePath = path.join(buildDirectory, `${role}.min.js`);
1030
1088
 
@@ -1039,7 +1097,7 @@ Invalid certificate files, please check your:
1039
1097
  }
1040
1098
  });
1041
1099
 
1042
- this._applicationTemplateOptions = {
1100
+ this[kServerApplicationTemplateOptions] = {
1043
1101
  templateEngine: { compile },
1044
1102
  templatePath: path.join('.build', 'server', 'tmpl'),
1045
1103
  clientConfigFunction: (role, config, _httpRequest) => {
@@ -1065,8 +1123,8 @@ Invalid certificate files, please check your:
1065
1123
  },
1066
1124
  };
1067
1125
 
1068
- this.router.use(express.static('public'));
1069
- this.router.use('/build', express.static(buildDirectory));
1126
+ this.#router.use(express.static('public'));
1127
+ this.#router.use('/build', express.static(buildDirectory));
1070
1128
  }
1071
1129
 
1072
1130
  /**
@@ -1076,46 +1134,25 @@ Invalid certificate files, please check your:
1076
1134
  * first to explain your use-case :)
1077
1135
  */
1078
1136
  setCustomApplicationTemplateOptions(options) {
1079
- Object.assign(this._applicationTemplateOptions, options);
1137
+ Object.assign(this[kServerApplicationTemplateOptions], options);
1080
1138
  }
1081
1139
 
1082
- /**
1083
- * Attach and retrieve the global audit state of the application.
1084
- *
1085
- * The audit state is a {@link server.SharedState} instance that keeps track of
1086
- * global informations about the application such as, the number of connected
1087
- * clients, network latency estimation, etc.
1088
- *
1089
- * The audit state is created by the server on start up.
1090
- *
1091
- * @returns {Promise<server.SharedState>}
1092
- * @throws Will throw if called before `server.init()`
1093
- * @see {@link server.SharedState}
1094
- * @example
1095
- * const auditState = await server.getAuditState();
1096
- * auditState.onUpdate(() => console.log(auditState.getValues()), true);
1097
- */
1098
- async getAuditState() {
1099
- if (this.status === 'idle') {
1100
- throw new Error(`[soundworks.Server] Cannot access audit state before init`);
1101
- }
1102
1140
 
1103
- return this._auditState;
1104
- }
1105
1141
 
1142
+ //
1106
1143
  /** @private */
1107
- isProtected(role) {
1108
- if (this.config.env.auth && Array.isArray(this.config.env.auth.clients)) {
1109
- return this.config.env.auth.clients.includes(role);
1144
+ [kServerIsProtectedRole](role) {
1145
+ if (this.#config.env.auth && Array.isArray(this.#config.env.auth.clients)) {
1146
+ return this.#config.env.auth.clients.includes(role);
1110
1147
  }
1111
1148
 
1112
1149
  return false;
1113
1150
  }
1114
1151
 
1115
1152
  /** @private */
1116
- isValidConnectionToken(token) {
1153
+ [kServerIsValidConnectionToken](token) {
1117
1154
  // token should be in pending token list
1118
- if (!this._pendingConnectionTokens.has(token)) {
1155
+ if (!this.#pendingConnectionTokens.has(token)) {
1119
1156
  return false;
1120
1157
  }
1121
1158
 
@@ -1126,7 +1163,7 @@ Invalid certificate files, please check your:
1126
1163
  // token is valid only for 30 seconds (this is arbitrary)
1127
1164
  if (now > data.time + TOKEN_VALID_DURATION) {
1128
1165
  // delete the token, is too old
1129
- this._pendingConnectionTokens.delete(token);
1166
+ this.#pendingConnectionTokens.delete(token);
1130
1167
  return false;
1131
1168
  } else {
1132
1169
  return true;
@@ -1137,14 +1174,14 @@ Invalid certificate files, please check your:
1137
1174
  * Check if the given client is trusted, i.e. config.env.type == 'production'
1138
1175
  * and the client is protected behind a password.
1139
1176
  *
1140
- * @param {server.Client} client - Client to be tested
1177
+ * @param {ServerClient} client - Client to be tested
1141
1178
  * @returns {Boolean}
1142
1179
  */
1143
1180
  isTrustedClient(client) {
1144
- if (this.config.env.type !== 'production') {
1181
+ if (this.#config.env.type !== 'production') {
1145
1182
  return true;
1146
1183
  } else {
1147
- return this._trustedClients.has(client);
1184
+ return this.#trustedClients.has(client);
1148
1185
  }
1149
1186
  }
1150
1187
 
@@ -1159,13 +1196,13 @@ Invalid certificate files, please check your:
1159
1196
  */
1160
1197
  // for stateless interactions, e.g. POST files
1161
1198
  isTrustedToken(clientId, clientIp, token) {
1162
- if (this.config.env.type !== 'production') {
1199
+ if (this.#config.env.type !== 'production') {
1163
1200
  return true;
1164
1201
  } else {
1165
- for (let client of this._trustedClients) {
1166
- if (client.id === clientId && client.token === token) {
1202
+ for (let client of this.#trustedClients) {
1203
+ if (client.id === clientId && client[kServerClientToken] === token) {
1167
1204
  // check that given token is consistent with client ip and id
1168
- const { id, ip } = decryptData(client.token);
1205
+ const { id, ip } = decryptData(client[kServerClientToken]);
1169
1206
 
1170
1207
  if (clientId === id && clientIp === ip) {
1171
1208
  return true;