@soundworks/core 4.0.0-alpha.17 → 4.0.0-alpha.19

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/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ ## v4.0.0-alpha.19 - 17/01/2024
2
+
3
+ - Hotfix for #85
4
+
5
+ ## v4.0.0-alpha.18 - 15/01/2024
6
+
7
+ - Batch network message for SharedStates
8
+
1
9
  ## v4.0.0-alpha.17 - 16/12/2023
2
10
 
3
11
  - Support bundling the server to cjs for Max externals
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soundworks/core",
3
- "version": "4.0.0-alpha.17",
3
+ "version": "4.0.0-alpha.19",
4
4
  "description": "Open-source creative coding framework for distributed applications based on Web technologies",
5
5
  "authors": [
6
6
  "Benjamin Matuszewski",
@@ -223,9 +223,16 @@ ${JSON.stringify(initValues, null, 2)}`);
223
223
  // remove listeners
224
224
  this._client.transport.removeAllListeners(`${UPDATE_RESPONSE}-${this.id}-${this.remoteId}`);
225
225
  this._client.transport.removeAllListeners(`${UPDATE_NOTIFICATION}-${this.id}-${this.remoteId}`);
226
- this._client.transport.removeAllListeners(`${DELETE_RESPONSE}-${this.id}-${this.remoteId}`);
226
+ this._client.transport.removeAllListeners(`${UPDATE_ABORT}-${this.id}-${this.remoteId}`);
227
227
  this._client.transport.removeAllListeners(`${DELETE_NOTIFICATION}-${this.id}-${this.remoteId}`);
228
- this._client.transport.removeAllListeners(`${DETACH_RESPONSE}-${this.id}-${this.remoteId}`);
228
+
229
+ if (this._isOwner) {
230
+ this._client.transport.removeAllListeners(`${DELETE_RESPONSE}-${this.id}-${this.remoteId}`);
231
+ this._client.transport.removeAllListeners(`${DELETE_ERROR}-${this.id}-${this.remoteId}`);
232
+ } else {
233
+ this._client.transport.removeAllListeners(`${DETACH_RESPONSE}-${this.id}-${this.remoteId}`);
234
+ this._client.transport.removeAllListeners(`${DETACH_ERROR}-${this.id}-${this.remoteId}`);
235
+ }
229
236
  }
230
237
 
231
238
  /** @private */
@@ -126,7 +126,11 @@ class BaseSharedStateCollection {
126
126
  * current call and will be passed as third argument to all update listeners.
127
127
  */
128
128
  async set(updates, context = null) {
129
- return await this._controller.set(updates, context);
129
+ // hot fix for https://github.com/collective-soundworks/soundworks/issues/85
130
+ // to be cleaned soon
131
+ const promises = this._states.map(state => state.set(updates, context));
132
+ return Promise.all(promises);
133
+ // return await this._controller.set(updates, context);
130
134
  }
131
135
 
132
136
  /**
@@ -1,6 +1,7 @@
1
1
  import { isString, isFunction, isPlainObject } from '@ircam/sc-utils';
2
2
  import SharedState from './BaseSharedState.js';
3
3
  import SharedStateCollection from './BaseSharedStateCollection.js';
4
+ import BatchedTransport from './BatchedTransport.js';
4
5
  import PromiseStore from './PromiseStore.js';
5
6
  import {
6
7
  CREATE_REQUEST,
@@ -30,7 +31,8 @@ class BaseStateManager {
30
31
  * Must implement a basic EventEmitter API.
31
32
  */
32
33
  constructor(id, transport) {
33
- this.client = { id, transport };
34
+ // proxy transport with BatchedTransport;
35
+ this.client = { id, transport: new BatchedTransport(transport) };
34
36
 
35
37
  this._statesById = new Map(); // <id, state>
36
38
  this._cachedSchemas = new Map(); // <shemaName, definition>
@@ -388,7 +390,7 @@ class BaseStateManager {
388
390
  try {
389
391
  await collection._init();
390
392
  } catch (err) {
391
- console.log(err.message)
393
+ console.log(err.message);
392
394
  throw new Error(`Cannot create collection, schema "${schemaName}" does not exists`);
393
395
  }
394
396
 
@@ -0,0 +1,66 @@
1
+ import { BATCHED_TRANSPORT_CHANNEL } from './constants.js';
2
+
3
+ /**
4
+ * This class proxies transports given the SharedStateManager to batch messages
5
+ * @private
6
+ */
7
+ class BatchedTransport {
8
+ constructor(transport) {
9
+ this._transport = transport;
10
+ this._stack = [];
11
+ this._listeners = new Map();
12
+ this._sending = false;
13
+
14
+ this._transport.addListener(BATCHED_TRANSPORT_CHANNEL, stack => {
15
+ stack.forEach(entry => {
16
+ const [channel, args] = entry;
17
+ // server side the transport is the same EventEmitter instance
18
+ // for both state manager server and client, so channel might not exist
19
+ // one side or the other.
20
+ if (this._listeners.has(channel)) {
21
+ const callbacks = this._listeners.get(channel);
22
+ callbacks.forEach(callback => callback(...args));
23
+ }
24
+ });
25
+ });
26
+ }
27
+
28
+ addListener(channel, callback) {
29
+ if (!this._listeners.has(channel)) {
30
+ this._listeners.set(channel, new Set());
31
+ }
32
+
33
+ const callbacks = this._listeners.get(channel);
34
+ callbacks.add(callback);
35
+ }
36
+
37
+ async emit(channel, ...args) {
38
+ this._stack.push([channel, args]);
39
+
40
+ if (!this._sending) {
41
+ this._sending = true;
42
+ this._sending = await false;
43
+ const stack = this._stack;
44
+ this._stack = [];
45
+ this._transport.emit(BATCHED_TRANSPORT_CHANNEL, stack);
46
+ }
47
+ }
48
+
49
+ removeListener(channel, callback) {
50
+ if (!this._listeners.has(channel)) {
51
+ const callbacks = this._listeners.get(channel);
52
+ callbacks.delete(callback);
53
+ }
54
+ }
55
+
56
+ removeAllListeners(channel = null) {
57
+ if (channel === null) {
58
+ this._listeners.clear();
59
+ } else if (this._listeners.has(channel)) {
60
+ const callbacks = this._listeners.get(channel);
61
+ callbacks.clear();
62
+ }
63
+ }
64
+ }
65
+
66
+ export default BatchedTransport;
@@ -70,16 +70,7 @@ class SharedStatePrivate {
70
70
  let hasUpdates = false;
71
71
 
72
72
  for (let name in updates) {
73
- // from v3.1.0 - the `filteredUpdates` check is made using 'fast-deep-equal'
74
- // cf. https://github.com/epoberezkin/fast-deep-equal
75
- // therefore unchanged objects are not considered changed
76
- // nor propagated anymore.
77
- // until v3.0.4 - we checked the `schema[name].type === 'any'`, to always consider
78
- // objects as dirty, because if the state is attached locally, we
79
- // compare the Object instances instead of their values.
80
- // @note - this should be made more robust but how?
81
73
  const [newValue, changed] = this._parameters.set(name, updates[name]);
82
-
83
74
  // if `filterChange` is set to `false` we don't check if the value
84
75
  // has been changed or not, it is always propagated to client states
85
76
  const { event, filterChange } = this._parameters.getSchema(name);
@@ -96,25 +87,23 @@ class SharedStatePrivate {
96
87
  }
97
88
 
98
89
  if (hasUpdates) {
99
- // Collection Controller logic
90
+
91
+ // Collection controller logic
100
92
  if (isOwner && inCollection) {
101
- // notify the requester back
102
- // no need to check for attached clients as it is not observable
93
+ // notify the collection controller back
103
94
  client.transport.emit(
104
95
  `${UPDATE_RESPONSE}-${this.id}-${remoteId}`,
105
- reqId, filteredUpdates, context
96
+ reqId, filteredUpdates, context,
106
97
  );
107
98
 
108
99
  // loop through all private states
109
- for (let [stateId, state] of this._manager._sharedStatePrivateById) {
100
+ for (let [_stateId, state] of this._manager._sharedStatePrivateById) {
110
101
  // pick all states with same schema name and not this
111
102
  if (state !== this && this.schemaName === state.schemaName) {
112
103
  // notify all attached clients except those who belong to a collection
113
104
  // i.e. !isOwner && inCollection, they will be notified by their own
114
105
  // collection controller
115
106
  for (let [remoteId, clientInfos] of state._attachedClients) {
116
- const { client, isOwner, inCollection } = clientInfos;
117
-
118
107
  if (!clientInfos.isOwner && clientInfos.inCollection) {
119
108
  continue;
120
109
  } else {
@@ -122,7 +111,7 @@ class SharedStatePrivate {
122
111
 
123
112
  peer.transport.emit(
124
113
  `${UPDATE_NOTIFICATION}-${state.id}-${remoteId}`,
125
- filteredUpdates, context
114
+ filteredUpdates, context,
126
115
  );
127
116
  }
128
117
  }
@@ -131,7 +120,7 @@ class SharedStatePrivate {
131
120
  } else {
132
121
 
133
122
  // Normal case
134
-
123
+ //
135
124
  // We need to handle cases where:
136
125
  // - client state (client.id: 2) sends a request
137
126
  // - server attached state (client.id: -1) spot a problem and overrides the value
@@ -146,14 +135,14 @@ class SharedStatePrivate {
146
135
  // is synchronous it can break ordering if a subscription function makes
147
136
  // itself an update in reaction to an update. Propagating to server last
148
137
  // alllows to maintain network messages order consistent.
149
-
138
+ //
150
139
  // @note - remoteId correspond to unique remote state id
151
140
 
152
141
  // propagate RESPONSE to the client that originates the request if not the server
153
142
  if (client.id !== -1) {
154
143
  client.transport.emit(
155
144
  `${UPDATE_RESPONSE}-${this.id}-${remoteId}`,
156
- reqId, filteredUpdates, context
145
+ reqId, filteredUpdates, context,
157
146
  );
158
147
  }
159
148
 
@@ -164,7 +153,7 @@ class SharedStatePrivate {
164
153
  if (remoteId !== peerRemoteId && peer.id !== -1) {
165
154
  peer.transport.emit(
166
155
  `${UPDATE_NOTIFICATION}-${this.id}-${peerRemoteId}`,
167
- filteredUpdates, context
156
+ filteredUpdates, context,
168
157
  );
169
158
  }
170
159
  }
@@ -173,7 +162,7 @@ class SharedStatePrivate {
173
162
  if (client.id === -1) {
174
163
  client.transport.emit(
175
164
  `${UPDATE_RESPONSE}-${this.id}-${remoteId}`,
176
- reqId, filteredUpdates, context
165
+ reqId, filteredUpdates, context,
177
166
  );
178
167
  }
179
168
 
@@ -184,7 +173,7 @@ class SharedStatePrivate {
184
173
  if (remoteId !== peerRemoteId && peer.id === -1) {
185
174
  peer.transport.emit(
186
175
  `${UPDATE_NOTIFICATION}-${this.id}-${peerRemoteId}`,
187
- filteredUpdates, context
176
+ filteredUpdates, context,
188
177
  );
189
178
  }
190
179
  }
@@ -1,6 +1,9 @@
1
1
  // id of the server when owner of a state
2
2
  export const SERVER_ID = -1;
3
3
 
4
+ // batched transport channel
5
+ export const BATCHED_TRANSPORT_CHANNEL = 'b:t';
6
+
4
7
  // shared states channels
5
8
  export const CREATE_REQUEST = 's:c:req';
6
9
  export const CREATE_RESPONSE = 's:c:res';
@@ -1,16 +1,11 @@
1
1
  import { Worker } from 'node:worker_threads';
2
- import path from 'node:path';
3
- // import { fileURLToPath } from 'node:url';
4
-
5
2
  import querystring from 'querystring';
6
- import { WebSocketServer } from 'ws';
7
- import WebSocket from 'ws';
3
+ import { default as WebSocket, WebSocketServer } from 'ws';
8
4
 
9
5
  import Socket from './Socket.js';
10
6
  import networkLatencyWorker from './audit-network-latency.worker.js';
11
7
 
12
- // this crashes when bundling server to cjs module for Max, fallback to
13
- // string worker for now
8
+ // this crashes when bundling server to cjs module for Max, fallback to cjs worker
14
9
  // const __dirname = fileURLToPath(new URL('.', import.meta.url));
15
10
 
16
11
  /**
@@ -4,6 +4,7 @@ import { idGenerator, isString, isPlainObject } from '@ircam/sc-utils';
4
4
  import clonedeep from 'lodash/cloneDeep.js';
5
5
 
6
6
  import BaseStateManager from '../common/BaseStateManager.js';
7
+ import BatchedTransport from '../common/BatchedTransport.js';
7
8
  import ParameterBag from '../common/ParameterBag.js';
8
9
  import SharedStatePrivate from '../common/SharedStatePrivate.js';
9
10
  import {
@@ -344,6 +345,8 @@ class StateManager extends BaseStateManager {
344
345
  * @private
345
346
  */
346
347
  addClient(nodeId, transport) {
348
+ transport = new BatchedTransport(transport);
349
+
347
350
  const client = { id: nodeId, transport };
348
351
  this._clientByNodeId.set(nodeId, client);
349
352
 
@@ -360,7 +363,7 @@ class StateManager extends BaseStateManager {
360
363
  const remoteId = generateRemoteId.next().value;
361
364
  const state = new SharedStatePrivate(stateId, schemaName, schema, this, initValues);
362
365
 
363
- // attach client to the state as owner
366
+ // attach client to the state as owner
364
367
  const isOwner = true;
365
368
  state._attachClient(remoteId, client, isOwner, inCollection);
366
369
 
@@ -371,7 +374,7 @@ class StateManager extends BaseStateManager {
371
374
 
372
375
  client.transport.emit(
373
376
  CREATE_RESPONSE,
374
- reqId, stateId, remoteId, schemaName, schemaOption, currentValues
377
+ reqId, stateId, remoteId, schemaName, schemaOption, currentValues,
375
378
  );
376
379
 
377
380
  const isObservable = this[kIsObservableState](state);
@@ -393,7 +396,7 @@ class StateManager extends BaseStateManager {
393
396
 
394
397
  client.transport.emit(CREATE_ERROR, reqId, msg);
395
398
  }
396
- }
399
+ },
397
400
  );
398
401
 
399
402
  // ---------------------------------------------
@@ -434,7 +437,7 @@ class StateManager extends BaseStateManager {
434
437
 
435
438
  client.transport.emit(
436
439
  ATTACH_RESPONSE,
437
- reqId, state.id, remoteId, schemaName, schemaOption, currentValues
440
+ reqId, state.id, remoteId, schemaName, schemaOption, currentValues,
438
441
  );
439
442
 
440
443
  } else {
@@ -449,7 +452,7 @@ class StateManager extends BaseStateManager {
449
452
 
450
453
  client.transport.emit(ATTACH_ERROR, reqId, msg);
451
454
  }
452
- }
455
+ },
453
456
  );
454
457
 
455
458
  // ---------------------------------------------