@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/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,58 @@ 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
|
+
// for testing purpose
|
|
126
|
+
if (this.sockets._DEBUG_PREVENT_HEARTBEAT === true) {
|
|
127
|
+
clearInterval(this._heartbeatId);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// broadcast all "native" events
|
|
94
131
|
[
|
|
95
132
|
'close',
|
|
96
133
|
'error',
|
|
@@ -107,14 +144,14 @@ class Socket {
|
|
|
107
144
|
});
|
|
108
145
|
|
|
109
146
|
// ----------------------------------------------------------
|
|
110
|
-
//
|
|
147
|
+
// Binary socket
|
|
111
148
|
// ----------------------------------------------------------
|
|
112
149
|
this.binaryWs.addEventListener('message', e => {
|
|
113
150
|
const [channel, data] = unpackBinaryMessage(e.data);
|
|
114
151
|
this._emit(true, channel, data);
|
|
115
152
|
});
|
|
116
153
|
|
|
117
|
-
// broadcast all
|
|
154
|
+
// broadcast all "native" events
|
|
118
155
|
[
|
|
119
156
|
'close',
|
|
120
157
|
'error',
|
|
@@ -129,38 +166,6 @@ class Socket {
|
|
|
129
166
|
this._emit(true, eventName, e.data);
|
|
130
167
|
});
|
|
131
168
|
});
|
|
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
169
|
}
|
|
165
170
|
|
|
166
171
|
/**
|
|
@@ -171,7 +176,8 @@ class Socket {
|
|
|
171
176
|
*/
|
|
172
177
|
terminate() {
|
|
173
178
|
// clear ping/pong check
|
|
174
|
-
clearInterval(this.
|
|
179
|
+
clearInterval(this._heartbeatId);
|
|
180
|
+
|
|
175
181
|
// clean rooms
|
|
176
182
|
for (let [_key, room] of this.rooms) {
|
|
177
183
|
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.
|
|
@@ -34,6 +32,8 @@ class Sockets {
|
|
|
34
32
|
/** @private */
|
|
35
33
|
this._auditState = null;
|
|
36
34
|
|
|
35
|
+
/** @private */
|
|
36
|
+
this._DEBUG_PREVENT_HEARTBEAT = false;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
/**
|
|
@@ -45,8 +45,9 @@ class Sockets {
|
|
|
45
45
|
*/
|
|
46
46
|
async start(server, config, onConnectionCallback) {
|
|
47
47
|
// init audit stuff for network latency estimation
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
// @see note about __dirname
|
|
49
|
+
// const workerPathname = path.join(__dirname, 'audit-network-latency.worker.js');
|
|
50
|
+
this._latencyStatsWorker = new Worker(networkLatencyWorker, { eval: true });
|
|
50
51
|
this._auditState = await server.getAuditState();
|
|
51
52
|
|
|
52
53
|
this._auditState.onUpdate(updates => {
|
|
@@ -69,13 +70,13 @@ class Sockets {
|
|
|
69
70
|
|
|
70
71
|
// init ws server
|
|
71
72
|
this._wss = new WebSocketServer({
|
|
72
|
-
|
|
73
|
+
noServer: true,
|
|
73
74
|
path: `/${config.path}`, // @note - update according to existing config files (aka cosima-apps)
|
|
74
75
|
});
|
|
75
76
|
|
|
76
77
|
this._wss.on('connection', (ws, req) => {
|
|
77
78
|
const queryString = querystring.decode(req.url.split('?')[1]);
|
|
78
|
-
const { role, key } = queryString;
|
|
79
|
+
const { role, key, token } = queryString;
|
|
79
80
|
const binary = !!(parseInt(queryString.binary));
|
|
80
81
|
|
|
81
82
|
if (binary) {
|
|
@@ -96,8 +97,29 @@ class Sockets {
|
|
|
96
97
|
socket.addToRoom('*');
|
|
97
98
|
socket.addToRoom(role);
|
|
98
99
|
|
|
99
|
-
onConnectionCallback(role, socket);
|
|
100
|
+
onConnectionCallback(role, socket, token);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// check if client can connect
|
|
105
|
+
server.httpServer.on('upgrade', async (req, socket, head) => {
|
|
106
|
+
const queryString = querystring.decode(req.url.split('?')[1]);
|
|
107
|
+
|
|
108
|
+
const { role, token } = queryString;
|
|
109
|
+
|
|
110
|
+
if (server.isProtected(role)) {
|
|
111
|
+
// we don't have any ip in the upgrade request, so we just check the
|
|
112
|
+
// connection token is pending
|
|
113
|
+
const allowed = server.isValidConnectionToken(token);
|
|
114
|
+
|
|
115
|
+
if (!allowed) {
|
|
116
|
+
socket.destroy('not allowed');
|
|
117
|
+
}
|
|
100
118
|
}
|
|
119
|
+
|
|
120
|
+
this._wss.handleUpgrade(req, socket, head, (ws) => {
|
|
121
|
+
this._wss.emit('connection', ws, req);
|
|
122
|
+
});
|
|
101
123
|
});
|
|
102
124
|
}
|
|
103
125
|
|
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
import clonedeep from 'lodash.clonedeep';
|
|
1
|
+
import { idGenerator, isString, isPlainObject } from '@ircam/sc-utils';
|
|
2
|
+
import clonedeep from 'lodash/cloneDeep.js';
|
|
4
3
|
|
|
5
4
|
import BaseStateManager from '../common/BaseStateManager.js';
|
|
5
|
+
import BatchedTransport from '../common/BatchedTransport.js';
|
|
6
6
|
import ParameterBag from '../common/ParameterBag.js';
|
|
7
7
|
import SharedStatePrivate from '../common/SharedStatePrivate.js';
|
|
8
8
|
import {
|
|
9
|
-
SERVER_ID,
|
|
10
9
|
CREATE_REQUEST,
|
|
11
10
|
CREATE_RESPONSE,
|
|
12
11
|
CREATE_ERROR,
|
|
@@ -16,16 +15,22 @@ import {
|
|
|
16
15
|
ATTACH_ERROR,
|
|
17
16
|
OBSERVE_REQUEST,
|
|
18
17
|
OBSERVE_RESPONSE,
|
|
18
|
+
OBSERVE_ERROR,
|
|
19
19
|
OBSERVE_NOTIFICATION,
|
|
20
20
|
UNOBSERVE_NOTIFICATION,
|
|
21
21
|
DELETE_SCHEMA,
|
|
22
|
+
GET_SCHEMA_REQUEST,
|
|
23
|
+
GET_SCHEMA_RESPONSE,
|
|
24
|
+
GET_SCHEMA_ERROR,
|
|
22
25
|
PRIVATE_STATES,
|
|
23
26
|
} from '../common/constants.js';
|
|
24
|
-
|
|
27
|
+
|
|
25
28
|
|
|
26
29
|
const generateStateId = idGenerator();
|
|
27
30
|
const generateRemoteId = idGenerator();
|
|
28
31
|
|
|
32
|
+
const kIsObservableState = Symbol('StateManager::isObservableState');
|
|
33
|
+
|
|
29
34
|
/**
|
|
30
35
|
* @typedef {object} server.StateManager~schema
|
|
31
36
|
*
|
|
@@ -306,19 +311,25 @@ const generateRemoteId = idGenerator();
|
|
|
306
311
|
*/
|
|
307
312
|
class StateManager extends BaseStateManager {
|
|
308
313
|
constructor() {
|
|
309
|
-
|
|
310
|
-
const localClientId = SERVER_ID;
|
|
311
|
-
const localTransport = new EventEmitter();
|
|
312
|
-
|
|
313
|
-
super(localClientId, localTransport);
|
|
314
|
+
super();
|
|
314
315
|
|
|
315
316
|
this._clientByNodeId = new Map();
|
|
316
|
-
this.
|
|
317
|
+
this._sharedStatePrivateById = new Map();
|
|
317
318
|
this._schemas = new Map();
|
|
318
319
|
this._observers = new Set();
|
|
319
320
|
this._hooksBySchemaName = new Map(); // protected
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
init(id, transport) {
|
|
324
|
+
super.init(id, transport);
|
|
325
|
+
// add itself as client of the state manager server
|
|
326
|
+
this.addClient(id, transport);
|
|
327
|
+
}
|
|
320
328
|
|
|
321
|
-
|
|
329
|
+
[kIsObservableState](state) {
|
|
330
|
+
const { schemaName, isCollectionController } = state;
|
|
331
|
+
// is observable only if not in private state and not a controller state
|
|
332
|
+
return !PRIVATE_STATES.includes(schemaName) && !isCollectionController;
|
|
322
333
|
}
|
|
323
334
|
|
|
324
335
|
/**
|
|
@@ -334,114 +345,177 @@ class StateManager extends BaseStateManager {
|
|
|
334
345
|
* @private
|
|
335
346
|
*/
|
|
336
347
|
addClient(nodeId, transport) {
|
|
337
|
-
const
|
|
348
|
+
const batchedTransport = new BatchedTransport(transport);
|
|
349
|
+
const client = {
|
|
350
|
+
id: nodeId,
|
|
351
|
+
transport: batchedTransport,
|
|
352
|
+
};
|
|
353
|
+
|
|
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
|
+
const filter = null;
|
|
372
|
+
state._attachClient(remoteId, client, isOwner, filter);
|
|
373
|
+
|
|
374
|
+
this._sharedStatePrivateById.set(stateId, state);
|
|
375
|
+
|
|
376
|
+
const currentValues = state._parameters.getValues();
|
|
377
|
+
const schemaOption = requireSchema ? schema : null;
|
|
378
|
+
|
|
379
|
+
client.transport.emit(
|
|
380
|
+
CREATE_RESPONSE,
|
|
381
|
+
reqId, stateId, remoteId, schemaName, schemaOption, currentValues,
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
const isObservable = this[kIsObservableState](state);
|
|
385
|
+
|
|
386
|
+
if (isObservable) {
|
|
387
|
+
this._observers.forEach(observer => {
|
|
388
|
+
observer.transport.emit(OBSERVE_NOTIFICATION, schemaName, stateId, nodeId);
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
} catch (err) {
|
|
392
|
+
const msg = `[stateManager] Cannot create state "${schemaName}", ${err.message}`;
|
|
393
|
+
console.error(msg);
|
|
394
|
+
|
|
395
|
+
client.transport.emit(CREATE_ERROR, reqId, msg);
|
|
396
|
+
}
|
|
397
|
+
} else {
|
|
398
|
+
const msg = `[stateManager] Cannot create state "${schemaName}", schema does not exists`;
|
|
399
|
+
console.error(msg);
|
|
400
|
+
|
|
401
|
+
client.transport.emit(CREATE_ERROR, reqId, msg);
|
|
366
402
|
}
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
console.error(msg);
|
|
370
|
-
client.transport.emit(CREATE_ERROR, reqId, msg);
|
|
371
|
-
}
|
|
372
|
-
});
|
|
403
|
+
},
|
|
404
|
+
);
|
|
373
405
|
|
|
374
406
|
// ---------------------------------------------
|
|
375
407
|
// ATTACH (when creator, is attached by default)
|
|
376
408
|
// ---------------------------------------------
|
|
377
|
-
client.transport.addListener(
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
409
|
+
client.transport.addListener(
|
|
410
|
+
ATTACH_REQUEST,
|
|
411
|
+
(reqId, schemaName, stateId = null, requireSchema = true, filter = null) => {
|
|
412
|
+
if (this._schemas.has(schemaName)) {
|
|
413
|
+
let state = null;
|
|
414
|
+
|
|
415
|
+
if (stateId !== null && this._sharedStatePrivateById.has(stateId)) {
|
|
416
|
+
state = this._sharedStatePrivateById.get(stateId);
|
|
417
|
+
} else if (stateId === null) {
|
|
418
|
+
// if no `stateId` given, we try to find the first state with the given
|
|
419
|
+
// `schemaName` in the list, this allow a client to attach to a global
|
|
420
|
+
// state created by the server (or some persistant client) without
|
|
421
|
+
// having to know the `stateId` (e.g. some global state...)
|
|
422
|
+
for (let existingState of this._sharedStatePrivateById.values()) {
|
|
423
|
+
if (existingState.schemaName === schemaName) {
|
|
424
|
+
state = existingState;
|
|
425
|
+
break;
|
|
426
|
+
}
|
|
392
427
|
}
|
|
393
428
|
}
|
|
394
|
-
}
|
|
395
429
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
430
|
+
if (state !== null) {
|
|
431
|
+
// @note - we use a unique remote id to allow a client to attach
|
|
432
|
+
// several times to the same state.
|
|
433
|
+
// i.e. same state -> several remote attach on the same node
|
|
434
|
+
const remoteId = generateRemoteId.next().value;
|
|
435
|
+
const isOwner = false;
|
|
436
|
+
const currentValues = state._parameters.getValues();
|
|
437
|
+
const schema = this._schemas.get(schemaName);
|
|
438
|
+
const schemaOption = requireSchema ? schema : null;
|
|
439
|
+
|
|
440
|
+
// if filter given, check that all filter entries are valid schema keys
|
|
441
|
+
// @todo - improve error reportin: report invalid filters
|
|
442
|
+
if (filter !== null) {
|
|
443
|
+
const keys = Object.keys(schema);
|
|
444
|
+
const isValid = filter.reduce((acc, key) => acc && keys.includes(key), true);
|
|
445
|
+
|
|
446
|
+
if (!isValid) {
|
|
447
|
+
const msg = `[stateManager] Cannot attach, invalid filter (${filter.join(', ')}) for schema "${schemaName}"`;
|
|
448
|
+
console.error(msg);
|
|
449
|
+
|
|
450
|
+
return client.transport.emit(ATTACH_ERROR, reqId, msg);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
state._attachClient(remoteId, client, isOwner, filter);
|
|
455
|
+
|
|
456
|
+
client.transport.emit(
|
|
457
|
+
ATTACH_RESPONSE,
|
|
458
|
+
reqId, state.id, remoteId, schemaName, schemaOption, currentValues, filter,
|
|
459
|
+
);
|
|
460
|
+
|
|
461
|
+
} else {
|
|
462
|
+
const msg = `[stateManager] Cannot attach, no existing state for schema "${schemaName}" with stateId: "${stateId}"`;
|
|
463
|
+
console.error(msg);
|
|
464
|
+
|
|
465
|
+
client.transport.emit(ATTACH_ERROR, reqId, msg);
|
|
466
|
+
}
|
|
408
467
|
} else {
|
|
409
|
-
const msg = `Cannot attach,
|
|
410
|
-
client.transport.emit(ATTACH_ERROR, reqId, msg);
|
|
468
|
+
const msg = `[stateManager] Cannot attach, schema "${schemaName}" does not exists`;
|
|
411
469
|
console.error(msg);
|
|
470
|
+
|
|
471
|
+
client.transport.emit(ATTACH_ERROR, reqId, msg);
|
|
412
472
|
}
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
client.transport.emit(ATTACH_ERROR, reqId, msg);
|
|
416
|
-
console.error(msg);
|
|
417
|
-
}
|
|
418
|
-
});
|
|
473
|
+
},
|
|
474
|
+
);
|
|
419
475
|
|
|
420
476
|
// ---------------------------------------------
|
|
421
477
|
// OBSERVE PEERS (be notified when a state is created, lazy)
|
|
422
478
|
// ---------------------------------------------
|
|
423
|
-
client.transport.addListener(OBSERVE_REQUEST, reqId => {
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
this._serverStatesById.forEach(state => {
|
|
427
|
-
const { schemaName, id, _creatorId } = state;
|
|
428
|
-
// only track application states
|
|
429
|
-
// (e.g. do not propagate infos about audit state)
|
|
430
|
-
if (!PRIVATE_STATES.includes(schemaName)) {
|
|
431
|
-
statesInfos.push([schemaName, id, _creatorId]);
|
|
432
|
-
}
|
|
433
|
-
});
|
|
479
|
+
client.transport.addListener(OBSERVE_REQUEST, (reqId, observedSchemaName) => {
|
|
480
|
+
if (observedSchemaName === null || this._schemas.has(observedSchemaName)) {
|
|
481
|
+
const statesInfos = [];
|
|
434
482
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
this._observers.add(client);
|
|
483
|
+
this._sharedStatePrivateById.forEach(state => {
|
|
484
|
+
const isObservable = this[kIsObservableState](state);
|
|
438
485
|
|
|
439
|
-
|
|
486
|
+
if (isObservable) {
|
|
487
|
+
const { schemaName, id, _creatorId } = state;
|
|
488
|
+
statesInfos.push([schemaName, id, _creatorId]);
|
|
489
|
+
}
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
// add client to observers first because if some synchronous server side
|
|
493
|
+
// callback throws, the client would never be added to the list
|
|
494
|
+
this._observers.add(client);
|
|
495
|
+
|
|
496
|
+
client.transport.emit(OBSERVE_RESPONSE, reqId, ...statesInfos);
|
|
497
|
+
} else {
|
|
498
|
+
const msg = `[stateManager] Cannot observe, schema "${observedSchemaName}" does not exists`;
|
|
499
|
+
client.transport.emit(OBSERVE_ERROR, reqId, msg);
|
|
500
|
+
}
|
|
440
501
|
});
|
|
441
502
|
|
|
442
503
|
client.transport.addListener(UNOBSERVE_NOTIFICATION, () => {
|
|
443
504
|
this._observers.delete(client);
|
|
444
505
|
});
|
|
506
|
+
|
|
507
|
+
// ---------------------------------------------
|
|
508
|
+
// GET SCHEMA
|
|
509
|
+
// ---------------------------------------------
|
|
510
|
+
client.transport.addListener(GET_SCHEMA_REQUEST, (reqId, schemaName) => {
|
|
511
|
+
if (this._schemas.has(schemaName)) {
|
|
512
|
+
const schema = this._schemas.get(schemaName);
|
|
513
|
+
client.transport.emit(GET_SCHEMA_RESPONSE, reqId, schemaName, schema);
|
|
514
|
+
} else {
|
|
515
|
+
const msg = `[stateManager] Cannot get schema, schema "${schemaName}" does not exists`;
|
|
516
|
+
client.transport.emit(GET_SCHEMA_ERROR, reqId, msg);
|
|
517
|
+
}
|
|
518
|
+
});
|
|
445
519
|
}
|
|
446
520
|
|
|
447
521
|
/**
|
|
@@ -455,18 +529,22 @@ class StateManager extends BaseStateManager {
|
|
|
455
529
|
* @private
|
|
456
530
|
*/
|
|
457
531
|
removeClient(nodeId) {
|
|
458
|
-
for (let [_id, state] of this.
|
|
532
|
+
for (let [_id, state] of this._sharedStatePrivateById.entries()) {
|
|
459
533
|
let deleteState = false;
|
|
460
534
|
|
|
461
535
|
// define if the client is the creator of the state, in which case
|
|
462
536
|
// everybody must delete it
|
|
463
|
-
for (let [remoteId,
|
|
537
|
+
for (let [remoteId, clientInfos] of state._attachedClients) {
|
|
538
|
+
const attachedClient = clientInfos.client;
|
|
539
|
+
|
|
464
540
|
if (nodeId === attachedClient.id && remoteId === state._creatorRemoteId) {
|
|
465
541
|
deleteState = true;
|
|
466
542
|
}
|
|
467
543
|
}
|
|
468
544
|
|
|
469
|
-
for (let [remoteId,
|
|
545
|
+
for (let [remoteId, clientInfos] of state._attachedClients) {
|
|
546
|
+
const attachedClient = clientInfos.client;
|
|
547
|
+
|
|
470
548
|
if (nodeId === attachedClient.id) {
|
|
471
549
|
state._detachClient(remoteId, attachedClient);
|
|
472
550
|
}
|
|
@@ -477,7 +555,7 @@ class StateManager extends BaseStateManager {
|
|
|
477
555
|
attachedClient.transport.emit(`${DELETE_NOTIFICATION}-${state.id}-${remoteId}`);
|
|
478
556
|
}
|
|
479
557
|
|
|
480
|
-
this.
|
|
558
|
+
this._sharedStatePrivateById.delete(state.id);
|
|
481
559
|
}
|
|
482
560
|
}
|
|
483
561
|
}
|
|
@@ -514,10 +592,18 @@ class StateManager extends BaseStateManager {
|
|
|
514
592
|
* })
|
|
515
593
|
*/
|
|
516
594
|
registerSchema(schemaName, schema) {
|
|
595
|
+
if (!isString(schemaName)) {
|
|
596
|
+
throw new Error(`[stateManager.registerSchema] Invalid schema name "${schemaName}", should be a string`);
|
|
597
|
+
}
|
|
598
|
+
|
|
517
599
|
if (this._schemas.has(schemaName)) {
|
|
518
600
|
throw new Error(`[stateManager.registerSchema] cannot register schema with name: "${schemaName}", schema name already exists`);
|
|
519
601
|
}
|
|
520
602
|
|
|
603
|
+
if (!isPlainObject(schema)) {
|
|
604
|
+
throw new Error(`[stateManager.registerSchema] Invalid schema, should be an object`);
|
|
605
|
+
}
|
|
606
|
+
|
|
521
607
|
ParameterBag.validateSchema(schema);
|
|
522
608
|
|
|
523
609
|
this._schemas.set(schemaName, clonedeep(schema));
|
|
@@ -536,14 +622,15 @@ class StateManager extends BaseStateManager {
|
|
|
536
622
|
*/
|
|
537
623
|
deleteSchema(schemaName) {
|
|
538
624
|
// @note: deleting schema
|
|
539
|
-
for (let [_id, state] of this.
|
|
625
|
+
for (let [_id, state] of this._sharedStatePrivateById) {
|
|
540
626
|
if (state.schemaName === schemaName) {
|
|
541
|
-
for (let [remoteId,
|
|
627
|
+
for (let [remoteId, clientInfos] of state._attachedClients) {
|
|
628
|
+
const attached = clientInfos.client;
|
|
542
629
|
state._detachClient(remoteId, attached);
|
|
543
630
|
attached.transport.emit(`${DELETE_NOTIFICATION}-${state.id}-${remoteId}`);
|
|
544
631
|
}
|
|
545
632
|
|
|
546
|
-
this.
|
|
633
|
+
this._sharedStatePrivateById.delete(state.id);
|
|
547
634
|
}
|
|
548
635
|
}
|
|
549
636
|
|