@soundworks/core 4.0.0-alpha.4 → 4.0.0-alpha.6

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/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # `soundworks`
1
+ # soundworks
2
2
 
3
3
  [![npm version](https://badge.fury.io/js/@soundworks%2Fcore.svg)](https://badge.fury.io/js/@soundworks%2Fcore)
4
4
  [![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/)
@@ -21,7 +21,7 @@ npx @soundworks/create@latest
21
21
 
22
22
  ![./assets/soundworks-create-min.gif](./assets/soundworks-create-min.gif)
23
23
 
24
- See [https://soundworks.dev/guides/getting-started.html](https://soundworks.dev/guides/getting-started.html) for more informations on the wizard and how to start with `soundworks`.
24
+ See [https://soundworks.dev/tutorials/getting-started.html](https://soundworks.dev/tutorials/getting-started.html) for more informations on the wizard and how to start with `soundworks`.
25
25
 
26
26
  <!--
27
27
  ## Documentation
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soundworks/core",
3
- "version": "4.0.0-alpha.4",
3
+ "version": "4.0.0-alpha.6",
4
4
  "description": "Open-source creative coding framework for distributed applications based on Web technologies",
5
5
  "authors": [
6
6
  "Benjamin Matuszewski",
@@ -43,30 +43,29 @@
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",
55
+ "@ircam/sc-utils": "^1.0.1",
56
56
  "chalk": "^5.2.0",
57
57
  "columnify": "^1.6.0",
58
58
  "compression": "^1.7.1",
59
+ "cross-fetch": "^3.1.5",
59
60
  "express": "^4.18.1",
60
61
  "fast-deep-equal": "^3.1.3",
61
62
  "fast-text-encoding": "^1.0.0",
62
- "is-plain-obj": "^4.1.0",
63
63
  "isomorphic-ws": "^5.0.0",
64
64
  "keyv": "^4.2.2",
65
65
  "keyv-file": "^0.2.0",
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",
@@ -81,7 +80,7 @@
81
80
  "docdash": "^2.0.1",
82
81
  "dotenv": "^16.0.3",
83
82
  "eslint": "^8.32.0",
84
- "eslint-plugin-jsdoc": "^39.6.4",
83
+ "eslint-plugin-jsdoc": "^40.0.0",
85
84
  "generate-changelog": "^1.8.0",
86
85
  "jsdoc": "^4.0.0",
87
86
  "markdown-toc": "^1.2.0",
@@ -1,4 +1,4 @@
1
- import isPlainObject from 'is-plain-obj';
1
+ import { isBrowser, isPlainObject } from '@ircam/sc-utils';
2
2
 
3
3
  import ContextManager from './ContextManager.js';
4
4
  import PluginManager from './PluginManager.js';
@@ -10,7 +10,6 @@ import {
10
10
  CLIENT_HANDSHAKE_ERROR,
11
11
  AUDIT_STATE_NAME,
12
12
  } from '../common/constants.js';
13
- import { isBrowser } from '../common/utils.js';
14
13
  import logger from '../common/logger.js';
15
14
 
16
15
  /**
@@ -199,6 +198,12 @@ class Client {
199
198
  */
200
199
  this.status = 'idle';
201
200
 
201
+ /**
202
+ * Token of the client if connected through HTTP authentication.
203
+ * @private
204
+ */
205
+ this.token = null;
206
+
202
207
  /** @private */
203
208
  this._onStatusChangeCallbacks = new Set();
204
209
  /** @private */
@@ -241,9 +246,10 @@ class Client {
241
246
  try {
242
247
  await new Promise((resolve, reject) => {
243
248
  // wait for handshake response before starting stateManager and pluginManager
244
- this.socket.addListener(CLIENT_HANDSHAKE_RESPONSE, async ({ id, uuid }) => {
249
+ this.socket.addListener(CLIENT_HANDSHAKE_RESPONSE, async ({ id, uuid, token }) => {
245
250
  this.id = id;
246
251
  this.uuid = uuid;
252
+ this.token = token;
247
253
 
248
254
  resolve();
249
255
  });
@@ -251,7 +257,7 @@ class Client {
251
257
  this.socket.addListener(CLIENT_HANDSHAKE_ERROR, (err) => {
252
258
  let msg = ``;
253
259
 
254
- switch(err.type) {
260
+ switch (err.type) {
255
261
  case 'invalid-client-type':
256
262
  msg = `[soundworks:Client] ${err.message}`;
257
263
  break;
@@ -275,7 +281,7 @@ class Client {
275
281
 
276
282
  this.socket.send(CLIENT_HANDSHAKE_REQUEST, payload);
277
283
  });
278
- } catch(err) {
284
+ } catch (err) {
279
285
  throw new Error(err);
280
286
  }
281
287
 
@@ -209,7 +209,7 @@ class Context {
209
209
  const reqId = storeRequestPromise(resolve, reject);
210
210
  this.client.socket.send(CONTEXT_ENTER_REQUEST, reqId, this.name);
211
211
  });
212
- } catch(err) {
212
+ } catch (err) {
213
213
  throw new Error(err);
214
214
  }
215
215
  }
@@ -243,7 +243,7 @@ class Context {
243
243
  const reqId = storeRequestPromise(resolve, reject);
244
244
  this.client.socket.send(CONTEXT_EXIT_REQUEST, reqId, this.name);
245
245
  });
246
- } catch(err) {
246
+ } catch (err) {
247
247
  throw new Error(err);
248
248
  }
249
249
  }
@@ -78,7 +78,7 @@ class ContextManager {
78
78
  this._contextStartPromises.set(contextName, startPromise);
79
79
  await startPromise;
80
80
  context.status = 'started';
81
- } catch(err) {
81
+ } catch (err) {
82
82
  context.status = 'errored';
83
83
  throw new Error(err);
84
84
  }
@@ -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,3 +1,5 @@
1
+ import { isBrowser } from '@ircam/sc-utils';
2
+ import fetch from 'cross-fetch';
1
3
  import WebSocket from 'isomorphic-ws';
2
4
 
3
5
  import {
@@ -7,7 +9,6 @@ import {
7
9
  unpackStringMessage,
8
10
  } from '../common/sockets-utils.js';
9
11
  import logger from '../common/logger.js';
10
- import { isBrowser } from '../common/utils.js';
11
12
 
12
13
  // WebSocket events:
13
14
  //
@@ -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
  };
@@ -1,7 +1,6 @@
1
- import isPlainObject from 'is-plain-obj';
1
+ import { isPlainObject, isString } from '@ircam/sc-utils';
2
2
 
3
3
  import logger from './logger.js';
4
- import { isString } from './utils.js';
5
4
 
6
5
  /**
7
6
  * Shared functionnality between server-side and client-size plugin manager
@@ -111,9 +110,9 @@ class BasePluginManager {
111
110
  try {
112
111
  await Promise.all(promises);
113
112
  this.status = 'started';
114
- } catch(err) {
113
+ } catch (err) {
115
114
  this.status = 'errored';
116
- throw new Error(err);
115
+ throw err; // throw initial error
117
116
  }
118
117
  }
119
118
 
@@ -170,7 +169,7 @@ class BasePluginManager {
170
169
  this._instanceStartPromises.set(id, startPromise);
171
170
 
172
171
  await startPromise;
173
- } catch(err) {
172
+ } catch (err) {
174
173
  errored = true;
175
174
  this._propagateStateChange(instance, 'errored');
176
175
  throw err;
@@ -1,3 +1,4 @@
1
+ import { isPlainObject } from '@ircam/sc-utils';
1
2
  import ParameterBag from './ParameterBag.js';
2
3
  import {
3
4
  DELETE_REQUEST,
@@ -34,7 +35,7 @@ class BaseSharedState {
34
35
  try {
35
36
  /** @private */
36
37
  this._parameters = new ParameterBag(schema, initValues);
37
- } catch(err) {
38
+ } catch (err) {
38
39
  console.error(err.stack);
39
40
 
40
41
  throw new Error(`Error creating or attaching state "${schemaName}" w/ values:\n
@@ -137,7 +138,6 @@ ${JSON.stringify(initValues, null, 2)}`);
137
138
  this._client.transport.addListener(`${DETACH_ERROR}-${this.id}`, (reqId, msg) => {
138
139
  rejectRequest(reqId, msg);
139
140
  });
140
-
141
141
  }
142
142
  }
143
143
 
@@ -149,7 +149,7 @@ ${JSON.stringify(initValues, null, 2)}`);
149
149
  // Monkey patch detach so it throws if called twice. Doing nothing blocks
150
150
  // the process on a second `detach` call as the Promise never resolves
151
151
  this.detach = () => {
152
- 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`);
153
153
  };
154
154
  }
155
155
 
@@ -255,16 +255,23 @@ ${JSON.stringify(initValues, null, 2)}`);
255
255
  * const updates = await state.set({ myParam: Math.random() });
256
256
  */
257
257
  async set(updates, context = null) {
258
+ if (!isPlainObject(updates)) {
259
+ throw new ReferenceError(`[SharedState] State "${this.schemaName}": state.set(updates[, context]) should receive an object as first parameter`);
260
+ }
261
+
262
+ if (context !== null && !isPlainObject(context)) {
263
+ throw new ReferenceError(`[SharedState] State "${this.schemaName}": state.set(updates[, context]) should receive an object as second parameter`);
264
+ }
265
+
258
266
  // handle immediate option
259
267
  const immediateNewValues = {};
260
268
  const immediateOldValues = {};
261
269
  let propagateNow = false;
262
270
 
263
271
  for (let name in updates) {
264
- // throw early (client-side and not only server-side) if parameter is undefined
265
- if (!this._parameters.has(name)) {
266
- throw new ReferenceError(`[stateManager] Cannot set value of undefined parameter "${name}"`);
267
- }
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]);
268
275
 
269
276
  // @note: general idea...
270
277
  // if immediate=true
@@ -303,21 +310,41 @@ ${JSON.stringify(initValues, null, 2)}`);
303
310
  }
304
311
 
305
312
  /**
306
- * 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.
307
315
  *
308
316
  * @param {string} name - Name of the param.
309
317
  * @throws Throws if `name` does not correspond to an existing field
310
318
  * of the state.
311
319
  * @return {mixed}
312
320
  * @example
313
- * const value = state.get('name');
321
+ * const value = state.get('paramName');
314
322
  */
315
323
  get(name) {
316
324
  return this._parameters.get(name);
317
325
  }
318
326
 
319
327
  /**
320
- * 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.
321
348
  *
322
349
  * @return {object}
323
350
  * @example
@@ -327,6 +354,23 @@ ${JSON.stringify(initValues, null, 2)}`);
327
354
  return this._parameters.getValues();
328
355
  }
329
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
+
330
374
  /**
331
375
  * Get the schema from which the state has been created.
332
376
  *
@@ -407,14 +451,14 @@ ${JSON.stringify(initValues, null, 2)}`);
407
451
  if (this._isOwner) {
408
452
  return this.detach();
409
453
  } else {
410
- throw new Error(`[stateManager] can delete state "${this.schemaName}", only owner of the state (i.e. the node that "create[d]" it) can delete it`);
454
+ throw new Error(`[SharedState] Cannot delete state "${this.schemaName}", only the owner of the state (i.e. the node that created it) can delete the state. Use "detach" instead.`);
411
455
  }
412
456
  }
413
457
 
414
458
  /**
415
459
  * Subscribe to state updates.
416
460
  *
417
- * @param {client.SharedState~onUpdateCallback|client.SharedState~onUpdateCallback} callback
461
+ * @param {client.SharedState~onUpdateCallback|server.SharedState~onUpdateCallback} callback
418
462
  * Callback to execute when an update is applied on the state.
419
463
  * @param {Boolean} [executeListener=false] - Execute the callback immediately
420
464
  * with current state values. (`oldValues` will be set to `{}`, and `context` to `null`)
@@ -449,8 +493,8 @@ ${JSON.stringify(initValues, null, 2)}`);
449
493
  /**
450
494
  * Register a function to execute when detaching from the state
451
495
  *
452
- * @param {Function} callback - callback to execute when detaching from the state.
453
- * 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
454
498
  * creator.
455
499
  */
456
500
  onDetach(callback) {
@@ -462,7 +506,7 @@ ${JSON.stringify(initValues, null, 2)}`);
462
506
  * Register a function to execute when the state is deleted. Only called if the
463
507
  * node was the creator of the state. Is called after `onDetach`
464
508
  *
465
- * @param {Function} callback - callback to execute when the state is deleted.
509
+ * @param {Function} callback - Callback to execute when the state is deleted.
466
510
  */
467
511
  onDelete(callback) {
468
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;