@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
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/** @private */
|
|
2
|
+
class BaseSharedStateCollection {
|
|
3
|
+
constructor(stateManager, schemaName, options = {}) {
|
|
4
|
+
this._stateManager = stateManager;
|
|
5
|
+
this._schemaName = schemaName;
|
|
6
|
+
this._options = Object.assign({ excludeLocal: false }, options);
|
|
7
|
+
|
|
8
|
+
this._schema = null;
|
|
9
|
+
this._states = [];
|
|
10
|
+
|
|
11
|
+
this._onUpdateCallbacks = new Set();
|
|
12
|
+
this._onAttachCallbacks = new Set();
|
|
13
|
+
this._onDetachCallbacks = new Set();
|
|
14
|
+
this._unobserve = null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** @private */
|
|
18
|
+
async _init() {
|
|
19
|
+
this._schema = await this._stateManager.getSchema(this._schemaName);
|
|
20
|
+
|
|
21
|
+
this._unobserve = await this._stateManager.observe(this._schemaName, async (schemaName, stateId) => {
|
|
22
|
+
const state = await this._stateManager.attach(schemaName, stateId);
|
|
23
|
+
this._states.push(state);
|
|
24
|
+
|
|
25
|
+
state.onDetach(() => {
|
|
26
|
+
const index = this._states.indexOf(state);
|
|
27
|
+
this._states.splice(index, 1);
|
|
28
|
+
|
|
29
|
+
this._onDetachCallbacks.forEach(callback => callback(state));
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
state.onUpdate((newValues, oldValues, context) => {
|
|
33
|
+
Array.from(this._onUpdateCallbacks).forEach(callback => {
|
|
34
|
+
callback(state, newValues, oldValues, context);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
this._onAttachCallbacks.forEach(callback => callback(state));
|
|
39
|
+
}, this._options);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Size of the collection, alias `size`
|
|
44
|
+
* @type {number}
|
|
45
|
+
* @readonly
|
|
46
|
+
*/
|
|
47
|
+
get length() {
|
|
48
|
+
return this._states.length;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Size of the collection, , alias `length`
|
|
53
|
+
* @type {number}
|
|
54
|
+
* @readonly
|
|
55
|
+
*/
|
|
56
|
+
get size() {
|
|
57
|
+
return this._states.length;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Name of the schema from which the collection has been created.
|
|
62
|
+
* @type {String}
|
|
63
|
+
* @readonly
|
|
64
|
+
*/
|
|
65
|
+
get schemaName() {
|
|
66
|
+
return this._schemaName;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Definition of schema from which the collection has been created.
|
|
71
|
+
*
|
|
72
|
+
* @param {string} [name=null] - If given, returns only the definition
|
|
73
|
+
* corresponding to the given param name.
|
|
74
|
+
* @throws Throws if `name` does not correspond to an existing field
|
|
75
|
+
* of the schema.
|
|
76
|
+
* @return {object}
|
|
77
|
+
* @example
|
|
78
|
+
* const schema = collection.getSchema();
|
|
79
|
+
*/
|
|
80
|
+
getSchema(name = null) {
|
|
81
|
+
if (name) {
|
|
82
|
+
return this._schema[name];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return this._schema;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Get the default values as declared in the schema.
|
|
90
|
+
*
|
|
91
|
+
* @return {object}
|
|
92
|
+
* @example
|
|
93
|
+
* const defaults = state.getDefaults();
|
|
94
|
+
*/
|
|
95
|
+
getDefaults() {
|
|
96
|
+
const defaults = {};
|
|
97
|
+
for (let name in this._schema) {
|
|
98
|
+
defaults[name] = this._schema[name].default;
|
|
99
|
+
}
|
|
100
|
+
return defaults;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Return the current values of all the states in the collection.
|
|
105
|
+
* @return {Object[]}
|
|
106
|
+
*/
|
|
107
|
+
getValues() {
|
|
108
|
+
return this._states.map(state => state.getValues());
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Return the current param value of all the states in the collection.
|
|
113
|
+
*
|
|
114
|
+
* @param {String} name - Name of the parameter
|
|
115
|
+
* @return {any[]}
|
|
116
|
+
*/
|
|
117
|
+
get(name) {
|
|
118
|
+
return this._states.map(state => state.get(name));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Update all states of the collection with given values.
|
|
123
|
+
* @param {object} updates - key / value pairs of updates to apply to the state.
|
|
124
|
+
* @param {mixed} [context=null] - optionnal contextual object that will be propagated
|
|
125
|
+
* alongside the updates of the state. The context is valid only for the
|
|
126
|
+
* current call and will be passed as third argument to all update listeners.
|
|
127
|
+
*/
|
|
128
|
+
async set(updates, context = null) {
|
|
129
|
+
// hot fix for https://github.com/collective-soundworks/soundworks/issues/85
|
|
130
|
+
// to be cleaned soon
|
|
131
|
+
const promises = this._states.map(state => state.set(updates, context));
|
|
132
|
+
return Promise.all(promises);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Subscribe to any state update of the collection.
|
|
137
|
+
*
|
|
138
|
+
* @param {server.SharedStateCollection~onUpdateCallback|client.SharedStateCollection~onUpdateCallback}
|
|
139
|
+
* callback - Callback to execute when an update is applied on a state.
|
|
140
|
+
* @param {Boolean} [executeListener=false] - Execute the callback immediately
|
|
141
|
+
* for all underlying states with current state values. (`oldValues` will be
|
|
142
|
+
* set to `{}`, and `context` to `null`)
|
|
143
|
+
* @returns {Function} - Function that delete the registered listener.
|
|
144
|
+
*/
|
|
145
|
+
onUpdate(callback, executeListener = false) {
|
|
146
|
+
this._onUpdateCallbacks.add(callback);
|
|
147
|
+
|
|
148
|
+
if (executeListener === true) {
|
|
149
|
+
this._states.forEach(state => {
|
|
150
|
+
const currentValues = state.getValues();
|
|
151
|
+
const oldValues = {};
|
|
152
|
+
const context = null;
|
|
153
|
+
|
|
154
|
+
callback(state, currentValues, oldValues, context);
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return () => this._onUpdateCallbacks.delete(callback);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Register a function to execute when a state is added to the collection.
|
|
163
|
+
*
|
|
164
|
+
* @param {Function} callback - callback to execute when a state is added to
|
|
165
|
+
* the collection.
|
|
166
|
+
* @param {Function} executeListener - execute the callback with the states
|
|
167
|
+
* already present in the collection.
|
|
168
|
+
* @returns {Function} - Function that delete the registered listener.
|
|
169
|
+
*/
|
|
170
|
+
onAttach(callback, executeListener = false) {
|
|
171
|
+
if (executeListener === true) {
|
|
172
|
+
this._states.forEach(state => callback(state));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
this._onAttachCallbacks.add(callback);
|
|
176
|
+
|
|
177
|
+
return () => this._onAttachCallbacks.delete(callback);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Register a function to execute when a state is removed from the collection.
|
|
182
|
+
*
|
|
183
|
+
* @param {Function} callback - callback to execute when a state is removed
|
|
184
|
+
* from the collection.
|
|
185
|
+
* @returns {Function} - Function that delete the registered listener.
|
|
186
|
+
*/
|
|
187
|
+
onDetach(callback) {
|
|
188
|
+
this._onDetachCallbacks.add(callback);
|
|
189
|
+
|
|
190
|
+
return () => this._onDetachCallbacks.delete(callback);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Detach from the collection, i.e. detach all underlying shared states.
|
|
195
|
+
* @type {number}
|
|
196
|
+
*/
|
|
197
|
+
async detach() {
|
|
198
|
+
this._unobserve();
|
|
199
|
+
this._onUpdateCallbacks.clear();
|
|
200
|
+
|
|
201
|
+
const promises = this._states.map(state => state.detach());
|
|
202
|
+
await Promise.all(promises);
|
|
203
|
+
|
|
204
|
+
this._onDetachCallbacks.clear();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Execute the given function once for each states of the collection (see `Array.forEach`).
|
|
209
|
+
*
|
|
210
|
+
* @param {Function} func - A function to execute for each element in the array.
|
|
211
|
+
* Its return value is discarded.
|
|
212
|
+
*/
|
|
213
|
+
forEach(func) {
|
|
214
|
+
this._states.forEach(func);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Creates a new array populated with the results of calling a provided function
|
|
219
|
+
* on every state of the collection (see `Array.map`).
|
|
220
|
+
*
|
|
221
|
+
* @param {Function} func - A function to execute for each element in the array.
|
|
222
|
+
* Its return value is added as a single element in the new array.
|
|
223
|
+
*/
|
|
224
|
+
map(func) {
|
|
225
|
+
return this._states.map(func);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Creates a shallow copy of a portion of the collection, filtered down to just
|
|
230
|
+
* the estates that pass the test implemented by the provided function (see `Array.filter`).
|
|
231
|
+
*
|
|
232
|
+
* @param {Function} func - A function to execute for each element in the array.
|
|
233
|
+
* It should return a truthy to keep the element in the resulting array, and a f
|
|
234
|
+
* alsy value otherwise.
|
|
235
|
+
*/
|
|
236
|
+
filter(func) {
|
|
237
|
+
return this._states.filter(func);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Sort the elements of the collection in place (see `Array.sort`).
|
|
242
|
+
*
|
|
243
|
+
* @param {Function} func - Function that defines the sort order.
|
|
244
|
+
*/
|
|
245
|
+
sort(func) {
|
|
246
|
+
this._states.sort(func);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Returns the first element of the collection that satisfies the provided testing
|
|
251
|
+
* function. If no values satisfy the testing function, undefined is returned.
|
|
252
|
+
*
|
|
253
|
+
* @param {Function} func - Function to execute for each element in the array.
|
|
254
|
+
* It should return a truthy value to indicate a matching element has been found.
|
|
255
|
+
* @return {}
|
|
256
|
+
*/
|
|
257
|
+
find(func) {
|
|
258
|
+
return this._states.find(func);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Iterable API, e.g. for use in `for .. of` loops
|
|
263
|
+
*/
|
|
264
|
+
[Symbol.iterator]() {
|
|
265
|
+
let index = 0;
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
next: () => {
|
|
269
|
+
if (index >= this._states.length) {
|
|
270
|
+
return { value: undefined, done: true };
|
|
271
|
+
} else {
|
|
272
|
+
return { value: this._states[index++], done: false };
|
|
273
|
+
}
|
|
274
|
+
},
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export default BaseSharedStateCollection;
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
+
import { isString, isFunction, isPlainObject } from '@ircam/sc-utils';
|
|
1
2
|
import SharedState from './BaseSharedState.js';
|
|
3
|
+
import SharedStateCollection from './BaseSharedStateCollection.js';
|
|
4
|
+
import BatchedTransport from './BatchedTransport.js';
|
|
5
|
+
import ParameterBag from './ParameterBag.js';
|
|
6
|
+
import PromiseStore from './PromiseStore.js';
|
|
2
7
|
import {
|
|
3
8
|
CREATE_REQUEST,
|
|
4
9
|
CREATE_RESPONSE,
|
|
@@ -8,15 +13,14 @@ import {
|
|
|
8
13
|
ATTACH_ERROR,
|
|
9
14
|
OBSERVE_REQUEST,
|
|
10
15
|
OBSERVE_RESPONSE,
|
|
16
|
+
OBSERVE_ERROR,
|
|
11
17
|
OBSERVE_NOTIFICATION,
|
|
12
18
|
UNOBSERVE_NOTIFICATION,
|
|
13
19
|
DELETE_SCHEMA,
|
|
20
|
+
GET_SCHEMA_REQUEST,
|
|
21
|
+
GET_SCHEMA_RESPONSE,
|
|
22
|
+
GET_SCHEMA_ERROR,
|
|
14
23
|
} from './constants.js';
|
|
15
|
-
import {
|
|
16
|
-
storeRequestPromise,
|
|
17
|
-
resolveRequest,
|
|
18
|
-
rejectRequest,
|
|
19
|
-
} from './promise-store.js';
|
|
20
24
|
|
|
21
25
|
/**
|
|
22
26
|
* @private
|
|
@@ -28,12 +32,16 @@ class BaseStateManager {
|
|
|
28
32
|
* Must implement a basic EventEmitter API.
|
|
29
33
|
*/
|
|
30
34
|
constructor(id, transport) {
|
|
31
|
-
|
|
35
|
+
// proxy transport with BatchedTransport;
|
|
36
|
+
this.client = { id, transport: new BatchedTransport(transport) };
|
|
37
|
+
|
|
38
|
+
this._statesById = new Map(); // <id, state>
|
|
39
|
+
this._cachedSchemas = new Map(); // <shemaName, definition>
|
|
40
|
+
|
|
41
|
+
this._observeListeners = new Set(); // Set <[observedSchemaName, callback, options]>
|
|
42
|
+
this._observeRequestCallbacks = new Map(); // Map <reqId, [observedSchemaName, callback, options]>
|
|
32
43
|
|
|
33
|
-
this.
|
|
34
|
-
this._observeListeners = new Set();
|
|
35
|
-
this._cachedSchemas = new Map();
|
|
36
|
-
this._observeRequestCallbacks = new Map();
|
|
44
|
+
this._promiseStore = new PromiseStore();
|
|
37
45
|
|
|
38
46
|
// ---------------------------------------------
|
|
39
47
|
// CREATE
|
|
@@ -51,11 +59,11 @@ class BaseStateManager {
|
|
|
51
59
|
const state = new SharedState(stateId, remoteId, schemaName, schema, this.client, true, this, initValues);
|
|
52
60
|
this._statesById.set(state.id, state);
|
|
53
61
|
|
|
54
|
-
|
|
62
|
+
this._promiseStore.resolve(reqId, state);
|
|
55
63
|
});
|
|
56
64
|
|
|
57
65
|
this.client.transport.addListener(CREATE_ERROR, (reqId, msg) => {
|
|
58
|
-
|
|
66
|
+
this._promiseStore.reject(reqId, msg);
|
|
59
67
|
});
|
|
60
68
|
|
|
61
69
|
// ---------------------------------------------
|
|
@@ -75,11 +83,11 @@ class BaseStateManager {
|
|
|
75
83
|
const state = new SharedState(stateId, remoteId, schemaName, schema, this.client, false, this, currentValues);
|
|
76
84
|
this._statesById.set(state.id, state);
|
|
77
85
|
|
|
78
|
-
|
|
86
|
+
this._promiseStore.resolve(reqId, state);
|
|
79
87
|
});
|
|
80
88
|
|
|
81
89
|
this.client.transport.addListener(ATTACH_ERROR, (reqId, msg) => {
|
|
82
|
-
|
|
90
|
+
this._promiseStore.reject(reqId, msg);
|
|
83
91
|
});
|
|
84
92
|
|
|
85
93
|
// ---------------------------------------------
|
|
@@ -88,28 +96,52 @@ class BaseStateManager {
|
|
|
88
96
|
this.client.transport.addListener(OBSERVE_RESPONSE, async (reqId, ...list) => {
|
|
89
97
|
// retrieve the callback that have been stored in observe to make sure
|
|
90
98
|
// we don't call another callback that may have been registered earlier.
|
|
91
|
-
const
|
|
99
|
+
const observeInfos = this._observeRequestCallbacks.get(reqId);
|
|
100
|
+
const [observedSchemaName, callback, options] = observeInfos;
|
|
101
|
+
|
|
102
|
+
// move observeInfos from `_observeRequestCallbacks` to `_observeListeners`
|
|
103
|
+
// to guarantee order of execution, @see not in `.observe`
|
|
92
104
|
this._observeRequestCallbacks.delete(reqId);
|
|
105
|
+
this._observeListeners.add(observeInfos);
|
|
93
106
|
|
|
94
107
|
const promises = list.map(([schemaName, stateId, nodeId]) => {
|
|
95
|
-
|
|
108
|
+
const filter = this._filterObserve(observedSchemaName, schemaName, nodeId, options);
|
|
109
|
+
|
|
110
|
+
if (!filter) {
|
|
111
|
+
return callback(schemaName, stateId, nodeId);
|
|
112
|
+
} else {
|
|
113
|
+
return Promise.resolve();
|
|
114
|
+
}
|
|
96
115
|
});
|
|
97
116
|
|
|
98
117
|
await Promise.all(promises);
|
|
99
118
|
|
|
100
119
|
const unsubscribe = () => {
|
|
101
|
-
this._observeListeners.delete(
|
|
120
|
+
this._observeListeners.delete(observeInfos);
|
|
102
121
|
|
|
122
|
+
// no more listeners, we can stop receiving notifications from the server
|
|
103
123
|
if (this._observeListeners.size === 0) {
|
|
104
124
|
this.client.transport.emit(UNOBSERVE_NOTIFICATION);
|
|
105
125
|
}
|
|
106
126
|
};
|
|
107
|
-
|
|
127
|
+
|
|
128
|
+
this._promiseStore.resolve(reqId, unsubscribe);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Observe error can occur if observed schema name does not exists
|
|
132
|
+
this.client.transport.addListener(OBSERVE_ERROR, (reqId, msg) => {
|
|
133
|
+
this._observeRequestCallbacks.delete(reqId);
|
|
134
|
+
this._promiseStore.reject(reqId, msg);
|
|
108
135
|
});
|
|
109
136
|
|
|
110
|
-
this.client.transport.addListener(OBSERVE_NOTIFICATION, (
|
|
111
|
-
|
|
112
|
-
|
|
137
|
+
this.client.transport.addListener(OBSERVE_NOTIFICATION, (schemaName, stateId, nodeId) => {
|
|
138
|
+
this._observeListeners.forEach(observeInfos => {
|
|
139
|
+
const [observedSchemaName, callback, options] = observeInfos;
|
|
140
|
+
const filter = this._filterObserve(observedSchemaName, schemaName, nodeId, options);
|
|
141
|
+
|
|
142
|
+
if (!filter) {
|
|
143
|
+
callback(schemaName, stateId, nodeId);
|
|
144
|
+
}
|
|
113
145
|
});
|
|
114
146
|
});
|
|
115
147
|
|
|
@@ -119,6 +151,59 @@ class BaseStateManager {
|
|
|
119
151
|
this.client.transport.addListener(DELETE_SCHEMA, schemaName => {
|
|
120
152
|
this._cachedSchemas.delete(schemaName);
|
|
121
153
|
});
|
|
154
|
+
|
|
155
|
+
// ---------------------------------------------
|
|
156
|
+
// GET SCHEMA
|
|
157
|
+
// ---------------------------------------------
|
|
158
|
+
this.client.transport.addListener(GET_SCHEMA_RESPONSE, (reqId, schemaName, schema) => {
|
|
159
|
+
if (!this._cachedSchemas.has(schemaName)) {
|
|
160
|
+
this._cachedSchemas.set(schemaName, schema);
|
|
161
|
+
}
|
|
162
|
+
// return a populated schema
|
|
163
|
+
const parameterBag = new ParameterBag(schema);
|
|
164
|
+
this._promiseStore.resolve(reqId, parameterBag.getSchema());
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
this.client.transport.addListener(GET_SCHEMA_ERROR, (reqId, msg) => {
|
|
168
|
+
this._promiseStore.reject(reqId, msg);
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** @private */
|
|
173
|
+
_filterObserve(observedSchemaName, schemaName, creatorId, options) {
|
|
174
|
+
let filter = true;
|
|
175
|
+
// schema name filter filer
|
|
176
|
+
if (observedSchemaName === null || observedSchemaName === schemaName) {
|
|
177
|
+
filter = false;
|
|
178
|
+
}
|
|
179
|
+
// filter state created by client if excludeLocal is true
|
|
180
|
+
if (options.excludeLocal === true && creatorId === this.client.id) {
|
|
181
|
+
filter = true;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return filter;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Return the schema from a given registered schema name
|
|
189
|
+
*
|
|
190
|
+
* @param {String} schemaName - Name of the schema as given on registration
|
|
191
|
+
* (cf. ServerStateManager)
|
|
192
|
+
* @example
|
|
193
|
+
* const schema = await client.stateManager.getSchema('my-schema');
|
|
194
|
+
*/
|
|
195
|
+
async getSchema(schemaName) {
|
|
196
|
+
if (this._cachedSchemas.has(schemaName)) {
|
|
197
|
+
const schema = this._cachedSchemas.get(schemaName);
|
|
198
|
+
// return a populated schema
|
|
199
|
+
const parameterBag = new ParameterBag(schema);
|
|
200
|
+
return parameterBag.getSchema();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return new Promise((resolve, reject) => {
|
|
204
|
+
const reqId = this._promiseStore.add(resolve, reject, 'get-schema');
|
|
205
|
+
this.client.transport.emit(GET_SCHEMA_REQUEST, reqId, schemaName);
|
|
206
|
+
});
|
|
122
207
|
}
|
|
123
208
|
|
|
124
209
|
/**
|
|
@@ -135,7 +220,7 @@ class BaseStateManager {
|
|
|
135
220
|
*/
|
|
136
221
|
async create(schemaName, initValues = {}) {
|
|
137
222
|
return new Promise((resolve, reject) => {
|
|
138
|
-
const reqId =
|
|
223
|
+
const reqId = this._promiseStore.add(resolve, reject, 'create-request');
|
|
139
224
|
const requireSchema = this._cachedSchemas.has(schemaName) ? false : true;
|
|
140
225
|
this.client.transport.emit(CREATE_REQUEST, reqId, schemaName, requireSchema, initValues);
|
|
141
226
|
});
|
|
@@ -157,8 +242,7 @@ class BaseStateManager {
|
|
|
157
242
|
*/
|
|
158
243
|
async attach(schemaName, stateId = null) {
|
|
159
244
|
return new Promise((resolve, reject) => {
|
|
160
|
-
|
|
161
|
-
const reqId = storeRequestPromise(resolve, reject);
|
|
245
|
+
const reqId = this._promiseStore.add(resolve, reject, 'attach-request');
|
|
162
246
|
const requireSchema = this._cachedSchemas.has(schemaName) ? false : true;
|
|
163
247
|
this.client.transport.emit(ATTACH_REQUEST, reqId, schemaName, stateId, requireSchema);
|
|
164
248
|
});
|
|
@@ -170,30 +254,167 @@ class BaseStateManager {
|
|
|
170
254
|
* the state of all other clients of the application, to monitor them and/or take
|
|
171
255
|
* control over them from a single point.
|
|
172
256
|
*
|
|
173
|
-
*
|
|
257
|
+
* Notes:
|
|
258
|
+
* - The order of execution is not guaranted between nodes, i.e. an state attached
|
|
259
|
+
* in the `observe` callback could be created before the `async create` method resolves.
|
|
260
|
+
* - Filtering, i.e. `observedSchemaName` and `options.excludeLocal` are handled
|
|
261
|
+
* on the node side, the server just notify all state creation activity and
|
|
262
|
+
* the node executes the given callbacks according to the different filter rules.
|
|
263
|
+
* Such strategy allows to share the observe notifications between all observers.
|
|
264
|
+
*
|
|
265
|
+
* Alternative signatures:
|
|
266
|
+
* - .observe(callback)
|
|
267
|
+
* - .observe(schemaName, callback)
|
|
268
|
+
* - .observe(callback, options)
|
|
269
|
+
* - .observe(schemaName, callback, options)
|
|
270
|
+
*
|
|
271
|
+
* @param {string} [schemaName] - optionnal schema name to filter the observed
|
|
272
|
+
* states.
|
|
174
273
|
* @param {server.StateManager~ObserveCallback|client.StateManager~ObserveCallback}
|
|
175
274
|
* callback - Function to be called when a new state is created on the network.
|
|
176
|
-
* @
|
|
177
|
-
*
|
|
178
|
-
*
|
|
275
|
+
* @param {object} options - Options.
|
|
276
|
+
* @param {boolean} [options.excludeLocal = false] - If set to true, exclude states
|
|
277
|
+
* created locallly, i.e. by the same node, from the collection.
|
|
278
|
+
* @returns {Promise<Function>} - Returns a Promise that resolves when the given
|
|
279
|
+
* callback as been executed on each existing states. The promise value is a
|
|
280
|
+
* function which allows to stop observing the states on the network.
|
|
179
281
|
* @example
|
|
180
|
-
* client.stateManager.observe(async (schemaName, stateId
|
|
282
|
+
* client.stateManager.observe(async (schemaName, stateId) => {
|
|
181
283
|
* if (schemaName === 'something') {
|
|
182
284
|
* const state = await this.client.stateManager.attach(schemaName, stateId);
|
|
183
285
|
* console.log(state.getValues());
|
|
184
286
|
* }
|
|
185
287
|
* });
|
|
186
288
|
*/
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
289
|
+
// note: all filtering is done only on client-side as it is really more simple to
|
|
290
|
+
// handle this way and the network overhead is very low for observe notifications:
|
|
291
|
+
// i.e. schemaName, stateId, nodeId
|
|
292
|
+
async observe(...args) {
|
|
293
|
+
|
|
294
|
+
const defaultOptions = {
|
|
295
|
+
excludeLocal: false,
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
let observedSchemaName;
|
|
299
|
+
let callback;
|
|
300
|
+
let options;
|
|
301
|
+
|
|
302
|
+
switch (args.length) {
|
|
303
|
+
case 1: {
|
|
304
|
+
// variation: .observe(callback)
|
|
305
|
+
if (!isFunction(args[0])) {
|
|
306
|
+
throw new TypeError(`[stateManager] Invalid arguments, argument 1 should be a function"`);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
observedSchemaName = null;
|
|
310
|
+
callback = args[0];
|
|
311
|
+
options = defaultOptions;
|
|
312
|
+
break;
|
|
313
|
+
}
|
|
314
|
+
case 2: {
|
|
315
|
+
// variation: .observe(schemaName, callback)
|
|
316
|
+
if (isString(args[0])) {
|
|
317
|
+
if (!isFunction(args[1])) {
|
|
318
|
+
throw new TypeError(`[stateManager] Invalid arguments, argument 2 should be a function"`);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
observedSchemaName = args[0];
|
|
322
|
+
callback = args[1];
|
|
323
|
+
options = defaultOptions;
|
|
324
|
+
|
|
325
|
+
// variation: .observe(callback, options) API
|
|
326
|
+
} else if (isFunction(args[0])) {
|
|
327
|
+
if (!isPlainObject(args[1])) {
|
|
328
|
+
throw new TypeError(`[stateManager] Invalid arguments, argument 2 should be an object"`);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
observedSchemaName = null;
|
|
332
|
+
callback = args[0];
|
|
333
|
+
options = Object.assign(defaultOptions, args[1]);
|
|
334
|
+
|
|
335
|
+
} else {
|
|
336
|
+
throw new Error(`[stateManager] Invalid signature, refer to the StateManager.observe documentation"`);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
break;
|
|
340
|
+
}
|
|
341
|
+
case 3: {
|
|
342
|
+
// variation: .observe(schemaName, callback, options)
|
|
343
|
+
if (!isString(args[0])) {
|
|
344
|
+
throw new TypeError(`[stateManager] Invalid arguments, argument 1 should be a string"`);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (!isFunction(args[1])) {
|
|
348
|
+
throw new TypeError(`[stateManager] Invalid arguments, argument 2 should be a function"`);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (!isPlainObject(args[2])) {
|
|
352
|
+
throw new TypeError(`[stateManager] Invalid arguments, argument 2 should be an object"`);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
observedSchemaName = args[0];
|
|
356
|
+
callback = args[1];
|
|
357
|
+
options = Object.assign(defaultOptions, args[2]);
|
|
358
|
+
|
|
359
|
+
break;
|
|
360
|
+
}
|
|
361
|
+
// throw in all other cases
|
|
362
|
+
default: {
|
|
363
|
+
throw new Error(`[stateManager] Invalid signature, refer to the StateManager.observe documentation"`);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// resend request to get updated list of states
|
|
190
368
|
return new Promise((resolve, reject) => {
|
|
191
|
-
const reqId =
|
|
192
|
-
// store the callback
|
|
193
|
-
|
|
194
|
-
|
|
369
|
+
const reqId = this._promiseStore.add(resolve, reject, 'observe-request');
|
|
370
|
+
// store the callback for execution on the response. the returned Promise
|
|
371
|
+
// is fullfiled once callback has been executed with each existing states
|
|
372
|
+
const observeInfos = [observedSchemaName, callback, options];
|
|
373
|
+
this._observeRequestCallbacks.set(reqId, observeInfos);
|
|
374
|
+
|
|
375
|
+
// NOTE: do not store in `_observeListeners` yet as it can produce race
|
|
376
|
+
// conditions, e.g.:
|
|
377
|
+
// ```
|
|
378
|
+
// await client.stateManager.observe(async (schemaName, stateId, nodeId) => {});
|
|
379
|
+
// // client now receives OBSERVE_NOTIFICATIONS
|
|
380
|
+
// await otherClient.stateManager.create('a');
|
|
381
|
+
// // second observer added in between
|
|
382
|
+
// client.stateManager.observe(async (schemaName, stateId, nodeId) => {});
|
|
383
|
+
// ````
|
|
384
|
+
// OBSERVE_NOTIFICATION is received before the OBSERVE_RESPONSE, then the
|
|
385
|
+
// second observer is called twice:
|
|
386
|
+
// - OBSERVE_RESPONSE 1 []
|
|
387
|
+
// - OBSERVE_NOTIFICATION [ 'a', 1, 0 ]
|
|
388
|
+
// - OBSERVE_NOTIFICATION [ 'a', 1, 0 ] // this should not happen
|
|
389
|
+
// - OBSERVE_RESPONSE 1 [ [ 'a', 1, 0 ] ]
|
|
390
|
+
//
|
|
391
|
+
// cf. unit test `observe should properly behave in race condition`
|
|
392
|
+
|
|
393
|
+
this.client.transport.emit(OBSERVE_REQUEST, reqId, observedSchemaName);
|
|
195
394
|
});
|
|
196
395
|
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Returns a collection of all the states created from the schema name.
|
|
399
|
+
*
|
|
400
|
+
* @param {string} schemaName - Name of the schema.
|
|
401
|
+
* @param {object} options - Options.
|
|
402
|
+
* @param {boolean} [options.excludeLocal = false] - If set to true, exclude states
|
|
403
|
+
* created locallly, i.e. by the same node, from the collection.
|
|
404
|
+
* @returns {server.SharedStateCollection|client.SharedStateCollection}
|
|
405
|
+
*/
|
|
406
|
+
async getCollection(schemaName, options) {
|
|
407
|
+
const collection = new SharedStateCollection(this, schemaName, options);
|
|
408
|
+
|
|
409
|
+
try {
|
|
410
|
+
await collection._init();
|
|
411
|
+
} catch (err) {
|
|
412
|
+
console.log(err.message);
|
|
413
|
+
throw new Error(`[stateManager] Cannot create collection, schema "${schemaName}" does not exists`);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return collection;
|
|
417
|
+
}
|
|
197
418
|
}
|
|
198
419
|
|
|
199
420
|
export default BaseStateManager;
|