@soundworks/core 4.0.0-alpha.2 → 4.0.0-alpha.20
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/.eslintrc +0 -1
- package/.jsdoc +57 -0
- package/CHANGELOG.md +26 -0
- package/README.md +17 -51
- package/package.json +24 -31
- package/src/client/Client.js +11 -5
- package/src/client/Context.js +12 -13
- package/src/client/ContextManager.js +1 -1
- package/src/client/PluginManager.js +1 -1
- package/src/client/SharedStateCollection.js +38 -0
- package/src/client/Socket.js +60 -44
- package/src/common/BasePlugin.js +31 -21
- package/src/common/BasePluginManager.js +34 -19
- package/src/common/BaseSharedState.js +201 -68
- package/src/common/BaseSharedStateCollection.js +279 -0
- package/src/common/BaseStateManager.js +257 -36
- package/src/common/BatchedTransport.js +66 -0
- package/src/common/ParameterBag.js +92 -30
- package/src/common/PromiseStore.js +48 -0
- package/src/common/SharedStatePrivate.js +52 -43
- package/src/common/constants.js +14 -1
- package/src/common/sockets-utils.js +3 -4
- package/src/server/Client.js +7 -2
- package/src/server/ContextManager.js +4 -4
- package/src/server/PluginManager.js +1 -1
- package/src/server/Server.js +255 -41
- package/src/server/SharedState.js +0 -1
- package/src/server/SharedStateCollection.js +39 -0
- package/src/server/Socket.js +50 -49
- package/src/server/Sockets.js +31 -11
- package/src/server/StateManager.js +162 -90
- package/src/server/audit-network-latency.worker.js +16 -4
- package/src/server/crypto.js +37 -0
- package/src/common/promise-store.js +0 -25
- package/src/common/utils.js +0 -22
- package/tsconfig.json +0 -21
package/src/common/BasePlugin.js
CHANGED
|
@@ -1,30 +1,14 @@
|
|
|
1
|
-
import merge from 'lodash.
|
|
1
|
+
import merge from 'lodash/merge.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* @private
|
|
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
|
|
@@ -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
|
|
@@ -14,7 +13,7 @@ class BasePluginManager {
|
|
|
14
13
|
/** @private */
|
|
15
14
|
this._node = node;
|
|
16
15
|
/** @private */
|
|
17
|
-
this.
|
|
16
|
+
this._dependencies = new Map();
|
|
18
17
|
/** @private */
|
|
19
18
|
this._instances = new Map();
|
|
20
19
|
/** @private */
|
|
@@ -68,11 +67,29 @@ class BasePluginManager {
|
|
|
68
67
|
throw new Error(`[soundworks.PluginManager] Invalid argument, "pluginManager.register" fourth optionnal argument should be an array`);
|
|
69
68
|
}
|
|
70
69
|
|
|
71
|
-
if (this.
|
|
72
|
-
throw new Error(`[soundworks:PluginManager] Plugin "${id}"
|
|
70
|
+
if (this._instances.has(id)) {
|
|
71
|
+
throw new Error(`[soundworks:PluginManager] Plugin "${id}" already registered`);
|
|
73
72
|
}
|
|
74
73
|
|
|
75
|
-
|
|
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);
|
|
76
93
|
}
|
|
77
94
|
|
|
78
95
|
/**
|
|
@@ -80,7 +97,7 @@ class BasePluginManager {
|
|
|
80
97
|
* @returns {string[]}
|
|
81
98
|
*/
|
|
82
99
|
getRegisteredPlugins() {
|
|
83
|
-
return Array.from(this.
|
|
100
|
+
return Array.from(this._instances.keys());
|
|
84
101
|
}
|
|
85
102
|
|
|
86
103
|
/**
|
|
@@ -97,23 +114,21 @@ class BasePluginManager {
|
|
|
97
114
|
|
|
98
115
|
this.status = 'inited';
|
|
99
116
|
// instanciate all plugins
|
|
100
|
-
for (let [
|
|
101
|
-
const instance = new ctor(this._node, id, options);
|
|
102
|
-
this._instances.set(id, instance);
|
|
117
|
+
for (let [_id, instance] of this._instances.entries()) {
|
|
103
118
|
instance.onStateChange(_values => this._propagateStateChange(instance));
|
|
104
119
|
}
|
|
105
120
|
|
|
106
121
|
// propagate all 'idle' statuses before start
|
|
107
122
|
this._propagateStateChange();
|
|
108
123
|
|
|
109
|
-
const promises = Array.from(this.
|
|
124
|
+
const promises = Array.from(this._instances.keys()).map(id => this.unsafeGet(id));
|
|
110
125
|
|
|
111
126
|
try {
|
|
112
127
|
await Promise.all(promises);
|
|
113
128
|
this.status = 'started';
|
|
114
|
-
} catch(err) {
|
|
129
|
+
} catch (err) {
|
|
115
130
|
this.status = 'errored';
|
|
116
|
-
throw
|
|
131
|
+
throw err; // throw initial error
|
|
117
132
|
}
|
|
118
133
|
}
|
|
119
134
|
|
|
@@ -126,8 +141,8 @@ class BasePluginManager {
|
|
|
126
141
|
|
|
127
142
|
/**
|
|
128
143
|
* Retrieve an fully started instance of a registered plugin, without checking
|
|
129
|
-
* that the pluginManager
|
|
130
|
-
* 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
|
|
131
146
|
*
|
|
132
147
|
* @private
|
|
133
148
|
*/
|
|
@@ -136,7 +151,7 @@ class BasePluginManager {
|
|
|
136
151
|
throw new Error(`[soundworks.PluginManager] Invalid argument, "pluginManager.get(name)" argument should be a string`);
|
|
137
152
|
}
|
|
138
153
|
|
|
139
|
-
if (!this.
|
|
154
|
+
if (!this._instances.has(id)) {
|
|
140
155
|
throw new Error(`[soundworks:PluginManager] Cannot get plugin "${id}", plugin is not registered`);
|
|
141
156
|
}
|
|
142
157
|
|
|
@@ -145,7 +160,7 @@ class BasePluginManager {
|
|
|
145
160
|
// to dynamically register and launch plugins at runtime.
|
|
146
161
|
//
|
|
147
162
|
// if (!this._instances.has(id)) {
|
|
148
|
-
// const { ctor, options } = this.
|
|
163
|
+
// const { ctor, options } = this._dependencies.get(id);
|
|
149
164
|
// const instance = new ctor(this._node, id, options);
|
|
150
165
|
// this._instances.set(id, instance);
|
|
151
166
|
// }
|
|
@@ -153,7 +168,7 @@ class BasePluginManager {
|
|
|
153
168
|
const instance = this._instances.get(id);
|
|
154
169
|
|
|
155
170
|
// recursively get the dependency chain
|
|
156
|
-
const
|
|
171
|
+
const deps = this._dependencies.get(id);
|
|
157
172
|
const promises = deps.map(id => this.unsafeGet(id));
|
|
158
173
|
|
|
159
174
|
await Promise.all(promises);
|
|
@@ -170,7 +185,7 @@ class BasePluginManager {
|
|
|
170
185
|
this._instanceStartPromises.set(id, startPromise);
|
|
171
186
|
|
|
172
187
|
await startPromise;
|
|
173
|
-
} catch(err) {
|
|
188
|
+
} catch (err) {
|
|
174
189
|
errored = true;
|
|
175
190
|
this._propagateStateChange(instance, 'errored');
|
|
176
191
|
throw err;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { isPlainObject } from '@ircam/sc-utils';
|
|
1
2
|
import ParameterBag from './ParameterBag.js';
|
|
3
|
+
import PromiseStore from './PromiseStore.js';
|
|
2
4
|
import {
|
|
3
5
|
DELETE_REQUEST,
|
|
4
6
|
DELETE_RESPONSE,
|
|
@@ -12,29 +14,41 @@ import {
|
|
|
12
14
|
UPDATE_ABORT,
|
|
13
15
|
UPDATE_NOTIFICATION,
|
|
14
16
|
} from './constants.js';
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
|
|
18
|
+
// @todo - replace isOwner by `type`:
|
|
19
|
+
// enum {
|
|
20
|
+
// 'owner'
|
|
21
|
+
// 'attached'
|
|
22
|
+
// 'collection-controlller'
|
|
23
|
+
// 'collection-attached'
|
|
24
|
+
// }
|
|
20
25
|
|
|
21
26
|
class BaseSharedState {
|
|
22
27
|
constructor(id, remoteId, schemaName, schema, client, isOwner, manager, initValues = {}) {
|
|
23
|
-
|
|
24
|
-
this.
|
|
25
|
-
|
|
26
|
-
|
|
28
|
+
/** @private */
|
|
29
|
+
this._id = id;
|
|
30
|
+
/** @private */
|
|
31
|
+
this._remoteId = remoteId;
|
|
32
|
+
/** @private */
|
|
33
|
+
this._schemaName = schemaName;
|
|
27
34
|
/** @private */
|
|
28
35
|
this._isOwner = isOwner; // may be the server or any client
|
|
29
36
|
/** @private */
|
|
30
37
|
this._client = client;
|
|
31
38
|
/** @private */
|
|
32
39
|
this._manager = manager;
|
|
40
|
+
/**
|
|
41
|
+
* true is the state has been detached or deleted
|
|
42
|
+
* @private
|
|
43
|
+
*/
|
|
44
|
+
this._detached = false;
|
|
45
|
+
/* @private */
|
|
46
|
+
this._promiseStore = new PromiseStore(this.constructor.name);
|
|
33
47
|
|
|
34
48
|
try {
|
|
35
49
|
/** @private */
|
|
36
50
|
this._parameters = new ParameterBag(schema, initValues);
|
|
37
|
-
} catch(err) {
|
|
51
|
+
} catch (err) {
|
|
38
52
|
console.error(err.stack);
|
|
39
53
|
|
|
40
54
|
throw new Error(`Error creating or attaching state "${schemaName}" w/ values:\n
|
|
@@ -51,13 +65,13 @@ ${JSON.stringify(initValues, null, 2)}`);
|
|
|
51
65
|
// add listener for state updates
|
|
52
66
|
this._client.transport.addListener(`${UPDATE_RESPONSE}-${this.id}-${this.remoteId}`, async (reqId, updates, context) => {
|
|
53
67
|
const updated = await this._commit(updates, context, true, true);
|
|
54
|
-
|
|
68
|
+
this._promiseStore.resolve(reqId, updated);
|
|
55
69
|
});
|
|
56
70
|
|
|
57
71
|
// retrieve values but do not propagate to subscriptions
|
|
58
72
|
this._client.transport.addListener(`${UPDATE_ABORT}-${this.id}-${this.remoteId}`, async (reqId, updates, context) => {
|
|
59
73
|
const updated = await this._commit(updates, context, false, true);
|
|
60
|
-
|
|
74
|
+
this._promiseStore.resolve(reqId, updated);
|
|
61
75
|
});
|
|
62
76
|
|
|
63
77
|
this._client.transport.addListener(`${UPDATE_NOTIFICATION}-${this.id}-${this.remoteId}`, async (updates, context) => {
|
|
@@ -87,70 +101,121 @@ ${JSON.stringify(initValues, null, 2)}`);
|
|
|
87
101
|
});
|
|
88
102
|
|
|
89
103
|
// ---------------------------------------------
|
|
90
|
-
//
|
|
104
|
+
// state has been deleted by its creator or the schema has been deleted
|
|
91
105
|
// ---------------------------------------------
|
|
92
|
-
this._client.transport.addListener(`${DELETE_NOTIFICATION}-${this.id}-${this.remoteId}`, () => {
|
|
106
|
+
this._client.transport.addListener(`${DELETE_NOTIFICATION}-${this.id}-${this.remoteId}`, async () => {
|
|
93
107
|
this._manager._statesById.delete(this.id);
|
|
94
108
|
this._clearTransport();
|
|
95
109
|
|
|
96
|
-
|
|
97
|
-
|
|
110
|
+
for (let callback of this._onDetachCallbacks) {
|
|
111
|
+
try {
|
|
112
|
+
await callback();
|
|
113
|
+
} catch (err) {
|
|
114
|
+
console.error(err.message);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
98
117
|
|
|
99
|
-
this.
|
|
118
|
+
if (this._isOwner) {
|
|
119
|
+
for (let callback of this._onDeleteCallbacks) {
|
|
120
|
+
await callback();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
this._onDetachCallbacks.clear();
|
|
125
|
+
this._onDeleteCallbacks.clear();
|
|
126
|
+
this._promiseStore.flush();
|
|
100
127
|
});
|
|
101
128
|
|
|
102
129
|
|
|
103
130
|
if (this._isOwner) {
|
|
104
131
|
// ---------------------------------------------
|
|
105
|
-
//
|
|
132
|
+
// the creator has called `.delete()`
|
|
106
133
|
// ---------------------------------------------
|
|
107
|
-
this._client.transport.addListener(`${DELETE_RESPONSE}-${this.id}-${this.remoteId}`, (reqId) => {
|
|
134
|
+
this._client.transport.addListener(`${DELETE_RESPONSE}-${this.id}-${this.remoteId}`, async (reqId) => {
|
|
108
135
|
this._manager._statesById.delete(this.id);
|
|
109
136
|
this._clearTransport();
|
|
110
137
|
|
|
111
|
-
|
|
112
|
-
|
|
138
|
+
for (let callback of this._onDetachCallbacks) {
|
|
139
|
+
await callback();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
for (let callback of this._onDeleteCallbacks) {
|
|
143
|
+
await callback();
|
|
144
|
+
}
|
|
113
145
|
|
|
114
|
-
this.
|
|
115
|
-
|
|
146
|
+
this._onDetachCallbacks.clear();
|
|
147
|
+
this._onDeleteCallbacks.clear();
|
|
148
|
+
this._promiseStore.resolve(reqId, this);
|
|
149
|
+
this._promiseStore.flush();
|
|
116
150
|
});
|
|
117
151
|
|
|
118
152
|
this._client.transport.addListener(`${DELETE_ERROR}-${this.id}`, (reqId, msg) => {
|
|
119
|
-
|
|
153
|
+
this._promiseStore.reject(reqId, msg);
|
|
120
154
|
});
|
|
121
155
|
|
|
122
156
|
} else {
|
|
123
157
|
// ---------------------------------------------
|
|
124
|
-
//
|
|
158
|
+
// the attached node has called `.detach()`
|
|
125
159
|
// ---------------------------------------------
|
|
126
|
-
this._client.transport.addListener(`${DETACH_RESPONSE}-${this.id}-${this.remoteId}`, (reqId) => {
|
|
160
|
+
this._client.transport.addListener(`${DETACH_RESPONSE}-${this.id}-${this.remoteId}`, async (reqId) => {
|
|
127
161
|
this._manager._statesById.delete(this.id);
|
|
128
162
|
this._clearTransport();
|
|
129
163
|
|
|
130
|
-
this._onDetachCallbacks
|
|
164
|
+
for (let callback of this._onDetachCallbacks) {
|
|
165
|
+
await callback();
|
|
166
|
+
}
|
|
131
167
|
|
|
132
|
-
this.
|
|
133
|
-
|
|
168
|
+
this._onDetachCallbacks.clear();
|
|
169
|
+
this._onDeleteCallbacks.clear();
|
|
170
|
+
this._promiseStore.resolve(reqId, this);
|
|
171
|
+
this._promiseStore.flush();
|
|
134
172
|
});
|
|
135
173
|
|
|
136
174
|
// the state does not exists anymore in the server (should not happen)
|
|
137
175
|
this._client.transport.addListener(`${DETACH_ERROR}-${this.id}`, (reqId, msg) => {
|
|
138
|
-
|
|
176
|
+
this._onDetachCallbacks.clear();
|
|
177
|
+
this._onDeleteCallbacks.clear();
|
|
178
|
+
this._promiseStore.reject(reqId, msg);
|
|
179
|
+
this._promiseStore.flush();
|
|
139
180
|
});
|
|
140
|
-
|
|
141
181
|
}
|
|
142
182
|
}
|
|
143
183
|
|
|
144
|
-
/**
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
184
|
+
/**
|
|
185
|
+
* Id of the state
|
|
186
|
+
* @type {Number}
|
|
187
|
+
* @readonly
|
|
188
|
+
*/
|
|
189
|
+
get id() {
|
|
190
|
+
return this._id;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Unique id of the state for the current node
|
|
195
|
+
* @readonly
|
|
196
|
+
* @type {Number}
|
|
197
|
+
* @private
|
|
198
|
+
*/
|
|
199
|
+
get remoteId() {
|
|
200
|
+
return this._remoteId;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Name of the schema
|
|
205
|
+
* @type {String}
|
|
206
|
+
* @readonly
|
|
207
|
+
*/
|
|
208
|
+
get schemaName() {
|
|
209
|
+
return this._schemaName;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Indicates if the node is the owner of the state
|
|
214
|
+
* @type {Boolean}
|
|
215
|
+
* @readonly
|
|
216
|
+
*/
|
|
217
|
+
get isOwner() {
|
|
218
|
+
return this._isOwner;
|
|
154
219
|
}
|
|
155
220
|
|
|
156
221
|
/** @private */
|
|
@@ -158,9 +223,16 @@ ${JSON.stringify(initValues, null, 2)}`);
|
|
|
158
223
|
// remove listeners
|
|
159
224
|
this._client.transport.removeAllListeners(`${UPDATE_RESPONSE}-${this.id}-${this.remoteId}`);
|
|
160
225
|
this._client.transport.removeAllListeners(`${UPDATE_NOTIFICATION}-${this.id}-${this.remoteId}`);
|
|
161
|
-
this._client.transport.removeAllListeners(`${
|
|
226
|
+
this._client.transport.removeAllListeners(`${UPDATE_ABORT}-${this.id}-${this.remoteId}`);
|
|
162
227
|
this._client.transport.removeAllListeners(`${DELETE_NOTIFICATION}-${this.id}-${this.remoteId}`);
|
|
163
|
-
|
|
228
|
+
|
|
229
|
+
if (this._isOwner) {
|
|
230
|
+
this._client.transport.removeAllListeners(`${DELETE_RESPONSE}-${this.id}-${this.remoteId}`);
|
|
231
|
+
this._client.transport.removeAllListeners(`${DELETE_ERROR}-${this.id}-${this.remoteId}`);
|
|
232
|
+
} else {
|
|
233
|
+
this._client.transport.removeAllListeners(`${DETACH_RESPONSE}-${this.id}-${this.remoteId}`);
|
|
234
|
+
this._client.transport.removeAllListeners(`${DETACH_ERROR}-${this.id}-${this.remoteId}`);
|
|
235
|
+
}
|
|
164
236
|
}
|
|
165
237
|
|
|
166
238
|
/** @private */
|
|
@@ -255,16 +327,27 @@ ${JSON.stringify(initValues, null, 2)}`);
|
|
|
255
327
|
* const updates = await state.set({ myParam: Math.random() });
|
|
256
328
|
*/
|
|
257
329
|
async set(updates, context = null) {
|
|
330
|
+
if (this._detached) {
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (!isPlainObject(updates)) {
|
|
335
|
+
throw new ReferenceError(`[SharedState] State "${this.schemaName}": state.set(updates[, context]) should receive an object as first parameter`);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (context !== null && !isPlainObject(context)) {
|
|
339
|
+
throw new ReferenceError(`[SharedState] State "${this.schemaName}": state.set(updates[, context]) should receive an object as second parameter`);
|
|
340
|
+
}
|
|
341
|
+
|
|
258
342
|
// handle immediate option
|
|
259
343
|
const immediateNewValues = {};
|
|
260
344
|
const immediateOldValues = {};
|
|
261
345
|
let propagateNow = false;
|
|
262
346
|
|
|
263
347
|
for (let name in updates) {
|
|
264
|
-
//
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
}
|
|
348
|
+
// try to coerce value early, so that eventual errors are triggered early
|
|
349
|
+
// on the node requesting the update
|
|
350
|
+
this._parameters.coerceValue(name, updates[name]);
|
|
268
351
|
|
|
269
352
|
// @note: general idea...
|
|
270
353
|
// if immediate=true
|
|
@@ -297,27 +380,47 @@ ${JSON.stringify(initValues, null, 2)}`);
|
|
|
297
380
|
|
|
298
381
|
// go through server-side normal behavior
|
|
299
382
|
return new Promise((resolve, reject) => {
|
|
300
|
-
const reqId =
|
|
383
|
+
const reqId = this._promiseStore.add(resolve, reject, 'update-request');
|
|
301
384
|
this._client.transport.emit(`${UPDATE_REQUEST}-${this.id}-${this.remoteId}`, reqId, updates, context);
|
|
302
385
|
});
|
|
303
386
|
}
|
|
304
387
|
|
|
305
388
|
/**
|
|
306
|
-
* Get the value of a
|
|
389
|
+
* Get the value of a parameter of the state. If the parameter is of `any` type,
|
|
390
|
+
* a deep copy is returned.
|
|
307
391
|
*
|
|
308
392
|
* @param {string} name - Name of the param.
|
|
309
393
|
* @throws Throws if `name` does not correspond to an existing field
|
|
310
394
|
* of the state.
|
|
311
395
|
* @return {mixed}
|
|
312
396
|
* @example
|
|
313
|
-
* const value = state.get('
|
|
397
|
+
* const value = state.get('paramName');
|
|
314
398
|
*/
|
|
315
399
|
get(name) {
|
|
316
400
|
return this._parameters.get(name);
|
|
317
401
|
}
|
|
318
402
|
|
|
319
403
|
/**
|
|
320
|
-
*
|
|
404
|
+
* Similar to `get` but returns a reference to the underlying value in case of
|
|
405
|
+
* `any` type. May be usefull if the underlying value is big (e.g. sensors
|
|
406
|
+
* recordings, etc.) and deep cloning expensive. Be aware that if changes are
|
|
407
|
+
* made on the returned object, the state of your application will become
|
|
408
|
+
* inconsistent.
|
|
409
|
+
*
|
|
410
|
+
* @param {string} name - Name of the param.
|
|
411
|
+
* @throws Throws if `name` does not correspond to an existing field
|
|
412
|
+
* of the state.
|
|
413
|
+
* @return {mixed}
|
|
414
|
+
* @example
|
|
415
|
+
* const value = state.getUnsafe('paramName');
|
|
416
|
+
*/
|
|
417
|
+
getUnsafe(name) {
|
|
418
|
+
return this._parameters.getUnsafe(name);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Get all the key / value pairs of the state. If a parameter is of `any`
|
|
423
|
+
* type, a deep copy is made.
|
|
321
424
|
*
|
|
322
425
|
* @return {object}
|
|
323
426
|
* @example
|
|
@@ -328,12 +431,29 @@ ${JSON.stringify(initValues, null, 2)}`);
|
|
|
328
431
|
}
|
|
329
432
|
|
|
330
433
|
/**
|
|
331
|
-
* Get the
|
|
434
|
+
* Get all the key / value pairs of the state. If a parameter is of `any`
|
|
435
|
+
* type, a deep copy is made.
|
|
436
|
+
* Similar to `getValues` but returns a reference to the underlying value in
|
|
437
|
+
* case of `any` type. May be usefull if the underlying value is big (e.g.
|
|
438
|
+
* sensors recordings, etc.) and deep cloning expensive. Be aware that if
|
|
439
|
+
* changes are made on the returned object, the state of your application will
|
|
440
|
+
* become inconsistent.
|
|
441
|
+
*
|
|
442
|
+
* @return {object}
|
|
443
|
+
* @example
|
|
444
|
+
* const values = state.getValues();
|
|
445
|
+
*/
|
|
446
|
+
getValuesUnsafe() {
|
|
447
|
+
return this._parameters.getValuesUnsafe();
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Definition of schema from which the state has been created.
|
|
332
452
|
*
|
|
333
|
-
* @param {string} [name=null] - If given, returns only the definition
|
|
334
|
-
* to the given param name.
|
|
453
|
+
* @param {string} [name=null] - If given, returns only the definition
|
|
454
|
+
* corresponding to the given param name.
|
|
335
455
|
* @throws Throws if `name` does not correspond to an existing field
|
|
336
|
-
* of the
|
|
456
|
+
* of the schema.
|
|
337
457
|
* @return {object}
|
|
338
458
|
* @example
|
|
339
459
|
* const schema = state.getSchema();
|
|
@@ -367,7 +487,7 @@ ${JSON.stringify(initValues, null, 2)}`);
|
|
|
367
487
|
|
|
368
488
|
/**
|
|
369
489
|
* Detach from the state. If the client is the creator of the state, the state
|
|
370
|
-
* is deleted and all attached nodes get notified
|
|
490
|
+
* is deleted and all attached nodes get notified.
|
|
371
491
|
*
|
|
372
492
|
* @example
|
|
373
493
|
* const state = await client.state.attach('globals');
|
|
@@ -375,16 +495,21 @@ ${JSON.stringify(initValues, null, 2)}`);
|
|
|
375
495
|
* await state.detach();
|
|
376
496
|
*/
|
|
377
497
|
async detach() {
|
|
498
|
+
if (this._detached) {
|
|
499
|
+
throw new Error(`[SharedState] State "${this.schemaName} (${this.id})" already detached, cannot detach twice`);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
this._detached = true; // mark detached early
|
|
378
503
|
this._onUpdateCallbacks.clear();
|
|
379
504
|
|
|
380
505
|
if (this._isOwner) {
|
|
381
506
|
return new Promise((resolve, reject) => {
|
|
382
|
-
const reqId =
|
|
507
|
+
const reqId = this._promiseStore.add(resolve, reject, 'delete-request');
|
|
383
508
|
this._client.transport.emit(`${DELETE_REQUEST}-${this.id}-${this.remoteId}`, reqId);
|
|
384
509
|
});
|
|
385
510
|
} else {
|
|
386
511
|
return new Promise((resolve, reject) => {
|
|
387
|
-
const reqId =
|
|
512
|
+
const reqId = this._promiseStore.add(resolve, reject, 'detach-request');
|
|
388
513
|
this._client.transport.emit(`${DETACH_REQUEST}-${this.id}-${this.remoteId}`, reqId);
|
|
389
514
|
});
|
|
390
515
|
}
|
|
@@ -392,9 +517,11 @@ ${JSON.stringify(initValues, null, 2)}`);
|
|
|
392
517
|
|
|
393
518
|
/**
|
|
394
519
|
* Delete the state. Only the creator/owner of the state can use this method.
|
|
395
|
-
*
|
|
396
|
-
*
|
|
397
|
-
*
|
|
520
|
+
*
|
|
521
|
+
* All nodes attached to the state will be detached, triggering any registered
|
|
522
|
+
* `onDetach` callbacks. The creator of the state will also have its own `onDelete`
|
|
523
|
+
* callback triggered. The local `onDeatch` and `onDelete` callbacks will be
|
|
524
|
+
* executed *before* the returned Promise resolves
|
|
398
525
|
*
|
|
399
526
|
* @throws Throws if the method is called by a node which is not the owner of
|
|
400
527
|
* the state.
|
|
@@ -405,16 +532,20 @@ ${JSON.stringify(initValues, null, 2)}`);
|
|
|
405
532
|
*/
|
|
406
533
|
async delete() {
|
|
407
534
|
if (this._isOwner) {
|
|
535
|
+
if (this._detached) {
|
|
536
|
+
throw new Error(`[SharedState] State "${this.schemaName} (${this.id})" already deleted, cannot delete twice`);
|
|
537
|
+
}
|
|
538
|
+
|
|
408
539
|
return this.detach();
|
|
409
540
|
} else {
|
|
410
|
-
throw new Error(`[
|
|
541
|
+
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
542
|
}
|
|
412
543
|
}
|
|
413
544
|
|
|
414
545
|
/**
|
|
415
546
|
* Subscribe to state updates.
|
|
416
547
|
*
|
|
417
|
-
* @param {client.SharedState~onUpdateCallback|
|
|
548
|
+
* @param {client.SharedState~onUpdateCallback|server.SharedState~onUpdateCallback} callback
|
|
418
549
|
* Callback to execute when an update is applied on the state.
|
|
419
550
|
* @param {Boolean} [executeListener=false] - Execute the callback immediately
|
|
420
551
|
* with current state values. (`oldValues` will be set to `{}`, and `context` to `null`)
|
|
@@ -447,10 +578,11 @@ ${JSON.stringify(initValues, null, 2)}`);
|
|
|
447
578
|
}
|
|
448
579
|
|
|
449
580
|
/**
|
|
450
|
-
* Register a function to execute when detaching from the state
|
|
581
|
+
* Register a function to execute when detaching from the state. The function
|
|
582
|
+
* will be executed before the `detach` promise resolves.
|
|
451
583
|
*
|
|
452
|
-
* @param {Function} callback -
|
|
453
|
-
*
|
|
584
|
+
* @param {Function} callback - Callback to execute when detaching from the state.
|
|
585
|
+
* Whether the client as called `detach`, or the state has been deleted by its
|
|
454
586
|
* creator.
|
|
455
587
|
*/
|
|
456
588
|
onDetach(callback) {
|
|
@@ -460,9 +592,10 @@ ${JSON.stringify(initValues, null, 2)}`);
|
|
|
460
592
|
|
|
461
593
|
/**
|
|
462
594
|
* Register a function to execute when the state is deleted. Only called if the
|
|
463
|
-
* node was the creator of the state. Is called after `onDetach`
|
|
595
|
+
* node was the creator of the state. Is called after `onDetach` and executed
|
|
596
|
+
* before the `delete` Promise resolves.
|
|
464
597
|
*
|
|
465
|
-
* @param {Function} callback -
|
|
598
|
+
* @param {Function} callback - Callback to execute when the state is deleted.
|
|
466
599
|
*/
|
|
467
600
|
onDelete(callback) {
|
|
468
601
|
this._onDeleteCallbacks.add(callback);
|