@soundworks/core 4.0.0-alpha.14 → 4.0.0-alpha.16

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.
@@ -21,6 +21,7 @@ export const DETACH_ERROR = 's:dt:err';
21
21
 
22
22
  export const OBSERVE_REQUEST = 's:o:req';
23
23
  export const OBSERVE_RESPONSE = 's:o:res';
24
+ export const OBSERVE_ERROR = 's:o:err';
24
25
  export const OBSERVE_NOTIFICATION = 's:o:not';
25
26
 
26
27
  export const UNOBSERVE_NOTIFICATION = 's:uo:not';
@@ -47,5 +48,5 @@ export const CLIENT_HANDSHAKE_RESPONSE = 'cl:h:res';
47
48
  export const CLIENT_HANDSHAKE_ERROR = 'cl:h:err';
48
49
 
49
50
  // audit state schema name
50
- export const AUDIT_STATE_NAME = 's:c:audit';
51
+ export const AUDIT_STATE_NAME = 'p:s:audit';
51
52
  export const PRIVATE_STATES = [AUDIT_STATE_NAME];
@@ -1,8 +1,7 @@
1
1
  import 'fast-text-encoding';
2
- import root from 'window-or-global';
3
2
 
4
- const encoder = new root.TextEncoder('utf-8');
5
- const decoder = new root.TextDecoder('utf-8');
3
+ const encoder = new TextEncoder('utf-8');
4
+ const decoder = new TextDecoder('utf-8');
6
5
 
7
6
  const types = [
8
7
  'Int8Array',
@@ -57,7 +56,7 @@ export function unpackBinaryMessage(buffer /* arraybuffer */) {
57
56
  const channel = decoder.decode(channelBuffer);
58
57
  const type = types[typeIndex];
59
58
  // slice (copy) the underlying ArrayBuffer to create a clean TypedArray
60
- const data = new root[type](buffer.slice(startOffset));
59
+ const data = new globalThis[type](buffer.slice(startOffset));
61
60
 
62
61
  return [channel, data];
63
62
  }
@@ -5,14 +5,13 @@ import path from 'node:path';
5
5
  import os from 'node:os';
6
6
  import { X509Certificate, createPrivateKey } from 'node:crypto';
7
7
 
8
- import { getTime } from '@ircam/sc-gettime';
9
- import { isPlainObject, idGenerator } from '@ircam/sc-utils';
8
+ import { isPlainObject, idGenerator, getTime } from '@ircam/sc-utils';
10
9
  import chalk from 'chalk';
11
10
  import compression from 'compression';
12
11
  import express from 'express';
13
12
  import Keyv from 'keyv';
14
13
  import { KeyvFile } from 'keyv-file';
15
- import merge from 'lodash.merge';
14
+ import merge from 'lodash/merge.js';
16
15
  import pem from 'pem';
17
16
  import compile from 'template-literal';
18
17
 
@@ -15,8 +15,11 @@ import BaseSharedStateCollection from '../common/BaseSharedStateCollection.js';
15
15
 
16
16
  /**
17
17
  * The `SharedStateCollection` interface represent a collection of all states
18
- * created from a given schema name on the network, at the execption of the ones
19
- * created by the current node.
18
+ * created from a given schema name on the network.
19
+ *
20
+ * It can optionnaly exclude the states created by the current node.
21
+ *
22
+ * See {@link server.StateManager#getCollection} for factory method API
20
23
  *
21
24
  * ```
22
25
  * const collection = await server.stateManager.getCollection('my-schema');
@@ -1,4 +1,4 @@
1
- import { getTime } from '@ircam/sc-gettime';
1
+ import { getTime } from '@ircam/sc-utils';
2
2
  import {
3
3
  packBinaryMessage,
4
4
  unpackBinaryMessage,
@@ -1,7 +1,7 @@
1
1
  import EventEmitter from 'node:events';
2
2
 
3
3
  import { idGenerator, isString, isPlainObject } from '@ircam/sc-utils';
4
- import clonedeep from 'lodash.clonedeep';
4
+ import clonedeep from 'lodash/cloneDeep.js';
5
5
 
6
6
  import BaseStateManager from '../common/BaseStateManager.js';
7
7
  import ParameterBag from '../common/ParameterBag.js';
@@ -17,6 +17,7 @@ import {
17
17
  ATTACH_ERROR,
18
18
  OBSERVE_REQUEST,
19
19
  OBSERVE_RESPONSE,
20
+ OBSERVE_ERROR,
20
21
  OBSERVE_NOTIFICATION,
21
22
  UNOBSERVE_NOTIFICATION,
22
23
  DELETE_SCHEMA,
@@ -27,6 +28,8 @@ import {
27
28
  const generateStateId = idGenerator();
28
29
  const generateRemoteId = idGenerator();
29
30
 
31
+ const kIsObservableState = Symbol('StateManager::isObservableState');
32
+
30
33
  /**
31
34
  * @typedef {object} server.StateManager~schema
32
35
  *
@@ -314,7 +317,7 @@ class StateManager extends BaseStateManager {
314
317
  super(localClientId, localTransport);
315
318
 
316
319
  this._clientByNodeId = new Map();
317
- this._serverStatesById = new Map();
320
+ this._sharedStatePrivateById = new Map();
318
321
  this._schemas = new Map();
319
322
  this._observers = new Set();
320
323
  this._hooksBySchemaName = new Map(); // protected
@@ -322,6 +325,12 @@ class StateManager extends BaseStateManager {
322
325
  this.addClient(localClientId, localTransport);
323
326
  }
324
327
 
328
+ [kIsObservableState](state) {
329
+ const { schemaName, isCollectionController } = state;
330
+ // is observable only if not in private state and not a controller state
331
+ return !PRIVATE_STATES.includes(schemaName) && !isCollectionController;
332
+ }
333
+
325
334
  /**
326
335
  * Add a client to the manager.
327
336
  *
@@ -341,105 +350,133 @@ class StateManager extends BaseStateManager {
341
350
  // ---------------------------------------------
342
351
  // CREATE
343
352
  // ---------------------------------------------
344
- client.transport.addListener(CREATE_REQUEST, (reqId, schemaName, requireSchema, initValues = {}) => {
345
- if (this._schemas.has(schemaName)) {
346
- try {
347
- const schema = this._schemas.get(schemaName);
348
- const stateId = generateStateId.next().value;
349
- const remoteId = generateRemoteId.next().value;
350
- // id, schemaName, schema, manager, initValues = {}
351
- const state = new SharedStatePrivate(stateId, schemaName, schema, this, initValues);
352
-
353
- state._attachClient(remoteId, client, true); // attach client to the state
354
- this._serverStatesById.set(stateId, state);
355
-
356
- const currentValues = state._parameters.getValues();
357
- const sendedSchema = requireSchema ? schema : null;
358
-
359
- client.transport.emit(CREATE_RESPONSE, reqId, stateId, remoteId, schemaName, sendedSchema, currentValues);
360
-
361
- this._observers.forEach(observer => {
362
- if (observer.id !== nodeId) {
363
- observer.transport.emit(OBSERVE_NOTIFICATION, schemaName, stateId, nodeId);
353
+ client.transport.addListener(
354
+ CREATE_REQUEST,
355
+ (reqId, schemaName, requireSchema, initValues = {}, inCollection = false) => {
356
+ if (this._schemas.has(schemaName)) {
357
+ try {
358
+ const schema = this._schemas.get(schemaName);
359
+ const stateId = generateStateId.next().value;
360
+ const remoteId = generateRemoteId.next().value;
361
+ const state = new SharedStatePrivate(stateId, schemaName, schema, this, initValues);
362
+
363
+ // attach client to the state as owner
364
+ const isOwner = true;
365
+ state._attachClient(remoteId, client, isOwner, inCollection);
366
+
367
+ this._sharedStatePrivateById.set(stateId, state);
368
+
369
+ const currentValues = state._parameters.getValues();
370
+ const schemaOption = requireSchema ? schema : null;
371
+
372
+ client.transport.emit(
373
+ CREATE_RESPONSE,
374
+ reqId, stateId, remoteId, schemaName, schemaOption, currentValues
375
+ );
376
+
377
+ const isObservable = this[kIsObservableState](state);
378
+
379
+ if (isObservable) {
380
+ this._observers.forEach(observer => {
381
+ observer.transport.emit(OBSERVE_NOTIFICATION, schemaName, stateId, nodeId);
382
+ });
364
383
  }
365
- });
366
- } catch (err) {
367
- client.transport.emit(CREATE_ERROR, reqId, err.message);
368
- console.error(err.message);
384
+ } catch (err) {
385
+ const msg = `Cannot create state "${schemaName}", ${err.message}`;
386
+ console.error(msg);
387
+
388
+ client.transport.emit(CREATE_ERROR, reqId, msg);
389
+ }
390
+ } else {
391
+ const msg = `Cannot create state "${schemaName}", schema does not exists`;
392
+ console.error(msg);
393
+
394
+ client.transport.emit(CREATE_ERROR, reqId, msg);
369
395
  }
370
- } else {
371
- const msg = `Cannot create state "${schemaName}", schema does not exists`;
372
- console.error(msg);
373
- client.transport.emit(CREATE_ERROR, reqId, msg);
374
396
  }
375
- });
397
+ );
376
398
 
377
399
  // ---------------------------------------------
378
400
  // ATTACH (when creator, is attached by default)
379
401
  // ---------------------------------------------
380
- client.transport.addListener(ATTACH_REQUEST, (reqId, schemaName, stateId = null, requireSchema = true) => {
381
- if (this._schemas.has(schemaName)) {
382
- let state = null;
383
-
384
- if (stateId !== null && this._serverStatesById.has(stateId)) {
385
- state = this._serverStatesById.get(stateId);
386
- } else if (stateId === null) {
387
- // if no `stateId` given, we try to find the first state with the given
388
- // `schemaName` in the list, this allow a client to attach to a global
389
- // state created by the server (or some persistant client) without
390
- // having to know the `stateId` (e.g. some global state...)
391
- for (let existingState of this._serverStatesById.values()) {
392
- if (existingState.schemaName === schemaName) {
393
- state = existingState;
394
- break;
402
+ client.transport.addListener(
403
+ ATTACH_REQUEST,
404
+ (reqId, schemaName, stateId = null, requireSchema = true, inCollection = false) => {
405
+ if (this._schemas.has(schemaName)) {
406
+ let state = null;
407
+
408
+ if (stateId !== null && this._sharedStatePrivateById.has(stateId)) {
409
+ state = this._sharedStatePrivateById.get(stateId);
410
+ } else if (stateId === null) {
411
+ // if no `stateId` given, we try to find the first state with the given
412
+ // `schemaName` in the list, this allow a client to attach to a global
413
+ // state created by the server (or some persistant client) without
414
+ // having to know the `stateId` (e.g. some global state...)
415
+ for (let existingState of this._sharedStatePrivateById.values()) {
416
+ if (existingState.schemaName === schemaName) {
417
+ state = existingState;
418
+ break;
419
+ }
395
420
  }
396
421
  }
397
- }
398
422
 
399
- if (state !== null) {
400
- // @note - we use a unique remote id to allow a client to attach
401
- // several times to the same state.
402
- // i.e. same state -> several remote attach on the same node
403
- const remoteId = generateRemoteId.next().value;
404
- const schema = this._schemas.get(schemaName);
405
- const currentValues = state._parameters.getValues();
406
-
407
- state._attachClient(remoteId, client, false);
408
- // send schema for client-side instanciation
409
- const sendedSchema = requireSchema ? schema : null;
410
- client.transport.emit(ATTACH_RESPONSE, reqId, state.id, remoteId, schemaName, sendedSchema, currentValues);
423
+ if (state !== null) {
424
+ // @note - we use a unique remote id to allow a client to attach
425
+ // several times to the same state.
426
+ // i.e. same state -> several remote attach on the same node
427
+ const remoteId = generateRemoteId.next().value;
428
+ const isOwner = false;
429
+ state._attachClient(remoteId, client, isOwner, inCollection);
430
+
431
+ const currentValues = state._parameters.getValues();
432
+ const schema = this._schemas.get(schemaName);
433
+ const schemaOption = requireSchema ? schema : null;
434
+
435
+ client.transport.emit(
436
+ ATTACH_RESPONSE,
437
+ reqId, state.id, remoteId, schemaName, schemaOption, currentValues
438
+ );
439
+
440
+ } else {
441
+ const msg = `Cannot attach, no existing state for schema "${schemaName}" with stateId: "${stateId}"`;
442
+ console.error(msg);
443
+
444
+ client.transport.emit(ATTACH_ERROR, reqId, msg);
445
+ }
411
446
  } else {
412
- const msg = `Cannot attach, no existing state for schema "${schemaName}" with stateId: "${stateId}"`;
413
- client.transport.emit(ATTACH_ERROR, reqId, msg);
447
+ const msg = `Cannot attach, schema "${schemaName}" does not exists`;
414
448
  console.error(msg);
449
+
450
+ client.transport.emit(ATTACH_ERROR, reqId, msg);
415
451
  }
416
- } else {
417
- const msg = `Cannot attach, schema "${schemaName}" does not exists`;
418
- client.transport.emit(ATTACH_ERROR, reqId, msg);
419
- console.error(msg);
420
452
  }
421
- });
453
+ );
422
454
 
423
455
  // ---------------------------------------------
424
456
  // OBSERVE PEERS (be notified when a state is created, lazy)
425
457
  // ---------------------------------------------
426
- client.transport.addListener(OBSERVE_REQUEST, reqId => {
427
- const statesInfos = [];
428
-
429
- this._serverStatesById.forEach(state => {
430
- const { schemaName, id, _creatorId } = state;
431
- // only track application states
432
- // (e.g. do not propagate infos about audit state)
433
- if (!PRIVATE_STATES.includes(schemaName) && _creatorId !== client.id) {
434
- statesInfos.push([schemaName, id, _creatorId]);
435
- }
436
- });
458
+ client.transport.addListener(OBSERVE_REQUEST, (reqId, observedSchemaName) => {
459
+ if (observedSchemaName === null || this._schemas.has(observedSchemaName)) {
460
+ const statesInfos = [];
437
461
 
438
- // add client to observers first because if some (sync) server side
439
- // callback throws, the client would never be added to the list
440
- this._observers.add(client);
462
+ this._sharedStatePrivateById.forEach(state => {
463
+ const isObservable = this[kIsObservableState](state);
441
464
 
442
- client.transport.emit(OBSERVE_RESPONSE, reqId, ...statesInfos);
465
+ if (isObservable) {
466
+ const { schemaName, id, _creatorId } = state;
467
+ statesInfos.push([schemaName, id, _creatorId]);
468
+ }
469
+ });
470
+
471
+ // add client to observers first because if some synchronous server side
472
+ // callback throws, the client would never be added to the list
473
+ this._observers.add(client);
474
+
475
+ client.transport.emit(OBSERVE_RESPONSE, reqId, ...statesInfos);
476
+ } else {
477
+ const msg = `Cannot observe, schema "${observedSchemaName}" does not exists`;
478
+ client.transport.emit(OBSERVE_ERROR, reqId, msg);
479
+ }
443
480
  });
444
481
 
445
482
  client.transport.addListener(UNOBSERVE_NOTIFICATION, () => {
@@ -458,18 +495,22 @@ class StateManager extends BaseStateManager {
458
495
  * @private
459
496
  */
460
497
  removeClient(nodeId) {
461
- for (let [_id, state] of this._serverStatesById.entries()) {
498
+ for (let [_id, state] of this._sharedStatePrivateById.entries()) {
462
499
  let deleteState = false;
463
500
 
464
501
  // define if the client is the creator of the state, in which case
465
502
  // everybody must delete it
466
- for (let [remoteId, attachedClient] of state._attachedClients.entries()) {
503
+ for (let [remoteId, clientInfos] of state._attachedClients) {
504
+ const attachedClient = clientInfos.client;
505
+
467
506
  if (nodeId === attachedClient.id && remoteId === state._creatorRemoteId) {
468
507
  deleteState = true;
469
508
  }
470
509
  }
471
510
 
472
- for (let [remoteId, attachedClient] of state._attachedClients.entries()) {
511
+ for (let [remoteId, clientInfos] of state._attachedClients) {
512
+ const attachedClient = clientInfos.client;
513
+
473
514
  if (nodeId === attachedClient.id) {
474
515
  state._detachClient(remoteId, attachedClient);
475
516
  }
@@ -480,7 +521,7 @@ class StateManager extends BaseStateManager {
480
521
  attachedClient.transport.emit(`${DELETE_NOTIFICATION}-${state.id}-${remoteId}`);
481
522
  }
482
523
 
483
- this._serverStatesById.delete(state.id);
524
+ this._sharedStatePrivateById.delete(state.id);
484
525
  }
485
526
  }
486
527
  }
@@ -547,14 +588,15 @@ class StateManager extends BaseStateManager {
547
588
  */
548
589
  deleteSchema(schemaName) {
549
590
  // @note: deleting schema
550
- for (let [_id, state] of this._serverStatesById.entries()) {
591
+ for (let [_id, state] of this._sharedStatePrivateById) {
551
592
  if (state.schemaName === schemaName) {
552
- for (let [remoteId, attached] of state._attachedClients.entries()) {
593
+ for (let [remoteId, clientInfos] of state._attachedClients) {
594
+ const attached = clientInfos.client;
553
595
  state._detachClient(remoteId, attached);
554
596
  attached.transport.emit(`${DELETE_NOTIFICATION}-${state.id}-${remoteId}`);
555
597
  }
556
598
 
557
- this._serverStatesById.delete(this.id);
599
+ this._sharedStatePrivateById.delete(state.id);
558
600
  }
559
601
  }
560
602
 
@@ -1,6 +1,6 @@
1
1
  import { parentPort } from 'node:worker_threads';
2
2
 
3
- import { getTime } from '@ircam/sc-gettime';
3
+ import { getTime } from '@ircam/sc-utils';
4
4
 
5
5
  let stack = [];
6
6
  let averageLatencyWindow = 5;
@@ -23,16 +23,15 @@ const encryptionIV = crypto
23
23
 
24
24
  export function encryptData(obj) {
25
25
  const data = JSON.stringify(obj);
26
- const cipher = crypto.createCipheriv(encryptionMethod, key, encryptionIV)
27
- return Buffer.from(
28
- cipher.update(data, 'utf8', 'hex') + cipher.final('hex')
29
- ).toString('base64')
26
+ const cipher = crypto.createCipheriv(encryptionMethod, key, encryptionIV);
27
+
28
+ return Buffer.from(cipher.update(data, 'utf8', 'hex') + cipher.final('hex')).toString('base64');
30
29
  }
31
30
 
32
31
  export function decryptData(encryptedData) {
33
- const buff = Buffer.from(encryptedData, 'base64')
34
- const decipher = crypto.createDecipheriv(encryptionMethod, key, encryptionIV)
32
+ const buff = Buffer.from(encryptedData, 'base64');
33
+ const decipher = crypto.createDecipheriv(encryptionMethod, key, encryptionIV);
35
34
 
36
- const decrypted = `${decipher.update(buff.toString('utf8'), 'hex', 'utf8')}${decipher.final('utf8')}`
35
+ const decrypted = `${decipher.update(buff.toString('utf8'), 'hex', 'utf8')}${decipher.final('utf8')}`;
37
36
  return JSON.parse(decrypted);
38
37
  }
@@ -1,25 +0,0 @@
1
- import { idGenerator } from '@ircam/sc-utils';
2
-
3
- const generateRequestId = idGenerator();
4
- const requestPromises = new Map();
5
-
6
- export function storeRequestPromise(resolve, reject) {
7
- const reqId = generateRequestId.next().value;
8
- requestPromises.set(reqId, { resolve, reject });
9
-
10
- return reqId;
11
- }
12
-
13
- export function resolveRequest(reqId, data) {
14
- const { resolve } = requestPromises.get(reqId);
15
- requestPromises.delete(reqId);
16
-
17
- resolve(data);
18
- }
19
-
20
- export function rejectRequest(reqId, data) {
21
- const { reject } = requestPromises.get(reqId);
22
- requestPromises.delete(reqId);
23
-
24
- reject(data);
25
- }
package/tsconfig.json DELETED
@@ -1,21 +0,0 @@
1
- {
2
- "include": ["src/**/*.js"],
3
-
4
- "compilerOptions": {
5
- // Tells TypeScript to read JS files, as
6
- // normally they are ignored as source files
7
- "allowJs": true,
8
- // Generate d.ts files
9
- "declaration": true,
10
- // This compiler run should
11
- // only output d.ts files
12
- "emitDeclarationOnly": true,
13
- // Types should go into this directory.
14
- // Removing this would place the .d.ts files
15
- // next to the .js files
16
- "outDir": "types",
17
- // go to js file when using IDE functions like
18
- // "Go to Definition" in VSCode
19
- "declarationMap": true
20
- }
21
- }