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

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 ADDED
@@ -0,0 +1,4 @@
1
+ ## v4.0.0-alpha.15 - 13/12/2023
2
+
3
+ - Fix several issues with shared states
4
+ - **Breaking Change**: by default, `StateManager::observe` and `SharedStateCollection` now retrieve locally created states
package/README.md CHANGED
@@ -3,57 +3,38 @@
3
3
  [![npm version](https://badge.fury.io/js/@soundworks%2Fcore.svg)](https://badge.fury.io/js/@soundworks%2Fcore)
4
4
  [![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/)
5
5
 
6
- ![soundworks-logo](./assets/logo-200x200.png)
6
+ ![soundworks-logo](./misc/assets/logo-200x200.png)
7
7
 
8
8
  Open-source creative coding framework for distributed applications based on Web technologies.
9
9
 
10
10
  Primarily focused on music, **soundworks** aims at supporting rapid development of real-time distributed applications using _JavaScript_. It provides abstractions to hide the complexity of the network and to foster very rapid-prototyping and trial-and-error workflows that are typical in artistic practices.
11
11
 
12
- *__WARNING: The version 4 of `@soundworks/core` is under heavy development.__*
13
-
14
- ## Getting Started
15
-
16
- The best and most simple way to start using **soundworks** is to use the `@soundworks/create` wizard.
17
-
18
- ```sh
19
- npx @soundworks/create@latest
20
- ```
21
-
22
- ![./assets/soundworks-create-min.gif](./assets/soundworks-create-min.gif)
23
-
24
- See [https://soundworks.dev/tutorials/getting-started.html](https://soundworks.dev/tutorials/getting-started.html) for more informations on the wizard and how to start with **soundworks**.
25
-
26
- <!--
27
12
  ## Documentation
28
13
 
29
14
  - Guides and Tutorials: [https://soundworks.dev](https://soundworks.dev)
30
15
  - API: [https://soundworks.dev/api](https://soundworks.dev/api)
31
- -->
32
16
 
33
- ## API
17
+ ## Getting started
34
18
 
35
- The API is not publicly published for now. To access the API documentation locally, just clone this repository, go to the v4 branch and launch some http server in the docs directory. For example, using the [serve](https://www.npmjs.com/package/serve) package:
19
+ The best and most simple way to start using **soundworks** is to use the `@soundworks/create` wizard.
36
20
 
37
21
  ```sh
38
- git clone https://github.com/collective-soundworks/soundworks.git
39
- cd soundworks
40
- git checkout v4
41
- serve docs
22
+ npx @soundworks/create@latest
42
23
  ```
43
24
 
44
- Feel welcome to open an [issue](https://github.com/collective-soundworks/soundworks/issues) or a PR if you find any inconsistency, error or missing information in the documentation.
25
+ ![soundworks-create](./misc/assets/soundworks-create-min.gif)
45
26
 
46
- ## Share with Us
27
+ See [https://soundworks.dev/tutorials/getting-started.html](https://soundworks.dev/tutorials/getting-started.html) for more informations on the wizard and how to start using **soundworks**.
47
28
 
48
- If you made an application using **soundworks** please let us know here: https://github.com/collective-soundworks/soundworks/discussions/61
29
+ ## Misc
49
30
 
50
- ## TypeScript Support
31
+ ### TypeScript support
51
32
 
52
33
  Basic TypeScript support will be proposed in a (hopefully) near future.
53
34
 
54
35
  However, for maintenance reasons, we aim at following the TC39 and W3C specifications as close as possible. Therefore, we will wait for the https://github.com/tc39/proposal-type-annotations proposal to reach stage 3 to update the source code in a more integrated manner.
55
36
 
56
- ## Install
37
+ ### Manual install
57
38
 
58
39
  Note that the `@soundworks/core` package is automatically installed when creating an application using the `@soundworks/create` wizard, so most of the time you should not have to install this package manually. See [https://soundworks.dev/guides/getting-started.html](https://soundworks.dev/guides/getting-started.html) for more informations on the **soundworks** wizard.
59
40
 
@@ -61,6 +42,10 @@ Note that the `@soundworks/core` package is automatically installed when creatin
61
42
  npm install @soundworks/core
62
43
  ```
63
44
 
45
+ ## Share with Us
46
+
47
+ If you made an application using **soundworks** please let us know here: https://github.com/collective-soundworks/soundworks/discussions/61
48
+
64
49
  ## Credits
65
50
 
66
51
  [https://soundworks.dev/credits.html](https://soundworks.dev/credits.html)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soundworks/core",
3
- "version": "4.0.0-alpha.14",
3
+ "version": "4.0.0-alpha.15",
4
4
  "description": "Open-source creative coding framework for distributed applications based on Web technologies",
5
5
  "authors": [
6
6
  "Benjamin Matuszewski",
@@ -43,51 +43,45 @@
43
43
  "node": ">=16.0.0"
44
44
  },
45
45
  "scripts": {
46
- "doc": "rm -Rf docs && jsdoc -c .jsdoc.json --verbose && cp -R assets docs/",
46
+ "doc": "rm -Rf docs && jsdoc -c .jsdoc.json --verbose && cp -R misc/assets docs/",
47
47
  "lint": "npx eslint src",
48
- "test": "mocha",
49
- "toc": "markdown-toc -i README.md",
48
+ "test": "npx mocha",
49
+ "test:all": "npx mocha tests/*/*.spec.js",
50
50
  "types": "rm -Rf types && tsc",
51
- "preversion": "npm run toc && git commit -am 'docs: build' --allow-empty"
51
+ "postversion": "node ./misc/scripts/check-changelog.js"
52
52
  },
53
53
  "dependencies": {
54
- "@ircam/sc-gettime": "^1.0.1",
55
- "@ircam/sc-utils": "^1.1.1",
54
+ "@ircam/sc-utils": "^1.3.2",
56
55
  "chalk": "^5.3.0",
57
56
  "columnify": "^1.6.0",
58
57
  "compression": "^1.7.1",
59
- "cross-fetch": "^3.1.8",
60
58
  "express": "^4.18.1",
61
59
  "fast-deep-equal": "^3.1.3",
62
60
  "fast-text-encoding": "^1.0.0",
63
61
  "isomorphic-ws": "^5.0.0",
64
- "keyv": "^4.2.2",
65
- "keyv-file": "^0.2.0",
66
- "lodash.clonedeep": "^4.5.0",
67
- "lodash.merge": "^4.6.2",
62
+ "keyv": "^4.5.4",
63
+ "keyv-file": "^0.3.0",
64
+ "lodash": "^4.17.21",
68
65
  "pem": "^1.14.8",
69
66
  "template-literal": "^1.0.4",
70
- "uuid": "^9.0.0",
71
- "window-or-global": "^1.0.1",
72
- "ws": "^8.12.0"
67
+ "uuid": "^9.0.1",
68
+ "ws": "^8.15.0"
73
69
  },
74
70
  "devDependencies": {
75
71
  "@ircam/eslint-config": "^1.3.0",
76
- "chai": "^4.3.6",
72
+ "chai": "^4.3.10",
77
73
  "chai-shallow-deep-equal": "^1.4.6",
78
74
  "commitizen": "^4.2.5",
79
75
  "cz-conventional-changelog": "^3.3.0",
80
- "docdash": "^2.0.1",
76
+ "docdash": "^2.0.2",
81
77
  "dotenv": "^16.3.1",
82
- "eslint": "^8.44.0",
83
- "eslint-plugin-jsdoc": "^46.4.3",
84
- "generate-changelog": "^1.8.0",
78
+ "eslint": "^8.55.0",
79
+ "eslint-plugin-jsdoc": "^46.9.0",
85
80
  "jsdoc": "^4.0.0",
86
- "markdown-toc": "^1.2.0",
87
81
  "mocha": "^10.2.0",
88
- "puppeteer": "^20.7.4",
82
+ "puppeteer": "^21.6.0",
89
83
  "tcp-ping": "^0.1.1",
90
- "typescript": "^5.1.6"
84
+ "typescript": "^5.3.3"
91
85
  },
92
86
  "optionalDependencies": {
93
87
  "bufferutil": "^4.0.5",
@@ -1,4 +1,5 @@
1
1
  import Client from './Client.js';
2
+ import PromiseStore from '../common/PromiseStore.js';
2
3
  import {
3
4
  CONTEXT_ENTER_REQUEST,
4
5
  CONTEXT_ENTER_RESPONSE,
@@ -7,11 +8,6 @@ import {
7
8
  CONTEXT_EXIT_RESPONSE,
8
9
  CONTEXT_EXIT_ERROR,
9
10
  } from '../common/constants.js';
10
- import {
11
- storeRequestPromise,
12
- resolveRequest,
13
- rejectRequest,
14
- } from '../common/promise-store.js';
15
11
 
16
12
  /**
17
13
  * Base class to extend in order to implement a new Context.
@@ -81,12 +77,15 @@ class Context {
81
77
  */
82
78
  this.status = 'idle';
83
79
 
80
+ /** @private */
81
+ this._promiseStore = new PromiseStore(this.constructor.name);
82
+
84
83
  this.client.socket.addListener(CONTEXT_ENTER_RESPONSE, (reqId, contextName) => {
85
84
  if (contextName !== this.name) {
86
85
  return;
87
86
  }
88
87
 
89
- resolveRequest(reqId);
88
+ this._promiseStore.resolve(reqId);
90
89
  });
91
90
 
92
91
  this.client.socket.addListener(CONTEXT_ENTER_ERROR, (reqId, contextName, msg) => {
@@ -94,7 +93,7 @@ class Context {
94
93
  return;
95
94
  }
96
95
 
97
- rejectRequest(reqId, msg);
96
+ this._promiseStore.reject(reqId, msg);
98
97
  });
99
98
 
100
99
  this.client.socket.addListener(CONTEXT_EXIT_RESPONSE, (reqId, contextName) => {
@@ -102,7 +101,7 @@ class Context {
102
101
  return;
103
102
  }
104
103
 
105
- resolveRequest(reqId);
104
+ this._promiseStore.resolve(reqId);
106
105
  });
107
106
 
108
107
  this.client.socket.addListener(CONTEXT_EXIT_ERROR, (reqId, contextName, msg) => {
@@ -110,7 +109,7 @@ class Context {
110
109
  return;
111
110
  }
112
111
 
113
- rejectRequest(reqId, msg);
112
+ this._promiseStore.reject(reqId, msg);
114
113
  });
115
114
 
116
115
  this.client.contextManager.register(this);
@@ -206,7 +205,7 @@ class Context {
206
205
  // we need the try/catch block to change the promise rejection into proper error
207
206
  try {
208
207
  await new Promise((resolve, reject) => {
209
- const reqId = storeRequestPromise(resolve, reject);
208
+ const reqId = this._promiseStore.add(resolve, reject, 'enter-context');
210
209
  this.client.socket.send(CONTEXT_ENTER_REQUEST, reqId, this.name);
211
210
  });
212
211
  } catch (err) {
@@ -240,7 +239,7 @@ class Context {
240
239
  // we need the try/catch block to change the promise rejection into proper error
241
240
  try {
242
241
  await new Promise((resolve, reject) => {
243
- const reqId = storeRequestPromise(resolve, reject);
242
+ const reqId = this._promiseStore.add(resolve, reject, 'exit-context');
244
243
  this.client.socket.send(CONTEXT_EXIT_REQUEST, reqId, this.name);
245
244
  });
246
245
  } catch (err) {
@@ -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 client.StateManager#getCollection} for factory method API
20
23
  *
21
24
  * ```
22
25
  * const collection = await client.stateManager.getCollection('my-schema');
@@ -1,5 +1,4 @@
1
1
  import { isBrowser } from '@ircam/sc-utils';
2
- import fetch from 'cross-fetch';
3
2
  import WebSocket from 'isomorphic-ws';
4
3
 
5
4
  import {
@@ -1,4 +1,4 @@
1
- import merge from 'lodash.merge';
1
+ import merge from 'lodash/merge.js';
2
2
 
3
3
  /**
4
4
  * @private
@@ -114,7 +114,7 @@ class BasePluginManager {
114
114
 
115
115
  this.status = 'inited';
116
116
  // instanciate all plugins
117
- for (let [id, instance] of this._instances.entries()) {
117
+ for (let [_id, instance] of this._instances.entries()) {
118
118
  instance.onStateChange(_values => this._propagateStateChange(instance));
119
119
  }
120
120
 
@@ -1,5 +1,6 @@
1
1
  import { isPlainObject } from '@ircam/sc-utils';
2
2
  import ParameterBag from './ParameterBag.js';
3
+ import PromiseStore from './PromiseStore.js';
3
4
  import {
4
5
  DELETE_REQUEST,
5
6
  DELETE_RESPONSE,
@@ -13,11 +14,6 @@ import {
13
14
  UPDATE_ABORT,
14
15
  UPDATE_NOTIFICATION,
15
16
  } from './constants.js';
16
- import {
17
- storeRequestPromise,
18
- resolveRequest,
19
- rejectRequest,
20
- } from './promise-store.js';
21
17
 
22
18
  class BaseSharedState {
23
19
  constructor(id, remoteId, schemaName, schema, client, isOwner, manager, initValues = {}) {
@@ -33,8 +29,13 @@ class BaseSharedState {
33
29
  this._client = client;
34
30
  /** @private */
35
31
  this._manager = manager;
36
-
32
+ /**
33
+ * true is the state has been detached or deleted
34
+ * @private
35
+ */
37
36
  this._detached = false;
37
+ /* @private */
38
+ this._promiseStore = new PromiseStore(this.constructor.name);
38
39
 
39
40
  try {
40
41
  /** @private */
@@ -56,13 +57,13 @@ ${JSON.stringify(initValues, null, 2)}`);
56
57
  // add listener for state updates
57
58
  this._client.transport.addListener(`${UPDATE_RESPONSE}-${this.id}-${this.remoteId}`, async (reqId, updates, context) => {
58
59
  const updated = await this._commit(updates, context, true, true);
59
- resolveRequest(reqId, updated);
60
+ this._promiseStore.resolve(reqId, updated);
60
61
  });
61
62
 
62
63
  // retrieve values but do not propagate to subscriptions
63
64
  this._client.transport.addListener(`${UPDATE_ABORT}-${this.id}-${this.remoteId}`, async (reqId, updates, context) => {
64
65
  const updated = await this._commit(updates, context, false, true);
65
- resolveRequest(reqId, updated);
66
+ this._promiseStore.resolve(reqId, updated);
66
67
  });
67
68
 
68
69
  this._client.transport.addListener(`${UPDATE_NOTIFICATION}-${this.id}-${this.remoteId}`, async (updates, context) => {
@@ -101,7 +102,7 @@ ${JSON.stringify(initValues, null, 2)}`);
101
102
  for (let callback of this._onDetachCallbacks) {
102
103
  try {
103
104
  await callback();
104
- } catch(err) {
105
+ } catch (err) {
105
106
  console.error(err.message);
106
107
  }
107
108
  }
@@ -112,7 +113,9 @@ ${JSON.stringify(initValues, null, 2)}`);
112
113
  }
113
114
  }
114
115
 
115
- this._clearDetach();
116
+ this._onDetachCallbacks.clear();
117
+ this._onDeleteCallbacks.clear();
118
+ this._promiseStore.flush();
116
119
  });
117
120
 
118
121
 
@@ -132,12 +135,14 @@ ${JSON.stringify(initValues, null, 2)}`);
132
135
  await callback();
133
136
  }
134
137
 
135
- this._clearDetach();
136
- resolveRequest(reqId, this);
138
+ this._onDetachCallbacks.clear();
139
+ this._onDeleteCallbacks.clear();
140
+ this._promiseStore.resolve(reqId, this);
141
+ this._promiseStore.flush();
137
142
  });
138
143
 
139
144
  this._client.transport.addListener(`${DELETE_ERROR}-${this.id}`, (reqId, msg) => {
140
- rejectRequest(reqId, msg);
145
+ this._promiseStore.reject(reqId, msg);
141
146
  });
142
147
 
143
148
  } else {
@@ -152,13 +157,18 @@ ${JSON.stringify(initValues, null, 2)}`);
152
157
  await callback();
153
158
  }
154
159
 
155
- this._clearDetach();
156
- resolveRequest(reqId, this);
160
+ this._onDetachCallbacks.clear();
161
+ this._onDeleteCallbacks.clear();
162
+ this._promiseStore.resolve(reqId, this);
163
+ this._promiseStore.flush();
157
164
  });
158
165
 
159
166
  // the state does not exists anymore in the server (should not happen)
160
167
  this._client.transport.addListener(`${DETACH_ERROR}-${this.id}`, (reqId, msg) => {
161
- rejectRequest(reqId, msg);
168
+ this._onDetachCallbacks.clear();
169
+ this._onDeleteCallbacks.clear();
170
+ this._promiseStore.reject(reqId, msg);
171
+ this._promiseStore.flush();
162
172
  });
163
173
  }
164
174
  }
@@ -200,13 +210,6 @@ ${JSON.stringify(initValues, null, 2)}`);
200
210
  return this._isOwner;
201
211
  }
202
212
 
203
- /** @private */
204
- _clearDetach() {
205
- this._onDetachCallbacks.clear();
206
- this._onDeleteCallbacks.clear();
207
- this._detached = true;
208
- }
209
-
210
213
  /** @private */
211
214
  _clearTransport() {
212
215
  // remove listeners
@@ -325,7 +328,7 @@ ${JSON.stringify(initValues, null, 2)}`);
325
328
  for (let name in updates) {
326
329
  // try to coerce value early, so that eventual errors are triggered early
327
330
  // on the node requesting the update
328
- const _ = this._parameters.coerceValue(name, updates[name]);
331
+ this._parameters.coerceValue(name, updates[name]);
329
332
 
330
333
  // @note: general idea...
331
334
  // if immediate=true
@@ -358,7 +361,7 @@ ${JSON.stringify(initValues, null, 2)}`);
358
361
 
359
362
  // go through server-side normal behavior
360
363
  return new Promise((resolve, reject) => {
361
- const reqId = storeRequestPromise(resolve, reject);
364
+ const reqId = this._promiseStore.add(resolve, reject, 'update-request');
362
365
  this._client.transport.emit(`${UPDATE_REQUEST}-${this.id}-${this.remoteId}`, reqId, updates, context);
363
366
  });
364
367
  }
@@ -477,16 +480,17 @@ ${JSON.stringify(initValues, null, 2)}`);
477
480
  throw new Error(`[SharedState] State "${this.schemaName} (${this.id})" already detached, cannot detach twice`);
478
481
  }
479
482
 
483
+ this._detached = true; // mark detached early
480
484
  this._onUpdateCallbacks.clear();
481
485
 
482
486
  if (this._isOwner) {
483
487
  return new Promise((resolve, reject) => {
484
- const reqId = storeRequestPromise(resolve, reject);
488
+ const reqId = this._promiseStore.add(resolve, reject, 'delete-request');
485
489
  this._client.transport.emit(`${DELETE_REQUEST}-${this.id}-${this.remoteId}`, reqId);
486
490
  });
487
491
  } else {
488
492
  return new Promise((resolve, reject) => {
489
- const reqId = storeRequestPromise(resolve, reject);
493
+ const reqId = this._promiseStore.add(resolve, reject, 'detach-request');
490
494
  this._client.transport.emit(`${DETACH_REQUEST}-${this.id}-${this.remoteId}`, reqId);
491
495
  });
492
496
  }
@@ -2,10 +2,13 @@
2
2
  * @private
3
3
  */
4
4
  class BaseSharedStateCollection {
5
- constructor(stateManager, schemaName) {
5
+ constructor(stateManager, schemaName, options = {}) {
6
6
  this._stateManager = stateManager;
7
7
  this._schemaName = schemaName;
8
+ this._options = Object.assign({ excludeLocal: false }, options);
9
+
8
10
  this._states = [];
11
+
9
12
  this._onUpdateCallbacks = new Set();
10
13
  this._onAttachCallbacks = new Set();
11
14
  this._onDetachCallbacks = new Set();
@@ -13,7 +16,7 @@ class BaseSharedStateCollection {
13
16
  }
14
17
 
15
18
  async _init() {
16
- this._unobserve = await this._stateManager.observe(this._schemaName, async (schemaName, stateId, nodeId) => {
19
+ this._unobserve = await this._stateManager.observe(this._schemaName, async (schemaName, stateId) => {
17
20
  const state = await this._stateManager.attach(schemaName, stateId);
18
21
 
19
22
  this._states.push(state);
@@ -32,28 +35,36 @@ class BaseSharedStateCollection {
32
35
  });
33
36
 
34
37
  this._onAttachCallbacks.forEach(callback => callback(state));
35
- });
38
+ }, this._options);
36
39
  }
37
40
 
38
41
  /**
39
- * Size of the collection
42
+ * Size of the collection, alias `size`
40
43
  * @type {number}
41
44
  */
42
45
  get length() {
43
46
  return this._states.length;
44
47
  }
45
48
 
49
+ /**
50
+ * Size of the collection, , alias `length`
51
+ * @type {number}
52
+ */
53
+ get size() {
54
+ return this._states.length;
55
+ }
56
+
46
57
  /**
47
58
  * Detach from the collection, i.e. detach all underlying shared states.
48
59
  * @type {number}
49
60
  */
50
61
  async detach() {
51
62
  this._unobserve();
63
+ this._onUpdateCallbacks.clear();
52
64
 
53
- const promises = Array.from(this._states).map(state => state.detach());
65
+ const promises = this._states.map(state => state.detach());
54
66
  await Promise.all(promises);
55
67
 
56
- this._onUpdateCallbacks.clear();
57
68
  this._onDetachCallbacks.clear();
58
69
  }
59
70
 
@@ -212,8 +223,8 @@ class BaseSharedStateCollection {
212
223
  } else {
213
224
  return { value: this._states[index++], done: false };
214
225
  }
215
- }
216
- }
226
+ },
227
+ };
217
228
  }
218
229
  }
219
230
 
@@ -1,6 +1,7 @@
1
- import { isString, isFunction } from '@ircam/sc-utils';
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 PromiseStore from './PromiseStore.js';
4
5
  import {
5
6
  CREATE_REQUEST,
6
7
  CREATE_RESPONSE,
@@ -10,15 +11,11 @@ import {
10
11
  ATTACH_ERROR,
11
12
  OBSERVE_REQUEST,
12
13
  OBSERVE_RESPONSE,
14
+ OBSERVE_ERROR,
13
15
  OBSERVE_NOTIFICATION,
14
16
  UNOBSERVE_NOTIFICATION,
15
17
  DELETE_SCHEMA,
16
18
  } from './constants.js';
17
- import {
18
- storeRequestPromise,
19
- resolveRequest,
20
- rejectRequest,
21
- } from './promise-store.js';
22
19
 
23
20
  /**
24
21
  * @private
@@ -32,10 +29,13 @@ class BaseStateManager {
32
29
  constructor(id, transport) {
33
30
  this.client = { id, transport };
34
31
 
35
- this._statesById = new Map();
36
- this._observeListeners = new Map(); // Map <callback, filterSchemaName>
37
- this._cachedSchemas = new Map();
38
- this._observeRequestCallbacks = new Map();
32
+ this._statesById = new Map(); // <id, state>
33
+ this._cachedSchemas = new Map(); // <shemaName, definition>
34
+
35
+ this._observeListeners = new Set(); // Set <[observedSchemaName, callback, options]>
36
+ this._observeRequestCallbacks = new Map(); // Map <reqId, [observedSchemaName, callback, options]>
37
+
38
+ this._promiseStore = new PromiseStore();
39
39
 
40
40
  // ---------------------------------------------
41
41
  // CREATE
@@ -53,11 +53,11 @@ class BaseStateManager {
53
53
  const state = new SharedState(stateId, remoteId, schemaName, schema, this.client, true, this, initValues);
54
54
  this._statesById.set(state.id, state);
55
55
 
56
- resolveRequest(reqId, state);
56
+ this._promiseStore.resolve(reqId, state);
57
57
  });
58
58
 
59
59
  this.client.transport.addListener(CREATE_ERROR, (reqId, msg) => {
60
- rejectRequest(reqId, msg);
60
+ this._promiseStore.reject(reqId, msg);
61
61
  });
62
62
 
63
63
  // ---------------------------------------------
@@ -77,11 +77,11 @@ class BaseStateManager {
77
77
  const state = new SharedState(stateId, remoteId, schemaName, schema, this.client, false, this, currentValues);
78
78
  this._statesById.set(state.id, state);
79
79
 
80
- resolveRequest(reqId, state);
80
+ this._promiseStore.resolve(reqId, state);
81
81
  });
82
82
 
83
83
  this.client.transport.addListener(ATTACH_ERROR, (reqId, msg) => {
84
- rejectRequest(reqId, msg);
84
+ this._promiseStore.reject(reqId, msg);
85
85
  });
86
86
 
87
87
  // ---------------------------------------------
@@ -90,11 +90,18 @@ class BaseStateManager {
90
90
  this.client.transport.addListener(OBSERVE_RESPONSE, async (reqId, ...list) => {
91
91
  // retrieve the callback that have been stored in observe to make sure
92
92
  // we don't call another callback that may have been registered earlier.
93
- const [callback, filterSchemaName] = this._observeRequestCallbacks.get(reqId);
93
+ const observeInfos = this._observeRequestCallbacks.get(reqId);
94
+ const [observedSchemaName, callback, options] = observeInfos;
95
+
96
+ // move observeInfos from `_observeRequestCallbacks` to `_observeListeners`
97
+ // to guarantee order of execution, @see not in `.observe`
94
98
  this._observeRequestCallbacks.delete(reqId);
99
+ this._observeListeners.add(observeInfos);
95
100
 
96
101
  const promises = list.map(([schemaName, stateId, nodeId]) => {
97
- if (filterSchemaName === '*' || filterSchemaName === schemaName) {
102
+ const filter = this._filterObserve(observedSchemaName, schemaName, nodeId, options);
103
+
104
+ if (!filter) {
98
105
  return callback(schemaName, stateId, nodeId);
99
106
  } else {
100
107
  return Promise.resolve();
@@ -104,19 +111,29 @@ class BaseStateManager {
104
111
  await Promise.all(promises);
105
112
 
106
113
  const unsubscribe = () => {
107
- this._observeListeners.delete(callback);
108
- // no more listeners, we can stop receiving notification from the server
114
+ this._observeListeners.delete(observeInfos);
115
+
116
+ // no more listeners, we can stop receiving notifications from the server
109
117
  if (this._observeListeners.size === 0) {
110
118
  this.client.transport.emit(UNOBSERVE_NOTIFICATION);
111
119
  }
112
120
  };
113
121
 
114
- resolveRequest(reqId, unsubscribe);
122
+ this._promiseStore.resolve(reqId, unsubscribe);
123
+ });
124
+
125
+ // Observe error can occur if observed schema name does not exists
126
+ this.client.transport.addListener(OBSERVE_ERROR, (reqId, msg) => {
127
+ this._observeRequestCallbacks.delete(reqId);
128
+ this._promiseStore.reject(reqId, msg);
115
129
  });
116
130
 
117
131
  this.client.transport.addListener(OBSERVE_NOTIFICATION, (schemaName, stateId, nodeId) => {
118
- this._observeListeners.forEach((filterSchemaName, callback) => {
119
- if (filterSchemaName === '*' || filterSchemaName === schemaName) {
132
+ this._observeListeners.forEach(observeInfos => {
133
+ const [observedSchemaName, callback, options] = observeInfos;
134
+ const filter = this._filterObserve(observedSchemaName, schemaName, nodeId, options);
135
+
136
+ if (!filter) {
120
137
  callback(schemaName, stateId, nodeId);
121
138
  }
122
139
  });
@@ -130,6 +147,21 @@ class BaseStateManager {
130
147
  });
131
148
  }
132
149
 
150
+ /** @private */
151
+ _filterObserve(observedSchemaName, schemaName, creatorId, options) {
152
+ let filter = true;
153
+ // schema name filter filer
154
+ if (observedSchemaName === null || observedSchemaName === schemaName) {
155
+ filter = false;
156
+ }
157
+ // filter state created by client if excludeLocal is true
158
+ if (options.excludeLocal === true && creatorId === this.client.id) {
159
+ filter = true;
160
+ }
161
+
162
+ return filter;
163
+ }
164
+
133
165
  /**
134
166
  * Create a `SharedState` instance from a registered schema.
135
167
  *
@@ -144,7 +176,7 @@ class BaseStateManager {
144
176
  */
145
177
  async create(schemaName, initValues = {}) {
146
178
  return new Promise((resolve, reject) => {
147
- const reqId = storeRequestPromise(resolve, reject);
179
+ const reqId = this._promiseStore.add(resolve, reject, 'create-create');
148
180
  const requireSchema = this._cachedSchemas.has(schemaName) ? false : true;
149
181
  this.client.transport.emit(CREATE_REQUEST, reqId, schemaName, requireSchema, initValues);
150
182
  });
@@ -167,7 +199,7 @@ class BaseStateManager {
167
199
  async attach(schemaName, stateId = null) {
168
200
  return new Promise((resolve, reject) => {
169
201
  // @todo - add a timeout
170
- const reqId = storeRequestPromise(resolve, reject);
202
+ const reqId = this._promiseStore.add(resolve, reject, 'attach-request');
171
203
  const requireSchema = this._cachedSchemas.has(schemaName) ? false : true;
172
204
  this.client.transport.emit(ATTACH_REQUEST, reqId, schemaName, stateId, requireSchema);
173
205
  });
@@ -180,20 +212,31 @@ class BaseStateManager {
180
212
  * control over them from a single point.
181
213
  *
182
214
  * Notes:
183
- * - The states that are created by the same node are not propagated through
184
- * the observe callback.
185
- * - The order of execution is not guaranted, i.e. an state attached in the
186
- * `observe` callback could be created before the `async create` method resolves.
215
+ * - The order of execution is not guaranted between nodes, i.e. an state attached
216
+ * in the `observe` callback could be created before the `async create` method resolves.
217
+ * - Filtering, i.e. `observedSchemaName` and `options.excludeLocal` are handled
218
+ * on the node side, the server just notify all state creation activity and
219
+ * the node executes the given callbacks according to the different filter rules.
220
+ * Such strategy allows to share the observe notifications between all observers.
187
221
  *
188
- * @param {String} [schemaName] - optionnal schema name to filter the observed
222
+ * Alternative signatures:
223
+ * - .observe(callback)
224
+ * - .observe(schemaName, callback)
225
+ * - .observe(callback, options)
226
+ * - .observe(schemaName, callback, options)
227
+ *
228
+ * @param {string} [schemaName] - optionnal schema name to filter the observed
189
229
  * states.
190
230
  * @param {server.StateManager~ObserveCallback|client.StateManager~ObserveCallback}
191
231
  * callback - Function to be called when a new state is created on the network.
232
+ * @param {object} options - Options.
233
+ * @param {boolean} [options.excludeLocal = false] - If set to true, exclude states
234
+ * created locallly, i.e. by the same node, from the collection.
192
235
  * @returns {Promise<Function>} - Returns a Promise that resolves when the given
193
236
  * callback as been executed on each existing states. The promise value is a
194
237
  * function which allows to stop observing the states on the network.
195
238
  * @example
196
- * client.stateManager.observe(async (schemaName, stateId, nodeId) => {
239
+ * client.stateManager.observe(async (schemaName, stateId) => {
197
240
  * if (schemaName === 'something') {
198
241
  * const state = await this.client.stateManager.attach(schemaName, stateId);
199
242
  * console.log(state.getValues());
@@ -204,50 +247,127 @@ class BaseStateManager {
204
247
  // handle this way and the network overhead is very low for observe notifications:
205
248
  // i.e. schemaName, stateId, nodeId
206
249
  async observe(...args) {
207
- let filterSchemaName;
250
+
251
+ const defaultOptions = {
252
+ excludeLocal: false,
253
+ };
254
+
255
+ let observedSchemaName;
208
256
  let callback;
257
+ let options;
209
258
 
210
- if (args.length === 1) {
211
- filterSchemaName = '*';
212
- callback = args[0];
259
+ switch (args.length) {
260
+ case 1: {
261
+ // variation: .observe(callback)
262
+ if (!isFunction(args[0])) {
263
+ throw new TypeError(`[stateManager] Invalid arguments, argument 1 should be a function"`);
264
+ }
213
265
 
214
- if (!isFunction(callback)) {
215
- throw new Error(`[stateManager] Invalid arguments, valid signatures are "stateManager.observe(callback)" or "stateManager.observe(schemaName, callback)"`);
266
+ observedSchemaName = null;
267
+ callback = args[0];
268
+ options = defaultOptions;
269
+ break;
216
270
  }
217
- } else if (args.length === 2) {
218
- filterSchemaName = args[0];
219
- callback = args[1];
271
+ case 2: {
272
+ // variation: .observe(schemaName, callback)
273
+ if (isString(args[0])) {
274
+ if (!isFunction(args[1])) {
275
+ throw new TypeError(`[stateManager] Invalid arguments, argument 2 should be a function"`);
276
+ }
277
+
278
+ observedSchemaName = args[0];
279
+ callback = args[1];
280
+ options = defaultOptions;
281
+
282
+ // variation: .observe(callback, options) API
283
+ } else if (isFunction(args[0])) {
284
+ if (!isPlainObject(args[1])) {
285
+ throw new TypeError(`[stateManager] Invalid arguments, argument 2 should be an object"`);
286
+ }
287
+
288
+ observedSchemaName = null;
289
+ callback = args[0];
290
+ options = Object.assign(defaultOptions, args[1]);
291
+
292
+ } else {
293
+ throw new Error(`[stateManager] Invalid signature, refer to the StateManager.observe documentation"`);
294
+ }
220
295
 
221
- if (!isString(filterSchemaName) || !isFunction(callback)) {
222
- throw new Error(`[stateManager] Invalid arguments, valid signatures are "stateManager.observe(callback)" or "stateManager.observe(schemaName, callback)"`);
296
+ break;
297
+ }
298
+ case 3: {
299
+ // variation: .observe(schemaName, callback, options)
300
+ if (!isString(args[0])) {
301
+ throw new TypeError(`[stateManager] Invalid arguments, argument 1 should be a string"`);
302
+ }
303
+
304
+ if (!isFunction(args[1])) {
305
+ throw new TypeError(`[stateManager] Invalid arguments, argument 2 should be a function"`);
306
+ }
307
+
308
+ if (!isPlainObject(args[2])) {
309
+ throw new TypeError(`[stateManager] Invalid arguments, argument 2 should be an object"`);
310
+ }
311
+
312
+ observedSchemaName = args[0];
313
+ callback = args[1];
314
+ options = Object.assign(defaultOptions, args[2]);
315
+
316
+ break;
317
+ }
318
+ // throw in all other cases
319
+ default: {
320
+ throw new Error(`[stateManager] Invalid signature, refer to the StateManager.observe documentation"`);
223
321
  }
224
- } else {
225
- throw new Error(`[stateManager] Invalid arguments, valid signatures are "stateManager.observe(callback)" or "stateManager.observe(schemaName, callback)"`);
226
322
  }
227
323
 
228
324
  // resend request to get updated list of states
229
325
  return new Promise((resolve, reject) => {
230
- const reqId = storeRequestPromise(resolve, reject);
326
+ const reqId = this._promiseStore.add(resolve, reject, 'observe-request');
231
327
  // store the callback for execution on the response. the returned Promise
232
328
  // is fullfiled once callback has been executed with each existing states
233
- this._observeRequestCallbacks.set(reqId, [callback, filterSchemaName]);
234
- // store the callback for execution on subsequent notifications
235
- this._observeListeners.set(callback, filterSchemaName);
236
-
237
- this.client.transport.emit(OBSERVE_REQUEST, reqId);
329
+ const observeInfos = [observedSchemaName, callback, options];
330
+ this._observeRequestCallbacks.set(reqId, observeInfos);
331
+
332
+ // NOTE: do not store in `_observeListeners` yet as it can produce race
333
+ // conditions, e.g.:
334
+ // ```
335
+ // await client.stateManager.observe(async (schemaName, stateId, nodeId) => {});
336
+ // // client now receives OBSERVE_NOTIFICATIONS
337
+ // await otherClient.stateManager.create('a');
338
+ // // second observer added in between
339
+ // client.stateManager.observe(async (schemaName, stateId, nodeId) => {});
340
+ // ````
341
+ // OBSERVE_NOTIFICATION is received before the OBSERVE_RESPONSE, then the
342
+ // second observer is called twice:
343
+ // - OBSERVE_RESPONSE 1 []
344
+ // - OBSERVE_NOTIFICATION [ 'a', 1, 0 ]
345
+ // - OBSERVE_NOTIFICATION [ 'a', 1, 0 ] // this should not happen
346
+ // - OBSERVE_RESPONSE 1 [ [ 'a', 1, 0 ] ]
347
+ //
348
+ // cf. unit test `observe should properly behave in race condition`
349
+
350
+ this.client.transport.emit(OBSERVE_REQUEST, reqId, observedSchemaName);
238
351
  });
239
352
  }
240
353
 
241
354
  /**
242
- * Returns a collection of all the states created from the schema name. Except
243
- * the ones created by the current node.
355
+ * Returns a collection of all the states created from the schema name.
244
356
  *
245
357
  * @param {string} schemaName - Name of the schema.
358
+ * @param {object} options - Options.
359
+ * @param {boolean} [options.excludeLocal = false] - If set to true, exclude states
360
+ * created locallly, i.e. by the same node, from the collection.
246
361
  * @returns {server.SharedStateCollection|client.SharedStateCollection}
247
362
  */
248
- async getCollection(schemaName) {
249
- const collection = new SharedStateCollection(this, schemaName);
250
- await collection._init();
363
+ async getCollection(schemaName, options) {
364
+ const collection = new SharedStateCollection(this, schemaName, options);
365
+
366
+ try {
367
+ await collection._init();
368
+ } catch (err) {
369
+ throw new Error(`Cannot create collection, schema "${schemaName}" does not exists`);
370
+ }
251
371
 
252
372
  return collection;
253
373
  }
@@ -1,4 +1,4 @@
1
- import cloneDeep from 'lodash.clonedeep';
1
+ import cloneDeep from 'lodash/cloneDeep.js';
2
2
  import equal from 'fast-deep-equal';
3
3
 
4
4
  export const sharedOptions = {
@@ -394,30 +394,34 @@ class ParameterBag {
394
394
  getSchema(name = null) {
395
395
  if (name === null) {
396
396
  return this._schema;
397
- } else {
398
- if (!this.has(name)) {
399
- throw new ReferenceError(`[SharedState] Cannot get schema description of undefined parameter "${name}"`);
400
- }
397
+ }
401
398
 
402
- return this._schema[name];
399
+ if (!this.has(name)) {
400
+ throw new ReferenceError(`[SharedState] Cannot get schema description of undefined parameter "${name}"`);
403
401
  }
402
+
403
+ return this._schema[name];
404
404
  }
405
405
 
406
406
  // return the default value, if initValue has been given, return init values
407
407
  getInitValues() {
408
408
  const initValues = {};
409
+
409
410
  for (let [name, def] of Object.entries(this._schema)) {
410
411
  initValues[name] = def.initValue;
411
412
  }
413
+
412
414
  return initValues;
413
415
  }
414
416
 
415
417
  // return the default value, if initValue has been given, return init values
416
418
  getDefaults() {
417
419
  const defaults = {};
420
+
418
421
  for (let [name, def] of Object.entries(this._schema)) {
419
- defaults[name] = def.defaults;
422
+ defaults[name] = def.default;
420
423
  }
424
+
421
425
  return defaults;
422
426
  }
423
427
  }
@@ -0,0 +1,41 @@
1
+ import { idGenerator } from '@ircam/sc-utils';
2
+
3
+ const generateRequestId = idGenerator();
4
+
5
+ export default class PromiseStore {
6
+ constructor(name) {
7
+ this.name = name;
8
+ this.store = new Map();
9
+ }
10
+
11
+ add(resolve, reject, type) {
12
+ const reqId = generateRequestId.next().value;
13
+ this.store.set(reqId, { resolve, reject, type });
14
+
15
+ return reqId;
16
+ }
17
+
18
+ resolve(reqId, data) {
19
+ const { resolve } = this.store.get(reqId);
20
+ this.store.delete(reqId);
21
+
22
+ resolve(data);
23
+ }
24
+
25
+ reject(reqId, msg) {
26
+ const { reject } = this.store.get(reqId);
27
+ this.store.delete(reqId);
28
+
29
+ reject(new Error(msg));
30
+ }
31
+
32
+ // reject all pendeing request
33
+ flush() {
34
+ for (let [_reqId, entry] of this.store) {
35
+ const { reject, type } = entry;
36
+ reject(new Error(`[${this.name}] Discard promise "${type}", cannot resolve`));
37
+ }
38
+
39
+ this.store.clear();
40
+ }
41
+ }
@@ -89,49 +89,46 @@ class SharedStatePrivate {
89
89
  }
90
90
 
91
91
  if (hasUpdates) {
92
- // send response to requester
93
- // client.transport.emit(`${UPDATE_RESPONSE}-${this.id}-${remoteId}`, reqId, filteredUpdates);
94
-
95
- // @note: we propagate server-side last, because as the server transport
96
- // is synchronous it can break ordering if a subscription function makes
97
- // itself an update in reaction to an update, therefore network messages
98
- // order would be broken,
99
-
100
- // we need to handle cases where:
101
- // client state (client.id: 2) sends a request
102
- // server attached state (client.id: -1) spot a problem and overrides the value
103
- // we want the remote client (id: 2) to receive in the right order:
92
+ // We need to handle cases where:
93
+ // - client state (client.id: 2) sends a request
94
+ // - server attached state (client.id: -1) spot a problem and overrides the value
95
+ // We want the remote client (id: 2) to receive in the right order:
104
96
  // * 1. the value it requested,
105
97
  // * 2. the value overriden by the server-side attached state (id: -1)
98
+ //
99
+ // such problem is now better solved with the the upateHook system, none
100
+ // nonetheway we don't want to introduce inconsistencies here
101
+ //
102
+ // Then we propagate server-side last, because as the server transport
103
+ // is synchronous it can break ordering if a subscription function makes
104
+ // itself an update in reaction to an update. Propagating to server last
105
+ // alllows to maintain network messages order consistent.
106
106
 
107
- // this problem could be solved properly with a reducer system:
108
- // if (dirty) {
109
- // -> call (async) reducer
110
- // -> get values from reducer
111
- //. -> dispatch to everybody
112
- // }
107
+ // @note - remoteId correspond to unique remote state id
108
+
109
+ // propagate RESPONSE to the client that originates the request if not the server
110
+ if (client.id !== -1) {
111
+ client.transport.emit(`${UPDATE_RESPONSE}-${this.id}-${remoteId}`, reqId, filteredUpdates, context);
112
+ }
113
113
 
114
- for (let [peerRemoteId, peer] of this._attachedClients.entries()) {
115
- // propagate notification to all other attached clients except server
114
+ // propagate NOTIFICATION to all other attached clients except server
115
+ for (let [peerRemoteId, peer] of this._attachedClients) {
116
116
  if (remoteId !== peerRemoteId && peer.id !== -1) {
117
117
  peer.transport.emit(`${UPDATE_NOTIFICATION}-${this.id}-${peerRemoteId}`, filteredUpdates, context);
118
118
  }
119
119
  }
120
120
 
121
- if (client.id !== -1) {
121
+ // propagate RESPONSE to server if it is the requester
122
+ if (client.id === -1) {
122
123
  client.transport.emit(`${UPDATE_RESPONSE}-${this.id}-${remoteId}`, reqId, filteredUpdates, context);
123
124
  }
124
125
 
125
- for (let [peerRemoteId, peer] of this._attachedClients.entries()) {
126
- // propagate notification to server
126
+ // propagate NOTIFICATION other attached state that belongs to server
127
+ for (let [peerRemoteId, peer] of this._attachedClients) {
127
128
  if (remoteId !== peerRemoteId && peer.id === -1) {
128
129
  peer.transport.emit(`${UPDATE_NOTIFICATION}-${this.id}-${peerRemoteId}`, filteredUpdates, context);
129
130
  }
130
131
  }
131
-
132
- if (client.id === -1) {
133
- client.transport.emit(`${UPDATE_RESPONSE}-${this.id}-${remoteId}`, reqId, filteredUpdates, context);
134
- }
135
132
  } else {
136
133
  // propagate back to the requester that the update has been aborted
137
134
  // ignore all other attached clients.
@@ -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,
@@ -359,17 +360,21 @@ class StateManager extends BaseStateManager {
359
360
  client.transport.emit(CREATE_RESPONSE, reqId, stateId, remoteId, schemaName, sendedSchema, currentValues);
360
361
 
361
362
  this._observers.forEach(observer => {
362
- if (observer.id !== nodeId) {
363
+ // prevent propagating infos about internal states
364
+ if (!PRIVATE_STATES.includes(schemaName)) {
363
365
  observer.transport.emit(OBSERVE_NOTIFICATION, schemaName, stateId, nodeId);
364
366
  }
365
367
  });
366
368
  } catch (err) {
367
- client.transport.emit(CREATE_ERROR, reqId, err.message);
368
- console.error(err.message);
369
+ const msg = `Cannot create state "${schemaName}", ${err.message}`;
370
+ console.error(msg);
371
+
372
+ client.transport.emit(CREATE_ERROR, reqId, msg);
369
373
  }
370
374
  } else {
371
375
  const msg = `Cannot create state "${schemaName}", schema does not exists`;
372
376
  console.error(msg);
377
+
373
378
  client.transport.emit(CREATE_ERROR, reqId, msg);
374
379
  }
375
380
  });
@@ -410,36 +415,43 @@ class StateManager extends BaseStateManager {
410
415
  client.transport.emit(ATTACH_RESPONSE, reqId, state.id, remoteId, schemaName, sendedSchema, currentValues);
411
416
  } else {
412
417
  const msg = `Cannot attach, no existing state for schema "${schemaName}" with stateId: "${stateId}"`;
413
- client.transport.emit(ATTACH_ERROR, reqId, msg);
414
418
  console.error(msg);
419
+
420
+ client.transport.emit(ATTACH_ERROR, reqId, msg);
415
421
  }
416
422
  } else {
417
423
  const msg = `Cannot attach, schema "${schemaName}" does not exists`;
418
- client.transport.emit(ATTACH_ERROR, reqId, msg);
419
424
  console.error(msg);
425
+
426
+ client.transport.emit(ATTACH_ERROR, reqId, msg);
420
427
  }
421
428
  });
422
429
 
423
430
  // ---------------------------------------------
424
431
  // OBSERVE PEERS (be notified when a state is created, lazy)
425
432
  // ---------------------------------------------
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
- });
433
+ client.transport.addListener(OBSERVE_REQUEST, (reqId, observedSchemaName) => {
434
+ if (observedSchemaName === null || this._schemas.has(observedSchemaName)) {
435
+ const statesInfos = [];
437
436
 
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);
437
+ this._serverStatesById.forEach(state => {
438
+ const { schemaName, id, _creatorId } = state;
439
+
440
+ // prevent propagating infos about internal states
441
+ if (!PRIVATE_STATES.includes(schemaName)) {
442
+ statesInfos.push([schemaName, id, _creatorId]);
443
+ }
444
+ });
441
445
 
442
- client.transport.emit(OBSERVE_RESPONSE, reqId, ...statesInfos);
446
+ // add client to observers first because if some synchronous server side
447
+ // callback throws, the client would never be added to the list
448
+ this._observers.add(client);
449
+
450
+ client.transport.emit(OBSERVE_RESPONSE, reqId, ...statesInfos);
451
+ } else {
452
+ const msg = `Cannot observe, schema "${observedSchemaName}" does not exists`;
453
+ client.transport.emit(OBSERVE_ERROR, reqId, msg);
454
+ }
443
455
  });
444
456
 
445
457
  client.transport.addListener(UNOBSERVE_NOTIFICATION, () => {
@@ -554,7 +566,7 @@ class StateManager extends BaseStateManager {
554
566
  attached.transport.emit(`${DELETE_NOTIFICATION}-${state.id}-${remoteId}`);
555
567
  }
556
568
 
557
- this._serverStatesById.delete(this.id);
569
+ this._serverStatesById.delete(state.id);
558
570
  }
559
571
  }
560
572
 
@@ -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
- }