@soundworks/core 4.0.0-alpha.1 → 4.0.0-alpha.3
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/README.md +25 -15
- package/package.json +2 -2
- package/src/client/Client.js +37 -3
- package/src/client/Context.js +37 -10
- package/src/client/Plugin.js +2 -1
- package/src/client/PluginManager.js +26 -17
- package/src/client/Socket.js +44 -39
- package/src/client/index.js +0 -33
- package/src/common/BasePluginManager.js +12 -3
- package/src/common/ParameterBag.js +33 -210
- package/src/common/utils.js +6 -0
- package/src/server/Context.js +162 -18
- package/src/server/ContextManager.js +4 -5
- package/src/server/Plugin.js +2 -1
- package/src/server/PluginManager.js +26 -6
- package/src/server/Server.js +189 -69
- package/src/server/Socket.js +105 -62
- package/src/server/Sockets.js +27 -22
- package/src/server/StateManager.js +232 -24
- package/src/server/index.js +0 -20
|
@@ -1,195 +1,6 @@
|
|
|
1
1
|
import cloneDeep from 'lodash.clonedeep';
|
|
2
2
|
import equal from 'fast-deep-equal';
|
|
3
3
|
|
|
4
|
-
/**
|
|
5
|
-
* @typedef {Object} server.SharedStateManagerServer~schema
|
|
6
|
-
*
|
|
7
|
-
* Description of a schema to be register by the {@link server.ServerStateManagerServer}
|
|
8
|
-
* A schema consists of a combinaison of key value pairs where the key is the
|
|
9
|
-
* name of the parameter, and the value is an object describing the parameter.
|
|
10
|
-
*
|
|
11
|
-
* Available types are:
|
|
12
|
-
* - {@link server.SharedStateManagerServer~schemaBooleanDef}
|
|
13
|
-
* - {@link server.SharedStateManagerServer~schemaStringDef}
|
|
14
|
-
* - {@link server.SharedStateManagerServer~schemaIntegerDef}
|
|
15
|
-
* - {@link server.SharedStateManagerServer~schemaFloatDef}
|
|
16
|
-
* - {@link server.SharedStateManagerServer~schemaEnumDef}
|
|
17
|
-
* - {@link server.SharedStateManagerServer~schemaAnyDef}
|
|
18
|
-
*
|
|
19
|
-
* @example
|
|
20
|
-
* {
|
|
21
|
-
* myBoolean: {
|
|
22
|
-
* type: 'boolean'
|
|
23
|
-
* default: false,
|
|
24
|
-
* },
|
|
25
|
-
* myFloat: {
|
|
26
|
-
* type: 'float'
|
|
27
|
-
* default: 0.1,
|
|
28
|
-
* min: -1,
|
|
29
|
-
* max: 1,
|
|
30
|
-
* event: true,
|
|
31
|
-
* }
|
|
32
|
-
* }
|
|
33
|
-
*/
|
|
34
|
-
/**
|
|
35
|
-
* @typedef {Object} server.SharedStateManagerServer~schemaBooleanDef
|
|
36
|
-
*
|
|
37
|
-
* @property {String} type='boolean' - Define a boolean parameter.
|
|
38
|
-
* @property {Boolean} default - Default value of the parameter.
|
|
39
|
-
* @property {Boolean} [nullable=false] - Define if the parameter is nullable. If
|
|
40
|
-
* set to `true` the parameter `default` is set to `null`.
|
|
41
|
-
* @property {Boolean} [event=false] - Define if the parameter is a volatile, e.g.
|
|
42
|
-
* set its value back to `null` after propagation. When `true`, `nullable` is
|
|
43
|
-
* automatically set to `true` and `default` to `null`.
|
|
44
|
-
* @property {Boolean} [filterChange=true] - Setting this option to `false` forces
|
|
45
|
-
* the propagation of a parameter even when its value do not change. It
|
|
46
|
-
* offers a kind of middle ground between the default bahavior (e.g. where
|
|
47
|
-
* only changed values are propagated) and the behavior of the `event` option
|
|
48
|
-
* (which has no state per se). As such, setting this options to `false` if
|
|
49
|
-
* `event=true` does not make sens.
|
|
50
|
-
* @property {Boolean} [immediate=false] - Setting this option to `true` will
|
|
51
|
-
* trigger any change (e.g. call the `onUpdate` listeners) immediately on the
|
|
52
|
-
* state that generate the update (i.e. calling `set`), before propagating the
|
|
53
|
-
* change on the network. This option can be usefull in cases the network
|
|
54
|
-
* would introduce a noticeable latency on the client. If for some reason
|
|
55
|
-
* the value is overriden server-side (e.g. in an `updateHook`) the listeners
|
|
56
|
-
* will be called again on when the "real" / final value will be received.
|
|
57
|
-
* @property {Object} [metas={}] - Optionnal metadata of the parameter.
|
|
58
|
-
*/
|
|
59
|
-
/**
|
|
60
|
-
* @typedef {Object} server.SharedStateManagerServer~schemaStringDef
|
|
61
|
-
*
|
|
62
|
-
* @property {String} type='string' - Define a boolean parameter.
|
|
63
|
-
* @property {Mixed} default - Default value of the parameter.
|
|
64
|
-
* @property {Boolean} [nullable=false] - Define if the parameter is nullable. If
|
|
65
|
-
* set to `true` the parameter `default` is set to `null`.
|
|
66
|
-
* @property {Boolean} [event=false] - Define if the parameter is a volatile, e.g.
|
|
67
|
-
* set its value back to `null` after propagation. When `true`, `nullable` is
|
|
68
|
-
* automatically set to `true` and `default` to `null`.
|
|
69
|
-
* @property {Boolean} [filterChange=true] - Setting this option to `false` forces
|
|
70
|
-
* the propagation of a parameter even when its value do not change. It
|
|
71
|
-
* offers a kind of middle ground between the default bahavior (e.g. where
|
|
72
|
-
* only changed values are propagated) and the behavior of the `event` option
|
|
73
|
-
* (which has no state per se). As such, setting this options to `false` if
|
|
74
|
-
* `event=true` does not make sens.
|
|
75
|
-
* @property {Boolean} [immediate=false] - Setting this option to `true` will
|
|
76
|
-
* trigger any change (e.g. call the `onUpdate` listeners) immediately on the
|
|
77
|
-
* state that generate the update (i.e. calling `set`), before propagating the
|
|
78
|
-
* change on the network. This option can be usefull in cases the network
|
|
79
|
-
* would introduce a noticeable latency on the client. If for some reason
|
|
80
|
-
* the value is overriden server-side (e.g. in an `updateHook`) the listeners
|
|
81
|
-
* will be called again on when the "real" / final value will be received.
|
|
82
|
-
* @property {Object} [metas={}] - Optionnal metadata of the parameter.
|
|
83
|
-
*/
|
|
84
|
-
/**
|
|
85
|
-
@typedef {Object} server.SharedStateManagerServer~schemaIntegerDef
|
|
86
|
-
*
|
|
87
|
-
* @property {String} type='integer' - Define a boolean parameter.
|
|
88
|
-
* @property {Mixed} default - Default value of the parameter.
|
|
89
|
-
* @property {Number} [min=-Infinity] - Minimum value of the parameter.
|
|
90
|
-
* @property {Number} [max=+Infinity] - Maximum value of the parameter.
|
|
91
|
-
* @property {Boolean} [nullable=false] - Define if the parameter is nullable. If
|
|
92
|
-
* set to `true` the parameter `default` is set to `null`.
|
|
93
|
-
* @property {Boolean} [event=false] - Define if the parameter is a volatile, e.g.
|
|
94
|
-
* set its value back to `null` after propagation. When `true`, `nullable` is
|
|
95
|
-
* automatically set to `true` and `default` to `null`.
|
|
96
|
-
* @property {Boolean} [filterChange=true] - Setting this option to `false` forces
|
|
97
|
-
* the propagation of a parameter even when its value do not change. It
|
|
98
|
-
* offers a kind of middle ground between the default bahavior (e.g. where
|
|
99
|
-
* only changed values are propagated) and the behavior of the `event` option
|
|
100
|
-
* (which has no state per se). As such, setting this options to `false` if
|
|
101
|
-
* `event=true` does not make sens.
|
|
102
|
-
* @property {Boolean} [immediate=false] - Setting this option to `true` will
|
|
103
|
-
* trigger any change (e.g. call the `onUpdate` listeners) immediately on the
|
|
104
|
-
* state that generate the update (i.e. calling `set`), before propagating the
|
|
105
|
-
* change on the network. This option can be usefull in cases the network
|
|
106
|
-
* would introduce a noticeable latency on the client. If for some reason
|
|
107
|
-
* the value is overriden server-side (e.g. in an `updateHook`) the listeners
|
|
108
|
-
* will be called again on when the "real" / final value will be received.
|
|
109
|
-
* @property {Object} [metas={}] - Optionnal metadata of the parameter.
|
|
110
|
-
*/
|
|
111
|
-
/**
|
|
112
|
-
* @typedef {Object} server.SharedStateManagerServer~schemaFloatDef
|
|
113
|
-
*
|
|
114
|
-
* @property {String} [type='float'] - Float parameter.
|
|
115
|
-
* @property {Mixed} default - Default value.
|
|
116
|
-
* @property {Number} [min=-Infinity] - Minimum value.
|
|
117
|
-
* @property {Number} [max=+Infinity] - Maximum value.
|
|
118
|
-
* @property {Boolean} [nullable=false] - Define if the parameter is nullable. If
|
|
119
|
-
* set to `true` the parameter `default` is set to `null`.
|
|
120
|
-
* @property {Boolean} [event=false] - Define if the parameter is a volatile, e.g.
|
|
121
|
-
* set its value back to `null` after propagation. When `true`, `nullable` is
|
|
122
|
-
* automatically set to `true` and `default` to `null`.
|
|
123
|
-
* @property {Boolean} [filterChange=true] - Setting this option to `false` forces
|
|
124
|
-
* the propagation of a parameter even when its value do not change. It
|
|
125
|
-
* offers a kind of middle ground between the default bahavior (e.g. where
|
|
126
|
-
* only changed values are propagated) and the behavior of the `event` option
|
|
127
|
-
* (which has no state per se). As such, setting this options to `false` if
|
|
128
|
-
* `event=true` does not make sens.
|
|
129
|
-
* @property {Boolean} [immediate=false] - Setting this option to `true` will
|
|
130
|
-
* trigger any change (e.g. call the `onUpdate` listeners) immediately on the
|
|
131
|
-
* state that generate the update (i.e. calling `set`), before propagating the
|
|
132
|
-
* change on the network. This option can be usefull in cases the network
|
|
133
|
-
* would introduce a noticeable latency on the client. If for some reason
|
|
134
|
-
* the value is overriden server-side (e.g. in an `updateHook`) the listeners
|
|
135
|
-
* will be called again on when the "real" / final value will be received.
|
|
136
|
-
* @property {Object} [metas={}] - Optionnal metadata of the parameter.
|
|
137
|
-
*/
|
|
138
|
-
/**
|
|
139
|
-
* @typedef {Object} server.SharedStateManagerServer~schemaEnumDef
|
|
140
|
-
*
|
|
141
|
-
* @property {String} [type='enum'] - Enum parameter.
|
|
142
|
-
* @property {Mixed} default - Default value of the parameter.
|
|
143
|
-
* @property {Array} list - Possible values of the parameter.
|
|
144
|
-
* @property {Boolean} [nullable=false] - Define if the parameter is nullable. If
|
|
145
|
-
* set to `true` the parameter `default` is set to `null`.
|
|
146
|
-
* @property {Boolean} [event=false] - Define if the parameter is a volatile, e.g.
|
|
147
|
-
* set its value back to `null` after propagation. When `true`, `nullable` is
|
|
148
|
-
* automatically set to `true` and `default` to `null`.
|
|
149
|
-
* @property {Boolean} [filterChange=true] - Setting this option to `false` forces
|
|
150
|
-
* the propagation of a parameter even when its value do not change. It
|
|
151
|
-
* offers a kind of middle ground between the default bahavior (e.g. where
|
|
152
|
-
* only changed values are propagated) and the behavior of the `event` option
|
|
153
|
-
* (which has no state per se). As such, setting this options to `false` if
|
|
154
|
-
* `event=true` does not make sens.
|
|
155
|
-
* @property {Boolean} [immediate=false] - Setting this option to `true` will
|
|
156
|
-
* trigger any change (e.g. call the `onUpdate` listeners) immediately on the
|
|
157
|
-
* state that generate the update (i.e. calling `set`), before propagating the
|
|
158
|
-
* change on the network. This option can be usefull in cases the network
|
|
159
|
-
* would introduce a noticeable latency on the client. If for some reason
|
|
160
|
-
* the value is overriden server-side (e.g. in an `updateHook`) the listeners
|
|
161
|
-
* will be called again on when the "real" / final value will be received.
|
|
162
|
-
* @property {Object} [metas={}] - Optionnal metadata of the parameter.
|
|
163
|
-
*/
|
|
164
|
-
/**
|
|
165
|
-
* @typedef {Object} server.SharedStateManagerServer~schemaAnyDef
|
|
166
|
-
*
|
|
167
|
-
* Note that the `any` type always return a shallow copy of the state internal
|
|
168
|
-
* value. Mutating the returned value will not modify the internal state.
|
|
169
|
-
*
|
|
170
|
-
* @property {String} [type='any'] - Parameter of any type.
|
|
171
|
-
* @property {Mixed} default - Default value of the parameter.
|
|
172
|
-
* @property {Boolean} [nullable=false] - Define if the parameter is nullable. If
|
|
173
|
-
* set to `true` the parameter `default` is set to `null`.
|
|
174
|
-
* @property {Boolean} [event=false] - Define if the parameter is a volatile, e.g.
|
|
175
|
-
* set its value back to `null` after propagation. When `true`, `nullable` is
|
|
176
|
-
* automatically set to `true` and `default` to `null`.
|
|
177
|
-
* @property {Boolean} [filterChange=true] - Setting this option to `false` forces
|
|
178
|
-
* the propagation of a parameter even when its value do not change. It
|
|
179
|
-
* offers a kind of middle ground between the default bahavior (e.g. where
|
|
180
|
-
* only changed values are propagated) and the behavior of the `event` option
|
|
181
|
-
* (which has no state per se). As such, setting this options to `false` if
|
|
182
|
-
* `event=true` does not make sens.
|
|
183
|
-
* @property {Boolean} [immediate=false] - Setting this option to `true` will
|
|
184
|
-
* trigger any change (e.g. call the `onUpdate` listeners) immediately on the
|
|
185
|
-
* state that generate the update (i.e. calling `set`), before propagating the
|
|
186
|
-
* change on the network. This option can be usefull in cases the network
|
|
187
|
-
* would introduce a noticeable latency on the client. If for some reason
|
|
188
|
-
* the value is overriden server-side (e.g. in an `updateHook`) the listeners
|
|
189
|
-
* will be called again on when the "real" / final value will be received.
|
|
190
|
-
* @property {Object} [metas={}] - Optionnal metadata of the parameter.
|
|
191
|
-
*/
|
|
192
|
-
|
|
193
4
|
export const sharedOptions = {
|
|
194
5
|
nullable: false,
|
|
195
6
|
event: false, // if event=true, nullable=true
|
|
@@ -206,7 +17,7 @@ export const types = {
|
|
|
206
17
|
},
|
|
207
18
|
coerceFunction: (name, def, value) => {
|
|
208
19
|
if (typeof value !== 'boolean') {
|
|
209
|
-
throw new TypeError(`[
|
|
20
|
+
throw new TypeError(`[SharedState] Invalid value for boolean param "${name}": ${value}`);
|
|
210
21
|
}
|
|
211
22
|
|
|
212
23
|
return value;
|
|
@@ -219,7 +30,7 @@ export const types = {
|
|
|
219
30
|
},
|
|
220
31
|
coerceFunction: (name, def, value) => {
|
|
221
32
|
if (typeof value !== 'string') {
|
|
222
|
-
throw new TypeError(`[
|
|
33
|
+
throw new TypeError(`[SharedState] Invalid value for string param "${name}": ${value}`);
|
|
223
34
|
}
|
|
224
35
|
|
|
225
36
|
return value;
|
|
@@ -252,7 +63,7 @@ export const types = {
|
|
|
252
63
|
},
|
|
253
64
|
coerceFunction: (name, def, value) => {
|
|
254
65
|
if (!(typeof value === 'number' && Math.floor(value) === value)) {
|
|
255
|
-
throw new TypeError(`[
|
|
66
|
+
throw new TypeError(`[SharedState] Invalid value for integer param "${name}": ${value}`);
|
|
256
67
|
}
|
|
257
68
|
|
|
258
69
|
return Math.max(def.min, Math.min(def.max, value));
|
|
@@ -285,7 +96,7 @@ export const types = {
|
|
|
285
96
|
},
|
|
286
97
|
coerceFunction: (name, def, value) => {
|
|
287
98
|
if (typeof value !== 'number' || value !== value) { // reject NaN
|
|
288
|
-
throw new TypeError(`[
|
|
99
|
+
throw new TypeError(`[SharedState] Invalid value for float param "${name}": ${value}`);
|
|
289
100
|
}
|
|
290
101
|
|
|
291
102
|
return Math.max(def.min, Math.min(def.max, value));
|
|
@@ -298,7 +109,7 @@ export const types = {
|
|
|
298
109
|
},
|
|
299
110
|
coerceFunction: (name, def, value) => {
|
|
300
111
|
if (def.list.indexOf(value) === -1) {
|
|
301
|
-
throw new TypeError(`[
|
|
112
|
+
throw new TypeError(`[SharedState] Invalid value for enum param "${name}": ${value}`);
|
|
302
113
|
}
|
|
303
114
|
|
|
304
115
|
|
|
@@ -328,11 +139,11 @@ class ParameterBag {
|
|
|
328
139
|
const def = schema[name];
|
|
329
140
|
|
|
330
141
|
if (!Object.prototype.hasOwnProperty.call(def, 'type')) {
|
|
331
|
-
throw new TypeError(`[
|
|
142
|
+
throw new TypeError(`[StateManager.registerSchema] Invalid schema definition - param "${name}": "type" key is required`);
|
|
332
143
|
}
|
|
333
144
|
|
|
334
145
|
if (!Object.prototype.hasOwnProperty.call(types, def.type)) {
|
|
335
|
-
throw new TypeError(`[
|
|
146
|
+
throw new TypeError(`[StateManager.registerSchema] Invalid schema definition - param "${name}": "{ type: '${def.type}' }" does not exists`);
|
|
336
147
|
}
|
|
337
148
|
|
|
338
149
|
const required = types[def.type].required;
|
|
@@ -341,7 +152,7 @@ class ParameterBag {
|
|
|
341
152
|
if (def.event === true && key === 'default') {
|
|
342
153
|
// do nothing, default is always null for `event` params
|
|
343
154
|
} else if (!Object.prototype.hasOwnProperty.call(def, key)) {
|
|
344
|
-
throw new TypeError(`[
|
|
155
|
+
throw new TypeError(`[StateManager.registerSchema] Invalid schema definition - param "${name}" (type "${def.type}"): "${key}" key is required`);
|
|
345
156
|
}
|
|
346
157
|
});
|
|
347
158
|
}
|
|
@@ -349,7 +160,7 @@ class ParameterBag {
|
|
|
349
160
|
|
|
350
161
|
constructor(schema, initValues = {}) {
|
|
351
162
|
if (!schema) {
|
|
352
|
-
throw new Error(`[
|
|
163
|
+
throw new Error(`[ParameterBag] schema is mandatory`);
|
|
353
164
|
}
|
|
354
165
|
|
|
355
166
|
schema = cloneDeep(schema);
|
|
@@ -382,7 +193,7 @@ class ParameterBag {
|
|
|
382
193
|
// make shure initValues make sens according to the given schema
|
|
383
194
|
for (let name in initValues) {
|
|
384
195
|
if (!Object.prototype.hasOwnProperty.call(schema, name)) {
|
|
385
|
-
throw new ReferenceError(`[
|
|
196
|
+
throw new ReferenceError(`[StateManager.create] init value defined for undefined param "${name}"`);
|
|
386
197
|
}
|
|
387
198
|
}
|
|
388
199
|
|
|
@@ -422,7 +233,7 @@ class ParameterBag {
|
|
|
422
233
|
/**
|
|
423
234
|
* Define if the parameter exists.
|
|
424
235
|
*
|
|
425
|
-
* @param {
|
|
236
|
+
* @param {string} name - Name of the parameter.
|
|
426
237
|
* @return {Boolean}
|
|
427
238
|
*/
|
|
428
239
|
has(name) {
|
|
@@ -432,7 +243,7 @@ class ParameterBag {
|
|
|
432
243
|
/**
|
|
433
244
|
* Return values of all parameters as a flat object.
|
|
434
245
|
*
|
|
435
|
-
* @return {
|
|
246
|
+
* @return {object}
|
|
436
247
|
*/
|
|
437
248
|
getValues() {
|
|
438
249
|
let values = {};
|
|
@@ -448,15 +259,17 @@ class ParameterBag {
|
|
|
448
259
|
* Return the value of the given parameter. If the parameter is of `any` type,
|
|
449
260
|
* a deep copy is returned.
|
|
450
261
|
*
|
|
451
|
-
* @param {
|
|
262
|
+
* @param {string} name - Name of the parameter.
|
|
452
263
|
* @return {Mixed} - Value of the parameter.
|
|
453
264
|
*/
|
|
454
265
|
get(name) {
|
|
455
266
|
if (!this.has(name)) {
|
|
456
|
-
throw new ReferenceError(`[
|
|
267
|
+
throw new ReferenceError(`[SharedState] Cannot get value of undefined parameter "${name}"`);
|
|
457
268
|
}
|
|
458
269
|
|
|
459
270
|
if (this._schema[name].type === 'any') {
|
|
271
|
+
// we return a deep copy of the object as we don't want the client code to
|
|
272
|
+
// be able to modify our underlying data.
|
|
460
273
|
return cloneDeep(this._values[name]);
|
|
461
274
|
} else {
|
|
462
275
|
return this._values[name];
|
|
@@ -468,22 +281,22 @@ class ParameterBag {
|
|
|
468
281
|
* (aka if previous value is different from new value) all registered
|
|
469
282
|
* callbacks are registered.
|
|
470
283
|
*
|
|
471
|
-
* @param {
|
|
284
|
+
* @param {string} name - Name of the parameter.
|
|
472
285
|
* @param {Mixed} value - Value of the parameter.
|
|
473
|
-
* @param {
|
|
286
|
+
* @param {boolean} [forcePropagation=false] - if true, propagate value even
|
|
474
287
|
* if the value has not changed.
|
|
475
288
|
* @return {Array} - [new value, updated flag].
|
|
476
289
|
*/
|
|
477
290
|
set(name, value) {
|
|
478
291
|
if (!this.has(name)) {
|
|
479
|
-
throw new ReferenceError(`[
|
|
292
|
+
throw new ReferenceError(`[SharedState] Cannot set value of undefined parameter "${name}"`);
|
|
480
293
|
}
|
|
481
294
|
|
|
482
295
|
const def = this._schema[name];
|
|
483
296
|
const { coerceFunction } = types[def.type];
|
|
484
297
|
|
|
485
298
|
if (value === null && def.nullable === false) {
|
|
486
|
-
throw new TypeError(`[
|
|
299
|
+
throw new TypeError(`[SharedState] Invalid value for ${def.type} param "${name}": value is null and param is not nullable`);
|
|
487
300
|
} else if (value === null && def.nullable === true) {
|
|
488
301
|
value = null;
|
|
489
302
|
} else {
|
|
@@ -493,6 +306,16 @@ class ParameterBag {
|
|
|
493
306
|
const currentValue = this._values[name];
|
|
494
307
|
const updated = !equal(currentValue, value);
|
|
495
308
|
|
|
309
|
+
// we store a deep copy of the object as we don't want the client to be able
|
|
310
|
+
// to modify our underlying data, which leads unexpected behavior where the
|
|
311
|
+
// deep equal check to returns true, and therefore the update is not triggered.
|
|
312
|
+
//
|
|
313
|
+
// @see tests/common.state-manager.spec.js
|
|
314
|
+
// 'should copy stored value for "any" type to have a predictable behavior'
|
|
315
|
+
if (this._schema[name].type === 'any') {
|
|
316
|
+
value = cloneDeep(value);
|
|
317
|
+
}
|
|
318
|
+
|
|
496
319
|
this._values[name] = value;
|
|
497
320
|
|
|
498
321
|
// return tuple so that the state manager can handle the `filterChange` option
|
|
@@ -504,7 +327,7 @@ class ParameterBag {
|
|
|
504
327
|
* @note - prefer `state.set(state.getInitValues())`
|
|
505
328
|
* or `state.set(state.getDefaultValues())`
|
|
506
329
|
*
|
|
507
|
-
* @param {
|
|
330
|
+
* @param {string} [name=null] - Name of the parameter to reset.
|
|
508
331
|
*/
|
|
509
332
|
// reset(name = null) {
|
|
510
333
|
// if (name !== null) {
|
|
@@ -519,14 +342,14 @@ class ParameterBag {
|
|
|
519
342
|
/**
|
|
520
343
|
* Return the given schema along with the initialization values.
|
|
521
344
|
*
|
|
522
|
-
* @return {
|
|
345
|
+
* @return {object}
|
|
523
346
|
*/
|
|
524
347
|
getSchema(name = null) {
|
|
525
348
|
if (name === null) {
|
|
526
349
|
return this._schema;
|
|
527
350
|
} else {
|
|
528
351
|
if (!this.has(name)) {
|
|
529
|
-
throw new ReferenceError(`[
|
|
352
|
+
throw new ReferenceError(`[SharedState] Cannot get schema description of undefined parameter "${name}"`);
|
|
530
353
|
}
|
|
531
354
|
|
|
532
355
|
return this._schema[name];
|
package/src/common/utils.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import isPlainObj from 'is-plain-obj';
|
|
2
|
+
|
|
1
3
|
// https://stackoverflow.com/questions/17575790/environment-detection-node-js-or-browser
|
|
2
4
|
export const isBrowser = new Function('try {return this===window;}catch(e){ return false;}');
|
|
3
5
|
|
|
@@ -20,3 +22,7 @@ export function isFunction(func) {
|
|
|
20
22
|
Object.prototype.toString.call(func) == '[object AsyncFunction]';
|
|
21
23
|
}
|
|
22
24
|
|
|
25
|
+
export function isPlainObject(obj) {
|
|
26
|
+
return isPlainObj(obj);
|
|
27
|
+
}
|
|
28
|
+
|
package/src/server/Context.js
CHANGED
|
@@ -2,44 +2,116 @@ import Client from './Client.js';
|
|
|
2
2
|
import Server from './Server.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Base class to extend in order to
|
|
6
|
-
* a {@link client.Context}. If not defined, a default context will be created
|
|
5
|
+
* Base class to extend in order to implment the optionnal server-side counterpart
|
|
6
|
+
* of a {@link client.Context}. If not defined, a default context will be created
|
|
7
7
|
* and used by the server.
|
|
8
8
|
*
|
|
9
|
+
* In the `soundworks` paradigm, a client has a "role" (e.g. _player_, _controller_)
|
|
10
|
+
* see {@link client.Client#role}) and can be in different "contexts" (e.g. different
|
|
11
|
+
* part of the experience such as sections of a music piece, etc.). The
|
|
12
|
+
* {@link client.Context} and optionnal {@link serverContext} abstractions provide
|
|
13
|
+
* a simple and unified way to model these reccuring aspects of an application.
|
|
14
|
+
*
|
|
15
|
+
* If a `server.Context` is recognized as the server-side counterpart of a
|
|
16
|
+
* {@link client.Context}, based on their respective `name` (see {@link client.Context#name}
|
|
17
|
+
* and {@link server.Context#name}), `soundworks` will ensure the logic defined
|
|
18
|
+
* by the server-side Context will be executed at the beginning of the
|
|
19
|
+
* {@link client.Client#enter} and {@link client.Client#exit} methods. The example
|
|
20
|
+
* show how soundwords handles (and guarantees) the order of the `enter()` steps
|
|
21
|
+
* between the client-side and the server-side parts of the context. The same goes
|
|
22
|
+
* for the `exit()` method.
|
|
23
|
+
*
|
|
24
|
+
* ```
|
|
25
|
+
* // client-side
|
|
26
|
+
* import { Context } from '@soundworks/core/client.js';
|
|
27
|
+
*
|
|
28
|
+
* class MyContext extends Context {
|
|
29
|
+
* async enter() {
|
|
30
|
+
* // 1. client side context enter() starts
|
|
31
|
+
* // server-side logic is triggered first
|
|
32
|
+
* await super.enter();
|
|
33
|
+
* // 4. server-side context enter() is fully done
|
|
34
|
+
* // some async job can be done here
|
|
35
|
+
* await new Promise(resolve => setTimeout(resolve, 1000));
|
|
36
|
+
* // 5. client-side context enter() ends
|
|
37
|
+
* }
|
|
38
|
+
* }
|
|
39
|
+
*
|
|
40
|
+
* // Instanciate the context (assuming the `client.role` is 'test')
|
|
41
|
+
* const myContext = new MyContext(client);
|
|
42
|
+
*
|
|
43
|
+
* // At some point in the application, the client enters the context trigerring
|
|
44
|
+
* // the steps 1 to 5 described in the client-side and server-side `enter()`
|
|
45
|
+
* // implementations. Note that the server-side `enter()` is never called manually.
|
|
46
|
+
* await myContext.enter();
|
|
47
|
+
* ```
|
|
48
|
+
*
|
|
49
|
+
* ```
|
|
50
|
+
* // server-side
|
|
51
|
+
* import { Context } from '@soundworks/core/server.js';
|
|
52
|
+
*
|
|
53
|
+
* class MyContext extends Context {
|
|
54
|
+
* async enter(client) {
|
|
55
|
+
* // 2. server-side context enter() starts
|
|
56
|
+
* await super.enter(client);
|
|
57
|
+
* // some async job can be done here
|
|
58
|
+
* await new Promise(resolve => setTimeout(resolve, 1000));
|
|
59
|
+
* // 3. server-side context enter() ends
|
|
60
|
+
* }
|
|
61
|
+
* }
|
|
62
|
+
*
|
|
63
|
+
* // Instantiate the context
|
|
64
|
+
* const myContext = new Context(server);
|
|
65
|
+
* ```
|
|
66
|
+
*
|
|
9
67
|
* @memberof server
|
|
10
68
|
*/
|
|
11
69
|
class Context {
|
|
70
|
+
/**
|
|
71
|
+
* @param {client.Client} client - The soundworks client instance.
|
|
72
|
+
* @param {string|string[]} [roles=[]] - Optionnal list of client roles that can
|
|
73
|
+
* use this context. In large applications, this may be usefull to guarantee
|
|
74
|
+
* that a context can be consumed only by specific client roles, throwing an
|
|
75
|
+
* error if any other client role tries to use it. If empty, no access policy
|
|
76
|
+
* will be used.
|
|
77
|
+
* @throws Will throw if the first argument is not a soundworks server instance.
|
|
78
|
+
*/
|
|
12
79
|
constructor(server, roles = []) {
|
|
13
80
|
if (!(server instanceof Server)) {
|
|
14
81
|
throw new Error(`[soundworks:Context] Invalid argument, context "${this.constructor.name}" should receive a "soundworks.Server" instance as first argument`);
|
|
15
82
|
}
|
|
16
83
|
|
|
17
|
-
if (roles === null) {
|
|
18
|
-
throw new Error('Invalid client types');
|
|
19
|
-
}
|
|
20
|
-
|
|
21
84
|
/**
|
|
22
85
|
* soundworks server
|
|
23
86
|
* @type {server.Server}
|
|
87
|
+
* @readonly
|
|
24
88
|
*/
|
|
25
89
|
this.server = server;
|
|
26
90
|
|
|
27
91
|
/**
|
|
28
92
|
* List of clients that are currently in this context
|
|
29
|
-
* @type {Client[]}
|
|
93
|
+
* @type {server.Client[]}
|
|
30
94
|
*/
|
|
31
95
|
this.clients = new Set();
|
|
32
96
|
|
|
33
97
|
/**
|
|
34
98
|
* Status of the context ('idle', 'inited', 'started' or 'errored')
|
|
35
|
-
* @type {
|
|
99
|
+
* @type {string}
|
|
100
|
+
* @readonly
|
|
36
101
|
*/
|
|
37
102
|
this.status = 'idle';
|
|
38
103
|
|
|
39
104
|
/**
|
|
40
|
-
* List of client
|
|
105
|
+
* List of client roles that can use this context.
|
|
106
|
+
* @type {string[]}
|
|
107
|
+
* @readonly
|
|
41
108
|
*/
|
|
42
109
|
roles = Array.isArray(roles) ? roles : [roles];
|
|
110
|
+
|
|
111
|
+
if (roles.length === 0) {
|
|
112
|
+
roles = Object.keys(server.config.app.clients);
|
|
113
|
+
}
|
|
114
|
+
|
|
43
115
|
this.roles = new Set(roles);
|
|
44
116
|
|
|
45
117
|
// register in context manager
|
|
@@ -47,32 +119,87 @@ class Context {
|
|
|
47
119
|
}
|
|
48
120
|
|
|
49
121
|
/**
|
|
50
|
-
*
|
|
51
|
-
*
|
|
122
|
+
* Optionnal user-defined name of the context (defaults to the class name).
|
|
123
|
+
*
|
|
124
|
+
* The context manager will match the client-side and server-side contexts based
|
|
125
|
+
* on this name. If the {@link server.ContextManager} don't find a corresponding
|
|
126
|
+
* user-defined context with the same name, it will use a default (noop) context.
|
|
127
|
+
*
|
|
128
|
+
* @readonly
|
|
129
|
+
* @type {string}
|
|
130
|
+
* @example
|
|
131
|
+
* // server-side and client-side contexts are matched based on their respective `name`
|
|
132
|
+
* class MyContext extends Context {
|
|
133
|
+
* get name() {
|
|
134
|
+
* return 'my-user-defined-context-name';
|
|
135
|
+
* }
|
|
136
|
+
* }
|
|
52
137
|
*/
|
|
53
138
|
get name() {
|
|
54
139
|
return this.constructor.name;
|
|
55
140
|
}
|
|
56
141
|
|
|
57
142
|
/**
|
|
58
|
-
*
|
|
59
|
-
* the
|
|
143
|
+
* Start the context. This method is lazilly called when a client enters the
|
|
144
|
+
* context for the first time (cf. ${server.Context#enter}). If you know some
|
|
145
|
+
* some heavy and/or potentially long job has to be done when starting the context
|
|
146
|
+
* (e.g. connect to a database, parsing a long file) it may be a good practice
|
|
147
|
+
* to call it explicitely.
|
|
60
148
|
*
|
|
61
|
-
*
|
|
149
|
+
* This method should be implemented to perform operations that are valid for the
|
|
150
|
+
* whole lifetime of the context, regardless a client enters or exits the context.
|
|
151
|
+
*
|
|
152
|
+
* @example
|
|
153
|
+
* import { Context } from '@soundworks/core/server.js';
|
|
154
|
+
*
|
|
155
|
+
* class MyContext extends Context {
|
|
156
|
+
* async start() {
|
|
157
|
+
* await super.start();
|
|
158
|
+
* await this.doSomeLongJob();
|
|
159
|
+
* }
|
|
160
|
+
* }
|
|
161
|
+
*
|
|
162
|
+
* // Instantiate the context
|
|
163
|
+
* const myContext = new Context(server, ['test']);
|
|
164
|
+
* // manually start the context to perform the long operation before the first
|
|
165
|
+
* // client enters the context
|
|
166
|
+
* await myContext.start();
|
|
167
|
+
* ```
|
|
62
168
|
*/
|
|
63
169
|
async start() {}
|
|
64
170
|
|
|
65
171
|
/**
|
|
66
|
-
*
|
|
172
|
+
* Stop the context. The method that is automatically called when the server
|
|
173
|
+
* stops. It should be used to cleanup context wise operations made in `start()`
|
|
174
|
+
* (e.g. disconnect from a database, release a file handle).
|
|
67
175
|
*
|
|
68
176
|
* _WARNING: this method should never be called manually._
|
|
69
177
|
*/
|
|
70
178
|
async stop() {}
|
|
71
179
|
|
|
72
180
|
/**
|
|
73
|
-
*
|
|
181
|
+
* Enter the context. Implement this method to define the logic that should be
|
|
182
|
+
* done (e.g. creating a shared state, etc.) when a client enters the context.
|
|
183
|
+
*
|
|
184
|
+
* If the context has not been started yet, the `start` method is implicitely executed.
|
|
74
185
|
*
|
|
75
186
|
* _WARNING: this method should never be called manually._
|
|
187
|
+
*
|
|
188
|
+
* @param {server.Client} client - Server-side representation of the client
|
|
189
|
+
* that enters the context.
|
|
190
|
+
* @returns {Promise} - Promise that resolves when the context is entered.
|
|
191
|
+
* @example
|
|
192
|
+
* class MyContext extends Context {
|
|
193
|
+
* async enter(client) {
|
|
194
|
+
* await super.enter(client);
|
|
195
|
+
* registerTheClientSomewhere(client);
|
|
196
|
+
* }
|
|
197
|
+
*
|
|
198
|
+
* async exit(client) {
|
|
199
|
+
* await super.exit(client);
|
|
200
|
+
* unregisterTheClientSomewhere(client);
|
|
201
|
+
* }
|
|
202
|
+
* }
|
|
76
203
|
*/
|
|
77
204
|
async enter(client) {
|
|
78
205
|
if (!(client instanceof Client)) {
|
|
@@ -83,9 +210,26 @@ class Context {
|
|
|
83
210
|
}
|
|
84
211
|
|
|
85
212
|
/**
|
|
86
|
-
*
|
|
213
|
+
* Exit the context. Implement this method to define the logic that should be
|
|
214
|
+
* done (e.g. delete a shared state, etc.) when a client exits the context.
|
|
87
215
|
*
|
|
88
|
-
* _WARNING: this method should never be called manually._
|
|
216
|
+
* * _WARNING: this method should never be called manually._
|
|
217
|
+
*
|
|
218
|
+
* @param {server.Client} client - Server-side representation of the client
|
|
219
|
+
* that exits the context.
|
|
220
|
+
* @returns {Promise} - Promise that resolves when the context is exited.
|
|
221
|
+
* @example
|
|
222
|
+
* class MyContext extends Context {
|
|
223
|
+
* async enter(client) {
|
|
224
|
+
* await super.enter(client);
|
|
225
|
+
* this.state = await this.client.stateManager.create('my-context-state');
|
|
226
|
+
* }
|
|
227
|
+
*
|
|
228
|
+
* async exit(client) {
|
|
229
|
+
* await super.exit(client);
|
|
230
|
+
* await this.state.delete();
|
|
231
|
+
* }
|
|
232
|
+
* }
|
|
89
233
|
*/
|
|
90
234
|
async exit(client) {
|
|
91
235
|
if (!(client instanceof Client)) {
|
|
@@ -57,6 +57,7 @@ class ContextCollection {
|
|
|
57
57
|
* _WARNING: Most of the time, you should not have to manipulate the context manager directly._
|
|
58
58
|
*
|
|
59
59
|
* @memberof server
|
|
60
|
+
* @hideconstructor
|
|
60
61
|
*/
|
|
61
62
|
class ContextManager {
|
|
62
63
|
/**
|
|
@@ -91,10 +92,9 @@ class ContextManager {
|
|
|
91
92
|
/**
|
|
92
93
|
* Retrieve a started context from its name.
|
|
93
94
|
*
|
|
94
|
-
*
|
|
95
|
-
* @param {String} contextName - Name of the context.
|
|
95
|
+
* _WARNING: Most of the time, you should not have to call this method manually._
|
|
96
96
|
*
|
|
97
|
-
* @
|
|
97
|
+
* @param {server.Context#name} contextName - Name of the context.
|
|
98
98
|
*/
|
|
99
99
|
async get(contextName) {
|
|
100
100
|
if (!this._contexts.has(contextName)) {
|
|
@@ -136,9 +136,8 @@ class ContextManager {
|
|
|
136
136
|
if (!this._contexts.has(contextName)) {
|
|
137
137
|
// create default context for all client types
|
|
138
138
|
const ctor = createNamedContextClass(contextName);
|
|
139
|
-
const roles = Object.keys(this.server.config.app.clients);
|
|
140
139
|
// this will automatically register the context in the context manager
|
|
141
|
-
new ctor(this.server
|
|
140
|
+
new ctor(this.server);
|
|
142
141
|
}
|
|
143
142
|
|
|
144
143
|
// we ensure context is started, even lazilly after server.start()
|