@soundworks/core 4.0.0-alpha.2 → 4.0.0-alpha.20
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 +26 -0
- package/README.md +17 -51
- package/package.json +24 -31
- package/src/client/Client.js +11 -5
- 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 +60 -44
- package/src/common/BasePlugin.js +31 -21
- package/src/common/BasePluginManager.js +34 -19
- package/src/common/BaseSharedState.js +201 -68
- package/src/common/BaseSharedStateCollection.js +279 -0
- package/src/common/BaseStateManager.js +257 -36
- package/src/common/BatchedTransport.js +66 -0
- package/src/common/ParameterBag.js +92 -30
- package/src/common/PromiseStore.js +48 -0
- package/src/common/SharedStatePrivate.js +52 -43
- 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 +255 -41
- package/src/server/SharedState.js +0 -1
- package/src/server/SharedStateCollection.js +39 -0
- package/src/server/Socket.js +50 -49
- package/src/server/Sockets.js +31 -11
- package/src/server/StateManager.js +162 -90
- 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/server/Socket.js
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import { getTime } from '@ircam/sc-
|
|
1
|
+
import { getTime } from '@ircam/sc-utils';
|
|
2
|
+
import {
|
|
3
|
+
PING_INTERVAL,
|
|
4
|
+
PING_MESSAGE,
|
|
5
|
+
PONG_MESSAGE,
|
|
6
|
+
} from '../common/constants.js';
|
|
2
7
|
import {
|
|
3
8
|
packBinaryMessage,
|
|
4
9
|
unpackBinaryMessage,
|
|
@@ -32,17 +37,7 @@ import {
|
|
|
32
37
|
* @hideconstructor
|
|
33
38
|
*/
|
|
34
39
|
class Socket {
|
|
35
|
-
constructor(ws, binaryWs, rooms, sockets
|
|
36
|
-
/**
|
|
37
|
-
* Configuration object
|
|
38
|
-
*
|
|
39
|
-
* @type {object}
|
|
40
|
-
*/
|
|
41
|
-
this.config = {
|
|
42
|
-
pingInterval: 5 * 1000,
|
|
43
|
-
...options,
|
|
44
|
-
};
|
|
45
|
-
|
|
40
|
+
constructor(ws, binaryWs, rooms, sockets) {
|
|
46
41
|
/**
|
|
47
42
|
* `ws` socket instance configured with `binaryType=blob` (string)
|
|
48
43
|
*
|
|
@@ -81,16 +76,53 @@ class Socket {
|
|
|
81
76
|
this._stringListeners = new Map();
|
|
82
77
|
/** @private */
|
|
83
78
|
this._binaryListeners = new Map();
|
|
79
|
+
/** @private */
|
|
80
|
+
this._heartbeatId = null;
|
|
81
|
+
|
|
82
|
+
// heartbeat system (run only on string socket), adapted from:
|
|
83
|
+
// https://github.com/websockets/ws#how-to-detect-and-close-broken-connections
|
|
84
|
+
/** @private */
|
|
85
|
+
this._isAlive = true;
|
|
86
|
+
let msg = {
|
|
87
|
+
type: 'add-measurement',
|
|
88
|
+
value: {
|
|
89
|
+
ping: 0,
|
|
90
|
+
pong: 0,
|
|
91
|
+
},
|
|
92
|
+
};
|
|
84
93
|
|
|
85
94
|
// ----------------------------------------------------------
|
|
86
|
-
//
|
|
95
|
+
// String socket
|
|
96
|
+
// implements ping/pong behavior
|
|
87
97
|
// ----------------------------------------------------------
|
|
88
98
|
this.ws.addEventListener('message', e => {
|
|
99
|
+
if (e.data === PONG_MESSAGE) {
|
|
100
|
+
this._isAlive = true;
|
|
101
|
+
|
|
102
|
+
msg.value.pong = getTime();
|
|
103
|
+
this.sockets._latencyStatsWorker.postMessage(msg);
|
|
104
|
+
// do not propagate ping / pong messages
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
89
108
|
const [channel, args] = unpackStringMessage(e.data);
|
|
90
109
|
this._emit(false, channel, ...args);
|
|
91
110
|
});
|
|
92
111
|
|
|
93
|
-
|
|
112
|
+
this._heartbeatId = setInterval(() => {
|
|
113
|
+
if (this._isAlive === false) {
|
|
114
|
+
// emit a 'close' event to go trough all the disconnection pipeline
|
|
115
|
+
this._emit(false, 'close');
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
this._isAlive = false;
|
|
120
|
+
msg.value.ping = getTime();
|
|
121
|
+
|
|
122
|
+
this.ws.send(PING_MESSAGE);
|
|
123
|
+
}, PING_INTERVAL);
|
|
124
|
+
|
|
125
|
+
// broadcast all "native" events
|
|
94
126
|
[
|
|
95
127
|
'close',
|
|
96
128
|
'error',
|
|
@@ -107,14 +139,14 @@ class Socket {
|
|
|
107
139
|
});
|
|
108
140
|
|
|
109
141
|
// ----------------------------------------------------------
|
|
110
|
-
//
|
|
142
|
+
// Binary socket
|
|
111
143
|
// ----------------------------------------------------------
|
|
112
144
|
this.binaryWs.addEventListener('message', e => {
|
|
113
145
|
const [channel, data] = unpackBinaryMessage(e.data);
|
|
114
146
|
this._emit(true, channel, data);
|
|
115
147
|
});
|
|
116
148
|
|
|
117
|
-
// broadcast all
|
|
149
|
+
// broadcast all "native" events
|
|
118
150
|
[
|
|
119
151
|
'close',
|
|
120
152
|
'error',
|
|
@@ -129,38 +161,6 @@ class Socket {
|
|
|
129
161
|
this._emit(true, eventName, e.data);
|
|
130
162
|
});
|
|
131
163
|
});
|
|
132
|
-
|
|
133
|
-
// heartbeat system (run only on string socket), adapted from:
|
|
134
|
-
// https://github.com/websockets/ws#how-to-detect-and-close-broken-connections
|
|
135
|
-
this._isAlive = true;
|
|
136
|
-
let msg = {
|
|
137
|
-
type: 'add-measurement',
|
|
138
|
-
value: {
|
|
139
|
-
ping: 0,
|
|
140
|
-
pong: 0,
|
|
141
|
-
},
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
// heartbeat system, only on "regular" socket
|
|
145
|
-
this.ws.on('pong', () => {
|
|
146
|
-
this._isAlive = true;
|
|
147
|
-
|
|
148
|
-
msg.value.pong = getTime();
|
|
149
|
-
this.sockets._latencyStatsWorker.postMessage(msg);
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
this._intervalId = setInterval(() => {
|
|
153
|
-
if (this._isAlive === false) {
|
|
154
|
-
// emit a 'close' event to go trough all the disconnection pipeline
|
|
155
|
-
this._emit(false, 'close');
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
this._isAlive = false;
|
|
160
|
-
msg.value.ping = getTime();
|
|
161
|
-
|
|
162
|
-
this.ws.ping();
|
|
163
|
-
}, this.config.pingInterval);
|
|
164
164
|
}
|
|
165
165
|
|
|
166
166
|
/**
|
|
@@ -171,7 +171,8 @@ class Socket {
|
|
|
171
171
|
*/
|
|
172
172
|
terminate() {
|
|
173
173
|
// clear ping/pong check
|
|
174
|
-
clearInterval(this.
|
|
174
|
+
clearInterval(this._heartbeatId);
|
|
175
|
+
|
|
175
176
|
// clean rooms
|
|
176
177
|
for (let [_key, room] of this.rooms) {
|
|
177
178
|
room.delete(this);
|
package/src/server/Sockets.js
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
import { Worker } from 'node:worker_threads';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { fileURLToPath } from 'node:url';
|
|
4
|
-
|
|
5
|
-
import { WebSocketServer } from 'ws';
|
|
6
|
-
import WebSocket from 'ws';
|
|
7
2
|
import querystring from 'querystring';
|
|
3
|
+
import { default as WebSocket, WebSocketServer } from 'ws';
|
|
8
4
|
|
|
9
5
|
import Socket from './Socket.js';
|
|
6
|
+
import networkLatencyWorker from './audit-network-latency.worker.js';
|
|
10
7
|
|
|
11
|
-
|
|
8
|
+
// this crashes when bundling server to cjs module for Max, fallback to cjs worker
|
|
9
|
+
// const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
|
12
10
|
|
|
13
11
|
/**
|
|
14
12
|
* Manager all {@link server.Socket} instances.
|
|
@@ -45,8 +43,9 @@ class Sockets {
|
|
|
45
43
|
*/
|
|
46
44
|
async start(server, config, onConnectionCallback) {
|
|
47
45
|
// init audit stuff for network latency estimation
|
|
48
|
-
|
|
49
|
-
|
|
46
|
+
// @see note about __dirname
|
|
47
|
+
// const workerPathname = path.join(__dirname, 'audit-network-latency.worker.js');
|
|
48
|
+
this._latencyStatsWorker = new Worker(networkLatencyWorker, { eval: true });
|
|
50
49
|
this._auditState = await server.getAuditState();
|
|
51
50
|
|
|
52
51
|
this._auditState.onUpdate(updates => {
|
|
@@ -69,13 +68,13 @@ class Sockets {
|
|
|
69
68
|
|
|
70
69
|
// init ws server
|
|
71
70
|
this._wss = new WebSocketServer({
|
|
72
|
-
|
|
71
|
+
noServer: true,
|
|
73
72
|
path: `/${config.path}`, // @note - update according to existing config files (aka cosima-apps)
|
|
74
73
|
});
|
|
75
74
|
|
|
76
75
|
this._wss.on('connection', (ws, req) => {
|
|
77
76
|
const queryString = querystring.decode(req.url.split('?')[1]);
|
|
78
|
-
const { role, key } = queryString;
|
|
77
|
+
const { role, key, token } = queryString;
|
|
79
78
|
const binary = !!(parseInt(queryString.binary));
|
|
80
79
|
|
|
81
80
|
if (binary) {
|
|
@@ -96,9 +95,30 @@ class Sockets {
|
|
|
96
95
|
socket.addToRoom('*');
|
|
97
96
|
socket.addToRoom(role);
|
|
98
97
|
|
|
99
|
-
onConnectionCallback(role, socket);
|
|
98
|
+
onConnectionCallback(role, socket, token);
|
|
100
99
|
}
|
|
101
100
|
});
|
|
101
|
+
|
|
102
|
+
// check if client can connect
|
|
103
|
+
server.httpServer.on('upgrade', async (req, socket, head) => {
|
|
104
|
+
const queryString = querystring.decode(req.url.split('?')[1]);
|
|
105
|
+
|
|
106
|
+
const { role, token } = queryString;
|
|
107
|
+
|
|
108
|
+
if (server.isProtected(role)) {
|
|
109
|
+
// we don't have any ip in the upgrade request, so we just check the
|
|
110
|
+
// connection token is pending
|
|
111
|
+
const allowed = server.isValidConnectionToken(token);
|
|
112
|
+
|
|
113
|
+
if (!allowed) {
|
|
114
|
+
socket.destroy('not allowed');
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
this._wss.handleUpgrade(req, socket, head, (ws) => {
|
|
119
|
+
this._wss.emit('connection', ws, req);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
102
122
|
}
|
|
103
123
|
|
|
104
124
|
/**
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import EventEmitter from 'node:events';
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import { idGenerator, isString, isPlainObject } from '@ircam/sc-utils';
|
|
4
|
+
import clonedeep from 'lodash/cloneDeep.js';
|
|
4
5
|
|
|
5
6
|
import BaseStateManager from '../common/BaseStateManager.js';
|
|
7
|
+
import BatchedTransport from '../common/BatchedTransport.js';
|
|
6
8
|
import ParameterBag from '../common/ParameterBag.js';
|
|
7
9
|
import SharedStatePrivate from '../common/SharedStatePrivate.js';
|
|
8
10
|
import {
|
|
@@ -16,16 +18,22 @@ import {
|
|
|
16
18
|
ATTACH_ERROR,
|
|
17
19
|
OBSERVE_REQUEST,
|
|
18
20
|
OBSERVE_RESPONSE,
|
|
21
|
+
OBSERVE_ERROR,
|
|
19
22
|
OBSERVE_NOTIFICATION,
|
|
20
23
|
UNOBSERVE_NOTIFICATION,
|
|
21
24
|
DELETE_SCHEMA,
|
|
25
|
+
GET_SCHEMA_REQUEST,
|
|
26
|
+
GET_SCHEMA_RESPONSE,
|
|
27
|
+
GET_SCHEMA_ERROR,
|
|
22
28
|
PRIVATE_STATES,
|
|
23
29
|
} from '../common/constants.js';
|
|
24
|
-
|
|
30
|
+
|
|
25
31
|
|
|
26
32
|
const generateStateId = idGenerator();
|
|
27
33
|
const generateRemoteId = idGenerator();
|
|
28
34
|
|
|
35
|
+
const kIsObservableState = Symbol('StateManager::isObservableState');
|
|
36
|
+
|
|
29
37
|
/**
|
|
30
38
|
* @typedef {object} server.StateManager~schema
|
|
31
39
|
*
|
|
@@ -313,7 +321,7 @@ class StateManager extends BaseStateManager {
|
|
|
313
321
|
super(localClientId, localTransport);
|
|
314
322
|
|
|
315
323
|
this._clientByNodeId = new Map();
|
|
316
|
-
this.
|
|
324
|
+
this._sharedStatePrivateById = new Map();
|
|
317
325
|
this._schemas = new Map();
|
|
318
326
|
this._observers = new Set();
|
|
319
327
|
this._hooksBySchemaName = new Map(); // protected
|
|
@@ -321,6 +329,12 @@ class StateManager extends BaseStateManager {
|
|
|
321
329
|
this.addClient(localClientId, localTransport);
|
|
322
330
|
}
|
|
323
331
|
|
|
332
|
+
[kIsObservableState](state) {
|
|
333
|
+
const { schemaName, isCollectionController } = state;
|
|
334
|
+
// is observable only if not in private state and not a controller state
|
|
335
|
+
return !PRIVATE_STATES.includes(schemaName) && !isCollectionController;
|
|
336
|
+
}
|
|
337
|
+
|
|
324
338
|
/**
|
|
325
339
|
* Add a client to the manager.
|
|
326
340
|
*
|
|
@@ -334,114 +348,159 @@ class StateManager extends BaseStateManager {
|
|
|
334
348
|
* @private
|
|
335
349
|
*/
|
|
336
350
|
addClient(nodeId, transport) {
|
|
351
|
+
transport = new BatchedTransport(transport);
|
|
352
|
+
|
|
337
353
|
const client = { id: nodeId, transport };
|
|
338
354
|
this._clientByNodeId.set(nodeId, client);
|
|
339
355
|
|
|
340
356
|
// ---------------------------------------------
|
|
341
357
|
// CREATE
|
|
342
358
|
// ---------------------------------------------
|
|
343
|
-
client.transport.addListener(
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
359
|
+
client.transport.addListener(
|
|
360
|
+
CREATE_REQUEST,
|
|
361
|
+
(reqId, schemaName, requireSchema, initValues = {}) => {
|
|
362
|
+
if (this._schemas.has(schemaName)) {
|
|
363
|
+
try {
|
|
364
|
+
const schema = this._schemas.get(schemaName);
|
|
365
|
+
const stateId = generateStateId.next().value;
|
|
366
|
+
const remoteId = generateRemoteId.next().value;
|
|
367
|
+
const state = new SharedStatePrivate(stateId, schemaName, schema, this, initValues);
|
|
368
|
+
|
|
369
|
+
// attach client to the state as owner
|
|
370
|
+
const isOwner = true;
|
|
371
|
+
state._attachClient(remoteId, client, isOwner);
|
|
372
|
+
|
|
373
|
+
this._sharedStatePrivateById.set(stateId, state);
|
|
374
|
+
|
|
375
|
+
const currentValues = state._parameters.getValues();
|
|
376
|
+
const schemaOption = requireSchema ? schema : null;
|
|
377
|
+
|
|
378
|
+
client.transport.emit(
|
|
379
|
+
CREATE_RESPONSE,
|
|
380
|
+
reqId, stateId, remoteId, schemaName, schemaOption, currentValues,
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
const isObservable = this[kIsObservableState](state);
|
|
384
|
+
|
|
385
|
+
if (isObservable) {
|
|
386
|
+
this._observers.forEach(observer => {
|
|
387
|
+
observer.transport.emit(OBSERVE_NOTIFICATION, schemaName, stateId, nodeId);
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
} catch (err) {
|
|
391
|
+
const msg = `[stateManager] Cannot create state "${schemaName}", ${err.message}`;
|
|
392
|
+
console.error(msg);
|
|
393
|
+
|
|
394
|
+
client.transport.emit(CREATE_ERROR, reqId, msg);
|
|
395
|
+
}
|
|
396
|
+
} else {
|
|
397
|
+
const msg = `[stateManager] Cannot create state "${schemaName}", schema does not exists`;
|
|
398
|
+
console.error(msg);
|
|
399
|
+
|
|
400
|
+
client.transport.emit(CREATE_ERROR, reqId, msg);
|
|
366
401
|
}
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
console.error(msg);
|
|
370
|
-
client.transport.emit(CREATE_ERROR, reqId, msg);
|
|
371
|
-
}
|
|
372
|
-
});
|
|
402
|
+
},
|
|
403
|
+
);
|
|
373
404
|
|
|
374
405
|
// ---------------------------------------------
|
|
375
406
|
// ATTACH (when creator, is attached by default)
|
|
376
407
|
// ---------------------------------------------
|
|
377
|
-
client.transport.addListener(
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
408
|
+
client.transport.addListener(
|
|
409
|
+
ATTACH_REQUEST,
|
|
410
|
+
(reqId, schemaName, stateId = null, requireSchema = true) => {
|
|
411
|
+
if (this._schemas.has(schemaName)) {
|
|
412
|
+
let state = null;
|
|
413
|
+
|
|
414
|
+
if (stateId !== null && this._sharedStatePrivateById.has(stateId)) {
|
|
415
|
+
state = this._sharedStatePrivateById.get(stateId);
|
|
416
|
+
} else if (stateId === null) {
|
|
417
|
+
// if no `stateId` given, we try to find the first state with the given
|
|
418
|
+
// `schemaName` in the list, this allow a client to attach to a global
|
|
419
|
+
// state created by the server (or some persistant client) without
|
|
420
|
+
// having to know the `stateId` (e.g. some global state...)
|
|
421
|
+
for (let existingState of this._sharedStatePrivateById.values()) {
|
|
422
|
+
if (existingState.schemaName === schemaName) {
|
|
423
|
+
state = existingState;
|
|
424
|
+
break;
|
|
425
|
+
}
|
|
392
426
|
}
|
|
393
427
|
}
|
|
394
|
-
}
|
|
395
428
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
429
|
+
if (state !== null) {
|
|
430
|
+
// @note - we use a unique remote id to allow a client to attach
|
|
431
|
+
// several times to the same state.
|
|
432
|
+
// i.e. same state -> several remote attach on the same node
|
|
433
|
+
const remoteId = generateRemoteId.next().value;
|
|
434
|
+
const isOwner = false;
|
|
435
|
+
state._attachClient(remoteId, client, isOwner);
|
|
436
|
+
|
|
437
|
+
const currentValues = state._parameters.getValues();
|
|
438
|
+
const schema = this._schemas.get(schemaName);
|
|
439
|
+
const schemaOption = requireSchema ? schema : null;
|
|
440
|
+
|
|
441
|
+
client.transport.emit(
|
|
442
|
+
ATTACH_RESPONSE,
|
|
443
|
+
reqId, state.id, remoteId, schemaName, schemaOption, currentValues,
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
} else {
|
|
447
|
+
const msg = `[stateManager] Cannot attach, no existing state for schema "${schemaName}" with stateId: "${stateId}"`;
|
|
448
|
+
console.error(msg);
|
|
449
|
+
|
|
450
|
+
client.transport.emit(ATTACH_ERROR, reqId, msg);
|
|
451
|
+
}
|
|
408
452
|
} else {
|
|
409
|
-
const msg = `Cannot attach,
|
|
410
|
-
client.transport.emit(ATTACH_ERROR, reqId, msg);
|
|
453
|
+
const msg = `[stateManager] Cannot attach, schema "${schemaName}" does not exists`;
|
|
411
454
|
console.error(msg);
|
|
455
|
+
|
|
456
|
+
client.transport.emit(ATTACH_ERROR, reqId, msg);
|
|
412
457
|
}
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
client.transport.emit(ATTACH_ERROR, reqId, msg);
|
|
416
|
-
console.error(msg);
|
|
417
|
-
}
|
|
418
|
-
});
|
|
458
|
+
},
|
|
459
|
+
);
|
|
419
460
|
|
|
420
461
|
// ---------------------------------------------
|
|
421
462
|
// OBSERVE PEERS (be notified when a state is created, lazy)
|
|
422
463
|
// ---------------------------------------------
|
|
423
|
-
client.transport.addListener(OBSERVE_REQUEST, reqId => {
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
// (e.g. do not propagate infos about audit state)
|
|
430
|
-
if (!PRIVATE_STATES.includes(schemaName)) {
|
|
431
|
-
statesInfos.push([schemaName, id, _creatorId]);
|
|
432
|
-
}
|
|
433
|
-
});
|
|
464
|
+
client.transport.addListener(OBSERVE_REQUEST, (reqId, observedSchemaName) => {
|
|
465
|
+
if (observedSchemaName === null || this._schemas.has(observedSchemaName)) {
|
|
466
|
+
const statesInfos = [];
|
|
467
|
+
|
|
468
|
+
this._sharedStatePrivateById.forEach(state => {
|
|
469
|
+
const isObservable = this[kIsObservableState](state);
|
|
434
470
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
471
|
+
if (isObservable) {
|
|
472
|
+
const { schemaName, id, _creatorId } = state;
|
|
473
|
+
statesInfos.push([schemaName, id, _creatorId]);
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
// add client to observers first because if some synchronous server side
|
|
478
|
+
// callback throws, the client would never be added to the list
|
|
479
|
+
this._observers.add(client);
|
|
438
480
|
|
|
439
|
-
|
|
481
|
+
client.transport.emit(OBSERVE_RESPONSE, reqId, ...statesInfos);
|
|
482
|
+
} else {
|
|
483
|
+
const msg = `[stateManager] Cannot observe, schema "${observedSchemaName}" does not exists`;
|
|
484
|
+
client.transport.emit(OBSERVE_ERROR, reqId, msg);
|
|
485
|
+
}
|
|
440
486
|
});
|
|
441
487
|
|
|
442
488
|
client.transport.addListener(UNOBSERVE_NOTIFICATION, () => {
|
|
443
489
|
this._observers.delete(client);
|
|
444
490
|
});
|
|
491
|
+
|
|
492
|
+
// ---------------------------------------------
|
|
493
|
+
// GET SCHEMA
|
|
494
|
+
// ---------------------------------------------
|
|
495
|
+
client.transport.addListener(GET_SCHEMA_REQUEST, (reqId, schemaName) => {
|
|
496
|
+
if (this._schemas.has(schemaName)) {
|
|
497
|
+
const schema = this._schemas.get(schemaName);
|
|
498
|
+
client.transport.emit(GET_SCHEMA_RESPONSE, reqId, schemaName, schema);
|
|
499
|
+
} else {
|
|
500
|
+
const msg = `[stateManager] Cannot get schema, schema "${schemaName}" does not exists`;
|
|
501
|
+
client.transport.emit(GET_SCHEMA_ERROR, reqId, msg);
|
|
502
|
+
}
|
|
503
|
+
});
|
|
445
504
|
}
|
|
446
505
|
|
|
447
506
|
/**
|
|
@@ -455,18 +514,22 @@ class StateManager extends BaseStateManager {
|
|
|
455
514
|
* @private
|
|
456
515
|
*/
|
|
457
516
|
removeClient(nodeId) {
|
|
458
|
-
for (let [_id, state] of this.
|
|
517
|
+
for (let [_id, state] of this._sharedStatePrivateById.entries()) {
|
|
459
518
|
let deleteState = false;
|
|
460
519
|
|
|
461
520
|
// define if the client is the creator of the state, in which case
|
|
462
521
|
// everybody must delete it
|
|
463
|
-
for (let [remoteId,
|
|
522
|
+
for (let [remoteId, clientInfos] of state._attachedClients) {
|
|
523
|
+
const attachedClient = clientInfos.client;
|
|
524
|
+
|
|
464
525
|
if (nodeId === attachedClient.id && remoteId === state._creatorRemoteId) {
|
|
465
526
|
deleteState = true;
|
|
466
527
|
}
|
|
467
528
|
}
|
|
468
529
|
|
|
469
|
-
for (let [remoteId,
|
|
530
|
+
for (let [remoteId, clientInfos] of state._attachedClients) {
|
|
531
|
+
const attachedClient = clientInfos.client;
|
|
532
|
+
|
|
470
533
|
if (nodeId === attachedClient.id) {
|
|
471
534
|
state._detachClient(remoteId, attachedClient);
|
|
472
535
|
}
|
|
@@ -477,7 +540,7 @@ class StateManager extends BaseStateManager {
|
|
|
477
540
|
attachedClient.transport.emit(`${DELETE_NOTIFICATION}-${state.id}-${remoteId}`);
|
|
478
541
|
}
|
|
479
542
|
|
|
480
|
-
this.
|
|
543
|
+
this._sharedStatePrivateById.delete(state.id);
|
|
481
544
|
}
|
|
482
545
|
}
|
|
483
546
|
}
|
|
@@ -514,10 +577,18 @@ class StateManager extends BaseStateManager {
|
|
|
514
577
|
* })
|
|
515
578
|
*/
|
|
516
579
|
registerSchema(schemaName, schema) {
|
|
580
|
+
if (!isString(schemaName)) {
|
|
581
|
+
throw new Error(`[stateManager.registerSchema] Invalid schema name "${schemaName}", should be a string`);
|
|
582
|
+
}
|
|
583
|
+
|
|
517
584
|
if (this._schemas.has(schemaName)) {
|
|
518
585
|
throw new Error(`[stateManager.registerSchema] cannot register schema with name: "${schemaName}", schema name already exists`);
|
|
519
586
|
}
|
|
520
587
|
|
|
588
|
+
if (!isPlainObject(schema)) {
|
|
589
|
+
throw new Error(`[stateManager.registerSchema] Invalid schema, should be an object`);
|
|
590
|
+
}
|
|
591
|
+
|
|
521
592
|
ParameterBag.validateSchema(schema);
|
|
522
593
|
|
|
523
594
|
this._schemas.set(schemaName, clonedeep(schema));
|
|
@@ -536,14 +607,15 @@ class StateManager extends BaseStateManager {
|
|
|
536
607
|
*/
|
|
537
608
|
deleteSchema(schemaName) {
|
|
538
609
|
// @note: deleting schema
|
|
539
|
-
for (let [_id, state] of this.
|
|
610
|
+
for (let [_id, state] of this._sharedStatePrivateById) {
|
|
540
611
|
if (state.schemaName === schemaName) {
|
|
541
|
-
for (let [remoteId,
|
|
612
|
+
for (let [remoteId, clientInfos] of state._attachedClients) {
|
|
613
|
+
const attached = clientInfos.client;
|
|
542
614
|
state._detachClient(remoteId, attached);
|
|
543
615
|
attached.transport.emit(`${DELETE_NOTIFICATION}-${state.id}-${remoteId}`);
|
|
544
616
|
}
|
|
545
617
|
|
|
546
|
-
this.
|
|
618
|
+
this._sharedStatePrivateById.delete(state.id);
|
|
547
619
|
}
|
|
548
620
|
}
|
|
549
621
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
// NOTICE: we use this syntax so that the server can be bundled to cjs
|
|
2
|
+
// with esbuild, so we can ship a cjs server bundle into Max.
|
|
3
|
+
// Should move back to regular esm module once Max has fixed its loader
|
|
4
|
+
export default `
|
|
5
|
+
const { parentPort } = require('node:worker_threads');
|
|
4
6
|
|
|
5
7
|
let stack = [];
|
|
6
8
|
let averageLatencyWindow = 5;
|
|
@@ -9,12 +11,21 @@ let averageLatencyPeriod = 2;
|
|
|
9
11
|
let intervalId = null;
|
|
10
12
|
let meanLatency = 0;
|
|
11
13
|
|
|
12
|
-
|
|
14
|
+
let getTime;
|
|
15
|
+
let inited = new Promise(async (resolve) => {
|
|
16
|
+
module = await import('@ircam/sc-utils');
|
|
17
|
+
getTime = module.getTime;
|
|
18
|
+
resolve(true);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
parentPort.on('message', async msg => {
|
|
13
22
|
switch (msg.type) {
|
|
14
23
|
case 'config': {
|
|
15
24
|
averageLatencyWindow = msg.value.averageLatencyWindow;
|
|
16
25
|
averageLatencyPeriod = msg.value.averageLatencyPeriod;
|
|
17
26
|
|
|
27
|
+
await inited;
|
|
28
|
+
|
|
18
29
|
// launch compute in its own loop so that the number of computation is
|
|
19
30
|
// decoupled from the number of connected clients
|
|
20
31
|
clearInterval(intervalId);
|
|
@@ -55,3 +66,4 @@ function computeAverageLatency() {
|
|
|
55
66
|
});
|
|
56
67
|
}
|
|
57
68
|
}
|
|
69
|
+
`;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
3
|
+
|
|
4
|
+
// adapted from https://dev.to/jobizil/encrypt-and-decrypt-data-in-nodejs-using-aes-256-cbc-2l6d
|
|
5
|
+
// generate a new key and init vector each time the server restart, we don't need
|
|
6
|
+
// any persistency here
|
|
7
|
+
const secretKey = uuidv4();
|
|
8
|
+
const secretIv = uuidv4();
|
|
9
|
+
const encryptionMethod = 'aes-256-cbc';
|
|
10
|
+
|
|
11
|
+
const key = crypto
|
|
12
|
+
.createHash('sha512')
|
|
13
|
+
.update(secretKey)
|
|
14
|
+
.digest('hex')
|
|
15
|
+
.substring(0, 32);
|
|
16
|
+
|
|
17
|
+
const encryptionIV = crypto
|
|
18
|
+
.createHash('sha512')
|
|
19
|
+
.update(secretIv)
|
|
20
|
+
.digest('hex')
|
|
21
|
+
.substring(0, 16);
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
export function encryptData(obj) {
|
|
25
|
+
const data = JSON.stringify(obj);
|
|
26
|
+
const cipher = crypto.createCipheriv(encryptionMethod, key, encryptionIV);
|
|
27
|
+
|
|
28
|
+
return Buffer.from(cipher.update(data, 'utf8', 'hex') + cipher.final('hex')).toString('base64');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function decryptData(encryptedData) {
|
|
32
|
+
const buff = Buffer.from(encryptedData, 'base64');
|
|
33
|
+
const decipher = crypto.createDecipheriv(encryptionMethod, key, encryptionIV);
|
|
34
|
+
|
|
35
|
+
const decrypted = `${decipher.update(buff.toString('utf8'), 'hex', 'utf8')}${decipher.final('utf8')}`;
|
|
36
|
+
return JSON.parse(decrypted);
|
|
37
|
+
}
|