@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
@@ -0,0 +1,74 @@
1
+ import { idGenerator } from '@ircam/sc-utils';
2
+ import { v4 as uuid } from 'uuid';
3
+
4
+ const generateId = idGenerator();
5
+
6
+ export const kServerClientToken = Symbol('soundworks:server-client-token');
7
+
8
+ /**
9
+ * Server-side representation of a client.
10
+ *
11
+ * @hideconstructor
12
+ * @see {@link Client}
13
+ */
14
+ class ServerClient {
15
+ #role = null;
16
+ #id = null;
17
+ #uuid = null;
18
+ #socket = null;
19
+
20
+ /**
21
+ * @param {String} role - Role of the client
22
+ * @param {ServerSocket} socket - Socket connection with the client
23
+ */
24
+ constructor(role, socket) {
25
+ this.#role = role;
26
+ this.#id = generateId.next().value;
27
+ this.#uuid = uuid();
28
+ this.#socket = socket;
29
+
30
+ /**
31
+ * Set in `server[kServerOnSocketConnection]`
32
+ * @private
33
+ */
34
+ this[kServerClientToken] = null;
35
+ }
36
+
37
+ /**
38
+ * Client role, as specified in client side config {@link Client}.
39
+ *
40
+ * @type {String}
41
+ */
42
+ get role() {
43
+ return this.#role;
44
+ }
45
+
46
+ /**
47
+ * Session Id (incremented positive number).
48
+ *
49
+ * @type {Number}
50
+ */
51
+ get id() {
52
+ return this.#id;
53
+ }
54
+
55
+ /**
56
+ * Unique session Id (uuidv4).
57
+ *
58
+ * @type {String}
59
+ */
60
+ get uuid() {
61
+ return this.#uuid;
62
+ }
63
+
64
+ /**
65
+ * Socket connection with the remote {@link Client}.
66
+ *
67
+ * @type {ServerSocket}
68
+ */
69
+ get socket() {
70
+ return this.#socket;
71
+ }
72
+ }
73
+
74
+ export default ServerClient;
@@ -1,31 +1,38 @@
1
- import Client from './Client.js';
1
+ import ServerClient from './ServerClient.js';
2
2
  import Server from './Server.js';
3
3
 
4
+ import {
5
+ kServerContextManagerRegister,
6
+ } from './ServerContextManager.js';
7
+
8
+ export const kServerContextStatus = Symbol('soundworks:server-context-status');
9
+
4
10
  /**
5
- * Base class to extend in order to implment the optionnal server-side counterpart
6
- * of a {@link client.Context}. If not defined, a default context will be created
11
+ * Base class to extend in order to implement the optional server-side counterpart
12
+ * of a {@link ClientContext}. If not defined, a default context will be created
7
13
  * and used by the server.
8
14
  *
9
15
  * In the `soundworks` paradigm, a client has a "role" (e.g. _player_, _controller_)
10
- * see {@link client.Client#role}) and can be in different "contexts" (e.g. different
16
+ * see {@link Client#role}) and can be in different "contexts" (e.g. different
11
17
  * part of the experience such as sections of a music piece, etc.). The
12
- * {@link client.Context} and optionnal {@link serverContext} abstractions provide
13
- * a simple and unified way to model these reccuring aspects of an application.
18
+ * {@link ClientContext} and optional {@link ServerContext} abstractions provide
19
+ * a simple and unified way to model these recurring aspects of an application.
14
20
  *
15
- * If a `server.Context` is recognized as the server-side counterpart of a
16
- * {@link client.Context}, based on their respective `name` (see {@link client.Context#name}
17
- * and {@link server.Context#name}), `soundworks` will ensure the logic defined
18
- * by the server-side Context will be executed at the beginning of the
19
- * {@link client.Client#enter} and {@link client.Client#exit} methods. The example
20
- * show how soundwords handles (and guarantees) the order of the `enter()` steps
21
- * between the client-side and the server-side parts of the context. The same goes
22
- * for the `exit()` method.
21
+ * If a `ServerContext` is recognized as the server-side counterpart of a
22
+ * {@link ClientContext}, based on their respective `name` (see {@link ClientContext#name}
23
+ * and {@link ServerContext#name}), `soundworks` will ensure the logic defined
24
+ * by the ServerContext will be executed at the beginning of the
25
+ * {@link ClientContext#enter} and {@link ClientContext#exit} methods.
23
26
  *
24
- * ```
27
+ * The example above shows how the order of the `enter()` steps between the client-side
28
+ * and the server-side parts of a `context` is handled and guaranteed. The same goes for
29
+ * the `exit()` method.
30
+ *
31
+ * ```js
25
32
  * // client-side
26
- * import { Context } from '@soundworks/core/client.js';
33
+ * import { ClientContext } from '@soundworks/core/client.js';
27
34
  *
28
- * class MyContext extends Context {
35
+ * class MyContext extends ClientContext {
29
36
  * async enter() {
30
37
  * // 1. client side context enter() starts
31
38
  * // server-side logic is triggered first
@@ -37,20 +44,20 @@ import Server from './Server.js';
37
44
  * }
38
45
  * }
39
46
  *
40
- * // Instanciate the context (assuming the `client.role` is 'test')
47
+ * // Instantiate the context (assuming the `client.role` is 'test')
41
48
  * const myContext = new MyContext(client);
42
49
  *
43
- * // At some point in the application, the client enters the context trigerring
50
+ * // At some point in the application, the client enters the context triggering
44
51
  * // the steps 1 to 5 described in the client-side and server-side `enter()`
45
52
  * // implementations. Note that the server-side `enter()` is never called manually.
46
53
  * await myContext.enter();
47
54
  * ```
48
55
  *
49
- * ```
56
+ * ```js
50
57
  * // server-side
51
- * import { Context } from '@soundworks/core/server.js';
58
+ * import { ServerContext } from '@soundworks/core/server.js';
52
59
  *
53
- * class MyContext extends Context {
60
+ * class MyContext extends ServerContext {
54
61
  * async enter(client) {
55
62
  * // 2. server-side context enter() starts
56
63
  * await super.enter(client);
@@ -61,16 +68,18 @@ import Server from './Server.js';
61
68
  * }
62
69
  *
63
70
  * // Instantiate the context
64
- * const myContext = new Context(server);
71
+ * const myContext = new MyContext(server);
65
72
  * ```
66
- *
67
- * @memberof server
68
73
  */
69
- class Context {
74
+ class ServerContext {
75
+ #server = null;
76
+ #clients = new Set();
77
+ #roles = null;
78
+
70
79
  /**
71
- * @param {client.Client} client - The soundworks client instance.
72
- * @param {string|string[]} [roles=[]] - Optionnal list of client roles that can
73
- * use this context. In large applications, this may be usefull to guarantee
80
+ * @param {Server} server - The soundworks server instance.
81
+ * @param {string|string[]} [roles=[]] - Optional list of client roles that can
82
+ * use this context. In large applications, this may be useful to guarantee
74
83
  * that a context can be consumed only by specific client roles, throwing an
75
84
  * error if any other client role tries to use it. If empty, no access policy
76
85
  * will be used.
@@ -78,55 +87,65 @@ class Context {
78
87
  */
79
88
  constructor(server, roles = []) {
80
89
  if (!(server instanceof Server)) {
81
- throw new Error(`[soundworks:Context] Invalid argument, context "${this.constructor.name}" should receive a "soundworks.Server" instance as first argument`);
90
+ throw new TypeError(`Cannot construct '${this.constructor.name}': argument 1 must be an instance of Server`);
82
91
  }
83
92
 
84
- /**
85
- * soundworks server
86
- * @type {server.Server}
87
- * @readonly
88
- */
89
- this.server = server;
90
-
91
- /**
92
- * List of clients that are currently in this context
93
- * @type {server.Client[]}
94
- */
95
- this.clients = new Set();
96
-
97
- /**
98
- * Status of the context ('idle', 'inited', 'started' or 'errored')
99
- * @type {string}
100
- * @readonly
101
- */
102
- this.status = 'idle';
103
-
104
- /**
105
- * List of client roles that can use this context.
106
- * @type {string[]}
107
- * @readonly
108
- */
109
93
  roles = Array.isArray(roles) ? roles : [roles];
110
94
 
111
- if (roles.length === 0) {
112
- roles = Object.keys(server.config.app.clients);
113
- }
114
-
115
- this.roles = new Set(roles);
95
+ this.#server = server;
96
+ this.#roles = new Set(roles);
116
97
 
98
+ /** @private */
99
+ this[kServerContextStatus] = 'idle';
117
100
  // register in context manager
118
- this.server.contextManager.register(this);
101
+ this.#server.contextManager[kServerContextManagerRegister](this);
102
+ }
103
+
104
+ /**
105
+ * The soundworks server instance.
106
+ *
107
+ * @type {Server}
108
+ */
109
+ get server() {
110
+ return this.#server;
119
111
  }
120
112
 
121
113
  /**
122
- * Optionnal user-defined name of the context (defaults to the class name).
114
+ * List of clients that are currently in this context.
115
+ *
116
+ * @type {Set<ServerClient>}
117
+ */
118
+ get clients() {
119
+ return this.#clients;
120
+ }
121
+
122
+ /**
123
+ * List of client roles that can use this context. No access policy if empty.
124
+ *
125
+ * @type {Set<string>}
126
+ */
127
+ get roles() {
128
+ return this.#roles;
129
+ }
130
+
131
+ /**
132
+ * Status of the context ('idle', 'inited', 'started' or 'errored')
133
+ *
134
+ * @type {string}
135
+ */
136
+ get status() {
137
+ return this[kServerContextStatus];
138
+ }
139
+
140
+ /**
141
+ * Optional user-defined name of the context (defaults to the class name).
123
142
  *
124
143
  * The context manager will match the client-side and server-side contexts based
125
- * on this name. If the {@link server.ContextManager} don't find a corresponding
144
+ * on this name. If the {@link ServerContextManager} don't find a corresponding
126
145
  * user-defined context with the same name, it will use a default (noop) context.
127
146
  *
128
- * @readonly
129
147
  * @type {string}
148
+ *
130
149
  * @example
131
150
  * // server-side and client-side contexts are matched based on their respective `name`
132
151
  * class MyContext extends Context {
@@ -140,11 +159,11 @@ class Context {
140
159
  }
141
160
 
142
161
  /**
143
- * Start the context. This method is lazilly called when a client enters the
144
- * context for the first time (cf. ${server.Context#enter}). If you know some
162
+ * Start the context. This method is lazily called when a client enters the
163
+ * context for the first time (cf. ${ServerContext#enter}). If you know some
145
164
  * some heavy and/or potentially long job has to be done when starting the context
146
165
  * (e.g. connect to a database, parsing a long file) it may be a good practice
147
- * to call it explicitely.
166
+ * to call it explicitly.
148
167
  *
149
168
  * This method should be implemented to perform operations that are valid for the
150
169
  * whole lifetime of the context, regardless a client enters or exits the context.
@@ -181,11 +200,11 @@ class Context {
181
200
  * Enter the context. Implement this method to define the logic that should be
182
201
  * done (e.g. creating a shared state, etc.) when a client enters the context.
183
202
  *
184
- * If the context has not been started yet, the `start` method is implicitely executed.
203
+ * If the context has not been started yet, the `start` method is implicitly executed.
185
204
  *
186
205
  * _WARNING: this method should never be called manually._
187
206
  *
188
- * @param {server.Client} client - Server-side representation of the client
207
+ * @param {ServerClient} client - Server-side representation of the client
189
208
  * that enters the context.
190
209
  * @returns {Promise} - Promise that resolves when the context is entered.
191
210
  * @example
@@ -202,11 +221,11 @@ class Context {
202
221
  * }
203
222
  */
204
223
  async enter(client) {
205
- if (!(client instanceof Client)) {
206
- throw new Error(`[soundworks.Context] Invalid argument, ${this.name} context ".enter()" method should receive a server-side "soundworks.Client" instance argument`);
224
+ if (!(client instanceof ServerClient)) {
225
+ throw new TypeError(`Cannot execute 'enter' on ${this.constructor.name}: argument 1 must be an instance of ServerClient`);
207
226
  }
208
227
 
209
- this.clients.add(client);
228
+ this.#clients.add(client);
210
229
  }
211
230
 
212
231
  /**
@@ -215,7 +234,7 @@ class Context {
215
234
  *
216
235
  * * _WARNING: this method should never be called manually._
217
236
  *
218
- * @param {server.Client} client - Server-side representation of the client
237
+ * @param {ServerClient} client - Server-side representation of the client
219
238
  * that exits the context.
220
239
  * @returns {Promise} - Promise that resolves when the context is exited.
221
240
  * @example
@@ -232,12 +251,12 @@ class Context {
232
251
  * }
233
252
  */
234
253
  async exit(client) {
235
- if (!(client instanceof Client)) {
236
- throw new Error(`[soundworks.Context] Invalid argument, ${this.name}.exit() should receive a server-side "soundworks.Client" instance argument`);
254
+ if (!(client instanceof ServerClient)) {
255
+ throw new TypeError(`Cannot execute 'exit' on ${this.constructor.name}: argument 1 must be an instance of ServerClient`);
237
256
  }
238
257
 
239
- this.clients.delete(client);
258
+ this.#clients.delete(client);
240
259
  }
241
260
  }
242
261
 
243
- export default Context;
262
+ export default ServerContext;
@@ -1,4 +1,7 @@
1
- import Context from './Context.js';
1
+ import Server from './Server.js';
2
+ import ServerContext, {
3
+ kServerContextStatus,
4
+ } from './ServerContext.js';
2
5
  import {
3
6
  CONTEXT_ENTER_REQUEST,
4
7
  CONTEXT_ENTER_RESPONSE,
@@ -8,15 +11,22 @@ import {
8
11
  CONTEXT_EXIT_ERROR,
9
12
  } from '../common/constants.js';
10
13
 
14
+ export const kServerContextManagerStart = Symbol('soundworks:server-context-manager-start');
15
+ export const kServerContextManagerStop = Symbol('soundworks:server-context-manager-stop');
16
+ export const kServerContextManagerAddClient = Symbol('soundworks:server-context-manager-add-client');
17
+ export const kServerContextManagerRemoveClient = Symbol('soundworks:server-context-manager-remove-client');
18
+ export const kServerContextManagerRegister = Symbol('soundworks:server-context-manager-register');
19
+ // for testing purposes
20
+ export const kServerContextManagerContexts = Symbol('soundworks:server-context-manager-contexts');
21
+
11
22
  /**
12
23
  * Create a dummy server side context if a proper server-side context has not
13
- * been declared and registered, one DefaultContext is created per unknown
24
+ * been declared and registered. Ane DefaultContext is created per unknown
14
25
  * contextName and associated to all known client types.
15
- *
16
26
  * @private
17
27
  */
18
28
  function createNamedContextClass(contextName) {
19
- return class DefaultContext extends Context {
29
+ return class DefaultContext extends ServerContext {
20
30
  get name() {
21
31
  return contextName;
22
32
  }
@@ -25,122 +35,118 @@ function createNamedContextClass(contextName) {
25
35
 
26
36
  /** @private */
27
37
  class ContextCollection {
28
- constructor() {
29
- this.inner = [];
38
+ #inner = [];
39
+
40
+ // for testing purposes
41
+ get length() {
42
+ return this.#inner.length;
30
43
  }
31
44
 
32
45
  add(context) {
33
- this.inner.push(context);
46
+ this.#inner.push(context);
34
47
  }
35
48
 
36
49
  has(name) {
37
- return this.inner.find(c => c.name === name) !== undefined;
50
+ return this.#inner.find(c => c.name === name) !== undefined;
38
51
  }
39
52
 
40
53
  get(name) {
41
- return this.inner.find(c => c.name === name);
54
+ return this.#inner.find(c => c.name === name);
42
55
  }
43
56
 
44
57
  map(func) {
45
- return this.inner.map(func);
58
+ return this.#inner.map(func);
46
59
  }
47
60
 
48
61
  filter(func) {
49
- return this.inner.filter(func);
62
+ return this.#inner.filter(func);
50
63
  }
51
64
  }
52
65
 
53
66
  /**
54
- * Manage the different server-side contexts and their lifecycle. The `ContextManager`
55
- * is automatically instantiated by the {@link server.Server}.
67
+ * Manage the different server-side contexts and their lifecycle.
68
+ *
69
+ * The `ServerContextManager` is automatically instantiated by the {@link Server}.
56
70
  *
57
71
  * _WARNING: Most of the time, you should not have to manipulate the context manager directly._
58
72
  *
59
- * @memberof server
60
- * @hideconstructor
73
+ * @template T
61
74
  */
62
- class ContextManager {
75
+ class ServerContextManager {
76
+ #server = null;
77
+
78
+ #contextStartPromises = new Map();
79
+
63
80
  /**
64
- * @param {server.Server} server - Instance of the soundworks server.
81
+ * @param {Server} server - Instance of the soundworks server.
82
+ * @hideconstructor
65
83
  */
66
84
  constructor(server) {
85
+ if (!(server instanceof Server)) {
86
+ throw new TypeError(`Cannot construct 'ServerContextManager': argument 1 must be an instance of Server`);
87
+ }
88
+
89
+ this.#server = server;
67
90
  /** @private */
68
- this.server = server;
69
- /** @private */
70
- this._contexts = new ContextCollection();
71
- /** @private */
72
- this._contextStartPromises = new Map();
91
+ this[kServerContextManagerContexts] = new ContextCollection();
73
92
  }
74
93
 
75
94
  /**
76
95
  * Register a context in the manager.
77
- * This method is called in the {@link server.Context} constructor
96
+ * This method is called in the {@link ServerContext} constructor
78
97
  *
79
- * @param {server.Context} context - Context instance to register.
98
+ * @param {ServerContext} context - Context instance to register.
80
99
  *
81
100
  * @private
82
101
  */
83
- register(context) {
84
- // we must await the contructor initialization end to check the name and throw
85
- if (this._contexts.has(context.name)) {
86
- throw new Error(`[soundworks:ContextManager] Context "${context.name}" already registered`);
102
+ [kServerContextManagerRegister](context) {
103
+ // we must await the constructor initialization end to check the name and throw
104
+ if (this[kServerContextManagerContexts].has(context.name)) {
105
+ throw new DOMException(`Cannot register '${context.name}': a Context with same name has already been registered`, 'NotSupportedError');
87
106
  }
88
107
 
89
- this._contexts.add(context);
108
+ this[kServerContextManagerContexts].add(context);
90
109
  }
91
110
 
92
111
  /**
93
- * Retrieve a started context from its name.
94
- *
95
- * _WARNING: Most of the time, you should not have to call this method manually._
112
+ * Start all contexts registered before `await server.start()`.
113
+ * Called on {@link Server#start}
96
114
  *
97
- * @param {server.Context#name} contextName - Name of the context.
115
+ * @private
98
116
  */
99
- async get(contextName) {
100
- if (!this._contexts.has(contextName)) {
101
- throw new Error(`[soundworks:ContextManager] Can't get context "${contextName}", not registered`);
102
- }
103
-
104
- const context = this._contexts.get(contextName);
105
-
106
- if (this._contextStartPromises.has(contextName)) {
107
- const startPromise = this._contextStartPromises.get(contextName);
108
- await startPromise;
109
- } else {
110
- context.status = 'inited';
111
-
112
- try {
113
- const startPromise = context.start();
114
- this._contextStartPromises.set(contextName, startPromise);
115
- await startPromise;
116
- context.status = 'started';
117
- } catch (err) {
118
- context.status = 'errored';
119
- throw new Error(err);
120
- }
121
- }
117
+ async [kServerContextManagerStart]() {
118
+ const promises = this[kServerContextManagerContexts].map(context => this.get(context.name));
119
+ await Promise.all(promises);
120
+ }
122
121
 
123
- return context;
122
+ /**
123
+ * Stop all contexts. Called on {@link Server#stop}
124
+ *
125
+ * @private
126
+ */
127
+ async [kServerContextManagerStop]() {
128
+ const promises = this[kServerContextManagerContexts].map(context => context.stop());
129
+ await Promise.all(promises);
124
130
  }
125
131
 
126
132
  /**
127
133
  * Called when a client connects to the server (websocket handshake)
128
134
  *
129
- * @param {server.Client} client
135
+ * @param {ServerClient} client
130
136
  *
131
137
  * @private
132
138
  */
133
- addClient(client) {
139
+ [kServerContextManagerAddClient](client) {
134
140
  client.socket.addListener(CONTEXT_ENTER_REQUEST, async (reqId, contextName) => {
135
141
  // if no context found, create a DefaultContext on the fly
136
- if (!this._contexts.has(contextName)) {
142
+ if (!this[kServerContextManagerContexts].has(contextName)) {
137
143
  // create default context for all client types
138
144
  const ctor = createNamedContextClass(contextName);
139
145
  // this will automatically register the context in the context manager
140
- new ctor(this.server);
146
+ new ctor(this.#server);
141
147
  }
142
148
 
143
- // we ensure context is started, even lazilly after server.start()
149
+ // we ensure context is started, even lazily after server.start()
144
150
  let context;
145
151
  try {
146
152
  context = await this.get(contextName);
@@ -161,7 +167,7 @@ class ContextManager {
161
167
  CONTEXT_ENTER_ERROR,
162
168
  reqId,
163
169
  contextName,
164
- `[soundworks:ContextManager] Client already in context (if only one context is created .enter() has been called automatically)`,
170
+ `Client already in context: if only one context is created 'enter' is called automatically)`,
165
171
  );
166
172
  return;
167
173
  }
@@ -171,12 +177,12 @@ class ContextManager {
171
177
  context = this.defaultContext;
172
178
  }
173
179
 
174
- if (!context.roles.has(client.role)) {
180
+ if (context.roles.size > 0 && !context.roles.has(client.role)) {
175
181
  client.socket.send(
176
182
  CONTEXT_ENTER_ERROR,
177
183
  reqId,
178
184
  contextName,
179
- `[soundworks:ContextManager] Clients with role "${client.role}" are not declared as possible consumers of context "${contextName}"`,
185
+ `Clients with role "${client.role}" are not declared as possible consumers of "${contextName}"`,
180
186
  );
181
187
  return;
182
188
  }
@@ -191,12 +197,12 @@ class ContextManager {
191
197
  });
192
198
 
193
199
  client.socket.addListener(CONTEXT_EXIT_REQUEST, async (reqId, contextName) => {
194
- if (!this._contexts.has(contextName)) {
200
+ if (!this[kServerContextManagerContexts].has(contextName)) {
195
201
  client.socket.send(
196
202
  CONTEXT_EXIT_ERROR,
197
203
  reqId,
198
204
  contextName,
199
- `[soundworks:ContextManager] Cannot exit(), context ${contextName} does not exists`,
205
+ `Context ${contextName} does not exists`,
200
206
  );
201
207
  return;
202
208
  }
@@ -215,7 +221,7 @@ class ContextManager {
215
221
  CONTEXT_EXIT_ERROR,
216
222
  reqId,
217
223
  contextName,
218
- `[soundworks:ContextManager] Client with role "${client.role}" is not in context "${contextName}"`,
224
+ `Client with role "${client.role}" is not entered in context "${contextName}"`,
219
225
  );
220
226
  }
221
227
  });
@@ -224,16 +230,16 @@ class ContextManager {
224
230
  /**
225
231
  * Called when a client connects to the server (websocket 'close' event)
226
232
  *
227
- * @param {server.Client} client
233
+ * @param {ServerClient} client
228
234
  *
229
235
  * @private
230
236
  */
231
- async removeClient(client) {
237
+ async [kServerContextManagerRemoveClient](client) {
232
238
  client.socket.removeAllListeners(CONTEXT_ENTER_REQUEST);
233
239
  client.socket.removeAllListeners(CONTEXT_EXIT_REQUEST);
234
240
 
235
241
  // exit from all contexts
236
- const promises = this._contexts
242
+ const promises = this[kServerContextManagerContexts]
237
243
  .filter(context => context.clients.has(client))
238
244
  .map(context => context.exit(client));
239
245
 
@@ -241,25 +247,39 @@ class ContextManager {
241
247
  }
242
248
 
243
249
  /**
244
- * Start all contexts registered before `await server.start()`.
245
- * Called on {@link server.Server#start}
250
+ * Retrieve a started context from its name.
246
251
  *
247
- * @private
248
- */
249
- async start() {
250
- const promises = this._contexts.map(context => this.get(context.name));
251
- await Promise.all(promises);
252
- }
253
-
254
- /**
255
- * Stop all contexts. Called on {@link server.Server#stop}
252
+ * _WARNING: Most of the time, you should not have to call this method manually._
256
253
  *
257
- * @private
254
+ * @param {string} contextName - Name of the context.
255
+ * @return {Promise<T<ServerContext>>}
258
256
  */
259
- async stop() {
260
- const promises = this._contexts.map(context => context.stop());
261
- await Promise.all(promises);
257
+ async get(contextName) {
258
+ if (!this[kServerContextManagerContexts].has(contextName)) {
259
+ throw new ReferenceError(`Cannot execute 'get' on ServerContextManager: Context '${contextName}' is not defined`);
260
+ }
261
+
262
+ const context = this[kServerContextManagerContexts].get(contextName);
263
+
264
+ if (this.#contextStartPromises.has(contextName)) {
265
+ const startPromise = this.#contextStartPromises.get(contextName);
266
+ await startPromise;
267
+ } else {
268
+ context[kServerContextStatus] = 'inited';
269
+
270
+ try {
271
+ const startPromise = context.start();
272
+ this.#contextStartPromises.set(contextName, startPromise);
273
+ await startPromise;
274
+ context[kServerContextStatus] = 'started';
275
+ } catch (err) {
276
+ context[kServerContextStatus] = 'errored';
277
+ throw new Error(err);
278
+ }
279
+ }
280
+
281
+ return context;
262
282
  }
263
283
  }
264
284
 
265
- export default ContextManager;
285
+ export default ServerContextManager;