@soundworks/core 3.6.0 → 4.0.0-alpha.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 (125) hide show
  1. package/.eslintrc +16 -0
  2. package/LICENSE +1 -1
  3. package/README.md +32 -30
  4. package/package.json +57 -26
  5. package/src/client/Client.js +391 -0
  6. package/src/client/Context.js +225 -0
  7. package/src/client/ContextManager.js +111 -0
  8. package/src/client/Plugin.js +66 -0
  9. package/src/client/PluginManager.js +123 -0
  10. package/src/client/SharedState.js +84 -0
  11. package/src/client/Socket.js +389 -0
  12. package/src/client/StateManager.js +67 -0
  13. package/src/client/index.js +77 -0
  14. package/src/common/BasePlugin.js +163 -0
  15. package/src/common/BasePluginManager.js +227 -0
  16. package/src/common/BaseSharedState.js +473 -0
  17. package/src/common/BaseStateManager.js +200 -0
  18. package/{common → src/common}/ParameterBag.js +82 -122
  19. package/{common → src/common}/SharedStatePrivate.js +56 -64
  20. package/src/common/constants.js +51 -0
  21. package/src/common/logger.js +154 -0
  22. package/src/common/promise-store.js +25 -0
  23. package/src/common/sockets-utils.js +73 -0
  24. package/src/common/utils.js +22 -0
  25. package/src/server/Client.js +50 -0
  26. package/src/server/Context.js +99 -0
  27. package/src/server/ContextManager.js +266 -0
  28. package/src/server/Plugin.js +77 -0
  29. package/src/server/PluginManager.js +150 -0
  30. package/src/server/Server.js +849 -0
  31. package/src/server/SharedState.js +86 -0
  32. package/{server → src/server}/Socket.js +119 -94
  33. package/{server → src/server}/Sockets.js +73 -61
  34. package/{common/SharedStateManagerServer.js → src/server/StateManager.js} +183 -131
  35. package/src/server/audit-network-latency.worker.js +57 -0
  36. package/src/server/audit-schema.js +42 -0
  37. package/src/server/index.js +45 -0
  38. package/tsconfig.json +21 -0
  39. package/types/client/Client.d.ts +194 -0
  40. package/types/client/Client.d.ts.map +1 -0
  41. package/types/client/Context.d.ts +139 -0
  42. package/types/client/Context.d.ts.map +1 -0
  43. package/types/client/ContextManager.d.ts +45 -0
  44. package/types/client/ContextManager.d.ts.map +1 -0
  45. package/types/client/Plugin.d.ts +53 -0
  46. package/types/client/Plugin.d.ts.map +1 -0
  47. package/types/client/PluginManager.d.ts +96 -0
  48. package/types/client/PluginManager.d.ts.map +1 -0
  49. package/types/client/SharedState.d.ts +85 -0
  50. package/types/client/SharedState.d.ts.map +1 -0
  51. package/types/client/Socket.d.ts +138 -0
  52. package/types/client/Socket.d.ts.map +1 -0
  53. package/types/client/StateManager.d.ts +72 -0
  54. package/types/client/StateManager.d.ts.map +1 -0
  55. package/types/client/index.d.ts +11 -0
  56. package/types/client/index.d.ts.map +1 -0
  57. package/types/common/BasePlugin.d.ts +145 -0
  58. package/types/common/BasePlugin.d.ts.map +1 -0
  59. package/types/common/BasePluginManager.d.ts +88 -0
  60. package/types/common/BasePluginManager.d.ts.map +1 -0
  61. package/types/common/BaseSharedState.d.ts +173 -0
  62. package/types/common/BaseSharedState.d.ts.map +1 -0
  63. package/types/common/BaseStateManager.d.ts +70 -0
  64. package/types/common/BaseStateManager.d.ts.map +1 -0
  65. package/types/common/ParameterBag.d.ts +143 -0
  66. package/types/common/ParameterBag.d.ts.map +1 -0
  67. package/types/common/SharedStatePrivate.d.ts +20 -0
  68. package/types/common/SharedStatePrivate.d.ts.map +1 -0
  69. package/types/common/constants.d.ts +35 -0
  70. package/types/common/constants.d.ts.map +1 -0
  71. package/types/common/logger.d.ts +16 -0
  72. package/types/common/logger.d.ts.map +1 -0
  73. package/types/common/promise-store.d.ts +4 -0
  74. package/types/common/promise-store.d.ts.map +1 -0
  75. package/types/common/sockets-utils.d.ts +9 -0
  76. package/types/common/sockets-utils.d.ts.map +1 -0
  77. package/types/common/utils.d.ts +5 -0
  78. package/types/common/utils.d.ts.map +1 -0
  79. package/types/server/Client.d.ts +40 -0
  80. package/types/server/Client.d.ts.map +1 -0
  81. package/types/server/Context.d.ts +59 -0
  82. package/types/server/Context.d.ts.map +1 -0
  83. package/types/server/ContextManager.d.ts +69 -0
  84. package/types/server/ContextManager.d.ts.map +1 -0
  85. package/types/server/Plugin.d.ts +70 -0
  86. package/types/server/Plugin.d.ts.map +1 -0
  87. package/types/server/PluginManager.d.ts +91 -0
  88. package/types/server/PluginManager.d.ts.map +1 -0
  89. package/types/server/Server.d.ts +223 -0
  90. package/types/server/Server.d.ts.map +1 -0
  91. package/types/server/SharedState.d.ts +86 -0
  92. package/types/server/SharedState.d.ts.map +1 -0
  93. package/types/server/Socket.d.ts +128 -0
  94. package/types/server/Socket.d.ts.map +1 -0
  95. package/types/server/Sockets.d.ts +73 -0
  96. package/types/server/Sockets.d.ts.map +1 -0
  97. package/types/server/StateManager.d.ts +190 -0
  98. package/types/server/StateManager.d.ts.map +1 -0
  99. package/types/server/audit-network-latency.worker.d.ts +2 -0
  100. package/types/server/audit-network-latency.worker.d.ts.map +1 -0
  101. package/types/server/audit-schema.d.ts +27 -0
  102. package/types/server/audit-schema.d.ts.map +1 -0
  103. package/types/server/index.d.ts +7 -0
  104. package/types/server/index.d.ts.map +1 -0
  105. package/client/AbstractExperience.js +0 -54
  106. package/client/AbstractPlugin.js +0 -230
  107. package/client/Client.js +0 -190
  108. package/client/PluginManager.js +0 -267
  109. package/client/Socket.js +0 -338
  110. package/client/index.js +0 -48
  111. package/client.js +0 -28
  112. package/common/SharedState.js +0 -480
  113. package/common/SharedStateManagerClient.js +0 -194
  114. package/common/Signal.js +0 -40
  115. package/common/SignalAll.js +0 -44
  116. package/common/shared-state-utils.js +0 -90
  117. package/common/sockets-encoder-decoder.js +0 -73
  118. package/server/AbstractExperience.js +0 -126
  119. package/server/AbstractPlugin.js +0 -211
  120. package/server/Client.js +0 -70
  121. package/server/PluginManager.js +0 -177
  122. package/server/Server.js +0 -746
  123. package/server/index.js +0 -49
  124. package/server/utils/logger.js +0 -134
  125. package/server.js +0 -28
@@ -0,0 +1,99 @@
1
+ import Client from './Client.js';
2
+ import Server from './Server.js';
3
+
4
+ /**
5
+ * Base class to extend in order to create the server-side counterpart of
6
+ * a {@link client.Context}. If not defined, a default context will be created
7
+ * and used by the server.
8
+ *
9
+ * @memberof server
10
+ */
11
+ class Context {
12
+ constructor(server, roles = []) {
13
+ if (!(server instanceof Server)) {
14
+ throw new Error(`[soundworks:Context] Invalid argument, context "${this.constructor.name}" should receive a "soundworks.Server" instance as first argument`);
15
+ }
16
+
17
+ if (roles === null) {
18
+ throw new Error('Invalid client types');
19
+ }
20
+
21
+ /**
22
+ * soundworks server
23
+ * @type {server.Server}
24
+ */
25
+ this.server = server;
26
+
27
+ /**
28
+ * List of clients that are currently in this context
29
+ * @type {Client[]}
30
+ */
31
+ this.clients = new Set();
32
+
33
+ /**
34
+ * Status of the context ('idle', 'inited', 'started' or 'errored')
35
+ * @type {String}
36
+ */
37
+ this.status = 'idle';
38
+
39
+ /**
40
+ * List of client role associated with this context.
41
+ */
42
+ roles = Array.isArray(roles) ? roles : [roles];
43
+ this.roles = new Set(roles);
44
+
45
+ // register in context manager
46
+ this.server.contextManager.register(this);
47
+ }
48
+
49
+ /**
50
+ * Name of the context, default to the class name.
51
+ * Override the `get name()` getter to use a user-defined context name.
52
+ */
53
+ get name() {
54
+ return this.constructor.name;
55
+ }
56
+
57
+ /**
58
+ * Method automatically called when the server starts, or lazilly called if
59
+ * the context is created after `server.start()`
60
+ *
61
+ * _WARNING: this method should never be called manually._
62
+ */
63
+ async start() {}
64
+
65
+ /**
66
+ * Method automatically called when the server stops.
67
+ *
68
+ * _WARNING: this method should never be called manually._
69
+ */
70
+ async stop() {}
71
+
72
+ /**
73
+ * Method automatically called when the client enters the context.
74
+ *
75
+ * _WARNING: this method should never be called manually._
76
+ */
77
+ async enter(client) {
78
+ if (!(client instanceof Client)) {
79
+ throw new Error(`[soundworks.Context] Invalid argument, ${this.name} context ".enter()" method should receive a server-side "soundworks.Client" instance argument`);
80
+ }
81
+
82
+ this.clients.add(client);
83
+ }
84
+
85
+ /**
86
+ * Method automatically called when the client exits the context.
87
+ *
88
+ * _WARNING: this method should never be called manually._
89
+ */
90
+ async exit(client) {
91
+ if (!(client instanceof Client)) {
92
+ throw new Error(`[soundworks.Context] Invalid argument, ${this.name}.exit() should receive a server-side "soundworks.Client" instance argument`);
93
+ }
94
+
95
+ this.clients.delete(client);
96
+ }
97
+ }
98
+
99
+ export default Context;
@@ -0,0 +1,266 @@
1
+ import Context from './Context.js';
2
+ import {
3
+ CONTEXT_ENTER_REQUEST,
4
+ CONTEXT_ENTER_RESPONSE,
5
+ CONTEXT_ENTER_ERROR,
6
+ CONTEXT_EXIT_REQUEST,
7
+ CONTEXT_EXIT_RESPONSE,
8
+ CONTEXT_EXIT_ERROR,
9
+ } from '../common/constants.js';
10
+
11
+ /**
12
+ * 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
14
+ * contextName and associated to all known client types.
15
+ *
16
+ * @private
17
+ */
18
+ function createNamedContextClass(contextName) {
19
+ return class DefaultContext extends Context {
20
+ get name() {
21
+ return contextName;
22
+ }
23
+ };
24
+ }
25
+
26
+ /** @private */
27
+ class ContextCollection {
28
+ constructor() {
29
+ this.inner = [];
30
+ }
31
+
32
+ add(context) {
33
+ this.inner.push(context);
34
+ }
35
+
36
+ has(name) {
37
+ return this.inner.find(c => c.name === name) !== undefined;
38
+ }
39
+
40
+ get(name) {
41
+ return this.inner.find(c => c.name === name);
42
+ }
43
+
44
+ map(func) {
45
+ return this.inner.map(func);
46
+ }
47
+
48
+ filter(func) {
49
+ return this.inner.filter(func);
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Manage the different server-side contexts and their lifecycle. The `ContextManager`
55
+ * is automatically instantiated by the {@link server.Server}.
56
+ *
57
+ * _WARNING: Most of the time, you should not have to manipulate the context manager directly._
58
+ *
59
+ * @memberof server
60
+ */
61
+ class ContextManager {
62
+ /**
63
+ * @param {server.Server} server - Instance of the soundworks server.
64
+ */
65
+ constructor(server) {
66
+ /** @private */
67
+ this.server = server;
68
+ /** @private */
69
+ this._contexts = new ContextCollection();
70
+ /** @private */
71
+ this._contextStartPromises = new Map();
72
+ }
73
+
74
+ /**
75
+ * Register a context in the manager.
76
+ * This method is called in the {@link server.Context} constructor
77
+ *
78
+ * @param {server.Context} context - Context instance to register.
79
+ *
80
+ * @private
81
+ */
82
+ register(context) {
83
+ // we must await the contructor initialization end to check the name and throw
84
+ if (this._contexts.has(context.name)) {
85
+ throw new Error(`[soundworks:ContextManager] Context "${context.name}" already registered`);
86
+ }
87
+
88
+ this._contexts.add(context);
89
+ }
90
+
91
+ /**
92
+ * Retrieve a started context from its name.
93
+ *
94
+ * @see {server.Context#name}
95
+ * @param {String} contextName - Name of the context.
96
+ *
97
+ * @private
98
+ */
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
+ }
122
+
123
+ return context;
124
+ }
125
+
126
+ /**
127
+ * Called when a client connects to the server (websocket handshake)
128
+ *
129
+ * @param {server.Client} client
130
+ *
131
+ * @private
132
+ */
133
+ addClient(client) {
134
+ client.socket.addListener(CONTEXT_ENTER_REQUEST, async (reqId, contextName) => {
135
+ // if no context found, create a DefaultContext on the fly
136
+ if (!this._contexts.has(contextName)) {
137
+ // create default context for all client types
138
+ const ctor = createNamedContextClass(contextName);
139
+ const roles = Object.keys(this.server.config.app.clients);
140
+ // this will automatically register the context in the context manager
141
+ new ctor(this.server, roles);
142
+ }
143
+
144
+ // we ensure context is started, even lazilly after server.start()
145
+ let context;
146
+ try {
147
+ context = await this.get(contextName);
148
+ } catch(err) {
149
+ client.socket.send(
150
+ CONTEXT_ENTER_ERROR,
151
+ reqId,
152
+ contextName,
153
+ err.message,
154
+ );
155
+ return;
156
+ }
157
+
158
+ // don't do this check with default context to allow several parallel
159
+ // contexts client side with no server-side part.
160
+ if (context && context.clients.has(client)) {
161
+ client.socket.send(
162
+ CONTEXT_ENTER_ERROR,
163
+ reqId,
164
+ contextName,
165
+ `[soundworks:ContextManager] Client already in context (if only one context is created .enter() has been called automatically)`,
166
+ );
167
+ return;
168
+ }
169
+
170
+ // use default context if no server-side context part defined
171
+ if (context === undefined) {
172
+ context = this.defaultContext;
173
+ }
174
+
175
+ if (!context.roles.has(client.role)) {
176
+ client.socket.send(
177
+ CONTEXT_ENTER_ERROR,
178
+ reqId,
179
+ contextName,
180
+ `[soundworks:ContextManager] Clients with role "${client.role}" are not declared as possible consumers of context "${contextName}"`,
181
+ );
182
+ return;
183
+ }
184
+
185
+ try {
186
+ await context.enter(client);
187
+
188
+ client.socket.send(CONTEXT_ENTER_RESPONSE, reqId, contextName);
189
+ } catch(err) {
190
+ client.socket.send(CONTEXT_ENTER_ERROR, reqId, contextName, err.message);
191
+ }
192
+ });
193
+
194
+ client.socket.addListener(CONTEXT_EXIT_REQUEST, async (reqId, contextName) => {
195
+ if (!this._contexts.has(contextName)) {
196
+ client.socket.send(
197
+ CONTEXT_EXIT_ERROR,
198
+ reqId,
199
+ contextName,
200
+ `[soundworks:ContextManager] Cannot exit(), context ${contextName} does not exists`,
201
+ );
202
+ return;
203
+ }
204
+
205
+ let context = await this.get(contextName);
206
+
207
+ if (context.clients.has(client)) {
208
+ try {
209
+ await context.exit(client);
210
+ client.socket.send(CONTEXT_EXIT_RESPONSE, reqId, contextName);
211
+ } catch(err) {
212
+ client.socket.send(CONTEXT_EXIT_ERROR, reqId, contextName, err.message);
213
+ }
214
+ } else {
215
+ client.socket.send(
216
+ CONTEXT_EXIT_ERROR,
217
+ reqId,
218
+ contextName,
219
+ `[soundworks:ContextManager] Client with role "${client.role}" is not in context "${contextName}"`,
220
+ );
221
+ }
222
+ });
223
+ }
224
+
225
+ /**
226
+ * Called when a client connects to the server (websocket 'close' event)
227
+ *
228
+ * @param {server.Client} client
229
+ *
230
+ * @private
231
+ */
232
+ async removeClient(client) {
233
+ client.socket.removeAllListeners(CONTEXT_ENTER_REQUEST);
234
+ client.socket.removeAllListeners(CONTEXT_EXIT_REQUEST);
235
+
236
+ // exit from all contexts
237
+ const promises = this._contexts
238
+ .filter(context => context.clients.has(client))
239
+ .map(context => context.exit(client));
240
+
241
+ await Promise.all(promises);
242
+ }
243
+
244
+ /**
245
+ * Start all contexts registered before `await server.start()`.
246
+ * Called on {@link server.Server#start}
247
+ *
248
+ * @private
249
+ */
250
+ async start() {
251
+ const promises = this._contexts.map(context => this.get(context.name));
252
+ await Promise.all(promises);
253
+ }
254
+
255
+ /**
256
+ * Stop all contexts. Called on {@link server.Server#stop}
257
+ *
258
+ * @private
259
+ */
260
+ async stop() {
261
+ const promises = this._contexts.map(context => context.stop());
262
+ await Promise.all(promises);
263
+ }
264
+ }
265
+
266
+ export default ContextManager;
@@ -0,0 +1,77 @@
1
+ import BasePlugin from '../common/BasePlugin.js';
2
+
3
+ /**
4
+ * Callback executed when the plugin state is updated.
5
+ *
6
+ * @callback server.Plugin~onStateChangeCallback
7
+ * @param {server.Plugin#state} state - Current state of the plugin.
8
+ */
9
+
10
+ /**
11
+ * Delete the registered {@link server.Plugin~onStateChangeCallback}.
12
+ *
13
+ * @callback server.Plugin~deleteOnStateChangeCallback
14
+ */
15
+
16
+ /**
17
+ * Base class to extend in order to create new `soundworks` plugins.
18
+ *
19
+ * In the `soundworks` paradigm, a plugin is a component that allows to extend
20
+ * the framework capabilities by encapsulating common and reusable logic in
21
+ * an application wise perspective. For example, plugins are available to handle
22
+ * clock synchronization, to deal with the file system, etc. Plugin should always
23
+ * have both a client-side and a server-side part.
24
+ *
25
+ * See [https://soundworks.dev/guide/ecosystem](https://soundworks.dev/guide/ecosystem)
26
+ * for more informations on the available plugins.
27
+ *
28
+ * _Creating new plugins should be considered an advanced usage._
29
+ *
30
+ * @memberof server
31
+ * @extends BasePlugin
32
+ * @inheritdoc
33
+ */
34
+ class Plugin extends BasePlugin {
35
+ /**
36
+ * @param {server.Server} server - The soundworks server instance.
37
+ * @param {string} id - User defined id of the plugin as defined in
38
+ * {@link server.PluginManager#register}.
39
+ */
40
+ constructor(server, id) {
41
+ super(id);
42
+
43
+ /**
44
+ * Instance of soundworks server.
45
+ * @type {server.Server}
46
+ * @see {@link server.Server}
47
+ */
48
+ this.server = server;
49
+
50
+ /** @private */
51
+ this.clients = new Set();
52
+ }
53
+
54
+ /**
55
+ * Method called when a client (which registered the client-side plugin),
56
+ * connects to the application. Override this method if you need to perform
57
+ * some particular logic (e.g. creating a shared state) for each clients.
58
+ *
59
+ * @param {server.Client} client
60
+ */
61
+ async addClient(client) {
62
+ this.clients.add(client);
63
+ }
64
+
65
+ /**
66
+ * Method called when a client (which registered the client-side plugin),
67
+ * disconnects from the application. Override this method if you need to perform
68
+ * some particular logic (e.g. creating a shared state) for each clients.
69
+ *
70
+ * @param {server.Client} client
71
+ */
72
+ async removeClient(client) {
73
+ this.clients.delete(client);
74
+ }
75
+ }
76
+
77
+ export default Plugin;
@@ -0,0 +1,150 @@
1
+ import BasePluginManager from '../common/BasePluginManager.js';
2
+ import Plugin from './Plugin.js';
3
+ import Server from './Server.js';
4
+
5
+ /**
6
+ * Callback executed when a plugin internal state is updated.
7
+ *
8
+ * @callback server.PluginManager~onStateChangeCallback
9
+ * @param {object<server.Plugin#id, server.Plugin>} fullState - List of all plugins.
10
+ * @param {server.Plugin|null} initiator - Plugin that initiated the update or `null`
11
+ * if the change was initiated by the state manager (i.e. when the initialization
12
+ * of the plugins starts).
13
+ */
14
+
15
+ /**
16
+ * Delete the registered {@link server.PluginManager~onStateChangeCallback}.
17
+ *
18
+ * @callback server.PluginManager~deleteOnStateChangeCallback
19
+ */
20
+
21
+ /**
22
+ * The `PluginManager` allows to register and retrieve `soundworks` plugins.
23
+ *
24
+ * Plugins should always be registered both client-side and server-side,
25
+ * and before {@link client.Client#start} or {@link server.Server#start}
26
+ * are called for proper initialization.
27
+ *
28
+ * In some sitautions, you might want to register the same plugin factory several times
29
+ * using different ids (e.g. for watching several parts of the file system, etc.).
30
+ *
31
+ * Refer to the plugins' documentation for more precise examples, and the specific API
32
+ * they expose. See [https://soundworks.dev/guide/ecosystem](https://soundworks.dev/guide/ecosystem)
33
+ * for more informations on the available plugins.
34
+ *
35
+ * ```
36
+ * import { Server } from '@soundworks/core/server.js';
37
+ * import platformPlugin from '@soundworks/plugin-sync/server.js';
38
+ *
39
+ * const server = new Server(config);
40
+ * // register the plugin before `server.start()`
41
+ * server.pluginManager.register('sync', pluginSync);
42
+ *
43
+ * await server.start();
44
+ *
45
+ * const sync = await server.pluginManager.get('sync');
46
+ *
47
+ * setInterval(() => {
48
+ * // log the estimated global synced clock alongside the local clock.
49
+ * console.log(sync.getSyncTime(), sync.getLocalTime());
50
+ * }, 1000);
51
+ * ```
52
+ *
53
+ * @memberof server
54
+ * @extends BasePluginManager
55
+ * @inheritdoc
56
+ * @hideconstructor
57
+ */
58
+ class PluginManager extends BasePluginManager {
59
+ constructor(server) {
60
+ if (!(server instanceof Server)) {
61
+ throw new Error(`[soundworks.PluginManager] Invalid argument, "new PluginManager(server)" should receive an instance of "soundworks.Server as argument"`);
62
+ }
63
+
64
+ super(server);
65
+ }
66
+
67
+ register(id, factory = null, options = {}, deps = []) {
68
+ const ctor = factory(Plugin);
69
+
70
+ if (!(ctor.prototype instanceof Plugin)) {
71
+ throw new Error(`[soundworks.PluginManager] Invalid argument, "pluginManager.register" second argument should be a factory function returning a class extending the "Plugin" base class`);
72
+ }
73
+
74
+ super.register(id, ctor, options, deps);
75
+ }
76
+
77
+ /**
78
+ * Retrieve an fully started instance of a registered plugin.
79
+ *
80
+ * Be aware that the `get` method resolves only when the plugin is fully 'started',
81
+ * which is what we want 99.99% of the time. As such, and to prevent the application
82
+ * to be stuck in some kind of Promise dead lock, this method will throw if
83
+ * used before `server.init()` (or `server.start()`).
84
+ *
85
+ * To handle the remaining 0.01% cases and access plugin instances during their
86
+ * initialization (e.g. to display initialization screen, etc.), you should rely
87
+ * on the `onStateChange` method.
88
+ *
89
+ * _Note: the async API is designed to enable the dynamic creation of plugins (hopefully
90
+ * without brealing changes) in a future release._
91
+ *
92
+ * @param {server.Plugin#id} id - Id of the plugin as defined when registered.
93
+ * @returns {server.Plugin}
94
+ * @see {@link server.PluginManager#onStateChange}
95
+ *
96
+ * @private
97
+ */
98
+ async get(id) {
99
+ if (this.status !== 'started') {
100
+ throw new Error(`[soundworks.PluginManager] Cannot get plugin before "server.init()"`);
101
+ }
102
+
103
+ return super.getUnsafe(id);
104
+ }
105
+
106
+ // server only methods
107
+
108
+ /** private */
109
+ checkRegisteredPlugins(registeredPlugins) {
110
+ let missingPlugins = [];
111
+
112
+ for (let id of registeredPlugins) {
113
+ if (!this._instances.has(id)) {
114
+ missingPlugins.push(id);
115
+ }
116
+ }
117
+
118
+ if (missingPlugins.length > 0) {
119
+ throw new Error(`Invalid plugin list, the following plugins registered client-side: [${missingPlugins.join(', ')}] have not been registered server-side. Registered server-side plugins are: [${Array.from(this._instances.keys()).join(', ')}].`);
120
+ }
121
+ }
122
+
123
+ /** @private */
124
+ async addClient(client, registeredPlugins = []) {
125
+ let promises = [];
126
+
127
+ for (let pluginName of registeredPlugins) {
128
+ const plugin = await this.get(pluginName);
129
+ const promise = plugin.addClient(client);
130
+ promises.push(promise);
131
+ }
132
+
133
+ await Promise.all(promises);
134
+ }
135
+
136
+ /** @private */
137
+ async removeClient(client) {
138
+ let promises = [];
139
+
140
+ for (let plugin of this._instances.values()) {
141
+ if (plugin.clients.has(client)) {
142
+ promises.push(plugin.removeClient(client));
143
+ }
144
+ }
145
+
146
+ await Promise.all(promises);
147
+ }
148
+ }
149
+
150
+ export default PluginManager;