@soundworks/core 4.0.0-alpha.28 → 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 (127) hide show
  1. package/.jsdoc +3 -1
  2. package/CHANGELOG.md +11 -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-schema.js +1 -1
  34. package/src/server/crypto.js +2 -0
  35. package/src/server/index.js +18 -21
  36. package/types/client/Client.d.ts +74 -95
  37. package/types/client/{Context.d.ts → ClientContext.d.ts} +25 -26
  38. package/types/client/{ContextManager.d.ts → ClientContextManager.d.ts} +19 -22
  39. package/types/client/{Plugin.d.ts → ClientPlugin.d.ts} +17 -20
  40. package/types/client/{PluginManager.d.ts → ClientPluginManager.d.ts} +13 -36
  41. package/types/client/ClientSocket.d.ts +74 -0
  42. package/types/client/{StateManager.d.ts → ClientStateManager.d.ts} +11 -15
  43. package/types/client/index.d.ts +26 -2
  44. package/types/common/BasePlugin.d.ts +52 -46
  45. package/types/common/BasePluginManager.d.ts +68 -44
  46. package/types/common/BaseStateManager.d.ts +207 -97
  47. package/types/common/BatchedTransport.d.ts +1 -5
  48. package/types/common/ParameterBag.d.ts +16 -18
  49. package/types/common/PromiseStore.d.ts +1 -1
  50. package/types/common/SharedState.d.ts +297 -0
  51. package/types/common/{BaseSharedStateCollection.d.ts → SharedStateCollection.d.ts} +34 -18
  52. package/types/common/constants.d.ts +0 -2
  53. package/types/common/logger.d.ts +2 -1
  54. package/types/common/shared-state-types.d.ts +83 -0
  55. package/types/common/sockets-utils.d.ts +0 -5
  56. package/types/common/version.d.ts +1 -2
  57. package/types/server/Server.d.ts +78 -132
  58. package/types/server/ServerClient.d.ts +45 -0
  59. package/types/server/{Context.d.ts → ServerContext.d.ts} +53 -43
  60. package/types/server/ServerContextManager.d.ts +80 -0
  61. package/types/server/{Plugin.d.ts → ServerPlugin.d.ts} +19 -33
  62. package/types/server/{PluginManager.d.ts → ServerPluginManager.d.ts} +39 -37
  63. package/types/server/ServerSocket.d.ts +85 -0
  64. package/types/server/ServerSockets.d.ts +50 -0
  65. package/types/server/ServerStateManager.d.ts +189 -0
  66. package/types/server/SharedStatePrivate.d.ts +21 -0
  67. package/types/server/audit-network-latency.worker.d.ts +1 -2
  68. package/types/server/audit-schema.d.ts +0 -1
  69. package/types/server/crypto.d.ts +2 -1
  70. package/types/server/index.d.ts +24 -2
  71. package/src/client/ContextManager.js +0 -111
  72. package/src/client/SharedState.js +0 -84
  73. package/src/client/SharedStateCollection.js +0 -38
  74. package/src/server/Client.js +0 -55
  75. package/src/server/SharedState.js +0 -85
  76. package/src/server/SharedStateCollection.js +0 -39
  77. package/src/server/StateManager.js +0 -696
  78. package/types/client/Client.d.ts.map +0 -1
  79. package/types/client/Context.d.ts.map +0 -1
  80. package/types/client/ContextManager.d.ts.map +0 -1
  81. package/types/client/Plugin.d.ts.map +0 -1
  82. package/types/client/PluginManager.d.ts.map +0 -1
  83. package/types/client/SharedState.d.ts +0 -85
  84. package/types/client/SharedState.d.ts.map +0 -1
  85. package/types/client/SharedStateCollection.d.ts +0 -41
  86. package/types/client/SharedStateCollection.d.ts.map +0 -1
  87. package/types/client/Socket.d.ts +0 -149
  88. package/types/client/Socket.d.ts.map +0 -1
  89. package/types/client/StateManager.d.ts.map +0 -1
  90. package/types/client/index.d.ts.map +0 -1
  91. package/types/common/BasePlugin.d.ts.map +0 -1
  92. package/types/common/BasePluginManager.d.ts.map +0 -1
  93. package/types/common/BaseSharedState.d.ts +0 -244
  94. package/types/common/BaseSharedState.d.ts.map +0 -1
  95. package/types/common/BaseSharedStateCollection.d.ts.map +0 -1
  96. package/types/common/BaseStateManager.d.ts.map +0 -1
  97. package/types/common/BatchedTransport.d.ts.map +0 -1
  98. package/types/common/ParameterBag.d.ts.map +0 -1
  99. package/types/common/PromiseStore.d.ts.map +0 -1
  100. package/types/common/SharedStatePrivate.d.ts +0 -21
  101. package/types/common/SharedStatePrivate.d.ts.map +0 -1
  102. package/types/common/constants.d.ts.map +0 -1
  103. package/types/common/logger.d.ts.map +0 -1
  104. package/types/common/sockets-utils.d.ts.map +0 -1
  105. package/types/common/version.d.ts.map +0 -1
  106. package/types/server/Client.d.ts +0 -45
  107. package/types/server/Client.d.ts.map +0 -1
  108. package/types/server/Context.d.ts.map +0 -1
  109. package/types/server/ContextManager.d.ts +0 -69
  110. package/types/server/ContextManager.d.ts.map +0 -1
  111. package/types/server/Plugin.d.ts.map +0 -1
  112. package/types/server/PluginManager.d.ts.map +0 -1
  113. package/types/server/Server.d.ts.map +0 -1
  114. package/types/server/SharedState.d.ts +0 -86
  115. package/types/server/SharedState.d.ts.map +0 -1
  116. package/types/server/SharedStateCollection.d.ts +0 -41
  117. package/types/server/SharedStateCollection.d.ts.map +0 -1
  118. package/types/server/Socket.d.ts +0 -164
  119. package/types/server/Socket.d.ts.map +0 -1
  120. package/types/server/Sockets.d.ts +0 -80
  121. package/types/server/Sockets.d.ts.map +0 -1
  122. package/types/server/StateManager.d.ts +0 -405
  123. package/types/server/StateManager.d.ts.map +0 -1
  124. package/types/server/audit-network-latency.worker.d.ts.map +0 -1
  125. package/types/server/audit-schema.d.ts.map +0 -1
  126. package/types/server/crypto.d.ts.map +0 -1
  127. package/types/server/index.d.ts.map +0 -1
@@ -1,45 +1,43 @@
1
1
  import merge from 'lodash/merge.js';
2
2
 
3
+ export const kBasePluginStatus = Symbol('soundworks:base-plugin-status');
4
+
5
+ /**
6
+ * Callback executed when the plugin state is updated.
7
+ *
8
+ * @callback pluginOnStateChangeCallback
9
+ * @param {BasePlugin#state} state - Current state of the plugin.
10
+ */
11
+
3
12
  /**
4
- * @private
13
+ * Delete the registered {@link pluginOnStateChangeCallback}.
14
+ *
15
+ * @callback pluginDeleteOnStateChangeCallback
5
16
  */
17
+
18
+ /** @private */
6
19
  class BasePlugin {
20
+ #id = null;
21
+ #onStateChangeCallbacks = new Set();
22
+
7
23
  constructor(id) {
24
+ this.#id = id;
8
25
  /** @private */
9
- this._id = id;
10
- /** @private */
11
- this._type = this.constructor.name;
12
-
13
- /**
14
- * Options of the plugin.
15
- *
16
- * @type {object}
17
- */
18
- this.options = {};
26
+ this[kBasePluginStatus] = 'idle';
19
27
 
20
28
  /**
21
29
  * Placeholder that stores internal (local) state of the plugin. The state
22
30
  * should be modified through the `propagateStateChange` method to ensure
23
- * the change to be properly propagated to `onStateChange` callbacks.
31
+ * the change to be properly propagated to manager `onStateChange` callbacks.
24
32
  *
25
33
  * @type {object}
26
34
  * @protected
27
- * @see {@link client.Plugin#onStateChange}
28
- * @see {@link server.Plugin#onStateChange}
29
- * @see {@link client.Plugin#propagateStateChange}
30
- * @see {@link server.Plugin#propagateStateChange}
35
+ * @see {@link ClientPlugin#onStateChange}
36
+ * @see {@link ServerPlugin#onStateChange}
37
+ * @see {@link ClientPlugin#propagateStateChange}
38
+ * @see {@link ServerPlugin#propagateStateChange}
31
39
  */
32
40
  this.state = {};
33
-
34
- /**
35
- * Current status of the plugin, i.e. 'idle', 'inited', 'started', 'errored'
36
- *
37
- * @type {string}
38
- */
39
- this.status = 'idle';
40
-
41
- /** @private */
42
- this._onStateChangeCallbacks = new Set();
43
41
  }
44
42
 
45
43
  /**
@@ -47,11 +45,11 @@ class BasePlugin {
47
45
  *
48
46
  * @type {string}
49
47
  * @readonly
50
- * @see {@link client.PluginManager#register}
51
- * @see {@link server.PluginManager#register}
48
+ * @see {@link ClientPluginManager#register}
49
+ * @see {@link ServerPluginManager#register}
52
50
  */
53
51
  get id() {
54
- return this._id;
52
+ return this.#id;
55
53
  }
56
54
 
57
55
  /**
@@ -65,18 +63,28 @@ class BasePlugin {
65
63
  * @readonly
66
64
  */
67
65
  get type() {
68
- return this._type;
66
+ return this.constructor.name;
67
+ }
68
+
69
+ /**
70
+ * Current status of the plugin.
71
+ *
72
+ * @type {'idle'|'inited'|'started'|'errored'}
73
+ */
74
+ get status() {
75
+ return this[kBasePluginStatus];
69
76
  }
70
77
 
71
78
  /**
72
- * Start the plugin. This method is automatically called during the client or
73
- * server `init()` lifecyle step. After `start()` is fulfilled the plugin should
74
- * be ready to use.
79
+ * Start the plugin.
80
+ *
81
+ * This method is automatically called during the client or server `init()` lifecyle
82
+ * step. After `start()` is fulfilled the plugin should be ready to use.
75
83
  *
76
84
  * @example
77
85
  * // server-side couterpart of a plugin that creates a dedicated global shared
78
86
  * // state on which the server-side part can attach.
79
- * class MyPlugin extends Plugin {
87
+ * class MyPlugin extends ServerPlugin {
80
88
  * constructor(server, id) {
81
89
  * super(server, id);
82
90
  *
@@ -102,13 +110,14 @@ class BasePlugin {
102
110
  async start() {}
103
111
 
104
112
  /**
105
- * Stop the plugin. This method is automatically called during the client or server
106
- * `stop()` lifecyle step.
113
+ * Stop the plugin.
114
+ *
115
+ * This method is automatically called during the client or server `stop()` lifecyle step.
107
116
  *
108
117
  * @example
109
118
  * // server-side couterpart of a plugin that creates a dedicated global shared
110
- * // state on which the server-side part can attach.
111
- * class MyPlugin extends Plugin {
119
+ * // state on which the client-side part can attach.
120
+ * class MyPlugin extends ServerPlugin {
112
121
  * constructor(server, id) {
113
122
  * super(server, id);
114
123
  *
@@ -137,20 +146,17 @@ class BasePlugin {
137
146
  /**
138
147
  * Listen to the state changes propagated by {@link BasePlugin.propagateStateChange}
139
148
  *
140
- * @param {client.Plugin~onStateChangeCallback|server.Plugin~onStateChangeCallback} callback -
141
- * Callback to execute when a state change is propagated.
142
- * @returns {client.Plugin~deleteOnStateChangeCallback|server.Plugin~deleteOnStateChangeCallback}
143
- * Execute the function to delete the listener from the callback list.
144
- * @see {@link client.Plugin#propagateStateChange}
145
- * @see {@link server.Plugin#propagateStateChange}
149
+ * @param {pluginOnStateChangeCallback} callback - Callback to execute when a state change is propagated.
150
+ * @returns {pluginDeleteOnStateChangeCallback}
151
+ *
146
152
  * @example
147
153
  * const unsubscribe = plugin.onStateChange(pluginState => console.log(pluginState));
148
154
  * // stop listening state changes
149
155
  * unsubscribe();
150
156
  */
151
157
  onStateChange(callback) {
152
- this._onStateChangeCallbacks.add(callback);
153
- return () => this._onStateChangeCallbacks.delete(callback);
158
+ this.#onStateChangeCallbacks.add(callback);
159
+ return () => this.#onStateChangeCallbacks.delete(callback);
154
160
  }
155
161
 
156
162
  /**
@@ -159,14 +165,13 @@ class BasePlugin {
159
165
  * through the `PluginManager#onStateChange` listeners.
160
166
  *
161
167
  * @param {object} updates - Updates to be merged in the plugin state.
162
- * @see {@link client.Plugin#onStateChange}
163
- * @see {@link server.Plugin#onStateChange}
164
- * @see {@link client.PluginManager#onStateChange}
165
- * @see {@link server.PluginManager#onStateChange}
168
+ *
169
+ * @see {@link BasePlugin#onStateChange}
170
+ * @see {@link BasePluginManager#onStateChange}
166
171
  */
167
172
  propagateStateChange(updates) {
168
173
  merge(this.state, updates);
169
- this._onStateChangeCallbacks.forEach(callback => callback(this.state));
174
+ this.#onStateChangeCallbacks.forEach(callback => callback(this.state));
170
175
  }
171
176
  }
172
177
 
@@ -1,157 +1,135 @@
1
1
  import { isPlainObject, isString } from '@ircam/sc-utils';
2
2
 
3
+ import {
4
+ kBasePluginStatus,
5
+ } from './BasePlugin.js';
6
+
3
7
  import logger from './logger.js';
4
8
 
9
+ export const kPluginManagerStart = Symbol('soundworks:plugin-manager-start');
10
+ export const kPluginManagerStop = Symbol('soundworks:plugin-manager-stop');
11
+ export const kPluginManagerInstances = Symbol('soundworks:plugin-manager-instances');
12
+
5
13
  /**
6
- * Shared functionnality between server-side and client-size plugin manager
14
+ * Callback executed when a plugin internal state is updated.
7
15
  *
8
- * @private
16
+ * @callback pluginManagerOnStateChangeCallback
17
+ * @param {object<string, ClientPlugin|ServerPlugin>} fullState - List of all plugins.
18
+ * @param {ClientPlugin|ServerPlugin|null} initiator - Plugin that initiated the
19
+ * update. The value is `null` if the change was initiated by the state manager
20
+ * (e.g. when the initialization of the plugins starts).
9
21
  */
10
- class BasePluginManager {
11
- constructor(node) {
12
- // node may be either a soundworks server or a soundworks client
13
- /** @private */
14
- this._node = node;
15
- /** @private */
16
- this._dependencies = new Map();
17
- /** @private */
18
- this._instances = new Map();
19
- /** @private */
20
- this._instanceStartPromises = new Map();
21
- /** @private */
22
- this._onStateChangeCallbacks = new Set();
23
-
24
- this.status = 'idle';
25
- }
26
22
 
27
- /**
28
- * Register a plugin into soundworks.
29
- *
30
- * _A plugin must always be registered both on client-side and on server-side_
31
- *
32
- * Refer to the plugin documentation to check its options and proper way of
33
- * registering it.
34
- *
35
- * @param {string} id - Unique id of the plugin. Enables the registration of the
36
- * same plugin factory under different ids.
37
- * @param {Function} factory - Factory function that returns the Plugin class.
38
- * @param {object} [options={}] - Options to configure the plugin.
39
- * @param {array} [deps=[]] - List of plugins' names the plugin depends on, i.e.
40
- * the plugin initialization will start only after the plugins it depends on are
41
- * fully started themselves.
42
- * @see {@link client.PluginManager#register}
43
- * @see {@link server.PluginManager#register}
44
- * @example
45
- * // client-side
46
- * client.pluginManager.register('user-defined-id', pluginFactory);
47
- * // server-side
48
- * server.pluginManager.register('user-defined-id', pluginFactory);
49
- */
50
- register(id, ctor, options = {}, deps = []) {
51
- // For now we don't allow to register a plugin after `client|server.init()`.
52
- // This is subject to change in the future as we may want to dynamically
53
- // register new plugins during application lifetime.
54
- if (this._node.status === 'inited') {
55
- throw new Error(`[soundworks.PluginManager] Cannot register plugin (${id}) after "client.init()"`);
56
- }
23
+ /**
24
+ * Delete the registered {@link pluginManagerOnStateChangeCallback}.
25
+ *
26
+ * @callback pluginManagerDeleteOnStateChangeCallback
27
+ */
57
28
 
58
- if (!isString(id)) {
59
- throw new Error(`[soundworks.PluginManager] Invalid argument, "pluginManager.register" first argument should be a string`);
60
- }
29
+ /** @private */
30
+ class BasePluginManager {
31
+ // node may be either the server or a client
32
+ #node = null;
33
+ #status = 'idle';
34
+ #dependencies = new Map();
35
+ #instanceStartPromises = new Map();
36
+ #onStateChangeCallbacks = new Set();
61
37
 
62
- if (!isPlainObject(options)) {
63
- throw new Error(`[soundworks.PluginManager] Invalid argument, "pluginManager.register" third optionnal argument should be an object`);
64
- }
38
+ constructor(node) {
39
+ this.#node = node;
40
+ /** #private */
41
+ this[kPluginManagerInstances] = new Map();
42
+ }
65
43
 
66
- if (!Array.isArray(deps)) {
67
- throw new Error(`[soundworks.PluginManager] Invalid argument, "pluginManager.register" fourth optionnal argument should be an array`);
68
- }
44
+ #propagateStateChange(instance = null, status = null) {
45
+ if (instance !== null) {
46
+ // status is null if we forward some inner state change from the instance
47
+ if (status !== null) {
48
+ instance[kBasePluginStatus] = status;
49
+ }
69
50
 
70
- if (this._instances.has(id)) {
71
- throw new Error(`[soundworks:PluginManager] Plugin "${id}" already registered`);
51
+ const fullState = Object.fromEntries(this[kPluginManagerInstances]);
52
+ this.#onStateChangeCallbacks.forEach(callback => callback(fullState, instance));
53
+ } else {
54
+ const fullState = Object.fromEntries(this[kPluginManagerInstances]);
55
+ this.#onStateChangeCallbacks.forEach(callback => callback(fullState, null));
72
56
  }
73
-
74
- // we instanciate the plugin here, so that a plugin can register another one
75
- // in its own constructor.
76
- //
77
- // the dependencies must be created first, so that the instance can call
78
- // addDependency in its constructor
79
- this._dependencies.set(id, deps);
80
-
81
- const instance = new ctor(this._node, id, options);
82
- this._instances.set(id, instance);
83
57
  }
84
58
 
85
59
  /**
86
- * Manually add a dependency to a given plugin. Usefull to require a plugin
87
- * within a plugin
60
+ * Initialize all registered plugins.
61
+ *
62
+ * Executed during the `Client.init()` or `Server.init()` initialization step.
88
63
  *
89
- */
90
- addDependency(pluginId, dependencyId) {
91
- const deps = this._dependencies.get(pluginId);
92
- deps.push(dependencyId);
93
- }
94
-
95
- /**
96
- * Returns the list of the registered plugins ids
97
- * @returns {string[]}
98
- */
99
- getRegisteredPlugins() {
100
- return Array.from(this._instances.keys());
101
- }
102
-
103
- /**
104
- * Initialize all the registered plugin. Executed during the `Client.init()` or
105
- * `Server.init()` initialization step.
106
64
  * @private
107
65
  */
108
- async start() {
66
+ async [kPluginManagerStart]() {
109
67
  logger.title('starting registered plugins');
110
68
 
111
- if (this.status !== 'idle') {
69
+ if (this.#status !== 'idle') {
112
70
  throw new Error(`[soundworks:PluginManager] Cannot call "pluginManager.init()" twice`);
113
71
  }
114
72
 
115
- this.status = 'inited';
73
+ this.#status = 'inited';
116
74
  // instanciate all plugins
117
- for (let [_id, instance] of this._instances.entries()) {
118
- instance.onStateChange(_values => this._propagateStateChange(instance));
75
+ for (let instance of this[kPluginManagerInstances].values()) {
76
+ instance.onStateChange(() => this.#propagateStateChange(instance, null));
119
77
  }
120
78
 
121
- // propagate all 'idle' statuses before start
122
- this._propagateStateChange();
79
+ // propagate all 'idle' status before start
80
+ this.#propagateStateChange(null, null);
123
81
 
124
- const promises = Array.from(this._instances.keys()).map(id => this.unsafeGet(id));
82
+ const promises = Array.from(this[kPluginManagerInstances].keys()).map(id => this.getUnsafe(id));
125
83
 
126
84
  try {
127
85
  await Promise.all(promises);
128
- this.status = 'started';
86
+ this.#status = 'started';
129
87
  } catch (err) {
130
- this.status = 'errored';
88
+ this.#status = 'errored';
131
89
  throw err; // throw initial error
132
90
  }
133
91
  }
134
92
 
135
93
  /** @private */
136
- async stop() {
137
- for (let instance of this._instances.values()) {
94
+ async [kPluginManagerStop]() {
95
+ for (let instance of this[kPluginManagerInstances].values()) {
138
96
  await instance.stop();
139
97
  }
140
98
  }
141
99
 
142
100
  /**
143
- * Retrieve an fully started instance of a registered plugin, without checking
144
- * that the pluginManager has started. This is required for starting the plugin
145
- * manager itself and to require a plugin from within another plugin
101
+ * Status of the plugin manager
146
102
  *
103
+ * @type {'idle'|'inited'|'started'|'errored'}
104
+ */
105
+ get status() {
106
+ return this.#status;
107
+ }
108
+
109
+ /**
110
+ * Alias for existing plugins (i.e. plugin-scriptin), remove once updated
147
111
  * @private
148
112
  */
149
113
  async unsafeGet(id) {
114
+ return this.getUnsafe(id);
115
+ }
116
+
117
+ /**
118
+ * Retrieve an fully started instance of a registered plugin without checking
119
+ * that the pluginManager is started.
120
+ *
121
+ * This method is required for starting the plugin manager itself and to require
122
+ * a plugin from within another plugin.
123
+ *
124
+ * _Warning: Unless you are developing your own plugins, you should not have to use
125
+ * this method_
126
+ */
127
+ async getUnsafe(id) {
150
128
  if (!isString(id)) {
151
129
  throw new Error(`[soundworks.PluginManager] Invalid argument, "pluginManager.get(name)" argument should be a string`);
152
130
  }
153
131
 
154
- if (!this._instances.has(id)) {
132
+ if (!this[kPluginManagerInstances].has(id)) {
155
133
  throw new Error(`[soundworks:PluginManager] Cannot get plugin "${id}", plugin is not registered`);
156
134
  }
157
135
 
@@ -159,48 +137,124 @@ class BasePluginManager {
159
137
  // to be able to properly propagate the states. The code bellow should allow
160
138
  // to dynamically register and launch plugins at runtime.
161
139
  //
162
- // if (!this._instances.has(id)) {
163
- // const { ctor, options } = this._dependencies.get(id);
164
- // const instance = new ctor(this._node, id, options);
165
- // this._instances.set(id, instance);
140
+ // if (!this[kPluginManagerInstances].has(id)) {
141
+ // const { ctor, options } = this.#dependencies.get(id);
142
+ // const instance = new ctor(this.#node, id, options);
143
+ // this[kPluginManagerInstances].set(id, instance);
166
144
  // }
167
145
 
168
- const instance = this._instances.get(id);
146
+ const instance = this[kPluginManagerInstances].get(id);
169
147
 
170
148
  // recursively get the dependency chain
171
- const deps = this._dependencies.get(id);
172
- const promises = deps.map(id => this.unsafeGet(id));
149
+ const deps = this.#dependencies.get(id);
150
+ const promises = deps.map(id => this.getUnsafe(id));
173
151
 
174
152
  await Promise.all(promises);
175
153
 
176
154
  // 'plugin.start' has already been called, just await the start promise
177
- if (this._instanceStartPromises.has(id)) {
178
- await this._instanceStartPromises.get(id);
155
+ if (this.#instanceStartPromises.has(id)) {
156
+ await this.#instanceStartPromises.get(id);
179
157
  } else {
180
- this._propagateStateChange(instance, 'inited');
158
+ this.#propagateStateChange(instance, 'inited');
181
159
  let errored = false;
182
160
 
183
161
  try {
184
162
  const startPromise = instance.start();
185
- this._instanceStartPromises.set(id, startPromise);
163
+ this.#instanceStartPromises.set(id, startPromise);
186
164
 
187
165
  await startPromise;
188
166
  } catch (err) {
189
167
  errored = true;
190
- this._propagateStateChange(instance, 'errored');
168
+ this.#propagateStateChange(instance, 'errored');
191
169
  throw err;
192
170
  }
193
171
 
194
172
  // this looks silly but it prevents the try / catch to catch errors that could
195
173
  // be triggered by the propagate status callback, putting the plugin in errored state
196
174
  if (!errored) {
197
- this._propagateStateChange(instance, 'started');
175
+ this.#propagateStateChange(instance, 'started');
198
176
  }
199
177
  }
200
178
 
201
179
  return instance;
202
180
  }
203
181
 
182
+ /**
183
+ * Register a plugin into the manager.
184
+ *
185
+ * _A plugin must always be registered both on client-side and on server-side_
186
+ *
187
+ * Refer to the plugin documentation to check its options and proper way of
188
+ * registering it.
189
+ *
190
+ * @param {string} id - Unique id of the plugin. Enables the registration of the
191
+ * same plugin factory under different ids.
192
+ * @param {Function} ctor - The class returned by the plugin factory method.
193
+ * @param {object} [options={}] - Options to configure the plugin.
194
+ * @param {array} [deps=[]] - List of plugins' names the plugin depends on, i.e.
195
+ * the plugin initialization will begin only after the plugins it depends on are
196
+ * fully started themselves.
197
+ * @see {@link ClientPluginManager#register}
198
+ * @see {@link ServerPluginManager#register}
199
+ * @example
200
+ * // client-side
201
+ * client.pluginManager.register('user-defined-id', pluginFactory);
202
+ * // server-side
203
+ * server.pluginManager.register('user-defined-id', pluginFactory);
204
+ */
205
+ register(id, ctor, options = {}, deps = []) {
206
+ // For now we don't allow to register a plugin after `client|server.init()`.
207
+ // This is subject to change in the future as we may want to dynamically
208
+ // register new plugins during application lifetime.
209
+ if (this.#node.status === 'inited') {
210
+ throw new Error(`[soundworks.PluginManager] Cannot register plugin (${id}) after "client.init()"`);
211
+ }
212
+
213
+ if (!isString(id)) {
214
+ throw new Error(`[soundworks.PluginManager] Invalid argument, "pluginManager.register" first argument should be a string`);
215
+ }
216
+
217
+ if (!isPlainObject(options)) {
218
+ throw new Error(`[soundworks.PluginManager] Invalid argument, "pluginManager.register" third optionnal argument should be an object`);
219
+ }
220
+
221
+ if (!Array.isArray(deps)) {
222
+ throw new Error(`[soundworks.PluginManager] Invalid argument, "pluginManager.register" fourth optionnal argument should be an array`);
223
+ }
224
+
225
+ if (this[kPluginManagerInstances].has(id)) {
226
+ throw new Error(`[soundworks:PluginManager] Plugin "${id}" already registered`);
227
+ }
228
+
229
+ // We instanciate the plugin here, so that a plugin can register another one
230
+ // in its own constructor.
231
+ //
232
+ // The dependencies must be created first, so that the instance can call
233
+ // addDependency in its constructor
234
+ this.#dependencies.set(id, deps);
235
+
236
+ const instance = new ctor(this.#node, id, options);
237
+ this[kPluginManagerInstances].set(id, instance);
238
+ }
239
+
240
+ /**
241
+ * Manually add a dependency to a given plugin.
242
+ *
243
+ * Usefull to require a plugin within a plugin
244
+ */
245
+ addDependency(pluginId, dependencyId) {
246
+ const deps = this.#dependencies.get(pluginId);
247
+ deps.push(dependencyId);
248
+ }
249
+
250
+ /**
251
+ * Returns the list of the registered plugins ids
252
+ * @returns {string[]}
253
+ */
254
+ getRegisteredPlugins() {
255
+ return Array.from(this[kPluginManagerInstances].keys());
256
+ }
257
+
204
258
  /**
205
259
  * Propagate a notification each time a plugin is updated (status or inner state).
206
260
  * The callback will receive the list of all plugins as first parameter, and the
@@ -208,10 +262,8 @@ class BasePluginManager {
208
262
  *
209
263
  * _In most cases, you should not have to rely on this method._
210
264
  *
211
- * @param {client.PluginManager~onStateChangeCallback|server.PluginManager~onStateChangeCallback} callback
212
- * Callback to be executed on state change
213
- * @param {client.PluginManager~deleteOnStateChangeCallback|client.PluginManager~deleteOnStateChangeCallback}
214
- * Function to execute to listening for changes.
265
+ * @param {pluginManagerOnStateChangeCallback} callback - Callback to execute on state change
266
+ * @returns {pluginManagerDeleteOnStateChangeCallback} - Clear the subscription when executed
215
267
  * @example
216
268
  * const unsubscribe = client.pluginManager.onStateChange(pluginList, initiator => {
217
269
  * // log the current status of all plugins
@@ -227,24 +279,8 @@ class BasePluginManager {
227
279
  * unsubscribe();
228
280
  */
229
281
  onStateChange(callback) {
230
- this._onStateChangeCallbacks.add(callback);
231
- return () => this._onStateChangeCallbacks.delete(callback);
232
- }
233
-
234
- /** @private */
235
- _propagateStateChange(instance = null, status = null) {
236
- if (instance !== null) {
237
- // status is null if wew forward some inner state change from the instance
238
- if (status !== null) {
239
- instance.status = status;
240
- }
241
-
242
- const fullState = Object.fromEntries(this._instances);
243
- this._onStateChangeCallbacks.forEach(callback => callback(fullState, instance));
244
- } else {
245
- const fullState = Object.fromEntries(this._instances);
246
- this._onStateChangeCallbacks.forEach(callback => callback(fullState, null));
247
- }
282
+ this.#onStateChangeCallbacks.add(callback);
283
+ return () => this.#onStateChangeCallbacks.delete(callback);
248
284
  }
249
285
  }
250
286