@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.
@@ -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(`[stateManager] Invalid value for boolean param "${name}": ${value}`);
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(`[stateManager] Invalid value for string param "${name}": ${value}`);
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(`[stateManager] Invalid value for integer param "${name}": ${value}`);
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(`[stateManager] Invalid value for float param "${name}": ${value}`);
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(`[stateManager] Invalid value for enum param "${name}": ${value}`);
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(`[stateManager] Invalid schema definition - param "${name}": "type" key is required`);
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(`[stateManager] Invalid schema definition - param "${name}": "{ type: '${def.type}' }" does not exists`);
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(`[stateManager] Invalid schema definition - param "${name}" (type "${def.type}"): "${key}" key is required`);
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(`[stateManager] schema is mandatory`);
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(`[stateManager] init value defined for undefined param "${name}"`);
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 {String} name - Name of the parameter.
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 {Object}
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 {String} name - Name of the parameter.
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(`[stateManager] Cannot get value of undefined parameter "${name}"`);
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 {String} name - Name of the parameter.
284
+ * @param {string} name - Name of the parameter.
472
285
  * @param {Mixed} value - Value of the parameter.
473
- * @param {Boolean} [forcePropagation=false] - if true, propagate value even
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(`[stateManager] Cannot set value of undefined parameter "${name}"`);
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(`[stateManager] Invalid value for ${def.type} param "${name}": value is null and param is not nullable`);
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 {String} [name=null] - Name of the parameter to reset.
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 {Object}
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(`[stateManager] Cannot get schema description of undefined parameter "${name}"`);
352
+ throw new ReferenceError(`[SharedState] Cannot get schema description of undefined parameter "${name}"`);
530
353
  }
531
354
 
532
355
  return this._schema[name];
@@ -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
+
@@ -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 create the server-side counterpart of
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 {String}
99
+ * @type {string}
100
+ * @readonly
36
101
  */
37
102
  this.status = 'idle';
38
103
 
39
104
  /**
40
- * List of client role associated with this context.
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
- * Name of the context, default to the class name.
51
- * Override the `get name()` getter to use a user-defined context name.
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
- * Method automatically called when the server starts, or lazilly called if
59
- * the context is created after `server.start()`
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
- * _WARNING: this method should never be called manually._
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
- * Method automatically called when the server stops.
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
- * Method automatically called when the client enters the context.
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
- * Method automatically called when the client exits the context.
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
- * @see {server.Context#name}
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
- * @private
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, roles);
140
+ new ctor(this.server);
142
141
  }
143
142
 
144
143
  // we ensure context is started, even lazilly after server.start()