@soundworks/core 4.0.0-alpha.2 → 4.0.0-alpha.21
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 +33 -0
- package/README.md +17 -51
- package/package.json +24 -31
- package/src/client/Client.js +13 -7
- 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 +80 -44
- package/src/common/BasePlugin.js +31 -21
- package/src/common/BasePluginManager.js +34 -19
- package/src/common/BaseSharedState.js +304 -79
- package/src/common/BaseSharedStateCollection.js +324 -0
- package/src/common/BaseStateManager.js +353 -49
- package/src/common/BatchedTransport.js +73 -0
- package/src/common/ParameterBag.js +92 -30
- package/src/common/PromiseStore.js +53 -0
- package/src/common/SharedStatePrivate.js +87 -46
- 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 +263 -43
- package/src/server/SharedState.js +0 -1
- package/src/server/SharedStateCollection.js +39 -0
- package/src/server/Socket.js +55 -49
- package/src/server/Sockets.js +33 -11
- package/src/server/StateManager.js +187 -100
- 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
|
@@ -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,38 +13,43 @@ 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
|
-
|
|
16
|
-
|
|
17
|
-
resolveRequest,
|
|
18
|
-
rejectRequest,
|
|
19
|
-
} from './promise-store.js';
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* @private
|
|
23
|
-
*/
|
|
24
|
+
|
|
25
|
+
/** @private */
|
|
24
26
|
class BaseStateManager {
|
|
27
|
+
constructor() {
|
|
28
|
+
this._statesById = new Map(); // <id, state>
|
|
29
|
+
this._cachedSchemas = new Map(); // <shemaName, definition>
|
|
30
|
+
|
|
31
|
+
this._observeListeners = new Set(); // Set <[observedSchemaName, callback, options]>
|
|
32
|
+
this._observeRequestCallbacks = new Map(); // Map <reqId, [observedSchemaName, callback, options]>
|
|
33
|
+
|
|
34
|
+
this._promiseStore = new PromiseStore();
|
|
35
|
+
}
|
|
36
|
+
|
|
25
37
|
/**
|
|
38
|
+
* Executed on `client.init`
|
|
26
39
|
* @param {Number} id - Id of the node.
|
|
27
|
-
* @param {Object} transport - Transport to use for synchronizing the
|
|
40
|
+
* @param {Object} transport - Transport to use for synchronizing the states.
|
|
28
41
|
* Must implement a basic EventEmitter API.
|
|
29
42
|
*/
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
this.
|
|
34
|
-
this._observeListeners = new Set();
|
|
35
|
-
this._cachedSchemas = new Map();
|
|
36
|
-
this._observeRequestCallbacks = new Map();
|
|
43
|
+
/** @private */
|
|
44
|
+
init(id, transport) {
|
|
45
|
+
const batchedTransport = new BatchedTransport(transport);
|
|
46
|
+
this.client = { id, transport: batchedTransport };
|
|
37
47
|
|
|
38
48
|
// ---------------------------------------------
|
|
39
49
|
// CREATE
|
|
40
50
|
// ---------------------------------------------
|
|
41
51
|
|
|
42
|
-
this.client.transport.addListener(CREATE_RESPONSE, (reqId, stateId, remoteId, schemaName, schema, initValues) => {
|
|
52
|
+
this.client.transport.addListener(CREATE_RESPONSE, (reqId, stateId, remoteId, schemaName, schema, initValues, filter) => {
|
|
43
53
|
|
|
44
54
|
// cache schema when first dealing it to save some bandwidth
|
|
45
55
|
if (!this._cachedSchemas.has(schemaName)) {
|
|
@@ -48,20 +58,20 @@ class BaseStateManager {
|
|
|
48
58
|
|
|
49
59
|
schema = this._cachedSchemas.get(schemaName);
|
|
50
60
|
|
|
51
|
-
const state = new SharedState(stateId, remoteId, schemaName, schema, this.client, true, this, initValues);
|
|
61
|
+
const state = new SharedState(stateId, remoteId, schemaName, schema, this.client, true, this, initValues, null);
|
|
52
62
|
this._statesById.set(state.id, state);
|
|
53
63
|
|
|
54
|
-
|
|
64
|
+
this._promiseStore.resolve(reqId, state);
|
|
55
65
|
});
|
|
56
66
|
|
|
57
67
|
this.client.transport.addListener(CREATE_ERROR, (reqId, msg) => {
|
|
58
|
-
|
|
68
|
+
this._promiseStore.reject(reqId, msg);
|
|
59
69
|
});
|
|
60
70
|
|
|
61
71
|
// ---------------------------------------------
|
|
62
72
|
// ATTACH (when creator, is attached by default)
|
|
63
73
|
// ---------------------------------------------
|
|
64
|
-
this.client.transport.addListener(ATTACH_RESPONSE, (reqId, stateId, remoteId, schemaName, schema, currentValues) => {
|
|
74
|
+
this.client.transport.addListener(ATTACH_RESPONSE, (reqId, stateId, remoteId, schemaName, schema, currentValues, filter) => {
|
|
65
75
|
// cache schema when first dealing with it to save some bandwidth
|
|
66
76
|
// note: when we make the schemas dynamic at some point
|
|
67
77
|
// the server should be able to invalidate the schema and send
|
|
@@ -72,14 +82,14 @@ class BaseStateManager {
|
|
|
72
82
|
|
|
73
83
|
schema = this._cachedSchemas.get(schemaName);
|
|
74
84
|
|
|
75
|
-
const state = new SharedState(stateId, remoteId, schemaName, schema, this.client, false, this, currentValues);
|
|
85
|
+
const state = new SharedState(stateId, remoteId, schemaName, schema, this.client, false, this, currentValues, filter);
|
|
76
86
|
this._statesById.set(state.id, state);
|
|
77
87
|
|
|
78
|
-
|
|
88
|
+
this._promiseStore.resolve(reqId, state);
|
|
79
89
|
});
|
|
80
90
|
|
|
81
91
|
this.client.transport.addListener(ATTACH_ERROR, (reqId, msg) => {
|
|
82
|
-
|
|
92
|
+
this._promiseStore.reject(reqId, msg);
|
|
83
93
|
});
|
|
84
94
|
|
|
85
95
|
// ---------------------------------------------
|
|
@@ -88,28 +98,52 @@ class BaseStateManager {
|
|
|
88
98
|
this.client.transport.addListener(OBSERVE_RESPONSE, async (reqId, ...list) => {
|
|
89
99
|
// retrieve the callback that have been stored in observe to make sure
|
|
90
100
|
// we don't call another callback that may have been registered earlier.
|
|
91
|
-
const
|
|
101
|
+
const observeInfos = this._observeRequestCallbacks.get(reqId);
|
|
102
|
+
const [observedSchemaName, callback, options] = observeInfos;
|
|
103
|
+
|
|
104
|
+
// move observeInfos from `_observeRequestCallbacks` to `_observeListeners`
|
|
105
|
+
// to guarantee order of execution, @see not in `.observe`
|
|
92
106
|
this._observeRequestCallbacks.delete(reqId);
|
|
107
|
+
this._observeListeners.add(observeInfos);
|
|
93
108
|
|
|
94
109
|
const promises = list.map(([schemaName, stateId, nodeId]) => {
|
|
95
|
-
|
|
110
|
+
const filter = this._filterObserve(observedSchemaName, schemaName, nodeId, options);
|
|
111
|
+
|
|
112
|
+
if (!filter) {
|
|
113
|
+
return callback(schemaName, stateId, nodeId);
|
|
114
|
+
} else {
|
|
115
|
+
return Promise.resolve();
|
|
116
|
+
}
|
|
96
117
|
});
|
|
97
118
|
|
|
98
119
|
await Promise.all(promises);
|
|
99
120
|
|
|
100
121
|
const unsubscribe = () => {
|
|
101
|
-
this._observeListeners.delete(
|
|
122
|
+
this._observeListeners.delete(observeInfos);
|
|
102
123
|
|
|
124
|
+
// no more listeners, we can stop receiving notifications from the server
|
|
103
125
|
if (this._observeListeners.size === 0) {
|
|
104
126
|
this.client.transport.emit(UNOBSERVE_NOTIFICATION);
|
|
105
127
|
}
|
|
106
128
|
};
|
|
107
|
-
|
|
129
|
+
|
|
130
|
+
this._promiseStore.resolve(reqId, unsubscribe);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// Observe error can occur if observed schema name does not exists
|
|
134
|
+
this.client.transport.addListener(OBSERVE_ERROR, (reqId, msg) => {
|
|
135
|
+
this._observeRequestCallbacks.delete(reqId);
|
|
136
|
+
this._promiseStore.reject(reqId, msg);
|
|
108
137
|
});
|
|
109
138
|
|
|
110
|
-
this.client.transport.addListener(OBSERVE_NOTIFICATION, (
|
|
111
|
-
|
|
112
|
-
|
|
139
|
+
this.client.transport.addListener(OBSERVE_NOTIFICATION, (schemaName, stateId, nodeId) => {
|
|
140
|
+
this._observeListeners.forEach(observeInfos => {
|
|
141
|
+
const [observedSchemaName, callback, options] = observeInfos;
|
|
142
|
+
const filter = this._filterObserve(observedSchemaName, schemaName, nodeId, options);
|
|
143
|
+
|
|
144
|
+
if (!filter) {
|
|
145
|
+
callback(schemaName, stateId, nodeId);
|
|
146
|
+
}
|
|
113
147
|
});
|
|
114
148
|
});
|
|
115
149
|
|
|
@@ -119,6 +153,59 @@ class BaseStateManager {
|
|
|
119
153
|
this.client.transport.addListener(DELETE_SCHEMA, schemaName => {
|
|
120
154
|
this._cachedSchemas.delete(schemaName);
|
|
121
155
|
});
|
|
156
|
+
|
|
157
|
+
// ---------------------------------------------
|
|
158
|
+
// GET SCHEMA
|
|
159
|
+
// ---------------------------------------------
|
|
160
|
+
this.client.transport.addListener(GET_SCHEMA_RESPONSE, (reqId, schemaName, schema) => {
|
|
161
|
+
if (!this._cachedSchemas.has(schemaName)) {
|
|
162
|
+
this._cachedSchemas.set(schemaName, schema);
|
|
163
|
+
}
|
|
164
|
+
// return a populated schema
|
|
165
|
+
const parameterBag = new ParameterBag(schema);
|
|
166
|
+
this._promiseStore.resolve(reqId, parameterBag.getSchema());
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
this.client.transport.addListener(GET_SCHEMA_ERROR, (reqId, msg) => {
|
|
170
|
+
this._promiseStore.reject(reqId, msg);
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** @private */
|
|
175
|
+
_filterObserve(observedSchemaName, schemaName, creatorId, options) {
|
|
176
|
+
let filter = true;
|
|
177
|
+
// schema name filter filer
|
|
178
|
+
if (observedSchemaName === null || observedSchemaName === schemaName) {
|
|
179
|
+
filter = false;
|
|
180
|
+
}
|
|
181
|
+
// filter state created by client if excludeLocal is true
|
|
182
|
+
if (options.excludeLocal === true && creatorId === this.client.id) {
|
|
183
|
+
filter = true;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return filter;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Return the schema from a given registered schema name
|
|
191
|
+
*
|
|
192
|
+
* @param {String} schemaName - Name of the schema as given on registration
|
|
193
|
+
* (cf. ServerStateManager)
|
|
194
|
+
* @example
|
|
195
|
+
* const schema = await client.stateManager.getSchema('my-schema');
|
|
196
|
+
*/
|
|
197
|
+
async getSchema(schemaName) {
|
|
198
|
+
if (this._cachedSchemas.has(schemaName)) {
|
|
199
|
+
const schema = this._cachedSchemas.get(schemaName);
|
|
200
|
+
// return a populated schema
|
|
201
|
+
const parameterBag = new ParameterBag(schema);
|
|
202
|
+
return parameterBag.getSchema();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return new Promise((resolve, reject) => {
|
|
206
|
+
const reqId = this._promiseStore.add(resolve, reject, 'get-schema');
|
|
207
|
+
this.client.transport.emit(GET_SCHEMA_REQUEST, reqId, schemaName);
|
|
208
|
+
});
|
|
122
209
|
}
|
|
123
210
|
|
|
124
211
|
/**
|
|
@@ -135,7 +222,7 @@ class BaseStateManager {
|
|
|
135
222
|
*/
|
|
136
223
|
async create(schemaName, initValues = {}) {
|
|
137
224
|
return new Promise((resolve, reject) => {
|
|
138
|
-
const reqId =
|
|
225
|
+
const reqId = this._promiseStore.add(resolve, reject, 'create-request');
|
|
139
226
|
const requireSchema = this._cachedSchemas.has(schemaName) ? false : true;
|
|
140
227
|
this.client.transport.emit(CREATE_REQUEST, reqId, schemaName, requireSchema, initValues);
|
|
141
228
|
});
|
|
@@ -144,23 +231,64 @@ class BaseStateManager {
|
|
|
144
231
|
/**
|
|
145
232
|
* Attach to an existing `SharedState` instance.
|
|
146
233
|
*
|
|
234
|
+
* Alternative signatures:
|
|
235
|
+
* - `stateManager.attach(schemaName)`
|
|
236
|
+
* - `stateManager.attach(schemaName, stateId)`
|
|
237
|
+
* - `stateManager.attach(schemaName, filter)`
|
|
238
|
+
* - `stateManager.attach(schemaName, stateId, filter)`
|
|
239
|
+
*
|
|
147
240
|
* @param {String} schemaName - Name of the schema as given on registration
|
|
148
241
|
* (cf. ServerStateManager)
|
|
149
242
|
* @param {Object} [stateId=null] - Id of the state to attach to. If `null`,
|
|
150
243
|
* attach to the first state found with the given schema name (usefull for
|
|
151
244
|
* globally shared states owned by the server).
|
|
245
|
+
* @param {array|null} [filter=null] - Filter parameters of interest in the
|
|
246
|
+
* returned state. If set to `null`, no filter applied.
|
|
152
247
|
* @returns {client.SharedState|server.SharedState}
|
|
153
248
|
* @see {@link client.SharedState}
|
|
154
249
|
* @see {@link server.SharedState}
|
|
250
|
+
*
|
|
155
251
|
* @example
|
|
156
252
|
* const state = await client.stateManager.attach('my-schema');
|
|
157
253
|
*/
|
|
158
|
-
async attach(schemaName,
|
|
254
|
+
async attach(schemaName, stateIdOrFilter = null, filter = null) {
|
|
255
|
+
let stateId = null;
|
|
256
|
+
|
|
257
|
+
if (!isString(schemaName)) {
|
|
258
|
+
throw new TypeError(`Cannot execute 'attach' on 'StateManager': argument 1 should be either a number or an array`);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (arguments.length === 2) {
|
|
262
|
+
if (stateIdOrFilter === null) {
|
|
263
|
+
stateId = null;
|
|
264
|
+
filter = null;
|
|
265
|
+
} else if (Number.isFinite(stateIdOrFilter)) {
|
|
266
|
+
stateId = stateIdOrFilter;
|
|
267
|
+
filter = null;
|
|
268
|
+
} else if (Array.isArray(stateIdOrFilter)) {
|
|
269
|
+
stateId = null;
|
|
270
|
+
filter = stateIdOrFilter;
|
|
271
|
+
} else {
|
|
272
|
+
throw new TypeError(`Cannot execute 'attach' on 'StateManager': argument 2 should be either null, a number or an array`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (arguments.length === 3) {
|
|
277
|
+
stateId = stateIdOrFilter;
|
|
278
|
+
|
|
279
|
+
if (stateId !== null && !Number.isFinite(stateId)) {
|
|
280
|
+
throw new TypeError(`Cannot execute 'attach' on 'StateManager': argument 2 should be either null or a number`);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (filter !== null && !Array.isArray(filter)) {
|
|
284
|
+
throw new TypeError(`Cannot execute 'attach' on 'StateManager': argument 3 should be either null or an array`);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
159
288
|
return new Promise((resolve, reject) => {
|
|
160
|
-
|
|
161
|
-
const reqId = storeRequestPromise(resolve, reject);
|
|
289
|
+
const reqId = this._promiseStore.add(resolve, reject, 'attach-request');
|
|
162
290
|
const requireSchema = this._cachedSchemas.has(schemaName) ? false : true;
|
|
163
|
-
this.client.transport.emit(ATTACH_REQUEST, reqId, schemaName, stateId, requireSchema);
|
|
291
|
+
this.client.transport.emit(ATTACH_REQUEST, reqId, schemaName, stateId, requireSchema, filter);
|
|
164
292
|
});
|
|
165
293
|
}
|
|
166
294
|
|
|
@@ -170,30 +298,206 @@ class BaseStateManager {
|
|
|
170
298
|
* the state of all other clients of the application, to monitor them and/or take
|
|
171
299
|
* control over them from a single point.
|
|
172
300
|
*
|
|
173
|
-
*
|
|
301
|
+
* Notes:
|
|
302
|
+
* - The order of execution is not guaranted between nodes, i.e. an state attached
|
|
303
|
+
* in the `observe` callback could be created before the `async create` method resolves.
|
|
304
|
+
* - Filtering, i.e. `observedSchemaName` and `options.excludeLocal` are handled
|
|
305
|
+
* on the node side, the server just notify all state creation activity and
|
|
306
|
+
* the node executes the given callbacks according to the different filter rules.
|
|
307
|
+
* Such strategy allows to share the observe notifications between all observers.
|
|
308
|
+
*
|
|
309
|
+
* Alternative signatures:
|
|
310
|
+
* - `stateManager.observe(callback)`
|
|
311
|
+
* - `stateManager.observe(schemaName, callback)`
|
|
312
|
+
* - `stateManager.observe(callback, options)`
|
|
313
|
+
* - `stateManager.observe(schemaName, callback, options)`
|
|
314
|
+
*
|
|
315
|
+
* @param {string} [schemaName] - optionnal schema name to filter the observed
|
|
316
|
+
* states.
|
|
174
317
|
* @param {server.StateManager~ObserveCallback|client.StateManager~ObserveCallback}
|
|
175
318
|
* callback - Function to be called when a new state is created on the network.
|
|
176
|
-
* @
|
|
177
|
-
*
|
|
178
|
-
*
|
|
319
|
+
* @param {object} options - Options.
|
|
320
|
+
* @param {boolean} [options.excludeLocal = false] - If set to true, exclude states
|
|
321
|
+
* created locallly, i.e. by the same node, from the collection.
|
|
322
|
+
* @returns {Promise<Function>} - Returns a Promise that resolves when the given
|
|
323
|
+
* callback as been executed on each existing states. The promise value is a
|
|
324
|
+
* function which allows to stop observing the states on the network.
|
|
325
|
+
*
|
|
179
326
|
* @example
|
|
180
|
-
* client.stateManager.observe(async (schemaName, stateId
|
|
327
|
+
* client.stateManager.observe(async (schemaName, stateId) => {
|
|
181
328
|
* if (schemaName === 'something') {
|
|
182
329
|
* const state = await this.client.stateManager.attach(schemaName, stateId);
|
|
183
330
|
* console.log(state.getValues());
|
|
184
331
|
* }
|
|
185
332
|
* });
|
|
186
333
|
*/
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
334
|
+
// note: all filtering is done only on client-side as it is really more simple to
|
|
335
|
+
// handle this way and the network overhead is very low for observe notifications:
|
|
336
|
+
// i.e. schemaName, stateId, nodeId
|
|
337
|
+
async observe(...args) {
|
|
338
|
+
|
|
339
|
+
const defaultOptions = {
|
|
340
|
+
excludeLocal: false,
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
let observedSchemaName;
|
|
344
|
+
let callback;
|
|
345
|
+
let options;
|
|
346
|
+
|
|
347
|
+
switch (args.length) {
|
|
348
|
+
case 1: {
|
|
349
|
+
// variation: .observe(callback)
|
|
350
|
+
if (!isFunction(args[0])) {
|
|
351
|
+
throw new TypeError(`[stateManager] Invalid arguments, argument 1 should be a function"`);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
observedSchemaName = null;
|
|
355
|
+
callback = args[0];
|
|
356
|
+
options = defaultOptions;
|
|
357
|
+
break;
|
|
358
|
+
}
|
|
359
|
+
case 2: {
|
|
360
|
+
// variation: .observe(schemaName, callback)
|
|
361
|
+
if (isString(args[0])) {
|
|
362
|
+
if (!isFunction(args[1])) {
|
|
363
|
+
throw new TypeError(`[stateManager] Invalid arguments, argument 2 should be a function"`);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
observedSchemaName = args[0];
|
|
367
|
+
callback = args[1];
|
|
368
|
+
options = defaultOptions;
|
|
369
|
+
|
|
370
|
+
// variation: .observe(callback, options) API
|
|
371
|
+
} else if (isFunction(args[0])) {
|
|
372
|
+
if (!isPlainObject(args[1])) {
|
|
373
|
+
throw new TypeError(`[stateManager] Invalid arguments, argument 2 should be an object"`);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
observedSchemaName = null;
|
|
377
|
+
callback = args[0];
|
|
378
|
+
options = Object.assign(defaultOptions, args[1]);
|
|
379
|
+
|
|
380
|
+
} else {
|
|
381
|
+
throw new TypeError(`[stateManager] Invalid signature, refer to the StateManager.observe documentation"`);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
break;
|
|
385
|
+
}
|
|
386
|
+
case 3: {
|
|
387
|
+
// variation: .observe(schemaName, callback, options)
|
|
388
|
+
if (!isString(args[0])) {
|
|
389
|
+
throw new TypeError(`[stateManager] Invalid arguments, argument 1 should be a string"`);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (!isFunction(args[1])) {
|
|
393
|
+
throw new TypeError(`[stateManager] Invalid arguments, argument 2 should be a function"`);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (!isPlainObject(args[2])) {
|
|
397
|
+
throw new TypeError(`[stateManager] Invalid arguments, argument 2 should be an object"`);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
observedSchemaName = args[0];
|
|
401
|
+
callback = args[1];
|
|
402
|
+
options = Object.assign(defaultOptions, args[2]);
|
|
403
|
+
|
|
404
|
+
break;
|
|
405
|
+
}
|
|
406
|
+
// throw in all other cases
|
|
407
|
+
default: {
|
|
408
|
+
throw new Error(`[stateManager] Invalid signature, refer to the StateManager.observe documentation"`);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// resend request to get updated list of states
|
|
190
413
|
return new Promise((resolve, reject) => {
|
|
191
|
-
const reqId =
|
|
192
|
-
// store the callback
|
|
193
|
-
|
|
194
|
-
|
|
414
|
+
const reqId = this._promiseStore.add(resolve, reject, 'observe-request');
|
|
415
|
+
// store the callback for execution on the response. the returned Promise
|
|
416
|
+
// is fullfiled once callback has been executed with each existing states
|
|
417
|
+
const observeInfos = [observedSchemaName, callback, options];
|
|
418
|
+
this._observeRequestCallbacks.set(reqId, observeInfos);
|
|
419
|
+
|
|
420
|
+
// NOTE: do not store in `_observeListeners` yet as it can produce race
|
|
421
|
+
// conditions, e.g.:
|
|
422
|
+
// ```
|
|
423
|
+
// await client.stateManager.observe(async (schemaName, stateId, nodeId) => {});
|
|
424
|
+
// // client now receives OBSERVE_NOTIFICATIONS
|
|
425
|
+
// await otherClient.stateManager.create('a');
|
|
426
|
+
// // second observer added in between
|
|
427
|
+
// client.stateManager.observe(async (schemaName, stateId, nodeId) => {});
|
|
428
|
+
// ````
|
|
429
|
+
// OBSERVE_NOTIFICATION is received before the OBSERVE_RESPONSE, then the
|
|
430
|
+
// second observer is called twice:
|
|
431
|
+
// - OBSERVE_RESPONSE 1 []
|
|
432
|
+
// - OBSERVE_NOTIFICATION [ 'a', 1, 0 ]
|
|
433
|
+
// - OBSERVE_NOTIFICATION [ 'a', 1, 0 ] // this should not happen
|
|
434
|
+
// - OBSERVE_RESPONSE 1 [ [ 'a', 1, 0 ] ]
|
|
435
|
+
//
|
|
436
|
+
// cf. unit test `observe should properly behave in race condition`
|
|
437
|
+
|
|
438
|
+
this.client.transport.emit(OBSERVE_REQUEST, reqId, observedSchemaName);
|
|
195
439
|
});
|
|
196
440
|
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Returns a collection of all the states created from the schema name.
|
|
444
|
+
*
|
|
445
|
+
* Alternitive signatures:
|
|
446
|
+
* - `stateManager.getCollection(schemaName)`
|
|
447
|
+
* - `stateManager.getCollection(schemaName, filter)`
|
|
448
|
+
* - `stateManager.getCollection(schemaName, options)`
|
|
449
|
+
* - `stateManager.getCollection(schemaName, filter, options)`
|
|
450
|
+
*
|
|
451
|
+
* @param {string} schemaName - Name of the schema.
|
|
452
|
+
* @param {array|null} [filter=null] - Filter parameter of interest for each
|
|
453
|
+
* state of the collection. If set to `null`, no filter applied.
|
|
454
|
+
* @param {object} [options={}] - Options.
|
|
455
|
+
* @param {boolean} [options.excludeLocal=false] - If set to true, exclude states
|
|
456
|
+
* created by the same node from the collection.
|
|
457
|
+
* @returns {server.SharedStateCollection|client.SharedStateCollection}
|
|
458
|
+
*
|
|
459
|
+
* @example
|
|
460
|
+
* const collection = await client.stateManager.getCollection(schemaName);
|
|
461
|
+
*/
|
|
462
|
+
async getCollection(schemaName, filterOrOptions = null, options = {}) {
|
|
463
|
+
if (!isString(schemaName)) {
|
|
464
|
+
throw new TypeError(`[stateManager] Cannot execute 'getCollection' on 'StateManager': 'schemaName' should be a string"`);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
let filter;
|
|
468
|
+
|
|
469
|
+
if (arguments.length === 2) {
|
|
470
|
+
if (filterOrOptions === null) {
|
|
471
|
+
filter = null;
|
|
472
|
+
options = null;
|
|
473
|
+
} else if (Array.isArray(filterOrOptions)) {
|
|
474
|
+
filter = filterOrOptions;
|
|
475
|
+
options = {};
|
|
476
|
+
} else if (typeof filterOrOptions === 'object') {
|
|
477
|
+
filter = null;
|
|
478
|
+
options = filterOrOptions;
|
|
479
|
+
} else {
|
|
480
|
+
throw new TypeError(`[stateManager] Cannot execute 'getCollection' on 'StateManager': argument 2 should be either null, an array or an object"`);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (arguments.length === 3) {
|
|
485
|
+
filter = filterOrOptions;
|
|
486
|
+
|
|
487
|
+
if (filter !== null && !Array.isArray(filter)) {
|
|
488
|
+
throw new TypeError(`[stateManager] Cannot execute 'getCollection' on 'StateManager': 'filter' should be either an array or null"`);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if (options === null || typeof options !== 'object') {
|
|
492
|
+
throw new TypeError(`[stateManager] Cannot execute 'getCollection' on 'StateManager': 'options' should be an object"`);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const collection = new SharedStateCollection(this, schemaName, filter, options);
|
|
497
|
+
await collection._init();
|
|
498
|
+
|
|
499
|
+
return collection;
|
|
500
|
+
}
|
|
197
501
|
}
|
|
198
502
|
|
|
199
503
|
export default BaseStateManager;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { delay } from '@ircam/sc-utils';
|
|
2
|
+
|
|
3
|
+
import { BATCHED_TRANSPORT_CHANNEL } from './constants.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* This class proxies transports given the SharedStateManager to batch messages
|
|
7
|
+
*
|
|
8
|
+
* @param {number} [options.wait=0] - Wait for given number of milliseconds
|
|
9
|
+
* for stacking messages before emitting on the network. If 0 is given, network
|
|
10
|
+
* message is emitted on next microtask
|
|
11
|
+
* @private
|
|
12
|
+
*/
|
|
13
|
+
class BatchedTransport {
|
|
14
|
+
constructor(transport) {
|
|
15
|
+
this._transport = transport;
|
|
16
|
+
this._listeners = new Map();
|
|
17
|
+
this._stack = [];
|
|
18
|
+
this._sending = false;
|
|
19
|
+
|
|
20
|
+
this._transport.addListener(BATCHED_TRANSPORT_CHANNEL, stack => {
|
|
21
|
+
stack.forEach(entry => {
|
|
22
|
+
const [channel, args] = entry;
|
|
23
|
+
// server side the transport is the same EventEmitter instance
|
|
24
|
+
// for both state manager server and client, so channel might not exist
|
|
25
|
+
// one side or the other.
|
|
26
|
+
if (this._listeners.has(channel)) {
|
|
27
|
+
const callbacks = this._listeners.get(channel);
|
|
28
|
+
callbacks.forEach(callback => callback(...args));
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
addListener(channel, callback) {
|
|
35
|
+
if (!this._listeners.has(channel)) {
|
|
36
|
+
this._listeners.set(channel, new Set());
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const callbacks = this._listeners.get(channel);
|
|
40
|
+
callbacks.add(callback);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async emit(channel, ...args) {
|
|
44
|
+
this._stack.push([channel, args]);
|
|
45
|
+
|
|
46
|
+
if (!this._sending) {
|
|
47
|
+
this._sending = true;
|
|
48
|
+
await false;
|
|
49
|
+
this._sending = false;
|
|
50
|
+
const stack = this._stack;
|
|
51
|
+
this._stack = [];
|
|
52
|
+
this._transport.emit(BATCHED_TRANSPORT_CHANNEL, stack);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
removeListener(channel, callback) {
|
|
57
|
+
if (!this._listeners.has(channel)) {
|
|
58
|
+
const callbacks = this._listeners.get(channel);
|
|
59
|
+
callbacks.delete(callback);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
removeAllListeners(channel = null) {
|
|
64
|
+
if (channel === null) {
|
|
65
|
+
this._listeners.clear();
|
|
66
|
+
} else if (this._listeners.has(channel)) {
|
|
67
|
+
const callbacks = this._listeners.get(channel);
|
|
68
|
+
callbacks.clear();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export default BatchedTransport;
|