@soundworks/core 4.0.0-alpha.2 → 4.0.0-alpha.21

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.
@@ -1,13 +1,19 @@
1
+ import { isBrowser } from '@ircam/sc-utils';
1
2
  import WebSocket from 'isomorphic-ws';
2
3
 
4
+ import {
5
+ PING_INTERVAL,
6
+ PING_LATENCY_TOLERANCE,
7
+ PING_MESSAGE,
8
+ PONG_MESSAGE,
9
+ } from '../common/constants.js';
10
+ import logger from '../common/logger.js';
3
11
  import {
4
12
  packBinaryMessage,
5
13
  unpackBinaryMessage,
6
14
  packStringMessage,
7
15
  unpackStringMessage,
8
16
  } from '../common/sockets-utils.js';
9
- import logger from '../common/logger.js';
10
- import { isBrowser } from '../common/utils.js';
11
17
 
12
18
  // WebSocket events:
13
19
  //
@@ -64,6 +70,8 @@ class Socket {
64
70
  this._stringListeners = new Map();
65
71
  /** @private */
66
72
  this._binaryListeners = new Map();
73
+ /** @private */
74
+ this._heartbeatId = null;
67
75
  }
68
76
 
69
77
  /**
@@ -110,7 +118,11 @@ class Socket {
110
118
  };
111
119
  }
112
120
 
113
- const queryParams = `role=${role}&key=${key}`;
121
+ let queryParams = `role=${role}&key=${key}`;
122
+
123
+ if (config.token) {
124
+ queryParams += `&token=${config.token}`;
125
+ }
114
126
 
115
127
  // ----------------------------------------------------------
116
128
  // init string socket
@@ -126,39 +138,78 @@ class Socket {
126
138
  ws.addEventListener('open', connectEvent => {
127
139
  // parse incoming messages for pubsub
128
140
  this.ws = ws;
141
+
142
+ // Heartbeat ping / pong check, to detect if connection has been
143
+ // broken but not detected by the client
144
+ // @note (2024-04): unstable and creates more issues than it solves, disable
145
+ // for now and review later
146
+ // const heartbeat = () => {
147
+ // clearTimeout(this._heartbeatId);
148
+
149
+ // this._heartbeatId = setTimeout(() => {
150
+ // console.log('> HEARTBEAT missed, closing...');
151
+ // this.terminate();
152
+ // }, PING_INTERVAL + PING_LATENCY_TOLERANCE);
153
+ // };
154
+
155
+ // heartbeat();
156
+
129
157
  this.ws.addEventListener('message', e => {
158
+ if (e.data === PING_MESSAGE) {
159
+ // heartbeat();
160
+ this.ws.send(PONG_MESSAGE);
161
+ // do not propagate ping / pong messages
162
+ return;
163
+ }
164
+
130
165
  const [channel, args] = unpackStringMessage(e.data);
131
166
  this._emit(false, channel, ...args);
132
167
  });
133
168
 
134
169
  // broadcast all `WebSocket` native events
135
- ['close', 'error', 'upgrade', 'message'].forEach(eventName => {
136
- this.ws.addEventListener(eventName, (e) => {
137
- this._emit(false, eventName, e);
138
- });
170
+ this.ws.addEventListener('close', (e) => {
171
+ clearTimeout(this._heartbeatId);
172
+ this._emit(false, 'close', e);
173
+ });
174
+
175
+ this.ws.addEventListener('error', (e) => {
176
+ clearTimeout(this._heartbeatId);
177
+ this._emit(false, 'error', e);
178
+ });
179
+
180
+ this.ws.addEventListener('upgrade', (e) => {
181
+ this._emit(false, 'upgrade', e);
182
+ });
183
+
184
+ this.ws.addEventListener('message', (e) => {
185
+ this._emit(false, 'message', e);
139
186
  });
140
187
 
141
188
  // forward open event
142
189
  this._emit(false, 'open', connectEvent);
190
+
143
191
  // continue with raw socket
144
192
  resolve();
145
193
  });
146
194
 
147
195
  // cf. https://github.com/collective-soundworks/soundworks/issues/17
148
196
  ws.addEventListener('error', e => {
149
- if (e.type === 'error' && e.error.code === 'ECONNREFUSED') {
150
- if (!connectionRefusedLogged) {
151
- logger.log('[soundworks.Socket] Connection refused, waiting for the server to start');
152
- connectionRefusedLogged = true;
153
- }
154
-
197
+ if (e.type === 'error') {
155
198
  if (ws.terminate) {
156
199
  ws.terminate();
157
200
  } else {
158
201
  ws.close();
159
202
  }
160
203
 
161
- setTimeout(trySocket, 1000);
204
+ // for node clients, retry connection
205
+ if (e.error && e.error.code === 'ECONNREFUSED') {
206
+ if (!connectionRefusedLogged) {
207
+ logger.log('[soundworks.Socket] Connection refused, waiting for the server to start');
208
+ connectionRefusedLogged = true;
209
+ }
210
+
211
+ setTimeout(trySocket, 1000);
212
+ }
162
213
  }
163
214
  });
164
215
  };
@@ -166,36 +217,6 @@ class Socket {
166
217
  trySocket();
167
218
  });
168
219
 
169
- // @todo - review/fix
170
- // - the `ws.on` method only exists on node implementation, and the 'ping'
171
- // message is not received on addEventListener
172
- // - there seems to be no way to access the ping event in browsers...
173
- //
174
- // let pingTimeoutId = null;
175
- // const pingInterval = config.env.websockets.pingInterval;
176
- // // detect broken connection
177
- // // cf. https://github.com/websockets/ws#how-to-detect-and-close-broken-connections
178
- // const heartbeat = () => {
179
- // try {
180
- // console.log('ping received');
181
- // clearTimeout(pingTimeoutId);
182
-
183
- // // pingTimeoutId = setTimeout(() => {
184
- // // console.log('terminate');
185
- // // this.terminate();
186
- // // }, pingInterval + 2000);
187
- // } catch (err) {
188
- // console.error(err);
189
- // }
190
- // };
191
- //
192
- // this.ws.on('ping', heartbeat);
193
- // this.ws.addEventListener('close', () => {
194
- // clearTimeout(pingTimeoutId);
195
- // });
196
-
197
- // heartbeat();
198
-
199
220
  // ----------------------------------------------------------
200
221
  // init binary socket
201
222
  // ----------------------------------------------------------
@@ -234,15 +255,30 @@ class Socket {
234
255
  * Removes all listeners and immediately close the two sockets. Is automatically
235
256
  * called on `client.stop()`
236
257
  *
258
+ * Is also called when a disconnection is detected by the heartbeat (note that
259
+ * in this case, the launcher will call `client.stop()` but the listeners are
260
+ * already cleared so the event will be trigerred only once)
261
+ *
237
262
  * @private
238
263
  */
239
264
  async terminate() {
265
+ const closeListeners = this._stringListeners.get('close');
266
+ const closeBinaryListeners = this._binaryListeners.get('close');
267
+
240
268
  this.removeAllListeners();
241
269
  this.removeAllBinaryListeners();
242
270
 
243
271
  this.ws.close();
244
272
  this.binaryWs.close();
245
273
 
274
+ if (closeListeners) {
275
+ closeListeners.forEach(listener => listener());
276
+ }
277
+
278
+ if (closeBinaryListeners) {
279
+ closeBinaryListeners.forEach(listener => listener());
280
+ }
281
+
246
282
  return Promise.resolve();
247
283
  }
248
284
 
@@ -1,30 +1,14 @@
1
- import merge from 'lodash.merge';
1
+ import merge from 'lodash/merge.js';
2
2
 
3
3
  /**
4
4
  * @private
5
5
  */
6
6
  class BasePlugin {
7
7
  constructor(id) {
8
- /**
9
- * User defined ID of the plugin.
10
- *
11
- * @type {string}
12
- * @see {@link client.PluginManager#register}
13
- * @see {@link server.PluginManager#register}
14
- */
15
- this.id = id;
16
-
17
- /**
18
- * Type of the plugin, i.e. the ClassName.
19
- *
20
- * Usefull to do perform some logic based on certain types of plugins without
21
- * knowing under which `id` they have been registered. (e.g. creating some generic
22
- * views, etc.)
23
- *
24
- * @type {string}
25
- * @readonly
26
- */
27
- this.type = this.constructor.name;
8
+ /** @private */
9
+ this._id = id;
10
+ /** @private */
11
+ this._type = this.constructor.name;
28
12
 
29
13
  /**
30
14
  * Options of the plugin.
@@ -58,6 +42,32 @@ class BasePlugin {
58
42
  this._onStateChangeCallbacks = new Set();
59
43
  }
60
44
 
45
+ /**
46
+ * User defined ID of the plugin.
47
+ *
48
+ * @type {string}
49
+ * @readonly
50
+ * @see {@link client.PluginManager#register}
51
+ * @see {@link server.PluginManager#register}
52
+ */
53
+ get id() {
54
+ return this._id;
55
+ }
56
+
57
+ /**
58
+ * Type of the plugin, i.e. the ClassName.
59
+ *
60
+ * Usefull to do perform some logic based on certain types of plugins without
61
+ * knowing under which `id` they have been registered. (e.g. creating some generic
62
+ * views, etc.)
63
+ *
64
+ * @type {string}
65
+ * @readonly
66
+ */
67
+ get type() {
68
+ return this._type;
69
+ }
70
+
61
71
  /**
62
72
  * Start the plugin. This method is automatically called during the client or
63
73
  * server `init()` lifecyle step. After `start()` is fulfilled the plugin should
@@ -1,7 +1,6 @@
1
- import isPlainObject from 'is-plain-obj';
1
+ import { isPlainObject, isString } from '@ircam/sc-utils';
2
2
 
3
3
  import logger from './logger.js';
4
- import { isString } from './utils.js';
5
4
 
6
5
  /**
7
6
  * Shared functionnality between server-side and client-size plugin manager
@@ -14,7 +13,7 @@ class BasePluginManager {
14
13
  /** @private */
15
14
  this._node = node;
16
15
  /** @private */
17
- this._registeredPlugins = new Map();
16
+ this._dependencies = new Map();
18
17
  /** @private */
19
18
  this._instances = new Map();
20
19
  /** @private */
@@ -68,11 +67,29 @@ class BasePluginManager {
68
67
  throw new Error(`[soundworks.PluginManager] Invalid argument, "pluginManager.register" fourth optionnal argument should be an array`);
69
68
  }
70
69
 
71
- if (this._registeredPlugins.has(id)) {
72
- throw new Error(`[soundworks:PluginManager] Plugin "${id}" of type "${ctor.name}" already registered`);
70
+ if (this._instances.has(id)) {
71
+ throw new Error(`[soundworks:PluginManager] Plugin "${id}" already registered`);
73
72
  }
74
73
 
75
- this._registeredPlugins.set(id, { ctor, options, deps });
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
+ }
84
+
85
+ /**
86
+ * Manually add a dependency to a given plugin. Usefull to require a plugin
87
+ * within a plugin
88
+ *
89
+ */
90
+ addDependency(pluginId, dependencyId) {
91
+ const deps = this._dependencies.get(pluginId);
92
+ deps.push(dependencyId);
76
93
  }
77
94
 
78
95
  /**
@@ -80,7 +97,7 @@ class BasePluginManager {
80
97
  * @returns {string[]}
81
98
  */
82
99
  getRegisteredPlugins() {
83
- return Array.from(this._registeredPlugins.keys());
100
+ return Array.from(this._instances.keys());
84
101
  }
85
102
 
86
103
  /**
@@ -97,23 +114,21 @@ class BasePluginManager {
97
114
 
98
115
  this.status = 'inited';
99
116
  // instanciate all plugins
100
- for (let [id, { ctor, options }] of this._registeredPlugins.entries()) {
101
- const instance = new ctor(this._node, id, options);
102
- this._instances.set(id, instance);
117
+ for (let [_id, instance] of this._instances.entries()) {
103
118
  instance.onStateChange(_values => this._propagateStateChange(instance));
104
119
  }
105
120
 
106
121
  // propagate all 'idle' statuses before start
107
122
  this._propagateStateChange();
108
123
 
109
- const promises = Array.from(this._registeredPlugins.keys()).map(id => this.unsafeGet(id));
124
+ const promises = Array.from(this._instances.keys()).map(id => this.unsafeGet(id));
110
125
 
111
126
  try {
112
127
  await Promise.all(promises);
113
128
  this.status = 'started';
114
- } catch(err) {
129
+ } catch (err) {
115
130
  this.status = 'errored';
116
- throw new Error(err);
131
+ throw err; // throw initial error
117
132
  }
118
133
  }
119
134
 
@@ -126,8 +141,8 @@ class BasePluginManager {
126
141
 
127
142
  /**
128
143
  * Retrieve an fully started instance of a registered plugin, without checking
129
- * that the pluginManager is started. This is important for starting the plugin
130
- * manager itself.
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
131
146
  *
132
147
  * @private
133
148
  */
@@ -136,7 +151,7 @@ class BasePluginManager {
136
151
  throw new Error(`[soundworks.PluginManager] Invalid argument, "pluginManager.get(name)" argument should be a string`);
137
152
  }
138
153
 
139
- if (!this._registeredPlugins.has(id)) {
154
+ if (!this._instances.has(id)) {
140
155
  throw new Error(`[soundworks:PluginManager] Cannot get plugin "${id}", plugin is not registered`);
141
156
  }
142
157
 
@@ -145,7 +160,7 @@ class BasePluginManager {
145
160
  // to dynamically register and launch plugins at runtime.
146
161
  //
147
162
  // if (!this._instances.has(id)) {
148
- // const { ctor, options } = this._registeredPlugins.get(id);
163
+ // const { ctor, options } = this._dependencies.get(id);
149
164
  // const instance = new ctor(this._node, id, options);
150
165
  // this._instances.set(id, instance);
151
166
  // }
@@ -153,7 +168,7 @@ class BasePluginManager {
153
168
  const instance = this._instances.get(id);
154
169
 
155
170
  // recursively get the dependency chain
156
- const { deps } = this._registeredPlugins.get(id);
171
+ const deps = this._dependencies.get(id);
157
172
  const promises = deps.map(id => this.unsafeGet(id));
158
173
 
159
174
  await Promise.all(promises);
@@ -170,7 +185,7 @@ class BasePluginManager {
170
185
  this._instanceStartPromises.set(id, startPromise);
171
186
 
172
187
  await startPromise;
173
- } catch(err) {
188
+ } catch (err) {
174
189
  errored = true;
175
190
  this._propagateStateChange(instance, 'errored');
176
191
  throw err;