@soundworks/core 4.0.0-alpha.5 → 4.0.0-alpha.7
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/package.json +4 -5
- package/src/client/Client.js +8 -1
- package/src/client/PluginManager.js +1 -1
- package/src/client/SharedStateCollection.js +35 -0
- package/src/client/Socket.js +16 -8
- package/src/common/BasePlugin.js +30 -20
- package/src/common/BasePluginManager.js +30 -14
- package/src/common/BaseSharedState.js +48 -13
- package/src/common/BaseSharedStateCollection.js +179 -0
- package/src/common/BaseStateManager.js +70 -16
- package/src/common/ParameterBag.js +63 -16
- package/src/server/Client.js +6 -0
- package/src/server/PluginManager.js +1 -1
- package/src/server/Server.js +183 -17
- package/src/server/SharedStateCollection.js +36 -0
- package/src/server/Sockets.js +25 -4
- package/src/server/StateManager.js +4 -2
- package/src/server/crypto.js +38 -0
- package/.github/workflows/main.yml +0 -15
|
@@ -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/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.7",
|
|
4
4
|
"description": "Open-source creative coding framework for distributed applications based on Web technologies",
|
|
5
5
|
"authors": [
|
|
6
6
|
"Benjamin Matuszewski",
|
|
@@ -43,13 +43,12 @@
|
|
|
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",
|
|
@@ -57,6 +56,7 @@
|
|
|
57
56
|
"chalk": "^5.2.0",
|
|
58
57
|
"columnify": "^1.6.0",
|
|
59
58
|
"compression": "^1.7.1",
|
|
59
|
+
"cross-fetch": "^3.1.5",
|
|
60
60
|
"express": "^4.18.1",
|
|
61
61
|
"fast-deep-equal": "^3.1.3",
|
|
62
62
|
"fast-text-encoding": "^1.0.0",
|
|
@@ -66,7 +66,6 @@
|
|
|
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",
|
package/src/client/Client.js
CHANGED
|
@@ -198,6 +198,12 @@ class Client {
|
|
|
198
198
|
*/
|
|
199
199
|
this.status = 'idle';
|
|
200
200
|
|
|
201
|
+
/**
|
|
202
|
+
* Token of the client if connected through HTTP authentication.
|
|
203
|
+
* @private
|
|
204
|
+
*/
|
|
205
|
+
this.token = null;
|
|
206
|
+
|
|
201
207
|
/** @private */
|
|
202
208
|
this._onStatusChangeCallbacks = new Set();
|
|
203
209
|
/** @private */
|
|
@@ -240,9 +246,10 @@ class Client {
|
|
|
240
246
|
try {
|
|
241
247
|
await new Promise((resolve, reject) => {
|
|
242
248
|
// wait for handshake response before starting stateManager and pluginManager
|
|
243
|
-
this.socket.addListener(CLIENT_HANDSHAKE_RESPONSE, async ({ id, uuid }) => {
|
|
249
|
+
this.socket.addListener(CLIENT_HANDSHAKE_RESPONSE, async ({ id, uuid, token }) => {
|
|
244
250
|
this.id = id;
|
|
245
251
|
this.uuid = uuid;
|
|
252
|
+
this.token = token;
|
|
246
253
|
|
|
247
254
|
resolve();
|
|
248
255
|
});
|
|
@@ -83,7 +83,7 @@ class PluginManager extends BasePluginManager {
|
|
|
83
83
|
*/
|
|
84
84
|
constructor(client) {
|
|
85
85
|
if (!(client instanceof Client)) {
|
|
86
|
-
throw new Error(`[soundworks.PluginManager] Invalid argument, "new PluginManager(client)" should receive an instance of "soundworks.Client as argument
|
|
86
|
+
throw new Error(`[soundworks.PluginManager] Invalid argument, "new PluginManager(client)" should receive an instance of "soundworks.Client" as argument`);
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
super(client);
|
|
@@ -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,4 +1,5 @@
|
|
|
1
1
|
import { isBrowser } from '@ircam/sc-utils';
|
|
2
|
+
import fetch from 'cross-fetch';
|
|
2
3
|
import WebSocket from 'isomorphic-ws';
|
|
3
4
|
|
|
4
5
|
import {
|
|
@@ -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
|
};
|
package/src/common/BasePlugin.js
CHANGED
|
@@ -5,26 +5,10 @@ import merge from 'lodash.merge';
|
|
|
5
5
|
*/
|
|
6
6
|
class BasePlugin {
|
|
7
7
|
constructor(id) {
|
|
8
|
-
/**
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
* @see {@link client.PluginManager#register}
|
|
13
|
-
* @see {@link server.PluginManager#register}
|
|
14
|
-
*/
|
|
15
|
-
this.id = id;
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Type of the plugin, i.e. the ClassName.
|
|
19
|
-
*
|
|
20
|
-
* Usefull to do perform some logic based on certain types of plugins without
|
|
21
|
-
* knowing under which `id` they have been registered. (e.g. creating some generic
|
|
22
|
-
* views, etc.)
|
|
23
|
-
*
|
|
24
|
-
* @type {string}
|
|
25
|
-
* @readonly
|
|
26
|
-
*/
|
|
27
|
-
this.type = this.constructor.name;
|
|
8
|
+
/** @private */
|
|
9
|
+
this._id = id;
|
|
10
|
+
/** @private */
|
|
11
|
+
this._type = this.constructor.name;
|
|
28
12
|
|
|
29
13
|
/**
|
|
30
14
|
* Options of the plugin.
|
|
@@ -58,6 +42,32 @@ class BasePlugin {
|
|
|
58
42
|
this._onStateChangeCallbacks = new Set();
|
|
59
43
|
}
|
|
60
44
|
|
|
45
|
+
/**
|
|
46
|
+
* User defined ID of the plugin.
|
|
47
|
+
*
|
|
48
|
+
* @type {string}
|
|
49
|
+
* @readonly
|
|
50
|
+
* @see {@link client.PluginManager#register}
|
|
51
|
+
* @see {@link server.PluginManager#register}
|
|
52
|
+
*/
|
|
53
|
+
get id() {
|
|
54
|
+
return this._id;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Type of the plugin, i.e. the ClassName.
|
|
59
|
+
*
|
|
60
|
+
* Usefull to do perform some logic based on certain types of plugins without
|
|
61
|
+
* knowing under which `id` they have been registered. (e.g. creating some generic
|
|
62
|
+
* views, etc.)
|
|
63
|
+
*
|
|
64
|
+
* @type {string}
|
|
65
|
+
* @readonly
|
|
66
|
+
*/
|
|
67
|
+
get type() {
|
|
68
|
+
return this._type;
|
|
69
|
+
}
|
|
70
|
+
|
|
61
71
|
/**
|
|
62
72
|
* Start the plugin. This method is automatically called during the client or
|
|
63
73
|
* server `init()` lifecyle step. After `start()` is fulfilled the plugin should
|
|
@@ -13,7 +13,7 @@ class BasePluginManager {
|
|
|
13
13
|
/** @private */
|
|
14
14
|
this._node = node;
|
|
15
15
|
/** @private */
|
|
16
|
-
this.
|
|
16
|
+
this._dependencies = new Map();
|
|
17
17
|
/** @private */
|
|
18
18
|
this._instances = new Map();
|
|
19
19
|
/** @private */
|
|
@@ -67,11 +67,29 @@ class BasePluginManager {
|
|
|
67
67
|
throw new Error(`[soundworks.PluginManager] Invalid argument, "pluginManager.register" fourth optionnal argument should be an array`);
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
if (this.
|
|
71
|
-
throw new Error(`[soundworks:PluginManager] Plugin "${id}"
|
|
70
|
+
if (this._instances.has(id)) {
|
|
71
|
+
throw new Error(`[soundworks:PluginManager] Plugin "${id}" already registered`);
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
|
|
74
|
+
// we instanciate the plugin here, so that a plugin can register another one
|
|
75
|
+
// in its own constructor.
|
|
76
|
+
//
|
|
77
|
+
// the dependencies must be created first, so that the instance can call
|
|
78
|
+
// addDependency in its constructor
|
|
79
|
+
this._dependencies.set(id, deps);
|
|
80
|
+
|
|
81
|
+
const instance = new ctor(this._node, id, options);
|
|
82
|
+
this._instances.set(id, instance);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Manually add a dependency to a given plugin. Usefull to require a plugin
|
|
87
|
+
* within a plugin
|
|
88
|
+
*
|
|
89
|
+
*/
|
|
90
|
+
addDependency(pluginId, dependencyId) {
|
|
91
|
+
const deps = this._dependencies.get(pluginId);
|
|
92
|
+
deps.push(dependencyId);
|
|
75
93
|
}
|
|
76
94
|
|
|
77
95
|
/**
|
|
@@ -79,7 +97,7 @@ class BasePluginManager {
|
|
|
79
97
|
* @returns {string[]}
|
|
80
98
|
*/
|
|
81
99
|
getRegisteredPlugins() {
|
|
82
|
-
return Array.from(this.
|
|
100
|
+
return Array.from(this._instances.keys());
|
|
83
101
|
}
|
|
84
102
|
|
|
85
103
|
/**
|
|
@@ -96,16 +114,14 @@ class BasePluginManager {
|
|
|
96
114
|
|
|
97
115
|
this.status = 'inited';
|
|
98
116
|
// instanciate all plugins
|
|
99
|
-
for (let [id,
|
|
100
|
-
const instance = new ctor(this._node, id, options);
|
|
101
|
-
this._instances.set(id, instance);
|
|
117
|
+
for (let [id, instance] of this._instances.entries()) {
|
|
102
118
|
instance.onStateChange(_values => this._propagateStateChange(instance));
|
|
103
119
|
}
|
|
104
120
|
|
|
105
121
|
// propagate all 'idle' statuses before start
|
|
106
122
|
this._propagateStateChange();
|
|
107
123
|
|
|
108
|
-
const promises = Array.from(this.
|
|
124
|
+
const promises = Array.from(this._instances.keys()).map(id => this.unsafeGet(id));
|
|
109
125
|
|
|
110
126
|
try {
|
|
111
127
|
await Promise.all(promises);
|
|
@@ -125,8 +141,8 @@ class BasePluginManager {
|
|
|
125
141
|
|
|
126
142
|
/**
|
|
127
143
|
* Retrieve an fully started instance of a registered plugin, without checking
|
|
128
|
-
* that the pluginManager
|
|
129
|
-
* manager itself
|
|
144
|
+
* that the pluginManager has started. This is required for starting the plugin
|
|
145
|
+
* manager itself and to require a plugin from within another plugin
|
|
130
146
|
*
|
|
131
147
|
* @private
|
|
132
148
|
*/
|
|
@@ -135,7 +151,7 @@ class BasePluginManager {
|
|
|
135
151
|
throw new Error(`[soundworks.PluginManager] Invalid argument, "pluginManager.get(name)" argument should be a string`);
|
|
136
152
|
}
|
|
137
153
|
|
|
138
|
-
if (!this.
|
|
154
|
+
if (!this._instances.has(id)) {
|
|
139
155
|
throw new Error(`[soundworks:PluginManager] Cannot get plugin "${id}", plugin is not registered`);
|
|
140
156
|
}
|
|
141
157
|
|
|
@@ -144,7 +160,7 @@ class BasePluginManager {
|
|
|
144
160
|
// to dynamically register and launch plugins at runtime.
|
|
145
161
|
//
|
|
146
162
|
// if (!this._instances.has(id)) {
|
|
147
|
-
// const { ctor, options } = this.
|
|
163
|
+
// const { ctor, options } = this._dependencies.get(id);
|
|
148
164
|
// const instance = new ctor(this._node, id, options);
|
|
149
165
|
// this._instances.set(id, instance);
|
|
150
166
|
// }
|
|
@@ -152,7 +168,7 @@ class BasePluginManager {
|
|
|
152
168
|
const instance = this._instances.get(id);
|
|
153
169
|
|
|
154
170
|
// recursively get the dependency chain
|
|
155
|
-
const
|
|
171
|
+
const deps = this._dependencies.get(id);
|
|
156
172
|
const promises = deps.map(id => this.unsafeGet(id));
|
|
157
173
|
|
|
158
174
|
await Promise.all(promises);
|
|
@@ -138,7 +138,6 @@ ${JSON.stringify(initValues, null, 2)}`);
|
|
|
138
138
|
this._client.transport.addListener(`${DETACH_ERROR}-${this.id}`, (reqId, msg) => {
|
|
139
139
|
rejectRequest(reqId, msg);
|
|
140
140
|
});
|
|
141
|
-
|
|
142
141
|
}
|
|
143
142
|
}
|
|
144
143
|
|
|
@@ -150,7 +149,7 @@ ${JSON.stringify(initValues, null, 2)}`);
|
|
|
150
149
|
// Monkey patch detach so it throws if called twice. Doing nothing blocks
|
|
151
150
|
// the process on a second `detach` call as the Promise never resolves
|
|
152
151
|
this.detach = () => {
|
|
153
|
-
throw new Error(`[
|
|
152
|
+
throw new Error(`[SharedState] State "${this.schemaName} (${this.id})" already detached, cannot detach twice`);
|
|
154
153
|
};
|
|
155
154
|
}
|
|
156
155
|
|
|
@@ -270,10 +269,9 @@ ${JSON.stringify(initValues, null, 2)}`);
|
|
|
270
269
|
let propagateNow = false;
|
|
271
270
|
|
|
272
271
|
for (let name in updates) {
|
|
273
|
-
//
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
}
|
|
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]);
|
|
277
275
|
|
|
278
276
|
// @note: general idea...
|
|
279
277
|
// if immediate=true
|
|
@@ -312,21 +310,41 @@ ${JSON.stringify(initValues, null, 2)}`);
|
|
|
312
310
|
}
|
|
313
311
|
|
|
314
312
|
/**
|
|
315
|
-
* 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.
|
|
316
315
|
*
|
|
317
316
|
* @param {string} name - Name of the param.
|
|
318
317
|
* @throws Throws if `name` does not correspond to an existing field
|
|
319
318
|
* of the state.
|
|
320
319
|
* @return {mixed}
|
|
321
320
|
* @example
|
|
322
|
-
* const value = state.get('
|
|
321
|
+
* const value = state.get('paramName');
|
|
323
322
|
*/
|
|
324
323
|
get(name) {
|
|
325
324
|
return this._parameters.get(name);
|
|
326
325
|
}
|
|
327
326
|
|
|
328
327
|
/**
|
|
329
|
-
*
|
|
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.
|
|
330
348
|
*
|
|
331
349
|
* @return {object}
|
|
332
350
|
* @example
|
|
@@ -336,6 +354,23 @@ ${JSON.stringify(initValues, null, 2)}`);
|
|
|
336
354
|
return this._parameters.getValues();
|
|
337
355
|
}
|
|
338
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
|
+
|
|
339
374
|
/**
|
|
340
375
|
* Get the schema from which the state has been created.
|
|
341
376
|
*
|
|
@@ -423,7 +458,7 @@ ${JSON.stringify(initValues, null, 2)}`);
|
|
|
423
458
|
/**
|
|
424
459
|
* Subscribe to state updates.
|
|
425
460
|
*
|
|
426
|
-
* @param {client.SharedState~onUpdateCallback|
|
|
461
|
+
* @param {client.SharedState~onUpdateCallback|server.SharedState~onUpdateCallback} callback
|
|
427
462
|
* Callback to execute when an update is applied on the state.
|
|
428
463
|
* @param {Boolean} [executeListener=false] - Execute the callback immediately
|
|
429
464
|
* with current state values. (`oldValues` will be set to `{}`, and `context` to `null`)
|
|
@@ -458,8 +493,8 @@ ${JSON.stringify(initValues, null, 2)}`);
|
|
|
458
493
|
/**
|
|
459
494
|
* Register a function to execute when detaching from the state
|
|
460
495
|
*
|
|
461
|
-
* @param {Function} callback -
|
|
462
|
-
*
|
|
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
|
|
463
498
|
* creator.
|
|
464
499
|
*/
|
|
465
500
|
onDetach(callback) {
|
|
@@ -471,7 +506,7 @@ ${JSON.stringify(initValues, null, 2)}`);
|
|
|
471
506
|
* Register a function to execute when the state is deleted. Only called if the
|
|
472
507
|
* node was the creator of the state. Is called after `onDetach`
|
|
473
508
|
*
|
|
474
|
-
* @param {Function} callback -
|
|
509
|
+
* @param {Function} callback - Callback to execute when the state is deleted.
|
|
475
510
|
*/
|
|
476
511
|
onDelete(callback) {
|
|
477
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;
|