@soundworks/core 4.0.0-alpha.1 → 4.0.0-alpha.11

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,3 +1,5 @@
1
+ import { isBrowser } from '@ircam/sc-utils';
2
+ import fetch from 'cross-fetch';
1
3
  import WebSocket from 'isomorphic-ws';
2
4
 
3
5
  import {
@@ -7,7 +9,6 @@ import {
7
9
  unpackStringMessage,
8
10
  } from '../common/sockets-utils.js';
9
11
  import logger from '../common/logger.js';
10
- import { isBrowser } from '../common/utils.js';
11
12
 
12
13
  // WebSocket events:
13
14
  //
@@ -28,35 +29,34 @@ import { isBrowser } from '../common/utils.js';
28
29
  /**
29
30
  * The Socket class is a simple publish / subscribe wrapper built on top of the
30
31
  * [isomorphic-ws](https://github.com/heineiuo/isomorphic-ws) library.
31
- * An instance of `Socket` is automatically created by the `soundworks.Client`.
32
+ * An instance of `Socket` is automatically created by the `soundworks.Client`
33
+ * (see {@link client.Client#socket}).
32
34
  *
33
- * _Important: In most cases, you should consider using a {@link client.SharedState} rather than
34
- * sending messages directly through the sockets._
35
+ * _Important: In most cases, you should consider using a {@link client.SharedState}
36
+ * rather than directly using the sockets._
35
37
  *
36
38
  * The Socket class concurrently opens two different WebSockets:
37
- * - a socket configured with `binaryType = 'string'` for JSON compatible string
38
- * messages.
39
- * - a socket configured with `binaryType=arraybuffer` for efficient streaming
39
+ * - a socket configured with `binaryType = 'blob'` for JSON compatible data
40
+ * types (i.e. string, number, boolean, object, array and null).
41
+ * - a socket configured with `binaryType= 'arraybuffer'` for efficient streaming
40
42
  * of binary data.
41
43
  *
42
44
  * Both sockets re-emits all "native" ws events ('open', 'upgrade', 'close', 'error'
43
45
  * and 'message'.
44
46
  *
45
- * See {@link client.Client#socket}
46
- *
47
47
  * @memberof client
48
48
  * @hideconstructor
49
49
  */
50
50
  class Socket {
51
51
  constructor() {
52
52
  /**
53
- * WebSocket instance w/ string protocol, i.e. `binaryType = 'string'`.
53
+ * WebSocket instance configured with `binaryType = 'blob'`.
54
54
  *
55
55
  * @private
56
56
  */
57
57
  this.ws = null;
58
58
  /**
59
- * WebSocket instance w/ binary protocol, i.e. `binaryType = 'arraybuffer'`.
59
+ * WebSocket instance configured with `binaryType = 'arraybuffer'`.
60
60
  *
61
61
  * @private
62
62
  */
@@ -111,7 +111,11 @@ class Socket {
111
111
  };
112
112
  }
113
113
 
114
- const queryParams = `role=${role}&key=${key}`;
114
+ let queryParams = `role=${role}&key=${key}`;
115
+
116
+ if (config.token) {
117
+ queryParams += `&token=${config.token}`;
118
+ }
115
119
 
116
120
  // ----------------------------------------------------------
117
121
  // init string socket
@@ -147,19 +151,22 @@ class Socket {
147
151
 
148
152
  // cf. https://github.com/collective-soundworks/soundworks/issues/17
149
153
  ws.addEventListener('error', e => {
150
- if (e.type === 'error' && e.error.code === 'ECONNREFUSED') {
151
- if (!connectionRefusedLogged) {
152
- logger.log('[soundworks.Socket] Connection refused, waiting for the server to start');
153
- connectionRefusedLogged = true;
154
- }
155
-
154
+ if (e.type === 'error') {
156
155
  if (ws.terminate) {
157
156
  ws.terminate();
158
157
  } else {
159
158
  ws.close();
160
159
  }
161
160
 
162
- setTimeout(trySocket, 1000);
161
+ // for node clients, retry connection
162
+ if (e.error && e.error.code === 'ECONNREFUSED') {
163
+ if (!connectionRefusedLogged) {
164
+ logger.log('[soundworks.Socket] Connection refused, waiting for the server to start');
165
+ connectionRefusedLogged = true;
166
+ }
167
+
168
+ setTimeout(trySocket, 1000);
169
+ }
163
170
  }
164
171
  });
165
172
  };
@@ -231,6 +238,22 @@ class Socket {
231
238
  return Promise.resolve();
232
239
  }
233
240
 
241
+ /**
242
+ * Removes all listeners and immediately close the two sockets. Is automatically
243
+ * called on `client.stop()`
244
+ *
245
+ * @private
246
+ */
247
+ async terminate() {
248
+ this.removeAllListeners();
249
+ this.removeAllBinaryListeners();
250
+
251
+ this.ws.close();
252
+ this.binaryWs.close();
253
+
254
+ return Promise.resolve();
255
+ }
256
+
234
257
  /**
235
258
  * @param {boolean} binary - Emit to either the string or binary socket.
236
259
  * @param {string} channel - Channel name.
@@ -293,10 +316,11 @@ class Socket {
293
316
  }
294
317
 
295
318
  /**
296
- * Send JSON compatible messages on a given channel.
319
+ * Send messages with JSON compatible data types on a given channel.
297
320
  *
298
- * @param {string} channel - Channel name of the message.
299
- * @param {...*} args - Message list, as many as needed, of any serializable type).
321
+ * @param {string} channel - Channel name.
322
+ * @param {...*} args - Payload of the message. As many arguments as needed, of
323
+ * JSON compatible data types (i.e. string, number, boolean, object, array and null).
300
324
  */
301
325
  send(channel, ...args) {
302
326
  const msg = packStringMessage(channel, ...args);
@@ -304,10 +328,12 @@ class Socket {
304
328
  }
305
329
 
306
330
  /**
307
- * Listen to JSON compatible messages on a given channel.
331
+ * Listen messages with JSON compatible data types on a given channel.
308
332
  *
309
- * @param {string} channel - Channel name of the message.
310
- * @param {Function} callback - Callback to execute when a message is received.
333
+ * @param {string} channel - Channel name.
334
+ * @param {Function} callback - Callback to execute when a message is received,
335
+ * arguments of the callback function will match the arguments sent using the
336
+ * {@link server.Socket#send} method.
311
337
  */
312
338
  addListener(channel, callback) {
313
339
  this._addListener(this._stringListeners, channel, callback);
@@ -316,7 +342,7 @@ class Socket {
316
342
  /**
317
343
  * Remove a listener from JSON compatible messages on a given channel.
318
344
  *
319
- * @param {string} channel - Channel name of the message.
345
+ * @param {string} channel - Channel name.
320
346
  * @param {Function} callback - Callback to remove.
321
347
  */
322
348
  removeListener(channel, callback) {
@@ -324,9 +350,9 @@ class Socket {
324
350
  }
325
351
 
326
352
  /**
327
- * Remove all listeners from JSON compatible messages on a given channel.
353
+ * Remove all listeners of messages with JSON compatible data types.
328
354
  *
329
- * @param {string} channel - Channel name of the message.
355
+ * @param {string} channel - Channel name.
330
356
  */
331
357
  removeAllListeners(channel = null) {
332
358
  this._removeAllListeners(this._stringListeners, channel);
@@ -335,7 +361,7 @@ class Socket {
335
361
  /**
336
362
  * Send binary messages on a given channel.
337
363
  *
338
- * @param {string} channel - Channel name of the message.
364
+ * @param {string} channel - Channel name.
339
365
  * @param {TypedArray} typedArray - Binary data to be sent.
340
366
  */
341
367
  sendBinary(channel, typedArray) {
@@ -344,9 +370,9 @@ class Socket {
344
370
  }
345
371
 
346
372
  /**
347
- * Listen binary messages on a given channel.
373
+ * Listen binary messages on a given channel
348
374
  *
349
- * @param {string} channel - Channel name of the message.
375
+ * @param {string} channel - Channel name.
350
376
  * @param {Function} callback - Callback to execute when a message is received.
351
377
  */
352
378
  addBinaryListener(channel, callback) {
@@ -354,36 +380,23 @@ class Socket {
354
380
  }
355
381
 
356
382
  /**
357
- * Remove a listener from binary compatible messages on a given channel
383
+ * Remove a listener of binary compatible messages from a given channel
358
384
  *
359
- * @param {string} channel - Channel of the message.
360
- * @param {Function} callback - Callback to cancel.
385
+ * @param {string} channel - Channel name.
386
+ * @param {Function} callback - Callback to remove.
361
387
  */
362
388
  removeBinaryListener(channel, callback) {
363
389
  this._removeListener(this._binaryListeners, channel, callback);
364
390
  }
365
391
 
366
392
  /**
367
- * Remove all listeners from binary compatible messages on a given channel
393
+ * Remove all listeners of binary compatible messages on a given channel
368
394
  *
369
395
  * @param {string} channel - Channel of the message.
370
396
  */
371
397
  removeAllBinaryListeners(channel = null) {
372
398
  this._removeAllListeners(this._binaryListeners, channel);
373
399
  }
374
-
375
- /**
376
- * Removes all listeners and immediately close the two sockets.
377
- */
378
- async terminate() {
379
- this.removeAllListeners();
380
- this.removeAllBinaryListeners();
381
-
382
- this.ws.close();
383
- this.binaryWs.close();
384
-
385
- return Promise.resolve();
386
- }
387
400
  }
388
401
 
389
402
  export default Socket;
@@ -1,36 +1,3 @@
1
- /**
2
- * Configuration object for a client running in a browser runtime.
3
- *
4
- * @typedef BrowserClientConfig
5
- * @memberof client
6
- * @type {object}
7
- * @property {string} role - Role of the client in the application (e.g. 'player', 'controller').
8
- * @property {object} [app] - Application configration object.
9
- * @property {string} [app.name=''] - Name of the application.
10
- * @property {string} [app.author=''] - Name of the author.
11
- * @property {object} [env] - Environment configration object.
12
- * @property {string} [env.websockets={}] - Configuration options for websockets.
13
- * @property {string} [env.subpath=''] - If running behind a proxy, path to the application.
14
- */
15
-
16
- /**
17
- * Configuration object for a client running in a node runtime.
18
- *
19
- * @typedef NodeClientConfig
20
- * @memberof client
21
- * @type {object}
22
- * @property {string} role - Role of the client in the application (e.g. 'player', 'controller').
23
- * @property {object} [app] - Application configration object.
24
- * @property {string} [app.name=''] - Name of the application.
25
- * @property {string} [app.author=''] - Name of the author.
26
- * @property {object} env - Environment configration object.
27
- * @property {boolean} env.serverAddress - Domain name or IP of the server.
28
- * @property {boolean} env.useHttps - Define is the server run in http or in https.
29
- * @property {boolean} env.port - Port on which the server is listening.
30
- * @property {string} [env.websockets={}] - Configuration options for websockets.
31
- * @property {string} [env.subpath=''] - If running behind a proxy, path to the application.
32
- */
33
-
34
1
  /**
35
2
  * Client-side part of the `soundworks` framework.
36
3
  *
@@ -5,26 +5,10 @@ import merge from 'lodash.merge';
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,37 @@ 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);
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());
76
101
  }
77
102
 
78
103
  /**
@@ -89,26 +114,25 @@ class BasePluginManager {
89
114
 
90
115
  this.status = 'inited';
91
116
  // instanciate all plugins
92
- for (let [id, { ctor, options }] of this._registeredPlugins.entries()) {
93
- const instance = new ctor(this._node, id, options);
94
- this._instances.set(id, instance);
117
+ for (let [id, instance] of this._instances.entries()) {
95
118
  instance.onStateChange(_values => this._propagateStateChange(instance));
96
119
  }
97
120
 
98
121
  // propagate all 'idle' statuses before start
99
122
  this._propagateStateChange();
100
123
 
101
- const promises = Array.from(this._registeredPlugins.keys()).map(id => this.getUnsafe(id));
124
+ const promises = Array.from(this._instances.keys()).map(id => this.unsafeGet(id));
102
125
 
103
126
  try {
104
127
  await Promise.all(promises);
105
128
  this.status = 'started';
106
- } catch(err) {
129
+ } catch (err) {
107
130
  this.status = 'errored';
108
- throw new Error(err);
131
+ throw err; // throw initial error
109
132
  }
110
133
  }
111
134
 
135
+ /** @private */
112
136
  async stop() {
113
137
  for (let instance of this._instances.values()) {
114
138
  await instance.stop();
@@ -117,17 +141,17 @@ class BasePluginManager {
117
141
 
118
142
  /**
119
143
  * Retrieve an fully started instance of a registered plugin, without checking
120
- * that the pluginManager is started. This is important for starting the plugin
121
- * 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
122
146
  *
123
147
  * @private
124
148
  */
125
- async getUnsafe(id) {
149
+ async unsafeGet(id) {
126
150
  if (!isString(id)) {
127
151
  throw new Error(`[soundworks.PluginManager] Invalid argument, "pluginManager.get(name)" argument should be a string`);
128
152
  }
129
153
 
130
- if (!this._registeredPlugins.has(id)) {
154
+ if (!this._instances.has(id)) {
131
155
  throw new Error(`[soundworks:PluginManager] Cannot get plugin "${id}", plugin is not registered`);
132
156
  }
133
157
 
@@ -136,7 +160,7 @@ class BasePluginManager {
136
160
  // to dynamically register and launch plugins at runtime.
137
161
  //
138
162
  // if (!this._instances.has(id)) {
139
- // const { ctor, options } = this._registeredPlugins.get(id);
163
+ // const { ctor, options } = this._dependencies.get(id);
140
164
  // const instance = new ctor(this._node, id, options);
141
165
  // this._instances.set(id, instance);
142
166
  // }
@@ -144,8 +168,8 @@ class BasePluginManager {
144
168
  const instance = this._instances.get(id);
145
169
 
146
170
  // recursively get the dependency chain
147
- const { deps } = this._registeredPlugins.get(id);
148
- const promises = deps.map(id => this.getUnsafe(id));
171
+ const deps = this._dependencies.get(id);
172
+ const promises = deps.map(id => this.unsafeGet(id));
149
173
 
150
174
  await Promise.all(promises);
151
175
 
@@ -161,7 +185,7 @@ class BasePluginManager {
161
185
  this._instanceStartPromises.set(id, startPromise);
162
186
 
163
187
  await startPromise;
164
- } catch(err) {
188
+ } catch (err) {
165
189
  errored = true;
166
190
  this._propagateStateChange(instance, 'errored');
167
191
  throw err;