@soundworks/core 4.0.0-alpha.13 → 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.13",
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
 
@@ -118,9 +129,18 @@ class BaseSharedStateCollection {
118
129
  *
119
130
  * @param {Function} callback - callback to execute when a state is added to
120
131
  * the collection.
132
+ * @param {Function} executeListener - execute the callback with the states
133
+ * already present in the collection.
134
+ * @returns {Function} - Function that delete the registered listener.
121
135
  */
122
- onAttach(callback) {
136
+ onAttach(callback, executeListener = false) {
137
+ if (executeListener === true) {
138
+ this._states.forEach(state => callback(state));
139
+ }
140
+
123
141
  this._onAttachCallbacks.add(callback);
142
+
143
+ return () => this._onAttachCallbacks.delete(callback);
124
144
  }
125
145
 
126
146
  /**
@@ -128,9 +148,12 @@ class BaseSharedStateCollection {
128
148
  *
129
149
  * @param {Function} callback - callback to execute when a state is removed
130
150
  * from the collection.
151
+ * @returns {Function} - Function that delete the registered listener.
131
152
  */
132
153
  onDetach(callback) {
133
154
  this._onDetachCallbacks.add(callback);
155
+
156
+ return () => this._onDetachCallbacks.delete(callback);
134
157
  }
135
158
 
136
159
  /**
@@ -188,7 +211,7 @@ class BaseSharedStateCollection {
188
211
  }
189
212
 
190
213
  /**
191
- * Iterable API for use in `for .. of` loops
214
+ * Iterable API, e.g. for use in `for .. of` loops
192
215
  */
193
216
  [Symbol.iterator]() {
194
217
  let index = 0;
@@ -200,8 +223,8 @@ class BaseSharedStateCollection {
200
223
  } else {
201
224
  return { value: this._states[index++], done: false };
202
225
  }
203
- }
204
- }
226
+ },
227
+ };
205
228
  }
206
229
  }
207
230