@soundworks/core 4.0.0-alpha.7 → 4.0.0-alpha.9

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/README.md CHANGED
@@ -7,13 +7,13 @@
7
7
 
8
8
  Open-source creative coding framework for distributed applications based on Web technologies.
9
9
 
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.
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
12
  *__WARNING: The version 4 of `@soundworks/core` is under heavy development.__*
13
13
 
14
14
  ## Getting Started
15
15
 
16
- The best and most simple way to start using `soundworks` is to use the `@soundworks/create` wizard.
16
+ The best and most simple way to start using **soundworks** is to use the `@soundworks/create` wizard.
17
17
 
18
18
  ```sh
19
19
  npx @soundworks/create@latest
@@ -21,7 +21,7 @@ npx @soundworks/create@latest
21
21
 
22
22
  ![./assets/soundworks-create-min.gif](./assets/soundworks-create-min.gif)
23
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`.
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
25
 
26
26
  <!--
27
27
  ## Documentation
@@ -45,7 +45,7 @@ Feel welcome to open an [issue](https://github.com/collective-soundworks/soundwo
45
45
 
46
46
  ## Share with Us
47
47
 
48
- If you made an application using `soundworks` please let us know here: https://github.com/collective-soundworks/soundworks/discussions/61
48
+ If you made an application using **soundworks** please let us know here: https://github.com/collective-soundworks/soundworks/discussions/61
49
49
 
50
50
  ## TypeScript Support
51
51
 
@@ -55,7 +55,7 @@ However, for maintenance reasons, we aim at following the TC39 and W3C specifica
55
55
 
56
56
  ## Install
57
57
 
58
- 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.
58
+ 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
59
 
60
60
  ```
61
61
  npm install @soundworks/core
@@ -63,28 +63,9 @@ npm install @soundworks/core
63
63
 
64
64
  ## Credits
65
65
 
66
- `soundworks` has been initiated by [Norbert Schnell](https://github.com/NorbertSchnell), [Sébastien Robaszkiewicz](https://github.com/i-Robi), and [Benjamin Matuszewski](https://github.com/b-ma) at the [ISMM](http://ismm.ircam.fr/) team at [Ircam - Centre Pompidou](http://www.ircam.fr/) in the context of the [CoSiMa](http://cosima.ircam.fr/) research project founded by the French National Research Agency (ANR).
67
-
68
- Development is now led by Benjamin Matuszewski, in the [Sound Music Movement Interaction Team](https://www.stms-lab.fr/team/interaction-son-musique-mouvement/) from the Ircam's STMS-LAB.
69
-
70
- ### Supporting Research Projects
71
-
72
- Initial and futher developments has been supported by the following research projects:
73
-
74
- - The [DOTS](http://dots.ircam.fr/) project, funded by the French National Research Agency (ANR)
75
- - The Ircam projects _BeCoMe_ and _SO(a)P_
76
- - The _Constella(c)tions_ residency, funded by the STARTS program of the European Commission
77
- - The [RAPID-MIX project](http://rapidmix.goldsmithsdigital.com/), funded by the European Union’s Horizon 2020 research and innovation program
78
- - The [CoSiMa](http://cosima.ircam.fr/) project, funded by the French National Research Agency (ANR)
79
-
80
- ### Academic Papers
81
-
82
- - Benjamin Matuszewski. A Web-Based Framework for Distributed Music System Research and Creation. AES - Journal of the Audio Engineering Society Audio-Accoustics-Application, Audio Engineering Society Inc, 2020. <[hal-03033143](https://hal.archives-ouvertes.fr/hal-03033143)>
83
- - Benjamin Matuszewski. Soundworks - A Framework for Networked Music Systems on the Web - State of Affairs and New Developments. Proceedings of the Web Audio Conference (WAC) 2019, Dec 2019, Trondheim, Norway. <[hal-02387783](https://hal.archives-ouvertes.fr/hal-02387783)>
84
- - Benjamin Matuszewski, Norbert Schnell, Frédéric Bevilacqua. Interaction Topologies in Mobile-Based Situated Networked Music Systems. Wireless Communications and Mobile Computing, Hindawi Publishing Corporation, 2019, 2019, pp.9142490. ⟨10.1155/2019/9142490⟩. <[hal-02086673](https://hal.archives-ouvertes.fr/hal-02086673)>
85
- - Norbert Schnell, Sébastien Robaszkiewicz. Soundworks – A playground for artists and developers to create collaborative mobile web performances. `Proceedings of the Web Audio Conference (WAC'15), 2015, Paris, France. <[hal-01580797](https://hal.archives-ouvertes.fr/hal-01580797)>
66
+ [https://soundworks.dev/credits.html](https://soundworks.dev/credits.html)
86
67
 
87
68
  ## License
88
69
 
89
- [BSD-3-Clause](https://github.com/collective-soundworks/soundworks/blob/master/LICENSE)
70
+ [BSD-3-Clause](./LICENSE)
90
71
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soundworks/core",
3
- "version": "4.0.0-alpha.7",
3
+ "version": "4.0.0-alpha.9",
4
4
  "description": "Open-source creative coding framework for distributed applications based on Web technologies",
5
5
  "authors": [
6
6
  "Benjamin Matuszewski",
@@ -48,15 +48,15 @@
48
48
  "test": "mocha",
49
49
  "toc": "markdown-toc -i README.md",
50
50
  "types": "rm -Rf types && tsc",
51
- "version": "npm run toc"
51
+ "preversion": "npm run toc && git commit -am 'docs: build' --allow-empty"
52
52
  },
53
53
  "dependencies": {
54
- "@ircam/sc-gettime": "^1.0.0",
55
- "@ircam/sc-utils": "^1.0.1",
56
- "chalk": "^5.2.0",
54
+ "@ircam/sc-gettime": "^1.0.1",
55
+ "@ircam/sc-utils": "^1.1.1",
56
+ "chalk": "^5.3.0",
57
57
  "columnify": "^1.6.0",
58
58
  "compression": "^1.7.1",
59
- "cross-fetch": "^3.1.5",
59
+ "cross-fetch": "^3.1.8",
60
60
  "express": "^4.18.1",
61
61
  "fast-deep-equal": "^3.1.3",
62
62
  "fast-text-encoding": "^1.0.0",
@@ -65,7 +65,7 @@
65
65
  "keyv-file": "^0.2.0",
66
66
  "lodash.clonedeep": "^4.5.0",
67
67
  "lodash.merge": "^4.6.2",
68
- "pem": "^1.14.6",
68
+ "pem": "^1.14.8",
69
69
  "template-literal": "^1.0.4",
70
70
  "uuid": "^9.0.0",
71
71
  "window-or-global": "^1.0.1",
@@ -78,16 +78,16 @@
78
78
  "commitizen": "^4.2.5",
79
79
  "cz-conventional-changelog": "^3.3.0",
80
80
  "docdash": "^2.0.1",
81
- "dotenv": "^16.0.3",
82
- "eslint": "^8.32.0",
83
- "eslint-plugin-jsdoc": "^40.0.0",
81
+ "dotenv": "^16.3.1",
82
+ "eslint": "^8.44.0",
83
+ "eslint-plugin-jsdoc": "^46.4.3",
84
84
  "generate-changelog": "^1.8.0",
85
85
  "jsdoc": "^4.0.0",
86
86
  "markdown-toc": "^1.2.0",
87
87
  "mocha": "^10.2.0",
88
- "puppeteer": "^19.5.2",
88
+ "puppeteer": "^20.7.4",
89
89
  "tcp-ping": "^0.1.1",
90
- "typescript": "^4.9.3"
90
+ "typescript": "^5.1.6"
91
91
  },
92
92
  "optionalDependencies": {
93
93
  "bufferutil": "^4.0.5",
@@ -21,10 +21,12 @@ import {
21
21
 
22
22
  class BaseSharedState {
23
23
  constructor(id, remoteId, schemaName, schema, client, isOwner, manager, initValues = {}) {
24
- this.id = id;
25
- this.remoteId = remoteId;
26
- this.schemaName = schemaName;
27
-
24
+ /** @private */
25
+ this._id = id;
26
+ /** @private */
27
+ this._remoteId = remoteId;
28
+ /** @private */
29
+ this._schemaName = schemaName;
28
30
  /** @private */
29
31
  this._isOwner = isOwner; // may be the server or any client
30
32
  /** @private */
@@ -32,6 +34,8 @@ class BaseSharedState {
32
34
  /** @private */
33
35
  this._manager = manager;
34
36
 
37
+ this._detached = false;
38
+
35
39
  try {
36
40
  /** @private */
37
41
  this._parameters = new ParameterBag(schema, initValues);
@@ -88,14 +92,25 @@ ${JSON.stringify(initValues, null, 2)}`);
88
92
  });
89
93
 
90
94
  // ---------------------------------------------
91
- // DELETE initiated by creator, or schema deleted
95
+ // state has been deleted by its creator or the schema has been deleted
92
96
  // ---------------------------------------------
93
- this._client.transport.addListener(`${DELETE_NOTIFICATION}-${this.id}-${this.remoteId}`, () => {
97
+ this._client.transport.addListener(`${DELETE_NOTIFICATION}-${this.id}-${this.remoteId}`, async () => {
94
98
  this._manager._statesById.delete(this.id);
95
99
  this._clearTransport();
96
100
 
97
- this._onDetachCallbacks.forEach(callback => callback());
98
- this._onDeleteCallbacks.forEach(callback => callback());
101
+ for (let callback of this._onDetachCallbacks) {
102
+ try {
103
+ await callback();
104
+ } catch(err) {
105
+ console.error(err.message);
106
+ }
107
+ }
108
+
109
+ if (this._isOwner) {
110
+ for (let callback of this._onDeleteCallbacks) {
111
+ await callback();
112
+ }
113
+ }
99
114
 
100
115
  this._clearDetach();
101
116
  });
@@ -103,14 +118,19 @@ ${JSON.stringify(initValues, null, 2)}`);
103
118
 
104
119
  if (this._isOwner) {
105
120
  // ---------------------------------------------
106
- // DELETE (can only delete if creator)
121
+ // the creator has called `.delete()`
107
122
  // ---------------------------------------------
108
- this._client.transport.addListener(`${DELETE_RESPONSE}-${this.id}-${this.remoteId}`, (reqId) => {
123
+ this._client.transport.addListener(`${DELETE_RESPONSE}-${this.id}-${this.remoteId}`, async (reqId) => {
109
124
  this._manager._statesById.delete(this.id);
110
125
  this._clearTransport();
111
126
 
112
- this._onDetachCallbacks.forEach(callback => callback());
113
- this._onDeleteCallbacks.forEach(callback => callback());
127
+ for (let callback of this._onDetachCallbacks) {
128
+ await callback();
129
+ }
130
+
131
+ for (let callback of this._onDeleteCallbacks) {
132
+ await callback();
133
+ }
114
134
 
115
135
  this._clearDetach();
116
136
  resolveRequest(reqId, this);
@@ -122,13 +142,15 @@ ${JSON.stringify(initValues, null, 2)}`);
122
142
 
123
143
  } else {
124
144
  // ---------------------------------------------
125
- // DETACH
145
+ // the attached node has called `.detach()`
126
146
  // ---------------------------------------------
127
- this._client.transport.addListener(`${DETACH_RESPONSE}-${this.id}-${this.remoteId}`, (reqId) => {
147
+ this._client.transport.addListener(`${DETACH_RESPONSE}-${this.id}-${this.remoteId}`, async (reqId) => {
128
148
  this._manager._statesById.delete(this.id);
129
149
  this._clearTransport();
130
150
 
131
- this._onDetachCallbacks.forEach(func => func());
151
+ for (let callback of this._onDetachCallbacks) {
152
+ await callback();
153
+ }
132
154
 
133
155
  this._clearDetach();
134
156
  resolveRequest(reqId, this);
@@ -141,16 +163,48 @@ ${JSON.stringify(initValues, null, 2)}`);
141
163
  }
142
164
  }
143
165
 
166
+ /**
167
+ * Id of the state
168
+ * @type {Number}
169
+ * @readonly
170
+ */
171
+ get id() {
172
+ return this._id;
173
+ }
174
+
175
+ /**
176
+ * Unique id of the state for the current node
177
+ * @readonly
178
+ * @type {Number}
179
+ * @private
180
+ */
181
+ get remoteId() {
182
+ return this._remoteId;
183
+ }
184
+
185
+ /**
186
+ * Name of the schema
187
+ * @type {String}
188
+ * @readonly
189
+ */
190
+ get schemaName() {
191
+ return this._schemaName;
192
+ }
193
+
194
+ /**
195
+ * Indicates if the node is the owner of the state
196
+ * @type {Boolean}
197
+ * @readonly
198
+ */
199
+ get isOwner() {
200
+ return this._isOwner;
201
+ }
202
+
144
203
  /** @private */
145
204
  _clearDetach() {
146
205
  this._onDetachCallbacks.clear();
147
206
  this._onDeleteCallbacks.clear();
148
-
149
- // Monkey patch detach so it throws if called twice. Doing nothing blocks
150
- // the process on a second `detach` call as the Promise never resolves
151
- this.detach = () => {
152
- throw new Error(`[SharedState] State "${this.schemaName} (${this.id})" already detached, cannot detach twice`);
153
- };
207
+ this._detached = true;
154
208
  }
155
209
 
156
210
  /** @private */
@@ -411,7 +465,7 @@ ${JSON.stringify(initValues, null, 2)}`);
411
465
 
412
466
  /**
413
467
  * Detach from the state. If the client is the creator of the state, the state
414
- * is deleted and all attached nodes get notified
468
+ * is deleted and all attached nodes get notified.
415
469
  *
416
470
  * @example
417
471
  * const state = await client.state.attach('globals');
@@ -419,6 +473,10 @@ ${JSON.stringify(initValues, null, 2)}`);
419
473
  * await state.detach();
420
474
  */
421
475
  async detach() {
476
+ if (this._detached) {
477
+ throw new Error(`[SharedState] State "${this.schemaName} (${this.id})" already detached, cannot detach twice`);
478
+ }
479
+
422
480
  this._onUpdateCallbacks.clear();
423
481
 
424
482
  if (this._isOwner) {
@@ -436,9 +494,11 @@ ${JSON.stringify(initValues, null, 2)}`);
436
494
 
437
495
  /**
438
496
  * Delete the state. Only the creator/owner of the state can use this method.
439
- * All nodes attached to the state will be deteched, triggering the `onDetach`
440
- * callback. The creator of the state will also have its `onDelete` callback
441
- * triggered.
497
+ *
498
+ * All nodes attached to the state will be detached, triggering any registered
499
+ * `onDetach` callbacks. The creator of the state will also have its own `onDelete`
500
+ * callback triggered. The local `onDeatch` and `onDelete` callbacks will be
501
+ * executed *before* the returned Promise resolves
442
502
  *
443
503
  * @throws Throws if the method is called by a node which is not the owner of
444
504
  * the state.
@@ -449,6 +509,10 @@ ${JSON.stringify(initValues, null, 2)}`);
449
509
  */
450
510
  async delete() {
451
511
  if (this._isOwner) {
512
+ if (this._detached) {
513
+ throw new Error(`[SharedState] State "${this.schemaName} (${this.id})" already deleted, cannot delete twice`);
514
+ }
515
+
452
516
  return this.detach();
453
517
  } else {
454
518
  throw new Error(`[SharedState] Cannot delete state "${this.schemaName}", only the owner of the state (i.e. the node that created it) can delete the state. Use "detach" instead.`);
@@ -491,7 +555,8 @@ ${JSON.stringify(initValues, null, 2)}`);
491
555
  }
492
556
 
493
557
  /**
494
- * Register a function to execute when detaching from the state
558
+ * Register a function to execute when detaching from the state. The function
559
+ * will be executed before the `detach` promise resolves.
495
560
  *
496
561
  * @param {Function} callback - Callback to execute when detaching from the state.
497
562
  * Whether the client as called `detach`, or the state has been deleted by its
@@ -504,7 +569,8 @@ ${JSON.stringify(initValues, null, 2)}`);
504
569
 
505
570
  /**
506
571
  * Register a function to execute when the state is deleted. Only called if the
507
- * node was the creator of the state. Is called after `onDetach`
572
+ * node was the creator of the state. Is called after `onDetach` and executed
573
+ * before the `delete` Promise resolves.
508
574
  *
509
575
  * @param {Function} callback - Callback to execute when the state is deleted.
510
576
  */
@@ -179,8 +179,11 @@ class BaseStateManager {
179
179
  * the state of all other clients of the application, to monitor them and/or take
180
180
  * control over them from a single point.
181
181
  *
182
- * Note that the states that are created by the same node are not propagated through
182
+ * Notes:
183
+ * - The states that are created by the same node are not propagated through
183
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.
184
187
  *
185
188
  * @param {String} [schemaName] - optionnal schema name to filter the observed
186
189
  * states.
@@ -25,6 +25,7 @@ import StateManager from './StateManager.js';
25
25
  import Sockets from './Sockets.js';
26
26
  import logger from '../common/logger.js';
27
27
  import {
28
+ SERVER_ID,
28
29
  CLIENT_HANDSHAKE_REQUEST,
29
30
  CLIENT_HANDSHAKE_RESPONSE,
30
31
  CLIENT_HANDSHAKE_ERROR,
@@ -331,6 +332,15 @@ class Server {
331
332
  setTerminalTitle(this);
332
333
  }
333
334
 
335
+ /**
336
+ * Id of the server, a constant set to -1
337
+ * @type {Number}
338
+ * @readonly
339
+ */
340
+ get id() {
341
+ return SERVER_ID;
342
+ }
343
+
334
344
  /**
335
345
  * The `init` method is part of the initialization lifecycle of the `soundworks`
336
346
  * server. Most of the time, the `init` method will be implicitly called by the