@soundworks/core 5.6.0 → 5.8.0

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,15 +1,24 @@
1
1
  import warnings from './logs/warnings.js';
2
2
 
3
+ import {
4
+ checkValidFilters,
5
+ sanitizeOnUpdateParams,
6
+ } from './shared-state-utils.js';
7
+
3
8
  /**
4
9
  * Callback to execute when an update is triggered on one of the shared states
5
10
  * of the collection.
6
11
  *
7
12
  * @callback sharedStateCollectionOnUpdateCallback
8
13
  * @param {SharedState} state - The shared state instance that triggered the update.
9
- * @param {Object} newValues - Key / value pairs of the updates that have been
10
- * applied to the state.
11
- * @param {Object} oldValues - Key / value pairs of the updated params before
12
- * the updates has been applied to the state.
14
+ * @param {Object|any} newValues - Key / value pairs of the updates that have been
15
+ * applied to the state, or single value if the callback has been registered against
16
+ * a parameter name filter.
17
+ * @param {Object|any} oldValues - Key / value pairs of the updated params before
18
+ * the updates has been applied to the state, or single value if the callback has been
19
+ * registered against a parameter name filter.
20
+ * @param {sharedStateCollectionDeleteOnUpdateCallback} unsubscribe - Reference to unsubscribe
21
+ * function returned by the `SharedStateCollection#onUpdate` method.
13
22
  */
14
23
 
15
24
  /**
@@ -80,8 +89,8 @@ export const kSharedStateCollectionInit = Symbol('soundworks:shared-state-collec
80
89
  class SharedStateCollection {
81
90
  #stateManager = null;
82
91
  #className = null;
83
- #filter = null;
84
92
  #options = null;
93
+ #excludeLocal = null;
85
94
  #classDescription = null;
86
95
  #states = [];
87
96
  #onUpdateCallbacks = new Set();
@@ -90,30 +99,25 @@ class SharedStateCollection {
90
99
  #onChangeCallbacks = new Set();
91
100
  #unobserve = null;
92
101
 
93
- constructor(stateManager, className, filter = null, options = {}) {
102
+ constructor(stateManager, className, options = {}) {
94
103
  this.#stateManager = stateManager;
95
104
  this.#className = className;
96
- this.#filter = filter;
97
- this.#options = Object.assign({ excludeLocal: false }, options);
105
+ this.#options = {
106
+ whiteList: options.whiteList ?? null,
107
+ blackList: options.blackList ?? null,
108
+ };
109
+ this.#excludeLocal = options.excludeLocal ?? false;
98
110
  }
99
111
 
100
112
  /** @private */
101
113
  async [kSharedStateCollectionInit]() {
102
114
  this.#classDescription = await this.#stateManager.getClassDescription(this.#className);
103
-
104
- // if filter is set, check that it contains only valid param names
105
- if (this.#filter !== null) {
106
- const keys = Object.keys(this.#classDescription);
107
-
108
- for (let filter of this.#filter) {
109
- if (!keys.includes(filter)) {
110
- throw new ReferenceError(`Invalid filter key (${filter}) for class "${this.#className}"`);
111
- }
112
- }
113
- }
115
+ // this is catch by `getCollection`
116
+ checkValidFilters(this.#options, this.#className, this.#classDescription);
114
117
 
115
118
  this.#unobserve = await this.#stateManager.observe(this.#className, async (className, stateId) => {
116
- const state = await this.#stateManager.attach(className, stateId, this.#filter);
119
+ const state = await this.#stateManager.attach(className, stateId, this.#options);
120
+
117
121
  this.#states.push(state);
118
122
 
119
123
  state.onDetach(() => {
@@ -125,13 +129,26 @@ class SharedStateCollection {
125
129
  });
126
130
 
127
131
  state.onUpdate((newValues, oldValues) => {
128
- this.#onUpdateCallbacks.forEach(callback => callback(state, newValues, oldValues));
132
+ this.#onUpdateCallbacks.forEach(listenerPayload => {
133
+ this.#executeUpdateCallback(state, listenerPayload, newValues, oldValues);
134
+ });
135
+
129
136
  this.#onChangeCallbacks.forEach(callback => callback());
130
137
  });
131
138
 
132
139
  this.#onAttachCallbacks.forEach(callback => callback(state));
133
140
  this.#onChangeCallbacks.forEach(callback => callback());
134
- }, this.#options);
141
+ }, { excludeLocal: this.#excludeLocal });
142
+ }
143
+
144
+ #executeUpdateCallback(state, listenerPayload, newValues, oldValues) {
145
+ const { paramName, listener, unsubscribe } = listenerPayload;
146
+
147
+ if (paramName === null) {
148
+ return listener(state, newValues, oldValues, unsubscribe);
149
+ } else if (paramName in newValues) {
150
+ return listener(state, newValues[paramName], oldValues[paramName], unsubscribe);
151
+ }
135
152
  }
136
153
 
137
154
  /**
@@ -309,9 +326,37 @@ class SharedStateCollection {
309
326
  return Promise.all(promises);
310
327
  }
311
328
 
329
+ /**
330
+ * Subscribe to any updates in the collection.
331
+ *
332
+ * @overload
333
+ * @param {sharedStateCollectionOnUpdateCallback}
334
+ * callback - Callback to execute when an update is applied on a state.
335
+ * @param {Boolean} [executeListener=false] - Execute the callback immediately
336
+ * with current state values. Note that `oldValues` will be set to `{}`.
337
+ * @returns {sharedStateCollectionDeleteOnUpdateCallback} - Function that delete
338
+ * the registered listener when executed.
339
+ */
340
+ /**
341
+ * Subscribe to updates in the collection filtered by a given parameter name.
342
+ *
343
+ * @overload
344
+ * @param {SharedStateParameterName} paramName
345
+ * @param {sharedStateCollectionOnUpdateCallback}
346
+ * callback - Callback to execute when an update is applied on a state.
347
+ * @param {Boolean} [executeListener=false] - Execute the callback immediately
348
+ * with current state values. Note that `oldValues` will be set to `{}`.
349
+ * @returns {sharedStateCollectionDeleteOnUpdateCallback} - Function that delete
350
+ * the registered listener when executed.
351
+ */
312
352
  /**
313
353
  * Register a function to execute when any shared state of the collection is updated.
314
354
  *
355
+ * Alternative signatures:
356
+ * - `collection.onUpdate(callback, executeListener)`
357
+ * - `collection.onUpdate(paramName, callback, executeListener)`
358
+ *
359
+ * @param {SharedStateParameterName} paramName
315
360
  * @param {sharedStateCollectionOnUpdateCallback}
316
361
  * callback - Callback to execute when an update is applied on a state.
317
362
  * @param {Boolean} [executeListener=false] - Execute the callback immediately
@@ -319,28 +364,33 @@ class SharedStateCollection {
319
364
  * @returns {sharedStateCollectionDeleteOnUpdateCallback} - Function that delete
320
365
  * the registered listener when executed.
321
366
  */
322
- onUpdate(callback, executeListener = false) {
323
- this.#onUpdateCallbacks.add(callback);
367
+ onUpdate(...args) {
368
+ const { paramName, listener, executeListener } = sanitizeOnUpdateParams(this, ...args);
369
+
370
+ const listenerPayload = { paramName, listener };
371
+ this.#onUpdateCallbacks.add(listenerPayload);
372
+ // create unsubscribe and add to payload, to pass it back to the listeners as argument
373
+ listenerPayload.unsubscribe = () => this.#onUpdateCallbacks.delete(listenerPayload);
324
374
 
325
375
  if (executeListener === true) {
326
376
  // filter `event: true` parameters from currentValues, having them here is
327
377
  // misleading as we are in the context of a callback, not from an active read
328
- const description = this.getDescription();
378
+ const classDescription = this.getDescription();
329
379
 
330
380
  this.#states.forEach(state => {
331
381
  const currentValues = state.getValues();
332
382
 
333
- for (let name in description) {
334
- if (description[name].event === true) {
383
+ for (let name in classDescription) {
384
+ if (classDescription[name].event === true) {
335
385
  delete currentValues[name];
336
386
  }
337
387
  }
338
388
 
339
- callback(state, currentValues, {});
389
+ this.#executeUpdateCallback(state, listenerPayload, currentValues, {});
340
390
  });
341
391
  }
342
392
 
343
- return () => this.#onUpdateCallbacks.delete(callback);
393
+ return listenerPayload.unsubscribe;
344
394
  }
345
395
 
346
396
  /**
@@ -0,0 +1,102 @@
1
+ import {
2
+ isString,
3
+ isFunction,
4
+ } from '@ircam/sc-utils';
5
+
6
+ export function sanitizeOnUpdateParams(caller, paramNameOrListener, listenerOrExecuteCallback, someExecuteListener) {
7
+ const className = caller.constructor.name;
8
+
9
+ let paramName;
10
+ let listener;
11
+ let executeListener;
12
+
13
+ // updateUpdate(listener)
14
+ if (arguments.length === 2) {
15
+ if (!isFunction(paramNameOrListener)) {
16
+ throw new TypeError(`Cannot execute 'onUpdate(listener)' on ${className} (overload resolution failed): listener must be a function`);
17
+ }
18
+
19
+ paramName = null;
20
+ listener = paramNameOrListener;
21
+ executeListener = false;
22
+ }
23
+
24
+ // updateUpdate(paramName, listener)
25
+ // updateUpdate(listener, executeListener)
26
+ if (arguments.length === 3) {
27
+ if (isString(paramNameOrListener)) {
28
+ if (!isFunction(listenerOrExecuteCallback)) {
29
+ throw new TypeError(`Cannot execute 'onUpdate(paramName, listener)' on ${className} (overload resolution failed): listener must be a function`);
30
+ }
31
+
32
+ paramName = paramNameOrListener;
33
+ listener = listenerOrExecuteCallback;
34
+ executeListener = false;
35
+
36
+ } else if (isFunction(paramNameOrListener)) {
37
+ if (typeof listenerOrExecuteCallback !== 'boolean') {
38
+ throw new TypeError(`Cannot execute 'onUpdate(callback, executeListener)' on ${className} (overload resolution failed): executeListener must be a boolean`);
39
+ }
40
+
41
+ paramName = null;
42
+ listener = paramNameOrListener;
43
+ executeListener = listenerOrExecuteCallback;
44
+ } else {
45
+ throw new TypeError(`Cannot execute 'onUpdate' on ${className} (overload resolution failed): possible signatures 'onUpdate(paramName, listener)' or 'onUpdate(listener, executeListener)'`);
46
+ }
47
+ }
48
+
49
+ // onUpdate(paramName, listener, executeListener)
50
+ if (arguments.length === 4) {
51
+ if (!isString(paramNameOrListener)) {
52
+ throw new TypeError(`Cannot execute 'onUpdate(paramName, listener, executeListener)' on ${className} (overload resolution failed): paramName must be a string`);
53
+ }
54
+
55
+ if (!isFunction(listenerOrExecuteCallback)) {
56
+ throw new TypeError(`Cannot execute 'onUpdate(paramName, listener, executeListener)' on ${className} (overload resolution failed): listener must be a function`);
57
+ }
58
+
59
+ if (typeof someExecuteListener !== 'boolean') {
60
+ throw new TypeError(`Cannot execute 'onUpdate(paramName, listener, executeListener)' on ${className} (overload resolution failed): executeListener must be a boolean`);
61
+ }
62
+
63
+ paramName = paramNameOrListener;
64
+ listener = listenerOrExecuteCallback;
65
+ executeListener = someExecuteListener;
66
+ }
67
+
68
+ // Check that paramName exists in description
69
+ if (paramName !== null) {
70
+ try {
71
+ caller.getDescription(paramName);
72
+ } catch {
73
+ throw new ReferenceError(`Cannot execute 'onUpdate' on ${className}: parameter '${paramName}' does not exists`);
74
+ }
75
+ }
76
+
77
+ return { paramName, listener, executeListener };
78
+ }
79
+
80
+
81
+ /**
82
+ * Check that given filters match the class description
83
+ * Used in ServerStateManager and SharedStateCollection
84
+ * @private
85
+ */
86
+ export function checkValidFilters(options, className, classDescription) {
87
+ const classParams = Object.keys(classDescription);
88
+
89
+ for (let filterName of ['whiteList', 'backList']) {
90
+ if (!options[filterName]) {
91
+ continue;
92
+ }
93
+
94
+ const list = options[filterName];
95
+ const invalid = list.filter(paramName => !classParams.includes(paramName));
96
+
97
+ if (invalid.length > 0) {
98
+ const msg = `Invalid filter (${invalid.join(', ')}) for shared state class '${className}'`;
99
+ throw new Error(msg);
100
+ }
101
+ }
102
+ }
@@ -1,2 +1,2 @@
1
1
  // [WARNING] This file is generated, do not edit
2
- export default '5.6.0';
2
+ export default '5.8.0';