@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,4 +1,9 @@
1
- import { getTime } from '@ircam/sc-gettime';
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, options = {}) {
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
- // init string socket
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
- // broadcast all `ws` "native" events
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
- // init binary socket
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 `ws` "native" events
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._intervalId);
179
+ clearInterval(this._heartbeatId);
180
+
175
181
  // clean rooms
176
182
  for (let [_key, room] of this.rooms) {
177
183
  room.delete(this);
@@ -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
- const __dirname = fileURLToPath(new URL('.', import.meta.url));
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
- const workerPathname = path.join(__dirname, 'audit-network-latency.worker.js');
49
- this._latencyStatsWorker = new Worker(workerPathname);
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
- server: server.httpServer,
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 EventEmitter from 'node:events';
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
- import { idGenerator } from '../common/utils.js';
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
- // acts as a client of itself locally
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._serverStatesById = new Map();
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
- this.addClient(localClientId, localTransport);
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 client = { id: nodeId, transport };
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(CREATE_REQUEST, (reqId, schemaName, requireSchema, initValues = {}) => {
344
- if (this._schemas.has(schemaName)) {
345
- try {
346
- const schema = this._schemas.get(schemaName);
347
- const stateId = generateStateId.next().value;
348
- const remoteId = generateRemoteId.next().value;
349
- // id, schemaName, schema, manager, initValues = {}
350
- const state = new SharedStatePrivate(stateId, schemaName, schema, this, initValues);
351
-
352
- state._attachClient(remoteId, client, true); // attach client to the state
353
- this._serverStatesById.set(stateId, state);
354
-
355
- const currentValues = state._parameters.getValues();
356
- const sendedSchema = requireSchema ? schema : null;
357
-
358
- client.transport.emit(CREATE_RESPONSE, reqId, stateId, remoteId, schemaName, sendedSchema, currentValues);
359
-
360
- this._observers.forEach(observer => {
361
- observer.transport.emit(OBSERVE_NOTIFICATION, [schemaName, stateId, nodeId]);
362
- });
363
- } catch(err) {
364
- client.transport.emit(CREATE_ERROR, reqId, err.message);
365
- console.error(err.message);
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
- } else {
368
- const msg = `Cannot create state "${schemaName}", schema does not exists`;
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(ATTACH_REQUEST, (reqId, schemaName, stateId = null, requireSchema = true) => {
378
- if (this._schemas.has(schemaName)) {
379
- let state = null;
380
-
381
- if (stateId !== null && this._serverStatesById.has(stateId)) {
382
- state = this._serverStatesById.get(stateId);
383
- } else if (stateId === null) {
384
- // if no `stateId` given, we try to find the first state with the given
385
- // `schemaName` in the list, this allow a client to attach to a global
386
- // state created by the server (or some persistant client) without
387
- // having to know the `stateId` (e.g. some global state...)
388
- for (let existingState of this._serverStatesById.values()) {
389
- if (existingState.schemaName === schemaName) {
390
- state = existingState;
391
- break;
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
- if (state !== null) {
397
- // @note - we use a unique remote id to allow a client to attach
398
- // several times to the same state.
399
- // i.e. same state -> several remote attach on the same node
400
- const remoteId = generateRemoteId.next().value;
401
- const schema = this._schemas.get(schemaName);
402
- const currentValues = state._parameters.getValues();
403
-
404
- state._attachClient(remoteId, client, false);
405
- // send schema for client-side instanciation
406
- const sendedSchema = requireSchema ? schema : null;
407
- client.transport.emit(ATTACH_RESPONSE, reqId, state.id, remoteId, schemaName, sendedSchema, currentValues);
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, no existing state for schema "${schemaName}" with stateId: "${stateId}"`;
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
- } else {
414
- const msg = `Cannot attach, schema "${schemaName}" does not exists`;
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
- const statesInfos = [];
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
- // add client to observers first because if some (sync) server side
436
- // callback throws, the client would never be added to the list
437
- this._observers.add(client);
483
+ this._sharedStatePrivateById.forEach(state => {
484
+ const isObservable = this[kIsObservableState](state);
438
485
 
439
- client.transport.emit(OBSERVE_RESPONSE, reqId, ...statesInfos);
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._serverStatesById.entries()) {
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, attachedClient] of state._attachedClients.entries()) {
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, attachedClient] of state._attachedClients.entries()) {
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._serverStatesById.delete(state.id);
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._serverStatesById.entries()) {
625
+ for (let [_id, state] of this._sharedStatePrivateById) {
540
626
  if (state.schemaName === schemaName) {
541
- for (let [remoteId, attached] of state._attachedClients.entries()) {
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._serverStatesById.delete(this.id);
633
+ this._sharedStatePrivateById.delete(state.id);
547
634
  }
548
635
  }
549
636