@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.
@@ -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,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
- // 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
+ // 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
- // init binary socket
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 `ws` "native" events
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._intervalId);
174
+ clearInterval(this._heartbeatId);
175
+
175
176
  // clean rooms
176
177
  for (let [_key, room] of this.rooms) {
177
178
  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.
@@ -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
- const workerPathname = path.join(__dirname, 'audit-network-latency.worker.js');
49
- this._latencyStatsWorker = new Worker(workerPathname);
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
- server: server.httpServer,
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 clonedeep from 'lodash.clonedeep';
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
- import { idGenerator } from '../common/utils.js';
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._serverStatesById = new Map();
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(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
+ 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
- } 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
- });
402
+ },
403
+ );
373
404
 
374
405
  // ---------------------------------------------
375
406
  // ATTACH (when creator, is attached by default)
376
407
  // ---------------------------------------------
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;
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
- 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);
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, no existing state for schema "${schemaName}" with stateId: "${stateId}"`;
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
- } 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
- });
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
- 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
- });
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
- // 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);
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
- client.transport.emit(OBSERVE_RESPONSE, reqId, ...statesInfos);
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._serverStatesById.entries()) {
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, attachedClient] of state._attachedClients.entries()) {
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, attachedClient] of state._attachedClients.entries()) {
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._serverStatesById.delete(state.id);
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._serverStatesById.entries()) {
610
+ for (let [_id, state] of this._sharedStatePrivateById) {
540
611
  if (state.schemaName === schemaName) {
541
- for (let [remoteId, attached] of state._attachedClients.entries()) {
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._serverStatesById.delete(this.id);
618
+ this._sharedStatePrivateById.delete(state.id);
547
619
  }
548
620
  }
549
621
 
@@ -1,6 +1,8 @@
1
- import { parentPort } from 'node:worker_threads';
2
-
3
- import { getTime } from '@ircam/sc-gettime';
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
- parentPort.on('message', msg => {
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
+ }