@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 +7 -26
- package/package.json +12 -12
- package/src/common/BaseSharedState.js +93 -27
- package/src/common/BaseStateManager.js +4 -1
- package/src/server/Server.js +10 -0
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,
|
|
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
|
|
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
|

|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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](
|
|
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.
|
|
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
|
-
"
|
|
51
|
+
"preversion": "npm run toc && git commit -am 'docs: build' --allow-empty"
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
|
-
"@ircam/sc-gettime": "^1.0.
|
|
55
|
-
"@ircam/sc-utils": "^1.
|
|
56
|
-
"chalk": "^5.
|
|
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.
|
|
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.
|
|
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.
|
|
82
|
-
"eslint": "^8.
|
|
83
|
-
"eslint-plugin-jsdoc": "^
|
|
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": "^
|
|
88
|
+
"puppeteer": "^20.7.4",
|
|
89
89
|
"tcp-ping": "^0.1.1",
|
|
90
|
-
"typescript": "^
|
|
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
|
-
|
|
25
|
-
this.
|
|
26
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
98
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
113
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
-
*
|
|
440
|
-
*
|
|
441
|
-
*
|
|
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
|
-
*
|
|
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.
|
package/src/server/Server.js
CHANGED
|
@@ -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
|