@soundworks/core 4.0.0-alpha.5 → 4.0.0-alpha.7

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.
@@ -0,0 +1,31 @@
1
+ name: GH Pages
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - '*'
7
+
8
+ permissions:
9
+ contents: write
10
+
11
+ jobs:
12
+ deploy:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - name: Checkout code
16
+ uses: actions/checkout@v3
17
+
18
+ - name: 'Install Node'
19
+ uses: actions/setup-node@v3.6.0
20
+
21
+ - name: Install dependencies
22
+ run: npm install
23
+
24
+ - name: Build Docs
25
+ run: npm run doc
26
+
27
+ - name: Deploy
28
+ uses: JamesIves/github-pages-deploy-action@v4
29
+ with:
30
+ folder: docs
31
+
@@ -0,0 +1,18 @@
1
+ name: Unit Tests
2
+ on: push
3
+
4
+ jobs:
5
+ test:
6
+ runs-on: ubuntu-latest
7
+ steps:
8
+ - name: Checkout
9
+ uses: actions/checkout@v3
10
+
11
+ - name: 'Install Node'
12
+ uses: actions/setup-node@v3.6.0
13
+
14
+ - name: Install dependencies
15
+ run: npm install
16
+
17
+ - name: "Run Test"
18
+ run: npm test -- tests/**.spec.js
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soundworks/core",
3
- "version": "4.0.0-alpha.5",
3
+ "version": "4.0.0-alpha.7",
4
4
  "description": "Open-source creative coding framework for distributed applications based on Web technologies",
5
5
  "authors": [
6
6
  "Benjamin Matuszewski",
@@ -43,13 +43,12 @@
43
43
  "node": ">=16.0.0"
44
44
  },
45
45
  "scripts": {
46
- "doc": "npm run jsdoc",
47
- "jsdoc": "rm -Rf docs && jsdoc -c .jsdoc.json --verbose && cp -R assets docs/",
46
+ "doc": "rm -Rf docs && jsdoc -c .jsdoc.json --verbose && cp -R assets docs/",
48
47
  "lint": "npx eslint src",
49
48
  "test": "mocha",
50
49
  "toc": "markdown-toc -i README.md",
51
50
  "types": "rm -Rf types && tsc",
52
- "version": "npm run toc && npm run doc && git add docs"
51
+ "version": "npm run toc"
53
52
  },
54
53
  "dependencies": {
55
54
  "@ircam/sc-gettime": "^1.0.0",
@@ -57,6 +56,7 @@
57
56
  "chalk": "^5.2.0",
58
57
  "columnify": "^1.6.0",
59
58
  "compression": "^1.7.1",
59
+ "cross-fetch": "^3.1.5",
60
60
  "express": "^4.18.1",
61
61
  "fast-deep-equal": "^3.1.3",
62
62
  "fast-text-encoding": "^1.0.0",
@@ -66,7 +66,6 @@
66
66
  "lodash.clonedeep": "^4.5.0",
67
67
  "lodash.merge": "^4.6.2",
68
68
  "pem": "^1.14.6",
69
- "serve-static": "^1.15.0",
70
69
  "template-literal": "^1.0.4",
71
70
  "uuid": "^9.0.0",
72
71
  "window-or-global": "^1.0.1",
@@ -198,6 +198,12 @@ class Client {
198
198
  */
199
199
  this.status = 'idle';
200
200
 
201
+ /**
202
+ * Token of the client if connected through HTTP authentication.
203
+ * @private
204
+ */
205
+ this.token = null;
206
+
201
207
  /** @private */
202
208
  this._onStatusChangeCallbacks = new Set();
203
209
  /** @private */
@@ -240,9 +246,10 @@ class Client {
240
246
  try {
241
247
  await new Promise((resolve, reject) => {
242
248
  // wait for handshake response before starting stateManager and pluginManager
243
- this.socket.addListener(CLIENT_HANDSHAKE_RESPONSE, async ({ id, uuid }) => {
249
+ this.socket.addListener(CLIENT_HANDSHAKE_RESPONSE, async ({ id, uuid, token }) => {
244
250
  this.id = id;
245
251
  this.uuid = uuid;
252
+ this.token = token;
246
253
 
247
254
  resolve();
248
255
  });
@@ -83,7 +83,7 @@ class PluginManager extends BasePluginManager {
83
83
  */
84
84
  constructor(client) {
85
85
  if (!(client instanceof Client)) {
86
- throw new Error(`[soundworks.PluginManager] Invalid argument, "new PluginManager(client)" should receive an instance of "soundworks.Client as argument"`);
86
+ throw new Error(`[soundworks.PluginManager] Invalid argument, "new PluginManager(client)" should receive an instance of "soundworks.Client" as argument`);
87
87
  }
88
88
 
89
89
  super(client);
@@ -0,0 +1,35 @@
1
+ import BaseSharedStateCollection from '../common/BaseSharedStateCollection.js';
2
+
3
+ // this file exists only for documentation purposesSharedStateCollection.js
4
+
5
+ /**
6
+ * @callback client.SharedStateCollection~onUpdateCallback
7
+ * @param {client.SharedState} state - State that triggered the update.
8
+ * @param {Object} newValues - Key / value pairs of the updates that have been
9
+ * applied to the state.
10
+ * @param {Object} oldValues - Key / value pairs of the updated params before
11
+ * the updates has been applied to the state.
12
+ * @param {Mixed} [context=null] - Optionnal context object that has been passed
13
+ * with the values updates in the `set` call.
14
+ */
15
+
16
+ /**
17
+ * The `SharedStateCollection` interface represent a collection of all states
18
+ * created from a given schema name on the network, at the execption of the ones
19
+ * created by the current node.
20
+ *
21
+ * ```
22
+ * const collection = await client.stateManager.getCollection('my-schema');
23
+ * const allValues = collection.getValues();
24
+ * collection.onUpdate((state, newValues, oldValues, context) => {
25
+ * // do something
26
+ * });
27
+ * ```
28
+ * @memberof client
29
+ * @extends BaseSharedStateCollection
30
+ * @inheritdoc
31
+ * @hideconstructor
32
+ */
33
+ class SharedStateCollection extends BaseSharedStateCollection {}
34
+
35
+ export default SharedStateCollection;
@@ -1,4 +1,5 @@
1
1
  import { isBrowser } from '@ircam/sc-utils';
2
+ import fetch from 'cross-fetch';
2
3
  import WebSocket from 'isomorphic-ws';
3
4
 
4
5
  import {
@@ -110,7 +111,11 @@ class Socket {
110
111
  };
111
112
  }
112
113
 
113
- const queryParams = `role=${role}&key=${key}`;
114
+ let queryParams = `role=${role}&key=${key}`;
115
+
116
+ if (config.token) {
117
+ queryParams += `&token=${config.token}`;
118
+ }
114
119
 
115
120
  // ----------------------------------------------------------
116
121
  // init string socket
@@ -146,19 +151,22 @@ class Socket {
146
151
 
147
152
  // cf. https://github.com/collective-soundworks/soundworks/issues/17
148
153
  ws.addEventListener('error', e => {
149
- if (e.type === 'error' && e.error.code === 'ECONNREFUSED') {
150
- if (!connectionRefusedLogged) {
151
- logger.log('[soundworks.Socket] Connection refused, waiting for the server to start');
152
- connectionRefusedLogged = true;
153
- }
154
-
154
+ if (e.type === 'error') {
155
155
  if (ws.terminate) {
156
156
  ws.terminate();
157
157
  } else {
158
158
  ws.close();
159
159
  }
160
160
 
161
- setTimeout(trySocket, 1000);
161
+ // for node clients, retry connection
162
+ if (e.error && e.error.code === 'ECONNREFUSED') {
163
+ if (!connectionRefusedLogged) {
164
+ logger.log('[soundworks.Socket] Connection refused, waiting for the server to start');
165
+ connectionRefusedLogged = true;
166
+ }
167
+
168
+ setTimeout(trySocket, 1000);
169
+ }
162
170
  }
163
171
  });
164
172
  };
@@ -5,26 +5,10 @@ import merge from 'lodash.merge';
5
5
  */
6
6
  class BasePlugin {
7
7
  constructor(id) {
8
- /**
9
- * User defined ID of the plugin.
10
- *
11
- * @type {string}
12
- * @see {@link client.PluginManager#register}
13
- * @see {@link server.PluginManager#register}
14
- */
15
- this.id = id;
16
-
17
- /**
18
- * Type of the plugin, i.e. the ClassName.
19
- *
20
- * Usefull to do perform some logic based on certain types of plugins without
21
- * knowing under which `id` they have been registered. (e.g. creating some generic
22
- * views, etc.)
23
- *
24
- * @type {string}
25
- * @readonly
26
- */
27
- this.type = this.constructor.name;
8
+ /** @private */
9
+ this._id = id;
10
+ /** @private */
11
+ this._type = this.constructor.name;
28
12
 
29
13
  /**
30
14
  * Options of the plugin.
@@ -58,6 +42,32 @@ class BasePlugin {
58
42
  this._onStateChangeCallbacks = new Set();
59
43
  }
60
44
 
45
+ /**
46
+ * User defined ID of the plugin.
47
+ *
48
+ * @type {string}
49
+ * @readonly
50
+ * @see {@link client.PluginManager#register}
51
+ * @see {@link server.PluginManager#register}
52
+ */
53
+ get id() {
54
+ return this._id;
55
+ }
56
+
57
+ /**
58
+ * Type of the plugin, i.e. the ClassName.
59
+ *
60
+ * Usefull to do perform some logic based on certain types of plugins without
61
+ * knowing under which `id` they have been registered. (e.g. creating some generic
62
+ * views, etc.)
63
+ *
64
+ * @type {string}
65
+ * @readonly
66
+ */
67
+ get type() {
68
+ return this._type;
69
+ }
70
+
61
71
  /**
62
72
  * Start the plugin. This method is automatically called during the client or
63
73
  * server `init()` lifecyle step. After `start()` is fulfilled the plugin should
@@ -13,7 +13,7 @@ class BasePluginManager {
13
13
  /** @private */
14
14
  this._node = node;
15
15
  /** @private */
16
- this._registeredPlugins = new Map();
16
+ this._dependencies = new Map();
17
17
  /** @private */
18
18
  this._instances = new Map();
19
19
  /** @private */
@@ -67,11 +67,29 @@ class BasePluginManager {
67
67
  throw new Error(`[soundworks.PluginManager] Invalid argument, "pluginManager.register" fourth optionnal argument should be an array`);
68
68
  }
69
69
 
70
- if (this._registeredPlugins.has(id)) {
71
- throw new Error(`[soundworks:PluginManager] Plugin "${id}" of type "${ctor.name}" already registered`);
70
+ if (this._instances.has(id)) {
71
+ throw new Error(`[soundworks:PluginManager] Plugin "${id}" already registered`);
72
72
  }
73
73
 
74
- this._registeredPlugins.set(id, { ctor, options, deps });
74
+ // we instanciate the plugin here, so that a plugin can register another one
75
+ // in its own constructor.
76
+ //
77
+ // the dependencies must be created first, so that the instance can call
78
+ // addDependency in its constructor
79
+ this._dependencies.set(id, deps);
80
+
81
+ const instance = new ctor(this._node, id, options);
82
+ this._instances.set(id, instance);
83
+ }
84
+
85
+ /**
86
+ * Manually add a dependency to a given plugin. Usefull to require a plugin
87
+ * within a plugin
88
+ *
89
+ */
90
+ addDependency(pluginId, dependencyId) {
91
+ const deps = this._dependencies.get(pluginId);
92
+ deps.push(dependencyId);
75
93
  }
76
94
 
77
95
  /**
@@ -79,7 +97,7 @@ class BasePluginManager {
79
97
  * @returns {string[]}
80
98
  */
81
99
  getRegisteredPlugins() {
82
- return Array.from(this._registeredPlugins.keys());
100
+ return Array.from(this._instances.keys());
83
101
  }
84
102
 
85
103
  /**
@@ -96,16 +114,14 @@ class BasePluginManager {
96
114
 
97
115
  this.status = 'inited';
98
116
  // instanciate all plugins
99
- for (let [id, { ctor, options }] of this._registeredPlugins.entries()) {
100
- const instance = new ctor(this._node, id, options);
101
- this._instances.set(id, instance);
117
+ for (let [id, instance] of this._instances.entries()) {
102
118
  instance.onStateChange(_values => this._propagateStateChange(instance));
103
119
  }
104
120
 
105
121
  // propagate all 'idle' statuses before start
106
122
  this._propagateStateChange();
107
123
 
108
- const promises = Array.from(this._registeredPlugins.keys()).map(id => this.unsafeGet(id));
124
+ const promises = Array.from(this._instances.keys()).map(id => this.unsafeGet(id));
109
125
 
110
126
  try {
111
127
  await Promise.all(promises);
@@ -125,8 +141,8 @@ class BasePluginManager {
125
141
 
126
142
  /**
127
143
  * Retrieve an fully started instance of a registered plugin, without checking
128
- * that the pluginManager is started. This is important for starting the plugin
129
- * manager itself.
144
+ * that the pluginManager has started. This is required for starting the plugin
145
+ * manager itself and to require a plugin from within another plugin
130
146
  *
131
147
  * @private
132
148
  */
@@ -135,7 +151,7 @@ class BasePluginManager {
135
151
  throw new Error(`[soundworks.PluginManager] Invalid argument, "pluginManager.get(name)" argument should be a string`);
136
152
  }
137
153
 
138
- if (!this._registeredPlugins.has(id)) {
154
+ if (!this._instances.has(id)) {
139
155
  throw new Error(`[soundworks:PluginManager] Cannot get plugin "${id}", plugin is not registered`);
140
156
  }
141
157
 
@@ -144,7 +160,7 @@ class BasePluginManager {
144
160
  // to dynamically register and launch plugins at runtime.
145
161
  //
146
162
  // if (!this._instances.has(id)) {
147
- // const { ctor, options } = this._registeredPlugins.get(id);
163
+ // const { ctor, options } = this._dependencies.get(id);
148
164
  // const instance = new ctor(this._node, id, options);
149
165
  // this._instances.set(id, instance);
150
166
  // }
@@ -152,7 +168,7 @@ class BasePluginManager {
152
168
  const instance = this._instances.get(id);
153
169
 
154
170
  // recursively get the dependency chain
155
- const { deps } = this._registeredPlugins.get(id);
171
+ const deps = this._dependencies.get(id);
156
172
  const promises = deps.map(id => this.unsafeGet(id));
157
173
 
158
174
  await Promise.all(promises);
@@ -138,7 +138,6 @@ ${JSON.stringify(initValues, null, 2)}`);
138
138
  this._client.transport.addListener(`${DETACH_ERROR}-${this.id}`, (reqId, msg) => {
139
139
  rejectRequest(reqId, msg);
140
140
  });
141
-
142
141
  }
143
142
  }
144
143
 
@@ -150,7 +149,7 @@ ${JSON.stringify(initValues, null, 2)}`);
150
149
  // Monkey patch detach so it throws if called twice. Doing nothing blocks
151
150
  // the process on a second `detach` call as the Promise never resolves
152
151
  this.detach = () => {
153
- throw new Error(`[stateManager] State "${this.schemaName} (${this.id})" already detached, cannot detach twice`);
152
+ throw new Error(`[SharedState] State "${this.schemaName} (${this.id})" already detached, cannot detach twice`);
154
153
  };
155
154
  }
156
155
 
@@ -270,10 +269,9 @@ ${JSON.stringify(initValues, null, 2)}`);
270
269
  let propagateNow = false;
271
270
 
272
271
  for (let name in updates) {
273
- // throw early (client-side and not only server-side) if parameter is undefined
274
- if (!this._parameters.has(name)) {
275
- throw new ReferenceError(`[SharedState] State "${this.schemaName}": cannot set value of undefined parameter "${name}"`);
276
- }
272
+ // try to coerce value early, so that eventual errors are triggered early
273
+ // on the node requesting the update
274
+ const _ = this._parameters.coerceValue(name, updates[name]);
277
275
 
278
276
  // @note: general idea...
279
277
  // if immediate=true
@@ -312,21 +310,41 @@ ${JSON.stringify(initValues, null, 2)}`);
312
310
  }
313
311
 
314
312
  /**
315
- * Get the value of a paramter of the state.
313
+ * Get the value of a parameter of the state. If the parameter is of `any` type,
314
+ * a deep copy is returned.
316
315
  *
317
316
  * @param {string} name - Name of the param.
318
317
  * @throws Throws if `name` does not correspond to an existing field
319
318
  * of the state.
320
319
  * @return {mixed}
321
320
  * @example
322
- * const value = state.get('name');
321
+ * const value = state.get('paramName');
323
322
  */
324
323
  get(name) {
325
324
  return this._parameters.get(name);
326
325
  }
327
326
 
328
327
  /**
329
- * Get all the key / value pairs of the state.
328
+ * Similar to `get` but returns a reference to the underlying value in case of
329
+ * `any` type. May be usefull if the underlying value is big (e.g. sensors
330
+ * recordings, etc.) and deep cloning expensive. Be aware that if changes are
331
+ * made on the returned object, the state of your application will become
332
+ * inconsistent.
333
+ *
334
+ * @param {string} name - Name of the param.
335
+ * @throws Throws if `name` does not correspond to an existing field
336
+ * of the state.
337
+ * @return {mixed}
338
+ * @example
339
+ * const value = state.getUnsafe('paramName');
340
+ */
341
+ getUnsafe(name) {
342
+ return this._parameters.getUnsafe(name);
343
+ }
344
+
345
+ /**
346
+ * Get all the key / value pairs of the state. If a parameter is of `any`
347
+ * type, a deep copy is made.
330
348
  *
331
349
  * @return {object}
332
350
  * @example
@@ -336,6 +354,23 @@ ${JSON.stringify(initValues, null, 2)}`);
336
354
  return this._parameters.getValues();
337
355
  }
338
356
 
357
+ /**
358
+ * Get all the key / value pairs of the state. If a parameter is of `any`
359
+ * type, a deep copy is made.
360
+ * Similar to `getValues` but returns a reference to the underlying value in
361
+ * case of `any` type. May be usefull if the underlying value is big (e.g.
362
+ * sensors recordings, etc.) and deep cloning expensive. Be aware that if
363
+ * changes are made on the returned object, the state of your application will
364
+ * become inconsistent.
365
+ *
366
+ * @return {object}
367
+ * @example
368
+ * const values = state.getValues();
369
+ */
370
+ getValuesUnsafe() {
371
+ return this._parameters.getValuesUnsafe();
372
+ }
373
+
339
374
  /**
340
375
  * Get the schema from which the state has been created.
341
376
  *
@@ -423,7 +458,7 @@ ${JSON.stringify(initValues, null, 2)}`);
423
458
  /**
424
459
  * Subscribe to state updates.
425
460
  *
426
- * @param {client.SharedState~onUpdateCallback|client.SharedState~onUpdateCallback} callback
461
+ * @param {client.SharedState~onUpdateCallback|server.SharedState~onUpdateCallback} callback
427
462
  * Callback to execute when an update is applied on the state.
428
463
  * @param {Boolean} [executeListener=false] - Execute the callback immediately
429
464
  * with current state values. (`oldValues` will be set to `{}`, and `context` to `null`)
@@ -458,8 +493,8 @@ ${JSON.stringify(initValues, null, 2)}`);
458
493
  /**
459
494
  * Register a function to execute when detaching from the state
460
495
  *
461
- * @param {Function} callback - callback to execute when detaching from the state.
462
- * wether the client as called `detach`, or the state has been deleted by its
496
+ * @param {Function} callback - Callback to execute when detaching from the state.
497
+ * Whether the client as called `detach`, or the state has been deleted by its
463
498
  * creator.
464
499
  */
465
500
  onDetach(callback) {
@@ -471,7 +506,7 @@ ${JSON.stringify(initValues, null, 2)}`);
471
506
  * Register a function to execute when the state is deleted. Only called if the
472
507
  * node was the creator of the state. Is called after `onDetach`
473
508
  *
474
- * @param {Function} callback - callback to execute when the state is deleted.
509
+ * @param {Function} callback - Callback to execute when the state is deleted.
475
510
  */
476
511
  onDelete(callback) {
477
512
  this._onDeleteCallbacks.add(callback);
@@ -0,0 +1,179 @@
1
+ /**
2
+ * @private
3
+ */
4
+ class BaseSharedStateCollection {
5
+ constructor(stateManager, schemaName) {
6
+ this._stateManager = stateManager;
7
+ this._schemaName = schemaName;
8
+ this._states = [];
9
+ this._onUpdateCallbacks = new Set();
10
+ this._onAttachCallbacks = new Set();
11
+ this._onDetachCallbacks = new Set();
12
+ this._unobserve = null;
13
+ }
14
+
15
+ async _init() {
16
+ this._unobserve = await this._stateManager.observe(this._schemaName, async (schemaName, stateId, nodeId) => {
17
+ const state = await this._stateManager.attach(schemaName, stateId);
18
+
19
+ this._states.push(state);
20
+
21
+ state.onDetach(() => {
22
+ const index = this._states.indexOf(state);
23
+ this._states.splice(index, 1);
24
+
25
+ this._onDetachCallbacks.forEach(callback => callback(state));
26
+ });
27
+
28
+ state.onUpdate((newValues, oldValues, context) => {
29
+ Array.from(this._onUpdateCallbacks).forEach(callback => {
30
+ callback(state, newValues, oldValues, context);
31
+ });
32
+ });
33
+
34
+ this._onAttachCallbacks.forEach(callback => callback(state));
35
+ });
36
+ }
37
+
38
+ /**
39
+ * Size of the collection
40
+ * @type {number}
41
+ */
42
+ get length() {
43
+ return this._states.length;
44
+ }
45
+
46
+ /**
47
+ * Detach from the collection, i.e. detach all underlying shared states.
48
+ * @type {number}
49
+ */
50
+ async detach() {
51
+ this._unobserve();
52
+
53
+ const promises = Array.from(this._states).map(state => state.detach());
54
+ await Promise.all(promises);
55
+
56
+ this._onUpdateCallbacks.clear();
57
+ this._onDetachCallbacks.clear();
58
+ }
59
+
60
+ /**
61
+ * Return the current values of all the states in the collection.
62
+ * @return {Object[]}
63
+ */
64
+ getValues() {
65
+ return this._states.map(state => state.getValues());
66
+ }
67
+
68
+ /**
69
+ * Return the current param value of all the states in the collection.
70
+ *
71
+ * @param {String} name - Name of the parameter
72
+ * @return {any[]}
73
+ */
74
+ get(name) {
75
+ return this._states.map(state => state.get(name));
76
+ }
77
+
78
+ /**
79
+ * Subscribe to any state update of the collection.
80
+ *
81
+ * @param {server.SharedStateCollection~onUpdateCallback|client.SharedStateCollection~onUpdateCallback}
82
+ * callback - Callback to execute when an update is applied on a state.
83
+ * @param {Boolean} [executeListener=false] - Execute the callback immediately
84
+ * for all underlying states with current state values. (`oldValues` will be
85
+ * set to `{}`, and `context` to `null`)
86
+ * @returns {Function} - Function that delete the registered listener.
87
+ */
88
+ onUpdate(callback, executeListener = false) {
89
+ this._onUpdateCallbacks.add(callback);
90
+
91
+ if (executeListener === true) {
92
+ this._states.forEach(state => {
93
+ const currentValues = state.getValues();
94
+ const oldValues = {};
95
+ const context = null;
96
+
97
+ callback(state, currentValues, oldValues, context);
98
+ });
99
+ }
100
+
101
+ return () => this._onUpdateCallbacks.delete(callback);
102
+ }
103
+
104
+ /**
105
+ * Register a function to execute when a state is added to the collection.
106
+ *
107
+ * @param {Function} callback - callback to execute when a state is added to
108
+ * the collection.
109
+ */
110
+ onAttach(callback) {
111
+ this._onAttachCallbacks.add(callback);
112
+ }
113
+
114
+ /**
115
+ * Register a function to execute when a state is removed from the collection.
116
+ *
117
+ * @param {Function} callback - callback to execute when a state is removed
118
+ * from the collection.
119
+ */
120
+ onDetach(callback) {
121
+ this._onDetachCallbacks.add(callback);
122
+ }
123
+
124
+ /**
125
+ * Execute the given function once for each states of the collection (see `Array.forEach`).
126
+ *
127
+ * @param {Function} func - A function to execute for each element in the array.
128
+ * Its return value is discarded.
129
+ */
130
+ forEach(func) {
131
+ this._states.forEach(func);
132
+ }
133
+
134
+ /**
135
+ * Creates a new array populated with the results of calling a provided function
136
+ * on every state of the collection (see `Array.map`).
137
+ *
138
+ * @param {Function} func - A function to execute for each element in the array.
139
+ * Its return value is added as a single element in the new array.
140
+ */
141
+ map(func) {
142
+ return this._states.map(func);
143
+ }
144
+
145
+ /**
146
+ * Creates a shallow copy of a portion of the collection, filtered down to just
147
+ * the estates that pass the test implemented by the provided function (see `Array.filter`).
148
+ *
149
+ * @param {Function} func - A function to execute for each element in the array.
150
+ * It should return a truthy to keep the element in the resulting array, and a f
151
+ * alsy value otherwise.
152
+ */
153
+ filter(func) {
154
+ return this._states.filter(func);
155
+ }
156
+
157
+ /**
158
+ * Sort the elements of the collection in place (see `Array.sort`).
159
+ *
160
+ * @param {Function} func - Function that defines the sort order.
161
+ */
162
+ sort(func) {
163
+ this._states.sort(func);
164
+ }
165
+
166
+ /**
167
+ * Returns the first element of the collection that satisfies the provided testing
168
+ * function. If no values satisfy the testing function, undefined is returned.
169
+ *
170
+ * @param {Function} func - Function to execute for each element in the array.
171
+ * It should return a truthy value to indicate a matching element has been found.
172
+ * @return {}
173
+ */
174
+ find(func) {
175
+ return this._states.find(func);
176
+ }
177
+ }
178
+
179
+ export default BaseSharedStateCollection;