@soundworks/core 5.2.0 → 5.2.1

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,12 @@
1
+ ## v5.2.1 - 2025-08-11
2
+
3
+ - Fix: Use CancelablePromise to cancel pending promise rather than reject when a state is deleted
4
+
5
+ ## v5.2.0 - 2025-05-05
6
+
7
+ - Feat: add ServerStateManager#isClassDefined method
8
+ - Fix: Remove class description cache mechanism to avoid possible concurrency issues when recreating a SharedStateClass with same name
9
+
1
10
  ## v5.1.1 - 2025-04-28
2
11
 
3
12
  - Hotfix: run behind https proxy
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soundworks/core",
3
- "version": "5.2.0",
3
+ "version": "5.2.1",
4
4
  "description": "Open-source creative coding framework for distributed applications based on Web technologies",
5
5
  "collaborators": [
6
6
  "Benjamin Matuszewski",
@@ -55,6 +55,7 @@
55
55
  },
56
56
  "dependencies": {
57
57
  "@ircam/sc-utils": "^1.8.0",
58
+ "cancelable-promise": "^4.3.1",
58
59
  "chalk": "^5.3.0",
59
60
  "fast-deep-equal": "^3.1.3",
60
61
  "isomorphic-ws": "^5.0.0",
@@ -214,10 +214,10 @@ class ClientContext {
214
214
 
215
215
  // we need the try/catch block to change the promise rejection into proper error
216
216
  try {
217
- await new Promise((resolve, reject) => {
218
- const reqId = promiseStore.add(resolve, reject, 'ClientContext#enter');
219
- this.#client.socket.send(CONTEXT_ENTER_REQUEST, reqId, this.name);
220
- });
217
+ const { id: reqId, promise } = promiseStore.createPromise();
218
+ this.#client.socket.send(CONTEXT_ENTER_REQUEST, reqId, this.name);
219
+
220
+ await promise;
221
221
  } catch (err) {
222
222
  throw new Error(err);
223
223
  }
@@ -248,10 +248,10 @@ class ClientContext {
248
248
  async exit() {
249
249
  // we need the try/catch block to change the promise rejection into proper error
250
250
  try {
251
- await new Promise((resolve, reject) => {
252
- const reqId = promiseStore.add(resolve, reject, 'ClientContext#exit');
253
- this.#client.socket.send(CONTEXT_EXIT_REQUEST, reqId, this.name);
254
- });
251
+ const { id: reqId, promise } = promiseStore.createPromise();
252
+ this.#client.socket.send(CONTEXT_EXIT_REQUEST, reqId, this.name);
253
+
254
+ await promise;
255
255
  } catch (err) {
256
256
  throw new Error(err);
257
257
  }
@@ -18,7 +18,7 @@
18
18
  * Environment configuration object.
19
19
  *
20
20
  * @typedef ClientEnvConfig
21
- * @type {objecy}
21
+ * @type {object}
22
22
  * @property {number} env.port - Port on which the server is listening.
23
23
  * @property {string} env.serverAddress - Domain name or IP of the server.
24
24
  * Mandatory when node clients are declared
@@ -244,10 +244,10 @@ class BaseStateManager {
244
244
  throw new DOMException(`Cannot execute 'getClassDescription' on BaseStateManager: BaseStateManager is not inited`, 'InvalidStateError');
245
245
  }
246
246
 
247
- return new Promise((resolve, reject) => {
248
- const reqId = this.#promiseStore.add(resolve, reject, 'BaseStateManager#getClassDescription');
249
- this[kStateManagerClient].transport.emit(GET_CLASS_DESCRIPTION_REQUEST, reqId, className);
250
- });
247
+ const { id: reqId, promise } = this.#promiseStore.createPromise();
248
+ this[kStateManagerClient].transport.emit(GET_CLASS_DESCRIPTION_REQUEST, reqId, className);
249
+
250
+ return promise;
251
251
  }
252
252
 
253
253
  /**
@@ -272,10 +272,10 @@ class BaseStateManager {
272
272
  throw new DOMException(`Cannot execute 'create' on BaseStateManager: BaseStateManager is not inited`, 'InvalidStateError');
273
273
  }
274
274
 
275
- return new Promise((resolve, reject) => {
276
- const reqId = this.#promiseStore.add(resolve, reject, 'BaseStateManager#create');
277
- this[kStateManagerClient].transport.emit(CREATE_REQUEST, reqId, className, initValues);
278
- });
275
+ const { id: reqId, promise } = this.#promiseStore.createPromise();
276
+ this[kStateManagerClient].transport.emit(CREATE_REQUEST, reqId, className, initValues);
277
+
278
+ return promise;
279
279
  }
280
280
 
281
281
  /**
@@ -380,10 +380,10 @@ class BaseStateManager {
380
380
  }
381
381
  }
382
382
 
383
- return new Promise((resolve, reject) => {
384
- const reqId = this.#promiseStore.add(resolve, reject, 'BaseStateManager#attach');
385
- this[kStateManagerClient].transport.emit(ATTACH_REQUEST, reqId, className, stateId, filter);
386
- });
383
+ const { id: reqId, promise } = this.#promiseStore.createPromise();
384
+ this[kStateManagerClient].transport.emit(ATTACH_REQUEST, reqId, className, stateId, filter);
385
+
386
+ return promise;
387
387
  }
388
388
 
389
389
  /**
@@ -562,33 +562,33 @@ class BaseStateManager {
562
562
  }
563
563
 
564
564
  // resend request to get updated list of states
565
- return new Promise((resolve, reject) => {
566
- const reqId = this.#promiseStore.add(resolve, reject, 'BaseStateManager#observe');
567
- // store the callback for execution on the response. the returned Promise
568
- // is fulfilled once callback has been executed with each existing states
569
- const observeInfos = [observedClassName, callback, options];
570
- this.#observeRequestCallbacks.set(reqId, observeInfos);
571
-
572
- // NOTE: do not store in `_observeListeners` yet as it can produce race
573
- // conditions, e.g.:
574
- // ```
575
- // await client.stateManager.observe(async (className, stateId, nodeId) => {});
576
- // // client now receives OBSERVE_NOTIFICATIONS
577
- // await otherClient.stateManager.create('a');
578
- // // second observer added in between
579
- // client.stateManager.observe(async (className, stateId, nodeId) => {});
580
- // ````
581
- // OBSERVE_NOTIFICATION is received before the OBSERVE_RESPONSE, then the
582
- // second observer is called twice:
583
- // - OBSERVE_RESPONSE 1 []
584
- // - OBSERVE_NOTIFICATION [ 'a', 1, 0 ]
585
- // - OBSERVE_NOTIFICATION [ 'a', 1, 0 ] // this should not happen
586
- // - OBSERVE_RESPONSE 1 [ [ 'a', 1, 0 ] ]
587
- //
588
- // cf. unit test `observe should properly behave in race condition`
589
-
590
- this[kStateManagerClient].transport.emit(OBSERVE_REQUEST, reqId, observedClassName);
591
- });
565
+ const { id: reqId, promise } = this.#promiseStore.createPromise();
566
+ // store the callback for execution on the response. the returned Promise
567
+ // is fulfilled once callback has been executed with each existing states
568
+ const observeInfos = [observedClassName, callback, options];
569
+ this.#observeRequestCallbacks.set(reqId, observeInfos);
570
+
571
+ // NOTE: do not store in `_observeListeners` yet as it can produce race
572
+ // conditions, e.g.:
573
+ // ```
574
+ // await client.stateManager.observe(async (className, stateId, nodeId) => {});
575
+ // // client now receives OBSERVE_NOTIFICATIONS
576
+ // await otherClient.stateManager.create('a');
577
+ // // second observer added in between
578
+ // client.stateManager.observe(async (className, stateId, nodeId) => {});
579
+ // ````
580
+ // OBSERVE_NOTIFICATION is received before the OBSERVE_RESPONSE, then the
581
+ // second observer is called twice:
582
+ // - OBSERVE_RESPONSE 1 []
583
+ // - OBSERVE_NOTIFICATION [ 'a', 1, 0 ]
584
+ // - OBSERVE_NOTIFICATION [ 'a', 1, 0 ] // this should not happen
585
+ // - OBSERVE_RESPONSE 1 [ [ 'a', 1, 0 ] ]
586
+ //
587
+ // cf. unit test `observe should properly behave in race condition`
588
+
589
+ this[kStateManagerClient].transport.emit(OBSERVE_REQUEST, reqId, observedClassName);
590
+
591
+ return promise;
592
592
  }
593
593
 
594
594
  /**
@@ -1,11 +1,7 @@
1
1
  import { BATCHED_TRANSPORT_CHANNEL } from './constants.js';
2
2
 
3
3
  /**
4
- * This class proxies transports given the SharedStateManager to batch messages
5
- *
6
- * @param {number} [options.wait=0] - Wait for given number of milliseconds
7
- * for stacking messages before emitting on the network. If 0 is given, network
8
- * message is emitted on next microtask
4
+ * This class proxies transports, i.e. WebSockets, to batch message sends
9
5
  * @private
10
6
  */
11
7
  class BatchedTransport {
@@ -1,52 +1,68 @@
1
- import { idGenerator } from '@ircam/sc-utils';
1
+ import { counter } from '@ircam/sc-utils';
2
+ // common js module...
3
+ import cancelable from 'cancelable-promise';
4
+ const { CancelablePromise } = cancelable;
2
5
 
3
6
  /** @private */
4
7
  export default class PromiseStore {
5
8
  constructor(name) {
6
9
  this.name = name;
7
10
  this.store = new Map();
8
- this.generateRequestId = idGenerator();
11
+ this.generateId = counter();
9
12
  }
10
13
 
11
- add(resolve, reject, type, localParams) {
12
- const reqId = this.generateRequestId.next().value;
13
- this.store.set(reqId, { resolve, reject, type, localParams });
14
+ createPromise() {
15
+ const id = this.generateId();
16
+ let resolve, reject;
14
17
 
15
- return reqId;
18
+ const promise = new CancelablePromise((resolveFunc, rejectFunc) => {
19
+ resolve = resolveFunc;
20
+ reject = rejectFunc;
21
+ });
22
+
23
+ this.store.set(id, { promise, resolve, reject });
24
+
25
+ return { id, promise };
26
+ }
27
+
28
+ // associate data to merge within the data given when calling `resolve`
29
+ // cf. SharedState#set
30
+ associateResolveData(id, associatedData) {
31
+ const stored = this.store.get(id);
32
+ stored.associatedData = associatedData;
16
33
  }
17
34
 
18
- resolve(reqId, data) {
19
- if (this.store.has(reqId)) {
20
- const { resolve, localParams } = this.store.get(reqId);
21
- this.store.delete(reqId);
35
+ resolve(id, data) {
36
+ if (this.store.has(id)) {
37
+ const { resolve, associatedData } = this.store.get(id);
38
+ this.store.delete(id);
22
39
 
23
40
  // re-merge local params into network response
24
- if (localParams !== undefined) {
25
- Object.assign(data, localParams);
41
+ if (associatedData !== undefined) {
42
+ Object.assign(data, associatedData);
26
43
  }
27
44
 
28
45
  resolve(data);
29
46
  } else {
30
- throw new ReferenceError(`Cannot resolve request id (${reqId}): id does not exist`);
47
+ throw new ReferenceError(`Cannot resolve request id (${id}): id does not exist`);
31
48
  }
32
49
  }
33
50
 
34
- reject(reqId, msg) {
35
- if (this.store.has(reqId)) {
36
- const { reject } = this.store.get(reqId);
37
- this.store.delete(reqId);
51
+ reject(id, msg) {
52
+ if (this.store.has(id)) {
53
+ const { reject } = this.store.get(id);
54
+ this.store.delete(id);
38
55
 
39
56
  reject(new Error(msg));
40
57
  } else {
41
- throw new ReferenceError(`Cannot resolve request id (${reqId}): id does not exist`);
58
+ throw new ReferenceError(`Cannot resolve request id (${id}): id does not exist`);
42
59
  }
43
60
  }
44
61
 
45
- // reject all pending request
62
+ // cancel all pending request
46
63
  flush() {
47
- for (let [_reqId, entry] of this.store) {
48
- const { reject, type } = entry;
49
- reject(new Error(`Discard promise '${type}'`));
64
+ for (let [_id, entry] of this.store) {
65
+ entry.promise.cancel();
50
66
  }
51
67
 
52
68
  this.store.clear();
@@ -555,10 +555,13 @@ class SharedState {
555
555
  updates = sharedParams;
556
556
 
557
557
  // go through server-side normal behavior
558
- return new Promise((resolve, reject) => {
559
- const reqId = this[kSharedStatePromiseStore].add(resolve, reject, 'SharedState#set', forwardParams);
560
- this.#client.transport.emit(`${UPDATE_REQUEST}-${this.#id}-${this.#instanceId}`, reqId, updates);
561
- });
558
+ const { id: reqId, promise } = this[kSharedStatePromiseStore].createPromise();
559
+ if (forwardParams) {
560
+ this[kSharedStatePromiseStore].associateResolveData(reqId, forwardParams);
561
+ }
562
+ this.#client.transport.emit(`${UPDATE_REQUEST}-${this.#id}-${this.#instanceId}`, reqId, updates);
563
+
564
+ return promise;
562
565
  }
563
566
 
564
567
  /**
@@ -710,15 +713,13 @@ class SharedState {
710
713
  this.#onUpdateCallbacks.clear();
711
714
 
712
715
  if (this.#isOwner) {
713
- return new Promise((resolve, reject) => {
714
- const reqId = this[kSharedStatePromiseStore].add(resolve, reject, 'SharedState#delete');
715
- this.#client.transport.emit(`${DELETE_REQUEST}-${this.#id}-${this.#instanceId}`, reqId);
716
- });
716
+ const { id: reqId, promise } = this[kSharedStatePromiseStore].createPromise();
717
+ this.#client.transport.emit(`${DELETE_REQUEST}-${this.#id}-${this.#instanceId}`, reqId);
718
+ return promise;
717
719
  } else {
718
- return new Promise((resolve, reject) => {
719
- const reqId = this[kSharedStatePromiseStore].add(resolve, reject, 'SharedState#detach');
720
- this.#client.transport.emit(`${DETACH_REQUEST}-${this.#id}-${this.#instanceId}`, reqId);
721
- });
720
+ const { id: reqId, promise } = this[kSharedStatePromiseStore].createPromise();
721
+ this.#client.transport.emit(`${DETACH_REQUEST}-${this.#id}-${this.#instanceId}`, reqId);
722
+ return promise;
722
723
  }
723
724
  }
724
725
 
@@ -1,2 +1,2 @@
1
1
  // [WARNING] This file is generated, do not edit
2
- export default '5.2.0';
2
+ export default '5.2.1';
@@ -18,7 +18,25 @@ export type ClientAppConfig = {
18
18
  /**
19
19
  * Environment configuration object.
20
20
  */
21
- export type ClientEnvConfig = objecy;
21
+ export type ClientEnvConfig = {
22
+ /**
23
+ * - Port on which the server is listening.
24
+ */
25
+ port: number;
26
+ /**
27
+ * - Domain name or IP of the server.
28
+ * Mandatory when node clients are declared
29
+ */
30
+ serverAddress: string;
31
+ /**
32
+ * - Define is the server run in http or in https.
33
+ */
34
+ useHttps: boolean;
35
+ /**
36
+ * - If running behind a proxy, base URL of the application.
37
+ */
38
+ baseUrl?: string;
39
+ };
22
40
  /**
23
41
  * Configuration object for a client running in a browser runtime.
24
42
  */
@@ -1,10 +1,6 @@
1
1
  export default BatchedTransport;
2
2
  /**
3
- * This class proxies transports given the SharedStateManager to batch messages
4
- *
5
- * @param {number} [options.wait=0] - Wait for given number of milliseconds
6
- * for stacking messages before emitting on the network. If 0 is given, network
7
- * message is emitted on next microtask
3
+ * This class proxies transports, i.e. WebSockets, to batch message sends
8
4
  * @private
9
5
  */
10
6
  declare class BatchedTransport {
@@ -3,9 +3,13 @@ export default class PromiseStore {
3
3
  constructor(name: any);
4
4
  name: any;
5
5
  store: Map<any, any>;
6
- generateRequestId: any;
7
- add(resolve: any, reject: any, type: any, localParams: any): any;
8
- resolve(reqId: any, data: any): void;
9
- reject(reqId: any, msg: any): void;
6
+ generateId: any;
7
+ createPromise(): {
8
+ id: any;
9
+ promise: any;
10
+ };
11
+ associateResolveData(id: any, associatedData: any): void;
12
+ resolve(id: any, data: any): void;
13
+ reject(id: any, msg: any): void;
10
14
  flush(): void;
11
15
  }
@@ -1,2 +1,2 @@
1
- declare const _default: "5.2.0";
1
+ declare const _default: "5.2.1";
2
2
  export default _default;