@soundworks/core 4.0.0-alpha.4 → 4.0.0-alpha.6
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/.github/workflows/gh-pages.yml +31 -0
- package/.github/workflows/unit-tests.yml +18 -0
- package/README.md +2 -2
- package/package.json +6 -7
- package/src/client/Client.js +11 -5
- package/src/client/Context.js +2 -2
- package/src/client/ContextManager.js +1 -1
- package/src/client/SharedStateCollection.js +35 -0
- package/src/client/Socket.js +17 -9
- package/src/common/BasePluginManager.js +4 -5
- package/src/common/BaseSharedState.js +59 -15
- package/src/common/BaseSharedStateCollection.js +179 -0
- package/src/common/BaseStateManager.js +70 -16
- package/src/common/ParameterBag.js +63 -16
- package/src/common/promise-store.js +1 -1
- package/src/server/Client.js +7 -2
- package/src/server/ContextManager.js +4 -4
- package/src/server/Server.js +197 -27
- package/src/server/SharedState.js +0 -1
- package/src/server/SharedStateCollection.js +36 -0
- package/src/server/Sockets.js +25 -4
- package/src/server/StateManager.js +7 -5
- package/src/server/crypto.js +38 -0
- package/src/common/utils.js +0 -28
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
name: GH Pages
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- '*'
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: write
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
deploy:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
steps:
|
|
15
|
+
- name: Checkout code
|
|
16
|
+
uses: actions/checkout@v3
|
|
17
|
+
|
|
18
|
+
- name: 'Install Node'
|
|
19
|
+
uses: actions/setup-node@v3.6.0
|
|
20
|
+
|
|
21
|
+
- name: Install dependencies
|
|
22
|
+
run: npm install
|
|
23
|
+
|
|
24
|
+
- name: Build Docs
|
|
25
|
+
run: npm run doc
|
|
26
|
+
|
|
27
|
+
- name: Deploy
|
|
28
|
+
uses: JamesIves/github-pages-deploy-action@v4
|
|
29
|
+
with:
|
|
30
|
+
folder: docs
|
|
31
|
+
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
name: Unit Tests
|
|
2
|
+
on: push
|
|
3
|
+
|
|
4
|
+
jobs:
|
|
5
|
+
test:
|
|
6
|
+
runs-on: ubuntu-latest
|
|
7
|
+
steps:
|
|
8
|
+
- name: Checkout
|
|
9
|
+
uses: actions/checkout@v3
|
|
10
|
+
|
|
11
|
+
- name: 'Install Node'
|
|
12
|
+
uses: actions/setup-node@v3.6.0
|
|
13
|
+
|
|
14
|
+
- name: Install dependencies
|
|
15
|
+
run: npm install
|
|
16
|
+
|
|
17
|
+
- name: "Run Test"
|
|
18
|
+
run: npm test -- tests/**.spec.js
|
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# soundworks
|
|
2
2
|
|
|
3
3
|
[](https://badge.fury.io/js/@soundworks%2Fcore)
|
|
4
4
|
[](http://commitizen.github.io/cz-cli/)
|
|
@@ -21,7 +21,7 @@ npx @soundworks/create@latest
|
|
|
21
21
|
|
|
22
22
|

|
|
23
23
|
|
|
24
|
-
See [https://soundworks.dev/
|
|
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
|
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.6",
|
|
4
4
|
"description": "Open-source creative coding framework for distributed applications based on Web technologies",
|
|
5
5
|
"authors": [
|
|
6
6
|
"Benjamin Matuszewski",
|
|
@@ -43,30 +43,29 @@
|
|
|
43
43
|
"node": ">=16.0.0"
|
|
44
44
|
},
|
|
45
45
|
"scripts": {
|
|
46
|
-
"doc": "
|
|
47
|
-
"jsdoc": "rm -Rf docs && jsdoc -c .jsdoc.json --verbose && cp -R assets docs/",
|
|
46
|
+
"doc": "rm -Rf docs && jsdoc -c .jsdoc.json --verbose && cp -R assets docs/",
|
|
48
47
|
"lint": "npx eslint src",
|
|
49
48
|
"test": "mocha",
|
|
50
49
|
"toc": "markdown-toc -i README.md",
|
|
51
50
|
"types": "rm -Rf types && tsc",
|
|
52
|
-
"version": "npm run toc
|
|
51
|
+
"version": "npm run toc"
|
|
53
52
|
},
|
|
54
53
|
"dependencies": {
|
|
55
54
|
"@ircam/sc-gettime": "^1.0.0",
|
|
55
|
+
"@ircam/sc-utils": "^1.0.1",
|
|
56
56
|
"chalk": "^5.2.0",
|
|
57
57
|
"columnify": "^1.6.0",
|
|
58
58
|
"compression": "^1.7.1",
|
|
59
|
+
"cross-fetch": "^3.1.5",
|
|
59
60
|
"express": "^4.18.1",
|
|
60
61
|
"fast-deep-equal": "^3.1.3",
|
|
61
62
|
"fast-text-encoding": "^1.0.0",
|
|
62
|
-
"is-plain-obj": "^4.1.0",
|
|
63
63
|
"isomorphic-ws": "^5.0.0",
|
|
64
64
|
"keyv": "^4.2.2",
|
|
65
65
|
"keyv-file": "^0.2.0",
|
|
66
66
|
"lodash.clonedeep": "^4.5.0",
|
|
67
67
|
"lodash.merge": "^4.6.2",
|
|
68
68
|
"pem": "^1.14.6",
|
|
69
|
-
"serve-static": "^1.15.0",
|
|
70
69
|
"template-literal": "^1.0.4",
|
|
71
70
|
"uuid": "^9.0.0",
|
|
72
71
|
"window-or-global": "^1.0.1",
|
|
@@ -81,7 +80,7 @@
|
|
|
81
80
|
"docdash": "^2.0.1",
|
|
82
81
|
"dotenv": "^16.0.3",
|
|
83
82
|
"eslint": "^8.32.0",
|
|
84
|
-
"eslint-plugin-jsdoc": "^
|
|
83
|
+
"eslint-plugin-jsdoc": "^40.0.0",
|
|
85
84
|
"generate-changelog": "^1.8.0",
|
|
86
85
|
"jsdoc": "^4.0.0",
|
|
87
86
|
"markdown-toc": "^1.2.0",
|
package/src/client/Client.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import isPlainObject from '
|
|
1
|
+
import { isBrowser, isPlainObject } from '@ircam/sc-utils';
|
|
2
2
|
|
|
3
3
|
import ContextManager from './ContextManager.js';
|
|
4
4
|
import PluginManager from './PluginManager.js';
|
|
@@ -10,7 +10,6 @@ import {
|
|
|
10
10
|
CLIENT_HANDSHAKE_ERROR,
|
|
11
11
|
AUDIT_STATE_NAME,
|
|
12
12
|
} from '../common/constants.js';
|
|
13
|
-
import { isBrowser } from '../common/utils.js';
|
|
14
13
|
import logger from '../common/logger.js';
|
|
15
14
|
|
|
16
15
|
/**
|
|
@@ -199,6 +198,12 @@ class Client {
|
|
|
199
198
|
*/
|
|
200
199
|
this.status = 'idle';
|
|
201
200
|
|
|
201
|
+
/**
|
|
202
|
+
* Token of the client if connected through HTTP authentication.
|
|
203
|
+
* @private
|
|
204
|
+
*/
|
|
205
|
+
this.token = null;
|
|
206
|
+
|
|
202
207
|
/** @private */
|
|
203
208
|
this._onStatusChangeCallbacks = new Set();
|
|
204
209
|
/** @private */
|
|
@@ -241,9 +246,10 @@ class Client {
|
|
|
241
246
|
try {
|
|
242
247
|
await new Promise((resolve, reject) => {
|
|
243
248
|
// wait for handshake response before starting stateManager and pluginManager
|
|
244
|
-
this.socket.addListener(CLIENT_HANDSHAKE_RESPONSE, async ({ id, uuid }) => {
|
|
249
|
+
this.socket.addListener(CLIENT_HANDSHAKE_RESPONSE, async ({ id, uuid, token }) => {
|
|
245
250
|
this.id = id;
|
|
246
251
|
this.uuid = uuid;
|
|
252
|
+
this.token = token;
|
|
247
253
|
|
|
248
254
|
resolve();
|
|
249
255
|
});
|
|
@@ -251,7 +257,7 @@ class Client {
|
|
|
251
257
|
this.socket.addListener(CLIENT_HANDSHAKE_ERROR, (err) => {
|
|
252
258
|
let msg = ``;
|
|
253
259
|
|
|
254
|
-
switch(err.type) {
|
|
260
|
+
switch (err.type) {
|
|
255
261
|
case 'invalid-client-type':
|
|
256
262
|
msg = `[soundworks:Client] ${err.message}`;
|
|
257
263
|
break;
|
|
@@ -275,7 +281,7 @@ class Client {
|
|
|
275
281
|
|
|
276
282
|
this.socket.send(CLIENT_HANDSHAKE_REQUEST, payload);
|
|
277
283
|
});
|
|
278
|
-
} catch(err) {
|
|
284
|
+
} catch (err) {
|
|
279
285
|
throw new Error(err);
|
|
280
286
|
}
|
|
281
287
|
|
package/src/client/Context.js
CHANGED
|
@@ -209,7 +209,7 @@ class Context {
|
|
|
209
209
|
const reqId = storeRequestPromise(resolve, reject);
|
|
210
210
|
this.client.socket.send(CONTEXT_ENTER_REQUEST, reqId, this.name);
|
|
211
211
|
});
|
|
212
|
-
} catch(err) {
|
|
212
|
+
} catch (err) {
|
|
213
213
|
throw new Error(err);
|
|
214
214
|
}
|
|
215
215
|
}
|
|
@@ -243,7 +243,7 @@ class Context {
|
|
|
243
243
|
const reqId = storeRequestPromise(resolve, reject);
|
|
244
244
|
this.client.socket.send(CONTEXT_EXIT_REQUEST, reqId, this.name);
|
|
245
245
|
});
|
|
246
|
-
} catch(err) {
|
|
246
|
+
} catch (err) {
|
|
247
247
|
throw new Error(err);
|
|
248
248
|
}
|
|
249
249
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import BaseSharedStateCollection from '../common/BaseSharedStateCollection.js';
|
|
2
|
+
|
|
3
|
+
// this file exists only for documentation purposesSharedStateCollection.js
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @callback client.SharedStateCollection~onUpdateCallback
|
|
7
|
+
* @param {client.SharedState} state - State that triggered the update.
|
|
8
|
+
* @param {Object} newValues - Key / value pairs of the updates that have been
|
|
9
|
+
* applied to the state.
|
|
10
|
+
* @param {Object} oldValues - Key / value pairs of the updated params before
|
|
11
|
+
* the updates has been applied to the state.
|
|
12
|
+
* @param {Mixed} [context=null] - Optionnal context object that has been passed
|
|
13
|
+
* with the values updates in the `set` call.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
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.
|
|
20
|
+
*
|
|
21
|
+
* ```
|
|
22
|
+
* const collection = await client.stateManager.getCollection('my-schema');
|
|
23
|
+
* const allValues = collection.getValues();
|
|
24
|
+
* collection.onUpdate((state, newValues, oldValues, context) => {
|
|
25
|
+
* // do something
|
|
26
|
+
* });
|
|
27
|
+
* ```
|
|
28
|
+
* @memberof client
|
|
29
|
+
* @extends BaseSharedStateCollection
|
|
30
|
+
* @inheritdoc
|
|
31
|
+
* @hideconstructor
|
|
32
|
+
*/
|
|
33
|
+
class SharedStateCollection extends BaseSharedStateCollection {}
|
|
34
|
+
|
|
35
|
+
export default SharedStateCollection;
|
package/src/client/Socket.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { isBrowser } from '@ircam/sc-utils';
|
|
2
|
+
import fetch from 'cross-fetch';
|
|
1
3
|
import WebSocket from 'isomorphic-ws';
|
|
2
4
|
|
|
3
5
|
import {
|
|
@@ -7,7 +9,6 @@ import {
|
|
|
7
9
|
unpackStringMessage,
|
|
8
10
|
} from '../common/sockets-utils.js';
|
|
9
11
|
import logger from '../common/logger.js';
|
|
10
|
-
import { isBrowser } from '../common/utils.js';
|
|
11
12
|
|
|
12
13
|
// WebSocket events:
|
|
13
14
|
//
|
|
@@ -110,7 +111,11 @@ class Socket {
|
|
|
110
111
|
};
|
|
111
112
|
}
|
|
112
113
|
|
|
113
|
-
|
|
114
|
+
let queryParams = `role=${role}&key=${key}`;
|
|
115
|
+
|
|
116
|
+
if (config.token) {
|
|
117
|
+
queryParams += `&token=${config.token}`;
|
|
118
|
+
}
|
|
114
119
|
|
|
115
120
|
// ----------------------------------------------------------
|
|
116
121
|
// init string socket
|
|
@@ -146,19 +151,22 @@ class Socket {
|
|
|
146
151
|
|
|
147
152
|
// cf. https://github.com/collective-soundworks/soundworks/issues/17
|
|
148
153
|
ws.addEventListener('error', e => {
|
|
149
|
-
if (e.type === 'error'
|
|
150
|
-
if (!connectionRefusedLogged) {
|
|
151
|
-
logger.log('[soundworks.Socket] Connection refused, waiting for the server to start');
|
|
152
|
-
connectionRefusedLogged = true;
|
|
153
|
-
}
|
|
154
|
-
|
|
154
|
+
if (e.type === 'error') {
|
|
155
155
|
if (ws.terminate) {
|
|
156
156
|
ws.terminate();
|
|
157
157
|
} else {
|
|
158
158
|
ws.close();
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
-
|
|
161
|
+
// for node clients, retry connection
|
|
162
|
+
if (e.error && e.error.code === 'ECONNREFUSED') {
|
|
163
|
+
if (!connectionRefusedLogged) {
|
|
164
|
+
logger.log('[soundworks.Socket] Connection refused, waiting for the server to start');
|
|
165
|
+
connectionRefusedLogged = true;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
setTimeout(trySocket, 1000);
|
|
169
|
+
}
|
|
162
170
|
}
|
|
163
171
|
});
|
|
164
172
|
};
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import isPlainObject from '
|
|
1
|
+
import { isPlainObject, isString } from '@ircam/sc-utils';
|
|
2
2
|
|
|
3
3
|
import logger from './logger.js';
|
|
4
|
-
import { isString } from './utils.js';
|
|
5
4
|
|
|
6
5
|
/**
|
|
7
6
|
* Shared functionnality between server-side and client-size plugin manager
|
|
@@ -111,9 +110,9 @@ class BasePluginManager {
|
|
|
111
110
|
try {
|
|
112
111
|
await Promise.all(promises);
|
|
113
112
|
this.status = 'started';
|
|
114
|
-
} catch(err) {
|
|
113
|
+
} catch (err) {
|
|
115
114
|
this.status = 'errored';
|
|
116
|
-
throw
|
|
115
|
+
throw err; // throw initial error
|
|
117
116
|
}
|
|
118
117
|
}
|
|
119
118
|
|
|
@@ -170,7 +169,7 @@ class BasePluginManager {
|
|
|
170
169
|
this._instanceStartPromises.set(id, startPromise);
|
|
171
170
|
|
|
172
171
|
await startPromise;
|
|
173
|
-
} catch(err) {
|
|
172
|
+
} catch (err) {
|
|
174
173
|
errored = true;
|
|
175
174
|
this._propagateStateChange(instance, 'errored');
|
|
176
175
|
throw err;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isPlainObject } from '@ircam/sc-utils';
|
|
1
2
|
import ParameterBag from './ParameterBag.js';
|
|
2
3
|
import {
|
|
3
4
|
DELETE_REQUEST,
|
|
@@ -34,7 +35,7 @@ class BaseSharedState {
|
|
|
34
35
|
try {
|
|
35
36
|
/** @private */
|
|
36
37
|
this._parameters = new ParameterBag(schema, initValues);
|
|
37
|
-
} catch(err) {
|
|
38
|
+
} catch (err) {
|
|
38
39
|
console.error(err.stack);
|
|
39
40
|
|
|
40
41
|
throw new Error(`Error creating or attaching state "${schemaName}" w/ values:\n
|
|
@@ -137,7 +138,6 @@ ${JSON.stringify(initValues, null, 2)}`);
|
|
|
137
138
|
this._client.transport.addListener(`${DETACH_ERROR}-${this.id}`, (reqId, msg) => {
|
|
138
139
|
rejectRequest(reqId, msg);
|
|
139
140
|
});
|
|
140
|
-
|
|
141
141
|
}
|
|
142
142
|
}
|
|
143
143
|
|
|
@@ -149,7 +149,7 @@ ${JSON.stringify(initValues, null, 2)}`);
|
|
|
149
149
|
// Monkey patch detach so it throws if called twice. Doing nothing blocks
|
|
150
150
|
// the process on a second `detach` call as the Promise never resolves
|
|
151
151
|
this.detach = () => {
|
|
152
|
-
throw new Error(`[
|
|
152
|
+
throw new Error(`[SharedState] State "${this.schemaName} (${this.id})" already detached, cannot detach twice`);
|
|
153
153
|
};
|
|
154
154
|
}
|
|
155
155
|
|
|
@@ -255,16 +255,23 @@ ${JSON.stringify(initValues, null, 2)}`);
|
|
|
255
255
|
* const updates = await state.set({ myParam: Math.random() });
|
|
256
256
|
*/
|
|
257
257
|
async set(updates, context = null) {
|
|
258
|
+
if (!isPlainObject(updates)) {
|
|
259
|
+
throw new ReferenceError(`[SharedState] State "${this.schemaName}": state.set(updates[, context]) should receive an object as first parameter`);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (context !== null && !isPlainObject(context)) {
|
|
263
|
+
throw new ReferenceError(`[SharedState] State "${this.schemaName}": state.set(updates[, context]) should receive an object as second parameter`);
|
|
264
|
+
}
|
|
265
|
+
|
|
258
266
|
// handle immediate option
|
|
259
267
|
const immediateNewValues = {};
|
|
260
268
|
const immediateOldValues = {};
|
|
261
269
|
let propagateNow = false;
|
|
262
270
|
|
|
263
271
|
for (let name in updates) {
|
|
264
|
-
//
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
}
|
|
272
|
+
// try to coerce value early, so that eventual errors are triggered early
|
|
273
|
+
// on the node requesting the update
|
|
274
|
+
const _ = this._parameters.coerceValue(name, updates[name]);
|
|
268
275
|
|
|
269
276
|
// @note: general idea...
|
|
270
277
|
// if immediate=true
|
|
@@ -303,21 +310,41 @@ ${JSON.stringify(initValues, null, 2)}`);
|
|
|
303
310
|
}
|
|
304
311
|
|
|
305
312
|
/**
|
|
306
|
-
* Get the value of a
|
|
313
|
+
* Get the value of a parameter of the state. If the parameter is of `any` type,
|
|
314
|
+
* a deep copy is returned.
|
|
307
315
|
*
|
|
308
316
|
* @param {string} name - Name of the param.
|
|
309
317
|
* @throws Throws if `name` does not correspond to an existing field
|
|
310
318
|
* of the state.
|
|
311
319
|
* @return {mixed}
|
|
312
320
|
* @example
|
|
313
|
-
* const value = state.get('
|
|
321
|
+
* const value = state.get('paramName');
|
|
314
322
|
*/
|
|
315
323
|
get(name) {
|
|
316
324
|
return this._parameters.get(name);
|
|
317
325
|
}
|
|
318
326
|
|
|
319
327
|
/**
|
|
320
|
-
*
|
|
328
|
+
* Similar to `get` but returns a reference to the underlying value in case of
|
|
329
|
+
* `any` type. May be usefull if the underlying value is big (e.g. sensors
|
|
330
|
+
* recordings, etc.) and deep cloning expensive. Be aware that if changes are
|
|
331
|
+
* made on the returned object, the state of your application will become
|
|
332
|
+
* inconsistent.
|
|
333
|
+
*
|
|
334
|
+
* @param {string} name - Name of the param.
|
|
335
|
+
* @throws Throws if `name` does not correspond to an existing field
|
|
336
|
+
* of the state.
|
|
337
|
+
* @return {mixed}
|
|
338
|
+
* @example
|
|
339
|
+
* const value = state.getUnsafe('paramName');
|
|
340
|
+
*/
|
|
341
|
+
getUnsafe(name) {
|
|
342
|
+
return this._parameters.getUnsafe(name);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Get all the key / value pairs of the state. If a parameter is of `any`
|
|
347
|
+
* type, a deep copy is made.
|
|
321
348
|
*
|
|
322
349
|
* @return {object}
|
|
323
350
|
* @example
|
|
@@ -327,6 +354,23 @@ ${JSON.stringify(initValues, null, 2)}`);
|
|
|
327
354
|
return this._parameters.getValues();
|
|
328
355
|
}
|
|
329
356
|
|
|
357
|
+
/**
|
|
358
|
+
* Get all the key / value pairs of the state. If a parameter is of `any`
|
|
359
|
+
* type, a deep copy is made.
|
|
360
|
+
* Similar to `getValues` but returns a reference to the underlying value in
|
|
361
|
+
* case of `any` type. May be usefull if the underlying value is big (e.g.
|
|
362
|
+
* sensors recordings, etc.) and deep cloning expensive. Be aware that if
|
|
363
|
+
* changes are made on the returned object, the state of your application will
|
|
364
|
+
* become inconsistent.
|
|
365
|
+
*
|
|
366
|
+
* @return {object}
|
|
367
|
+
* @example
|
|
368
|
+
* const values = state.getValues();
|
|
369
|
+
*/
|
|
370
|
+
getValuesUnsafe() {
|
|
371
|
+
return this._parameters.getValuesUnsafe();
|
|
372
|
+
}
|
|
373
|
+
|
|
330
374
|
/**
|
|
331
375
|
* Get the schema from which the state has been created.
|
|
332
376
|
*
|
|
@@ -407,14 +451,14 @@ ${JSON.stringify(initValues, null, 2)}`);
|
|
|
407
451
|
if (this._isOwner) {
|
|
408
452
|
return this.detach();
|
|
409
453
|
} else {
|
|
410
|
-
throw new Error(`[
|
|
454
|
+
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.`);
|
|
411
455
|
}
|
|
412
456
|
}
|
|
413
457
|
|
|
414
458
|
/**
|
|
415
459
|
* Subscribe to state updates.
|
|
416
460
|
*
|
|
417
|
-
* @param {client.SharedState~onUpdateCallback|
|
|
461
|
+
* @param {client.SharedState~onUpdateCallback|server.SharedState~onUpdateCallback} callback
|
|
418
462
|
* Callback to execute when an update is applied on the state.
|
|
419
463
|
* @param {Boolean} [executeListener=false] - Execute the callback immediately
|
|
420
464
|
* with current state values. (`oldValues` will be set to `{}`, and `context` to `null`)
|
|
@@ -449,8 +493,8 @@ ${JSON.stringify(initValues, null, 2)}`);
|
|
|
449
493
|
/**
|
|
450
494
|
* Register a function to execute when detaching from the state
|
|
451
495
|
*
|
|
452
|
-
* @param {Function} callback -
|
|
453
|
-
*
|
|
496
|
+
* @param {Function} callback - Callback to execute when detaching from the state.
|
|
497
|
+
* Whether the client as called `detach`, or the state has been deleted by its
|
|
454
498
|
* creator.
|
|
455
499
|
*/
|
|
456
500
|
onDetach(callback) {
|
|
@@ -462,7 +506,7 @@ ${JSON.stringify(initValues, null, 2)}`);
|
|
|
462
506
|
* Register a function to execute when the state is deleted. Only called if the
|
|
463
507
|
* node was the creator of the state. Is called after `onDetach`
|
|
464
508
|
*
|
|
465
|
-
* @param {Function} callback -
|
|
509
|
+
* @param {Function} callback - Callback to execute when the state is deleted.
|
|
466
510
|
*/
|
|
467
511
|
onDelete(callback) {
|
|
468
512
|
this._onDeleteCallbacks.add(callback);
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @private
|
|
3
|
+
*/
|
|
4
|
+
class BaseSharedStateCollection {
|
|
5
|
+
constructor(stateManager, schemaName) {
|
|
6
|
+
this._stateManager = stateManager;
|
|
7
|
+
this._schemaName = schemaName;
|
|
8
|
+
this._states = [];
|
|
9
|
+
this._onUpdateCallbacks = new Set();
|
|
10
|
+
this._onAttachCallbacks = new Set();
|
|
11
|
+
this._onDetachCallbacks = new Set();
|
|
12
|
+
this._unobserve = null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async _init() {
|
|
16
|
+
this._unobserve = await this._stateManager.observe(this._schemaName, async (schemaName, stateId, nodeId) => {
|
|
17
|
+
const state = await this._stateManager.attach(schemaName, stateId);
|
|
18
|
+
|
|
19
|
+
this._states.push(state);
|
|
20
|
+
|
|
21
|
+
state.onDetach(() => {
|
|
22
|
+
const index = this._states.indexOf(state);
|
|
23
|
+
this._states.splice(index, 1);
|
|
24
|
+
|
|
25
|
+
this._onDetachCallbacks.forEach(callback => callback(state));
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
state.onUpdate((newValues, oldValues, context) => {
|
|
29
|
+
Array.from(this._onUpdateCallbacks).forEach(callback => {
|
|
30
|
+
callback(state, newValues, oldValues, context);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
this._onAttachCallbacks.forEach(callback => callback(state));
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Size of the collection
|
|
40
|
+
* @type {number}
|
|
41
|
+
*/
|
|
42
|
+
get length() {
|
|
43
|
+
return this._states.length;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Detach from the collection, i.e. detach all underlying shared states.
|
|
48
|
+
* @type {number}
|
|
49
|
+
*/
|
|
50
|
+
async detach() {
|
|
51
|
+
this._unobserve();
|
|
52
|
+
|
|
53
|
+
const promises = Array.from(this._states).map(state => state.detach());
|
|
54
|
+
await Promise.all(promises);
|
|
55
|
+
|
|
56
|
+
this._onUpdateCallbacks.clear();
|
|
57
|
+
this._onDetachCallbacks.clear();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Return the current values of all the states in the collection.
|
|
62
|
+
* @return {Object[]}
|
|
63
|
+
*/
|
|
64
|
+
getValues() {
|
|
65
|
+
return this._states.map(state => state.getValues());
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Return the current param value of all the states in the collection.
|
|
70
|
+
*
|
|
71
|
+
* @param {String} name - Name of the parameter
|
|
72
|
+
* @return {any[]}
|
|
73
|
+
*/
|
|
74
|
+
get(name) {
|
|
75
|
+
return this._states.map(state => state.get(name));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Subscribe to any state update of the collection.
|
|
80
|
+
*
|
|
81
|
+
* @param {server.SharedStateCollection~onUpdateCallback|client.SharedStateCollection~onUpdateCallback}
|
|
82
|
+
* callback - Callback to execute when an update is applied on a state.
|
|
83
|
+
* @param {Boolean} [executeListener=false] - Execute the callback immediately
|
|
84
|
+
* for all underlying states with current state values. (`oldValues` will be
|
|
85
|
+
* set to `{}`, and `context` to `null`)
|
|
86
|
+
* @returns {Function} - Function that delete the registered listener.
|
|
87
|
+
*/
|
|
88
|
+
onUpdate(callback, executeListener = false) {
|
|
89
|
+
this._onUpdateCallbacks.add(callback);
|
|
90
|
+
|
|
91
|
+
if (executeListener === true) {
|
|
92
|
+
this._states.forEach(state => {
|
|
93
|
+
const currentValues = state.getValues();
|
|
94
|
+
const oldValues = {};
|
|
95
|
+
const context = null;
|
|
96
|
+
|
|
97
|
+
callback(state, currentValues, oldValues, context);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return () => this._onUpdateCallbacks.delete(callback);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Register a function to execute when a state is added to the collection.
|
|
106
|
+
*
|
|
107
|
+
* @param {Function} callback - callback to execute when a state is added to
|
|
108
|
+
* the collection.
|
|
109
|
+
*/
|
|
110
|
+
onAttach(callback) {
|
|
111
|
+
this._onAttachCallbacks.add(callback);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Register a function to execute when a state is removed from the collection.
|
|
116
|
+
*
|
|
117
|
+
* @param {Function} callback - callback to execute when a state is removed
|
|
118
|
+
* from the collection.
|
|
119
|
+
*/
|
|
120
|
+
onDetach(callback) {
|
|
121
|
+
this._onDetachCallbacks.add(callback);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Execute the given function once for each states of the collection (see `Array.forEach`).
|
|
126
|
+
*
|
|
127
|
+
* @param {Function} func - A function to execute for each element in the array.
|
|
128
|
+
* Its return value is discarded.
|
|
129
|
+
*/
|
|
130
|
+
forEach(func) {
|
|
131
|
+
this._states.forEach(func);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Creates a new array populated with the results of calling a provided function
|
|
136
|
+
* on every state of the collection (see `Array.map`).
|
|
137
|
+
*
|
|
138
|
+
* @param {Function} func - A function to execute for each element in the array.
|
|
139
|
+
* Its return value is added as a single element in the new array.
|
|
140
|
+
*/
|
|
141
|
+
map(func) {
|
|
142
|
+
return this._states.map(func);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Creates a shallow copy of a portion of the collection, filtered down to just
|
|
147
|
+
* the estates that pass the test implemented by the provided function (see `Array.filter`).
|
|
148
|
+
*
|
|
149
|
+
* @param {Function} func - A function to execute for each element in the array.
|
|
150
|
+
* It should return a truthy to keep the element in the resulting array, and a f
|
|
151
|
+
* alsy value otherwise.
|
|
152
|
+
*/
|
|
153
|
+
filter(func) {
|
|
154
|
+
return this._states.filter(func);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Sort the elements of the collection in place (see `Array.sort`).
|
|
159
|
+
*
|
|
160
|
+
* @param {Function} func - Function that defines the sort order.
|
|
161
|
+
*/
|
|
162
|
+
sort(func) {
|
|
163
|
+
this._states.sort(func);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Returns the first element of the collection that satisfies the provided testing
|
|
168
|
+
* function. If no values satisfy the testing function, undefined is returned.
|
|
169
|
+
*
|
|
170
|
+
* @param {Function} func - Function to execute for each element in the array.
|
|
171
|
+
* It should return a truthy value to indicate a matching element has been found.
|
|
172
|
+
* @return {}
|
|
173
|
+
*/
|
|
174
|
+
find(func) {
|
|
175
|
+
return this._states.find(func);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export default BaseSharedStateCollection;
|