@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 +9 -0
- package/package.json +2 -1
- package/src/client/ClientContext.js +8 -8
- package/src/client/index.js +1 -1
- package/src/common/BaseStateManager.js +39 -39
- package/src/common/BatchedTransport.js +1 -5
- package/src/common/PromiseStore.js +38 -22
- package/src/common/SharedState.js +13 -12
- package/src/common/version.js +1 -1
- package/types/client/index.d.ts +19 -1
- package/types/common/BatchedTransport.d.ts +1 -5
- package/types/common/PromiseStore.d.ts +8 -4
- package/types/common/version.d.ts +1 -1
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.
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
}
|
package/src/client/index.js
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* Environment configuration object.
|
|
19
19
|
*
|
|
20
20
|
* @typedef ClientEnvConfig
|
|
21
|
-
* @type {
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
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
|
|
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 {
|
|
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.
|
|
11
|
+
this.generateId = counter();
|
|
9
12
|
}
|
|
10
13
|
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
+
createPromise() {
|
|
15
|
+
const id = this.generateId();
|
|
16
|
+
let resolve, reject;
|
|
14
17
|
|
|
15
|
-
|
|
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(
|
|
19
|
-
if (this.store.has(
|
|
20
|
-
const { resolve,
|
|
21
|
-
this.store.delete(
|
|
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 (
|
|
25
|
-
Object.assign(data,
|
|
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 (${
|
|
47
|
+
throw new ReferenceError(`Cannot resolve request id (${id}): id does not exist`);
|
|
31
48
|
}
|
|
32
49
|
}
|
|
33
50
|
|
|
34
|
-
reject(
|
|
35
|
-
if (this.store.has(
|
|
36
|
-
const { reject } = this.store.get(
|
|
37
|
-
this.store.delete(
|
|
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 (${
|
|
58
|
+
throw new ReferenceError(`Cannot resolve request id (${id}): id does not exist`);
|
|
42
59
|
}
|
|
43
60
|
}
|
|
44
61
|
|
|
45
|
-
//
|
|
62
|
+
// cancel all pending request
|
|
46
63
|
flush() {
|
|
47
|
-
for (let [
|
|
48
|
-
|
|
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
|
-
|
|
559
|
-
|
|
560
|
-
this
|
|
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
|
-
|
|
714
|
-
|
|
715
|
-
|
|
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
|
-
|
|
719
|
-
|
|
720
|
-
|
|
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
|
|
package/src/common/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// [WARNING] This file is generated, do not edit
|
|
2
|
-
export default '5.2.
|
|
2
|
+
export default '5.2.1';
|
package/types/client/index.d.ts
CHANGED
|
@@ -18,7 +18,25 @@ export type ClientAppConfig = {
|
|
|
18
18
|
/**
|
|
19
19
|
* Environment configuration object.
|
|
20
20
|
*/
|
|
21
|
-
export type ClientEnvConfig =
|
|
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
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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.
|
|
1
|
+
declare const _default: "5.2.1";
|
|
2
2
|
export default _default;
|