@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.
- package/.eslintrc +0 -1
- package/.jsdoc +57 -0
- package/CHANGELOG.md +33 -0
- package/README.md +17 -51
- package/package.json +24 -31
- package/src/client/Client.js +13 -7
- package/src/client/Context.js +12 -13
- package/src/client/ContextManager.js +1 -1
- package/src/client/PluginManager.js +1 -1
- package/src/client/SharedStateCollection.js +38 -0
- package/src/client/Socket.js +80 -44
- package/src/common/BasePlugin.js +31 -21
- package/src/common/BasePluginManager.js +34 -19
- package/src/common/BaseSharedState.js +304 -79
- package/src/common/BaseSharedStateCollection.js +324 -0
- package/src/common/BaseStateManager.js +353 -49
- package/src/common/BatchedTransport.js +73 -0
- package/src/common/ParameterBag.js +92 -30
- package/src/common/PromiseStore.js +53 -0
- package/src/common/SharedStatePrivate.js +87 -46
- package/src/common/constants.js +14 -1
- package/src/common/sockets-utils.js +3 -4
- package/src/server/Client.js +7 -2
- package/src/server/ContextManager.js +4 -4
- package/src/server/PluginManager.js +1 -1
- package/src/server/Server.js +263 -43
- package/src/server/SharedState.js +0 -1
- package/src/server/SharedStateCollection.js +39 -0
- package/src/server/Socket.js +55 -49
- package/src/server/Sockets.js +33 -11
- package/src/server/StateManager.js +187 -100
- package/src/server/audit-network-latency.worker.js +16 -4
- package/src/server/crypto.js +37 -0
- package/src/common/promise-store.js +0 -25
- package/src/common/utils.js +0 -22
- package/tsconfig.json +0 -21
package/src/client/Socket.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
136
|
-
this.
|
|
137
|
-
|
|
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'
|
|
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
|
-
|
|
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
|
|
package/src/common/BasePlugin.js
CHANGED
|
@@ -1,30 +1,14 @@
|
|
|
1
|
-
import merge from 'lodash.
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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 '
|
|
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.
|
|
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.
|
|
72
|
-
throw new Error(`[soundworks:PluginManager] Plugin "${id}"
|
|
70
|
+
if (this._instances.has(id)) {
|
|
71
|
+
throw new Error(`[soundworks:PluginManager] Plugin "${id}" already registered`);
|
|
73
72
|
}
|
|
74
73
|
|
|
75
|
-
|
|
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.
|
|
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 [
|
|
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.
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
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;
|