@soundworks/core 4.0.0-alpha.14 → 4.0.0-alpha.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.jsdoc ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "source": {
3
+ "include": [
4
+ "misc/assets/DOC.md",
5
+ "src/client",
6
+ "src/server",
7
+ "src/common"
8
+ ]
9
+ },
10
+ "plugins": [
11
+ "plugins/markdown"
12
+ ],
13
+ "opts": {
14
+ "projectTitle": "Soundworks",
15
+ "destination": "./docs",
16
+ "encoding": "utf8",
17
+ "template": "./node_modules/docdash"
18
+ },
19
+ "templates": {
20
+ "default": {
21
+ "useLongnameInNav": true
22
+ },
23
+ "footer": ""
24
+ },
25
+ "docdash": {
26
+ "sectionOrder": [
27
+ "Namespaces",
28
+ "Classes",
29
+ "Modules",
30
+ "Externals",
31
+ "Events",
32
+ "Mixins",
33
+ "Tutorials",
34
+ "Interfaces"
35
+ ],
36
+ "scripts": [
37
+ "assets/custom.css"
38
+ ],
39
+ "meta": {
40
+ "title": "soundworks",
41
+ "description": "Documentation of soundworks, a full-stack JavaScript framework for distributed WebAudio and multimedia applications"
42
+ },
43
+ "search": true,
44
+ "menu": {
45
+ "Website": {
46
+ "href":"http://soundworks.dev/",
47
+ "target":"_blank",
48
+ "class":"menu-item"
49
+ },
50
+ "Github": {
51
+ "href": "https://github.com/collective-soundworks/soundworks",
52
+ "target":"_blank",
53
+ "class":"menu-item"
54
+ }
55
+ }
56
+ }
57
+ }
package/CHANGELOG.md ADDED
@@ -0,0 +1,8 @@
1
+ ## v4.0.0-alpha.16 - 15/12/2023
2
+
3
+ - improve StateCollection performances
4
+
5
+ ## v4.0.0-alpha.15 - 13/12/2023
6
+
7
+ - Fix several issues with shared states
8
+ - **Breaking Change**: by default, `StateManager::observe` and `SharedStateCollection` now retrieve locally created states
package/README.md CHANGED
@@ -3,57 +3,38 @@
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/)
5
5
 
6
- ![soundworks-logo](./assets/logo-200x200.png)
6
+ ![soundworks-logo](./misc/assets/logo-200x200.png)
7
7
 
8
8
  Open-source creative coding framework for distributed applications based on Web technologies.
9
9
 
10
10
  Primarily focused on music, **soundworks** aims at supporting rapid development of real-time distributed applications using _JavaScript_. It provides abstractions to hide the complexity of the network and to foster very rapid-prototyping and trial-and-error workflows that are typical in artistic practices.
11
11
 
12
- *__WARNING: The version 4 of `@soundworks/core` is under heavy development.__*
13
-
14
- ## Getting Started
15
-
16
- The best and most simple way to start using **soundworks** is to use the `@soundworks/create` wizard.
17
-
18
- ```sh
19
- npx @soundworks/create@latest
20
- ```
21
-
22
- ![./assets/soundworks-create-min.gif](./assets/soundworks-create-min.gif)
23
-
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
-
26
- <!--
27
12
  ## Documentation
28
13
 
29
14
  - Guides and Tutorials: [https://soundworks.dev](https://soundworks.dev)
30
15
  - API: [https://soundworks.dev/api](https://soundworks.dev/api)
31
- -->
32
16
 
33
- ## API
17
+ ## Getting started
34
18
 
35
- The API is not publicly published for now. To access the API documentation locally, just clone this repository, go to the v4 branch and launch some http server in the docs directory. For example, using the [serve](https://www.npmjs.com/package/serve) package:
19
+ The best and most simple way to start using **soundworks** is to use the `@soundworks/create` wizard.
36
20
 
37
21
  ```sh
38
- git clone https://github.com/collective-soundworks/soundworks.git
39
- cd soundworks
40
- git checkout v4
41
- serve docs
22
+ npx @soundworks/create@latest
42
23
  ```
43
24
 
44
- Feel welcome to open an [issue](https://github.com/collective-soundworks/soundworks/issues) or a PR if you find any inconsistency, error or missing information in the documentation.
25
+ ![soundworks-create](./misc/assets/soundworks-create-min.gif)
45
26
 
46
- ## Share with Us
27
+ 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 using **soundworks**.
47
28
 
48
- If you made an application using **soundworks** please let us know here: https://github.com/collective-soundworks/soundworks/discussions/61
29
+ ## Misc
49
30
 
50
- ## TypeScript Support
31
+ ### TypeScript support
51
32
 
52
33
  Basic TypeScript support will be proposed in a (hopefully) near future.
53
34
 
54
35
  However, for maintenance reasons, we aim at following the TC39 and W3C specifications as close as possible. Therefore, we will wait for the https://github.com/tc39/proposal-type-annotations proposal to reach stage 3 to update the source code in a more integrated manner.
55
36
 
56
- ## Install
37
+ ### Manual install
57
38
 
58
39
  Note that the `@soundworks/core` package is automatically installed when creating an application using the `@soundworks/create` wizard, so most of the time you should not have to install this package manually. See [https://soundworks.dev/guides/getting-started.html](https://soundworks.dev/guides/getting-started.html) for more informations on the **soundworks** wizard.
59
40
 
@@ -61,6 +42,10 @@ Note that the `@soundworks/core` package is automatically installed when creatin
61
42
  npm install @soundworks/core
62
43
  ```
63
44
 
45
+ ## Share with Us
46
+
47
+ If you made an application using **soundworks** please let us know here: https://github.com/collective-soundworks/soundworks/discussions/61
48
+
64
49
  ## Credits
65
50
 
66
51
  [https://soundworks.dev/credits.html](https://soundworks.dev/credits.html)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soundworks/core",
3
- "version": "4.0.0-alpha.14",
3
+ "version": "4.0.0-alpha.16",
4
4
  "description": "Open-source creative coding framework for distributed applications based on Web technologies",
5
5
  "authors": [
6
6
  "Benjamin Matuszewski",
@@ -43,51 +43,45 @@
43
43
  "node": ">=16.0.0"
44
44
  },
45
45
  "scripts": {
46
- "doc": "rm -Rf docs && jsdoc -c .jsdoc.json --verbose && cp -R assets docs/",
46
+ "doc": "rm -Rf docs && jsdoc -c .jsdoc --verbose && cp -R misc/assets docs/",
47
47
  "lint": "npx eslint src",
48
- "test": "mocha",
49
- "toc": "markdown-toc -i README.md",
48
+ "test": "npx mocha",
49
+ "test:all": "npx mocha tests/*/*.spec.js",
50
50
  "types": "rm -Rf types && tsc",
51
- "preversion": "npm run toc && git commit -am 'docs: build' --allow-empty"
51
+ "postversion": "node ./misc/scripts/check-changelog.js"
52
52
  },
53
53
  "dependencies": {
54
- "@ircam/sc-gettime": "^1.0.1",
55
- "@ircam/sc-utils": "^1.1.1",
54
+ "@ircam/sc-utils": "^1.3.2",
56
55
  "chalk": "^5.3.0",
57
56
  "columnify": "^1.6.0",
58
57
  "compression": "^1.7.1",
59
- "cross-fetch": "^3.1.8",
60
58
  "express": "^4.18.1",
61
59
  "fast-deep-equal": "^3.1.3",
62
60
  "fast-text-encoding": "^1.0.0",
63
61
  "isomorphic-ws": "^5.0.0",
64
- "keyv": "^4.2.2",
65
- "keyv-file": "^0.2.0",
66
- "lodash.clonedeep": "^4.5.0",
67
- "lodash.merge": "^4.6.2",
62
+ "keyv": "^4.5.4",
63
+ "keyv-file": "^0.3.0",
64
+ "lodash": "^4.17.21",
68
65
  "pem": "^1.14.8",
69
66
  "template-literal": "^1.0.4",
70
- "uuid": "^9.0.0",
71
- "window-or-global": "^1.0.1",
72
- "ws": "^8.12.0"
67
+ "uuid": "^9.0.1",
68
+ "ws": "^8.15.0"
73
69
  },
74
70
  "devDependencies": {
75
71
  "@ircam/eslint-config": "^1.3.0",
76
- "chai": "^4.3.6",
72
+ "chai": "^4.3.10",
77
73
  "chai-shallow-deep-equal": "^1.4.6",
78
74
  "commitizen": "^4.2.5",
79
75
  "cz-conventional-changelog": "^3.3.0",
80
- "docdash": "^2.0.1",
76
+ "docdash": "^2.0.2",
81
77
  "dotenv": "^16.3.1",
82
- "eslint": "^8.44.0",
83
- "eslint-plugin-jsdoc": "^46.4.3",
84
- "generate-changelog": "^1.8.0",
78
+ "eslint": "^8.55.0",
79
+ "eslint-plugin-jsdoc": "^46.9.0",
85
80
  "jsdoc": "^4.0.0",
86
- "markdown-toc": "^1.2.0",
87
81
  "mocha": "^10.2.0",
88
- "puppeteer": "^20.7.4",
82
+ "puppeteer": "^21.6.0",
89
83
  "tcp-ping": "^0.1.1",
90
- "typescript": "^5.1.6"
84
+ "typescript": "^5.3.3"
91
85
  },
92
86
  "optionalDependencies": {
93
87
  "bufferutil": "^4.0.5",
@@ -1,4 +1,5 @@
1
1
  import Client from './Client.js';
2
+ import PromiseStore from '../common/PromiseStore.js';
2
3
  import {
3
4
  CONTEXT_ENTER_REQUEST,
4
5
  CONTEXT_ENTER_RESPONSE,
@@ -7,11 +8,6 @@ import {
7
8
  CONTEXT_EXIT_RESPONSE,
8
9
  CONTEXT_EXIT_ERROR,
9
10
  } from '../common/constants.js';
10
- import {
11
- storeRequestPromise,
12
- resolveRequest,
13
- rejectRequest,
14
- } from '../common/promise-store.js';
15
11
 
16
12
  /**
17
13
  * Base class to extend in order to implement a new Context.
@@ -81,12 +77,15 @@ class Context {
81
77
  */
82
78
  this.status = 'idle';
83
79
 
80
+ /** @private */
81
+ this._promiseStore = new PromiseStore(this.constructor.name);
82
+
84
83
  this.client.socket.addListener(CONTEXT_ENTER_RESPONSE, (reqId, contextName) => {
85
84
  if (contextName !== this.name) {
86
85
  return;
87
86
  }
88
87
 
89
- resolveRequest(reqId);
88
+ this._promiseStore.resolve(reqId);
90
89
  });
91
90
 
92
91
  this.client.socket.addListener(CONTEXT_ENTER_ERROR, (reqId, contextName, msg) => {
@@ -94,7 +93,7 @@ class Context {
94
93
  return;
95
94
  }
96
95
 
97
- rejectRequest(reqId, msg);
96
+ this._promiseStore.reject(reqId, msg);
98
97
  });
99
98
 
100
99
  this.client.socket.addListener(CONTEXT_EXIT_RESPONSE, (reqId, contextName) => {
@@ -102,7 +101,7 @@ class Context {
102
101
  return;
103
102
  }
104
103
 
105
- resolveRequest(reqId);
104
+ this._promiseStore.resolve(reqId);
106
105
  });
107
106
 
108
107
  this.client.socket.addListener(CONTEXT_EXIT_ERROR, (reqId, contextName, msg) => {
@@ -110,7 +109,7 @@ class Context {
110
109
  return;
111
110
  }
112
111
 
113
- rejectRequest(reqId, msg);
112
+ this._promiseStore.reject(reqId, msg);
114
113
  });
115
114
 
116
115
  this.client.contextManager.register(this);
@@ -206,7 +205,7 @@ class Context {
206
205
  // we need the try/catch block to change the promise rejection into proper error
207
206
  try {
208
207
  await new Promise((resolve, reject) => {
209
- const reqId = storeRequestPromise(resolve, reject);
208
+ const reqId = this._promiseStore.add(resolve, reject, 'enter-context');
210
209
  this.client.socket.send(CONTEXT_ENTER_REQUEST, reqId, this.name);
211
210
  });
212
211
  } catch (err) {
@@ -240,7 +239,7 @@ class Context {
240
239
  // we need the try/catch block to change the promise rejection into proper error
241
240
  try {
242
241
  await new Promise((resolve, reject) => {
243
- const reqId = storeRequestPromise(resolve, reject);
242
+ const reqId = this._promiseStore.add(resolve, reject, 'exit-context');
244
243
  this.client.socket.send(CONTEXT_EXIT_REQUEST, reqId, this.name);
245
244
  });
246
245
  } catch (err) {
@@ -15,8 +15,11 @@ import BaseSharedStateCollection from '../common/BaseSharedStateCollection.js';
15
15
 
16
16
  /**
17
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.
18
+ * created from a given schema name on the network.
19
+ *
20
+ * It can optionnaly exclude the states created by the current node.
21
+ *
22
+ * See {@link client.StateManager#getCollection} for factory method API
20
23
  *
21
24
  * ```
22
25
  * const collection = await client.stateManager.getCollection('my-schema');
@@ -1,5 +1,4 @@
1
1
  import { isBrowser } from '@ircam/sc-utils';
2
- import fetch from 'cross-fetch';
3
2
  import WebSocket from 'isomorphic-ws';
4
3
 
5
4
  import {
@@ -1,4 +1,4 @@
1
- import merge from 'lodash.merge';
1
+ import merge from 'lodash/merge.js';
2
2
 
3
3
  /**
4
4
  * @private
@@ -114,7 +114,7 @@ class BasePluginManager {
114
114
 
115
115
  this.status = 'inited';
116
116
  // instanciate all plugins
117
- for (let [id, instance] of this._instances.entries()) {
117
+ for (let [_id, instance] of this._instances.entries()) {
118
118
  instance.onStateChange(_values => this._propagateStateChange(instance));
119
119
  }
120
120
 
@@ -1,5 +1,6 @@
1
1
  import { isPlainObject } from '@ircam/sc-utils';
2
2
  import ParameterBag from './ParameterBag.js';
3
+ import PromiseStore from './PromiseStore.js';
3
4
  import {
4
5
  DELETE_REQUEST,
5
6
  DELETE_RESPONSE,
@@ -13,11 +14,14 @@ import {
13
14
  UPDATE_ABORT,
14
15
  UPDATE_NOTIFICATION,
15
16
  } from './constants.js';
16
- import {
17
- storeRequestPromise,
18
- resolveRequest,
19
- rejectRequest,
20
- } from './promise-store.js';
17
+
18
+ // @todo - replace isOwner by `type`:
19
+ // enum {
20
+ // 'owner'
21
+ // 'attached'
22
+ // 'collection-controlller'
23
+ // 'collection-attached'
24
+ // }
21
25
 
22
26
  class BaseSharedState {
23
27
  constructor(id, remoteId, schemaName, schema, client, isOwner, manager, initValues = {}) {
@@ -33,8 +37,13 @@ class BaseSharedState {
33
37
  this._client = client;
34
38
  /** @private */
35
39
  this._manager = manager;
36
-
40
+ /**
41
+ * true is the state has been detached or deleted
42
+ * @private
43
+ */
37
44
  this._detached = false;
45
+ /* @private */
46
+ this._promiseStore = new PromiseStore(this.constructor.name);
38
47
 
39
48
  try {
40
49
  /** @private */
@@ -56,13 +65,13 @@ ${JSON.stringify(initValues, null, 2)}`);
56
65
  // add listener for state updates
57
66
  this._client.transport.addListener(`${UPDATE_RESPONSE}-${this.id}-${this.remoteId}`, async (reqId, updates, context) => {
58
67
  const updated = await this._commit(updates, context, true, true);
59
- resolveRequest(reqId, updated);
68
+ this._promiseStore.resolve(reqId, updated);
60
69
  });
61
70
 
62
71
  // retrieve values but do not propagate to subscriptions
63
72
  this._client.transport.addListener(`${UPDATE_ABORT}-${this.id}-${this.remoteId}`, async (reqId, updates, context) => {
64
73
  const updated = await this._commit(updates, context, false, true);
65
- resolveRequest(reqId, updated);
74
+ this._promiseStore.resolve(reqId, updated);
66
75
  });
67
76
 
68
77
  this._client.transport.addListener(`${UPDATE_NOTIFICATION}-${this.id}-${this.remoteId}`, async (updates, context) => {
@@ -101,7 +110,7 @@ ${JSON.stringify(initValues, null, 2)}`);
101
110
  for (let callback of this._onDetachCallbacks) {
102
111
  try {
103
112
  await callback();
104
- } catch(err) {
113
+ } catch (err) {
105
114
  console.error(err.message);
106
115
  }
107
116
  }
@@ -112,7 +121,9 @@ ${JSON.stringify(initValues, null, 2)}`);
112
121
  }
113
122
  }
114
123
 
115
- this._clearDetach();
124
+ this._onDetachCallbacks.clear();
125
+ this._onDeleteCallbacks.clear();
126
+ this._promiseStore.flush();
116
127
  });
117
128
 
118
129
 
@@ -132,12 +143,14 @@ ${JSON.stringify(initValues, null, 2)}`);
132
143
  await callback();
133
144
  }
134
145
 
135
- this._clearDetach();
136
- resolveRequest(reqId, this);
146
+ this._onDetachCallbacks.clear();
147
+ this._onDeleteCallbacks.clear();
148
+ this._promiseStore.resolve(reqId, this);
149
+ this._promiseStore.flush();
137
150
  });
138
151
 
139
152
  this._client.transport.addListener(`${DELETE_ERROR}-${this.id}`, (reqId, msg) => {
140
- rejectRequest(reqId, msg);
153
+ this._promiseStore.reject(reqId, msg);
141
154
  });
142
155
 
143
156
  } else {
@@ -152,13 +165,18 @@ ${JSON.stringify(initValues, null, 2)}`);
152
165
  await callback();
153
166
  }
154
167
 
155
- this._clearDetach();
156
- resolveRequest(reqId, this);
168
+ this._onDetachCallbacks.clear();
169
+ this._onDeleteCallbacks.clear();
170
+ this._promiseStore.resolve(reqId, this);
171
+ this._promiseStore.flush();
157
172
  });
158
173
 
159
174
  // the state does not exists anymore in the server (should not happen)
160
175
  this._client.transport.addListener(`${DETACH_ERROR}-${this.id}`, (reqId, msg) => {
161
- rejectRequest(reqId, msg);
176
+ this._onDetachCallbacks.clear();
177
+ this._onDeleteCallbacks.clear();
178
+ this._promiseStore.reject(reqId, msg);
179
+ this._promiseStore.flush();
162
180
  });
163
181
  }
164
182
  }
@@ -200,13 +218,6 @@ ${JSON.stringify(initValues, null, 2)}`);
200
218
  return this._isOwner;
201
219
  }
202
220
 
203
- /** @private */
204
- _clearDetach() {
205
- this._onDetachCallbacks.clear();
206
- this._onDeleteCallbacks.clear();
207
- this._detached = true;
208
- }
209
-
210
221
  /** @private */
211
222
  _clearTransport() {
212
223
  // remove listeners
@@ -325,7 +336,7 @@ ${JSON.stringify(initValues, null, 2)}`);
325
336
  for (let name in updates) {
326
337
  // try to coerce value early, so that eventual errors are triggered early
327
338
  // on the node requesting the update
328
- const _ = this._parameters.coerceValue(name, updates[name]);
339
+ this._parameters.coerceValue(name, updates[name]);
329
340
 
330
341
  // @note: general idea...
331
342
  // if immediate=true
@@ -358,7 +369,7 @@ ${JSON.stringify(initValues, null, 2)}`);
358
369
 
359
370
  // go through server-side normal behavior
360
371
  return new Promise((resolve, reject) => {
361
- const reqId = storeRequestPromise(resolve, reject);
372
+ const reqId = this._promiseStore.add(resolve, reject, 'update-request');
362
373
  this._client.transport.emit(`${UPDATE_REQUEST}-${this.id}-${this.remoteId}`, reqId, updates, context);
363
374
  });
364
375
  }
@@ -426,12 +437,12 @@ ${JSON.stringify(initValues, null, 2)}`);
426
437
  }
427
438
 
428
439
  /**
429
- * Get the schema from which the state has been created.
440
+ * Definition of schema from which the state has been created.
430
441
  *
431
- * @param {string} [name=null] - If given, returns only the definition corresponding
432
- * to the given param name.
442
+ * @param {string} [name=null] - If given, returns only the definition
443
+ * corresponding to the given param name.
433
444
  * @throws Throws if `name` does not correspond to an existing field
434
- * of the state.
445
+ * of the schema.
435
446
  * @return {object}
436
447
  * @example
437
448
  * const schema = state.getSchema();
@@ -477,16 +488,17 @@ ${JSON.stringify(initValues, null, 2)}`);
477
488
  throw new Error(`[SharedState] State "${this.schemaName} (${this.id})" already detached, cannot detach twice`);
478
489
  }
479
490
 
491
+ this._detached = true; // mark detached early
480
492
  this._onUpdateCallbacks.clear();
481
493
 
482
494
  if (this._isOwner) {
483
495
  return new Promise((resolve, reject) => {
484
- const reqId = storeRequestPromise(resolve, reject);
496
+ const reqId = this._promiseStore.add(resolve, reject, 'delete-request');
485
497
  this._client.transport.emit(`${DELETE_REQUEST}-${this.id}-${this.remoteId}`, reqId);
486
498
  });
487
499
  } else {
488
500
  return new Promise((resolve, reject) => {
489
- const reqId = storeRequestPromise(resolve, reject);
501
+ const reqId = this._promiseStore.add(resolve, reject, 'detach-request');
490
502
  this._client.transport.emit(`${DETACH_REQUEST}-${this.id}-${this.remoteId}`, reqId);
491
503
  });
492
504
  }
@@ -1,11 +1,20 @@
1
+ import {
2
+ kCreateCollectionController,
3
+ kAttachInCollection,
4
+ } from './BaseStateManager.js';
5
+
1
6
  /**
2
7
  * @private
3
8
  */
4
9
  class BaseSharedStateCollection {
5
- constructor(stateManager, schemaName) {
10
+ constructor(stateManager, schemaName, options = {}) {
6
11
  this._stateManager = stateManager;
7
12
  this._schemaName = schemaName;
13
+ this._options = Object.assign({ excludeLocal: false }, options);
14
+
15
+ this._controller = null;
8
16
  this._states = [];
17
+
9
18
  this._onUpdateCallbacks = new Set();
10
19
  this._onAttachCallbacks = new Set();
11
20
  this._onDetachCallbacks = new Set();
@@ -13,9 +22,15 @@ class BaseSharedStateCollection {
13
22
  }
14
23
 
15
24
  async _init() {
16
- this._unobserve = await this._stateManager.observe(this._schemaName, async (schemaName, stateId, nodeId) => {
17
- const state = await this._stateManager.attach(schemaName, stateId);
25
+ this._controller = await this._stateManager[kCreateCollectionController](this._schemaName);
26
+ this._controller.onUpdate(async (updates, context) => {
27
+ for (let state of this._states) {
28
+ await state._commit(updates, context, true, false);
29
+ }
30
+ });
18
31
 
32
+ this._unobserve = await this._stateManager.observe(this._schemaName, async (schemaName, stateId) => {
33
+ const state = await this._stateManager[kAttachInCollection](schemaName, stateId);
19
34
  this._states.push(state);
20
35
 
21
36
  state.onDetach(() => {
@@ -32,11 +47,11 @@ class BaseSharedStateCollection {
32
47
  });
33
48
 
34
49
  this._onAttachCallbacks.forEach(callback => callback(state));
35
- });
50
+ }, this._options);
36
51
  }
37
52
 
38
53
  /**
39
- * Size of the collection
54
+ * Size of the collection, alias `size`
40
55
  * @type {number}
41
56
  */
42
57
  get length() {
@@ -44,17 +59,46 @@ class BaseSharedStateCollection {
44
59
  }
45
60
 
46
61
  /**
47
- * Detach from the collection, i.e. detach all underlying shared states.
62
+ * Size of the collection, , alias `length`
48
63
  * @type {number}
49
64
  */
50
- async detach() {
51
- this._unobserve();
65
+ get size() {
66
+ return this._states.length;
67
+ }
52
68
 
53
- const promises = Array.from(this._states).map(state => state.detach());
54
- await Promise.all(promises);
69
+ /**
70
+ * Name of the schema from which the collection has been created.
71
+ * @type {String}
72
+ * @readonly
73
+ */
74
+ get schemaName() {
75
+ return this._controller.schemaName;
76
+ }
55
77
 
56
- this._onUpdateCallbacks.clear();
57
- this._onDetachCallbacks.clear();
78
+ /**
79
+ * Definition of schema from which the collection has been created.
80
+ *
81
+ * @param {string} [name=null] - If given, returns only the definition
82
+ * corresponding to the given param name.
83
+ * @throws Throws if `name` does not correspond to an existing field
84
+ * of the schema.
85
+ * @return {object}
86
+ * @example
87
+ * const schema = collection.getSchema();
88
+ */
89
+ getSchema(name = null) {
90
+ return this._controller.getSchema(name);
91
+ }
92
+
93
+ /**
94
+ * Get the default values as declared in the schema.
95
+ *
96
+ * @return {object}
97
+ * @example
98
+ * const defaults = state.getDefaults();
99
+ */
100
+ getDefaults() {
101
+ return this._controller.getDefaults();
58
102
  }
59
103
 
60
104
  /**
@@ -83,8 +127,7 @@ class BaseSharedStateCollection {
83
127
  * current call and will be passed as third argument to all update listeners.
84
128
  */
85
129
  async set(updates, context = null) {
86
- const promises = this._states.map(state => state.set(updates, context));
87
- return Promise.all(promises);
130
+ return await this._controller.set(updates, context);
88
131
  }
89
132
 
90
133
  /**
@@ -145,6 +188,22 @@ class BaseSharedStateCollection {
145
188
  return () => this._onDetachCallbacks.delete(callback);
146
189
  }
147
190
 
191
+ /**
192
+ * Detach from the collection, i.e. detach all underlying shared states.
193
+ * @type {number}
194
+ */
195
+ async detach() {
196
+ this._unobserve();
197
+ this._onUpdateCallbacks.clear();
198
+
199
+ await this._controller.delete();
200
+
201
+ const promises = this._states.map(state => state.detach());
202
+ await Promise.all(promises);
203
+
204
+ this._onDetachCallbacks.clear();
205
+ }
206
+
148
207
  /**
149
208
  * Execute the given function once for each states of the collection (see `Array.forEach`).
150
209
  *
@@ -212,8 +271,8 @@ class BaseSharedStateCollection {
212
271
  } else {
213
272
  return { value: this._states[index++], done: false };
214
273
  }
215
- }
216
- }
274
+ },
275
+ };
217
276
  }
218
277
  }
219
278