@soundworks/core 4.0.0-alpha.15 → 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 +57 -0
- package/CHANGELOG.md +4 -0
- package/package.json +2 -2
- package/src/common/BaseSharedState.js +12 -4
- package/src/common/BaseSharedStateCollection.js +60 -12
- package/src/common/BaseStateManager.js +25 -2
- package/src/common/SharedStatePrivate.js +100 -39
- package/src/server/StateManager.js +107 -77
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
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@soundworks/core",
|
|
3
|
-
"version": "4.0.0-alpha.
|
|
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,7 +43,7 @@
|
|
|
43
43
|
"node": ">=16.0.0"
|
|
44
44
|
},
|
|
45
45
|
"scripts": {
|
|
46
|
-
"doc": "rm -Rf docs && jsdoc -c .jsdoc
|
|
46
|
+
"doc": "rm -Rf docs && jsdoc -c .jsdoc --verbose && cp -R misc/assets docs/",
|
|
47
47
|
"lint": "npx eslint src",
|
|
48
48
|
"test": "npx mocha",
|
|
49
49
|
"test:all": "npx mocha tests/*/*.spec.js",
|
|
@@ -15,6 +15,14 @@ import {
|
|
|
15
15
|
UPDATE_NOTIFICATION,
|
|
16
16
|
} from './constants.js';
|
|
17
17
|
|
|
18
|
+
// @todo - replace isOwner by `type`:
|
|
19
|
+
// enum {
|
|
20
|
+
// 'owner'
|
|
21
|
+
// 'attached'
|
|
22
|
+
// 'collection-controlller'
|
|
23
|
+
// 'collection-attached'
|
|
24
|
+
// }
|
|
25
|
+
|
|
18
26
|
class BaseSharedState {
|
|
19
27
|
constructor(id, remoteId, schemaName, schema, client, isOwner, manager, initValues = {}) {
|
|
20
28
|
/** @private */
|
|
@@ -429,12 +437,12 @@ ${JSON.stringify(initValues, null, 2)}`);
|
|
|
429
437
|
}
|
|
430
438
|
|
|
431
439
|
/**
|
|
432
|
-
*
|
|
440
|
+
* Definition of schema from which the state has been created.
|
|
433
441
|
*
|
|
434
|
-
* @param {string} [name=null] - If given, returns only the definition
|
|
435
|
-
* to the given param name.
|
|
442
|
+
* @param {string} [name=null] - If given, returns only the definition
|
|
443
|
+
* corresponding to the given param name.
|
|
436
444
|
* @throws Throws if `name` does not correspond to an existing field
|
|
437
|
-
* of the
|
|
445
|
+
* of the schema.
|
|
438
446
|
* @return {object}
|
|
439
447
|
* @example
|
|
440
448
|
* const schema = state.getSchema();
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import {
|
|
2
|
+
kCreateCollectionController,
|
|
3
|
+
kAttachInCollection,
|
|
4
|
+
} from './BaseStateManager.js';
|
|
5
|
+
|
|
1
6
|
/**
|
|
2
7
|
* @private
|
|
3
8
|
*/
|
|
@@ -7,6 +12,7 @@ class BaseSharedStateCollection {
|
|
|
7
12
|
this._schemaName = schemaName;
|
|
8
13
|
this._options = Object.assign({ excludeLocal: false }, options);
|
|
9
14
|
|
|
15
|
+
this._controller = null;
|
|
10
16
|
this._states = [];
|
|
11
17
|
|
|
12
18
|
this._onUpdateCallbacks = new Set();
|
|
@@ -16,9 +22,15 @@ class BaseSharedStateCollection {
|
|
|
16
22
|
}
|
|
17
23
|
|
|
18
24
|
async _init() {
|
|
19
|
-
this.
|
|
20
|
-
|
|
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
|
+
});
|
|
21
31
|
|
|
32
|
+
this._unobserve = await this._stateManager.observe(this._schemaName, async (schemaName, stateId) => {
|
|
33
|
+
const state = await this._stateManager[kAttachInCollection](schemaName, stateId);
|
|
22
34
|
this._states.push(state);
|
|
23
35
|
|
|
24
36
|
state.onDetach(() => {
|
|
@@ -55,17 +67,38 @@ class BaseSharedStateCollection {
|
|
|
55
67
|
}
|
|
56
68
|
|
|
57
69
|
/**
|
|
58
|
-
*
|
|
59
|
-
* @type {
|
|
70
|
+
* Name of the schema from which the collection has been created.
|
|
71
|
+
* @type {String}
|
|
72
|
+
* @readonly
|
|
60
73
|
*/
|
|
61
|
-
|
|
62
|
-
this.
|
|
63
|
-
|
|
74
|
+
get schemaName() {
|
|
75
|
+
return this._controller.schemaName;
|
|
76
|
+
}
|
|
64
77
|
|
|
65
|
-
|
|
66
|
-
|
|
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
|
+
}
|
|
67
92
|
|
|
68
|
-
|
|
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();
|
|
69
102
|
}
|
|
70
103
|
|
|
71
104
|
/**
|
|
@@ -94,8 +127,7 @@ class BaseSharedStateCollection {
|
|
|
94
127
|
* current call and will be passed as third argument to all update listeners.
|
|
95
128
|
*/
|
|
96
129
|
async set(updates, context = null) {
|
|
97
|
-
|
|
98
|
-
return Promise.all(promises);
|
|
130
|
+
return await this._controller.set(updates, context);
|
|
99
131
|
}
|
|
100
132
|
|
|
101
133
|
/**
|
|
@@ -156,6 +188,22 @@ class BaseSharedStateCollection {
|
|
|
156
188
|
return () => this._onDetachCallbacks.delete(callback);
|
|
157
189
|
}
|
|
158
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
|
+
|
|
159
207
|
/**
|
|
160
208
|
* Execute the given function once for each states of the collection (see `Array.forEach`).
|
|
161
209
|
*
|
|
@@ -17,6 +17,9 @@ import {
|
|
|
17
17
|
DELETE_SCHEMA,
|
|
18
18
|
} from './constants.js';
|
|
19
19
|
|
|
20
|
+
export const kCreateCollectionController = Symbol('BaseStateManager::createCollectionController');
|
|
21
|
+
export const kAttachInCollection = Symbol('BaseStateManager::attachInCollection');
|
|
22
|
+
|
|
20
23
|
/**
|
|
21
24
|
* @private
|
|
22
25
|
*/
|
|
@@ -176,12 +179,24 @@ class BaseStateManager {
|
|
|
176
179
|
*/
|
|
177
180
|
async create(schemaName, initValues = {}) {
|
|
178
181
|
return new Promise((resolve, reject) => {
|
|
179
|
-
const reqId = this._promiseStore.add(resolve, reject, 'create-
|
|
182
|
+
const reqId = this._promiseStore.add(resolve, reject, 'create-request');
|
|
180
183
|
const requireSchema = this._cachedSchemas.has(schemaName) ? false : true;
|
|
181
184
|
this.client.transport.emit(CREATE_REQUEST, reqId, schemaName, requireSchema, initValues);
|
|
182
185
|
});
|
|
183
186
|
}
|
|
184
187
|
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* @private
|
|
191
|
+
*/
|
|
192
|
+
async [kCreateCollectionController](schemaName) {
|
|
193
|
+
return new Promise((resolve, reject) => {
|
|
194
|
+
const reqId = this._promiseStore.add(resolve, reject, 'create-collection-controller-request');
|
|
195
|
+
const requireSchema = this._cachedSchemas.has(schemaName) ? false : true;
|
|
196
|
+
this.client.transport.emit(CREATE_REQUEST, reqId, schemaName, requireSchema, {}, true);
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
185
200
|
/**
|
|
186
201
|
* Attach to an existing `SharedState` instance.
|
|
187
202
|
*
|
|
@@ -198,13 +213,20 @@ class BaseStateManager {
|
|
|
198
213
|
*/
|
|
199
214
|
async attach(schemaName, stateId = null) {
|
|
200
215
|
return new Promise((resolve, reject) => {
|
|
201
|
-
// @todo - add a timeout
|
|
202
216
|
const reqId = this._promiseStore.add(resolve, reject, 'attach-request');
|
|
203
217
|
const requireSchema = this._cachedSchemas.has(schemaName) ? false : true;
|
|
204
218
|
this.client.transport.emit(ATTACH_REQUEST, reqId, schemaName, stateId, requireSchema);
|
|
205
219
|
});
|
|
206
220
|
}
|
|
207
221
|
|
|
222
|
+
async [kAttachInCollection](schemaName, stateId) {
|
|
223
|
+
return new Promise((resolve, reject) => {
|
|
224
|
+
const reqId = this._promiseStore.add(resolve, reject, 'attach-in-collection-request');
|
|
225
|
+
const requireSchema = this._cachedSchemas.has(schemaName) ? false : true;
|
|
226
|
+
this.client.transport.emit(ATTACH_REQUEST, reqId, schemaName, stateId, requireSchema, true);
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
208
230
|
/**
|
|
209
231
|
* Observe all the `SharedState` instances that are created on the network.
|
|
210
232
|
* This can be usefull for clients with some controller role that might want to track
|
|
@@ -366,6 +388,7 @@ class BaseStateManager {
|
|
|
366
388
|
try {
|
|
367
389
|
await collection._init();
|
|
368
390
|
} catch (err) {
|
|
391
|
+
console.log(err.message)
|
|
369
392
|
throw new Error(`Cannot create collection, schema "${schemaName}" does not exists`);
|
|
370
393
|
}
|
|
371
394
|
|
|
@@ -13,13 +13,14 @@ import {
|
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* The "real" state, this instance is kept private by the server.StateManager.
|
|
16
|
-
* It
|
|
16
|
+
* It can only be accessed through a SharedState proxy.
|
|
17
17
|
* @private
|
|
18
18
|
*/
|
|
19
19
|
class SharedStatePrivate {
|
|
20
20
|
constructor(id, schemaName, schema, manager, initValues = {}) {
|
|
21
21
|
this.id = id;
|
|
22
22
|
this.schemaName = schemaName;
|
|
23
|
+
this.isCollectionController = false;
|
|
23
24
|
|
|
24
25
|
this._manager = manager;
|
|
25
26
|
this._parameters = new ParameterBag(schema, initValues);
|
|
@@ -29,10 +30,16 @@ class SharedStatePrivate {
|
|
|
29
30
|
this._creatorId = null;
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
_attachClient(remoteId, client, isOwner = false) {
|
|
33
|
-
|
|
33
|
+
_attachClient(remoteId, client, isOwner = false, inCollection = false) {
|
|
34
|
+
|
|
35
|
+
// isOwner && inCollection -> collection controller
|
|
36
|
+
// !isOwner && inCollection -> attached in collection
|
|
37
|
+
|
|
38
|
+
const clientInfos = { client, isOwner, inCollection };
|
|
39
|
+
this._attachedClients.set(remoteId, clientInfos);
|
|
34
40
|
|
|
35
41
|
if (isOwner) {
|
|
42
|
+
this.isCollectionController = inCollection;
|
|
36
43
|
this._creatorRemoteId = remoteId;
|
|
37
44
|
this._creatorId = client.id;
|
|
38
45
|
}
|
|
@@ -89,44 +96,97 @@ class SharedStatePrivate {
|
|
|
89
96
|
}
|
|
90
97
|
|
|
91
98
|
if (hasUpdates) {
|
|
92
|
-
//
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
99
|
+
// Collection Controller logic
|
|
100
|
+
if (isOwner && inCollection) {
|
|
101
|
+
// notify the requester back
|
|
102
|
+
// no need to check for attached clients as it is not observable
|
|
103
|
+
client.transport.emit(
|
|
104
|
+
`${UPDATE_RESPONSE}-${this.id}-${remoteId}`,
|
|
105
|
+
reqId, filteredUpdates, context
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
// loop through all private states
|
|
109
|
+
for (let [stateId, state] of this._manager._sharedStatePrivateById) {
|
|
110
|
+
// pick all states with same schema name and not this
|
|
111
|
+
if (state !== this && this.schemaName === state.schemaName) {
|
|
112
|
+
// notify all attached clients except those who belong to a collection
|
|
113
|
+
// i.e. !isOwner && inCollection, they will be notified by their own
|
|
114
|
+
// collection controller
|
|
115
|
+
for (let [remoteId, clientInfos] of state._attachedClients) {
|
|
116
|
+
const { client, isOwner, inCollection } = clientInfos;
|
|
117
|
+
|
|
118
|
+
if (!clientInfos.isOwner && clientInfos.inCollection) {
|
|
119
|
+
continue;
|
|
120
|
+
} else {
|
|
121
|
+
const peer = clientInfos.client;
|
|
122
|
+
|
|
123
|
+
peer.transport.emit(
|
|
124
|
+
`${UPDATE_NOTIFICATION}-${state.id}-${remoteId}`,
|
|
125
|
+
filteredUpdates, context
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
} else {
|
|
113
132
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
133
|
+
// Normal case
|
|
134
|
+
|
|
135
|
+
// We need to handle cases where:
|
|
136
|
+
// - client state (client.id: 2) sends a request
|
|
137
|
+
// - server attached state (client.id: -1) spot a problem and overrides the value
|
|
138
|
+
// We want the remote client (id: 2) to receive in the right order:
|
|
139
|
+
// * 1. the value it requested,
|
|
140
|
+
// * 2. the value overriden by the server-side attached state (id: -1)
|
|
141
|
+
//
|
|
142
|
+
// such problem is now better solved with the the upateHook system, none
|
|
143
|
+
// nonetheway we don't want to introduce inconsistencies here
|
|
144
|
+
//
|
|
145
|
+
// Then we propagate server-side last, because as the server transport
|
|
146
|
+
// is synchronous it can break ordering if a subscription function makes
|
|
147
|
+
// itself an update in reaction to an update. Propagating to server last
|
|
148
|
+
// alllows to maintain network messages order consistent.
|
|
149
|
+
|
|
150
|
+
// @note - remoteId correspond to unique remote state id
|
|
151
|
+
|
|
152
|
+
// propagate RESPONSE to the client that originates the request if not the server
|
|
153
|
+
if (client.id !== -1) {
|
|
154
|
+
client.transport.emit(
|
|
155
|
+
`${UPDATE_RESPONSE}-${this.id}-${remoteId}`,
|
|
156
|
+
reqId, filteredUpdates, context
|
|
157
|
+
);
|
|
118
158
|
}
|
|
119
|
-
}
|
|
120
159
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
160
|
+
// propagate NOTIFICATION to all other attached clients except server
|
|
161
|
+
for (let [peerRemoteId, clientInfos] of this._attachedClients) {
|
|
162
|
+
const peer = clientInfos.client;
|
|
163
|
+
|
|
164
|
+
if (remoteId !== peerRemoteId && peer.id !== -1) {
|
|
165
|
+
peer.transport.emit(
|
|
166
|
+
`${UPDATE_NOTIFICATION}-${this.id}-${peerRemoteId}`,
|
|
167
|
+
filteredUpdates, context
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// propagate RESPONSE to server if it is the requester
|
|
173
|
+
if (client.id === -1) {
|
|
174
|
+
client.transport.emit(
|
|
175
|
+
`${UPDATE_RESPONSE}-${this.id}-${remoteId}`,
|
|
176
|
+
reqId, filteredUpdates, context
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// propagate NOTIFICATION other attached state that belongs to server
|
|
181
|
+
for (let [peerRemoteId, clientInfos] of this._attachedClients) {
|
|
182
|
+
const peer = clientInfos.client;
|
|
125
183
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
184
|
+
if (remoteId !== peerRemoteId && peer.id === -1) {
|
|
185
|
+
peer.transport.emit(
|
|
186
|
+
`${UPDATE_NOTIFICATION}-${this.id}-${peerRemoteId}`,
|
|
187
|
+
filteredUpdates, context
|
|
188
|
+
);
|
|
189
|
+
}
|
|
130
190
|
}
|
|
131
191
|
}
|
|
132
192
|
} else {
|
|
@@ -149,14 +209,15 @@ class SharedStatePrivate {
|
|
|
149
209
|
if (isOwner) {
|
|
150
210
|
// delete only if creator
|
|
151
211
|
client.transport.addListener(`${DELETE_REQUEST}-${this.id}-${remoteId}`, (reqId) => {
|
|
152
|
-
this._manager.
|
|
212
|
+
this._manager._sharedStatePrivateById.delete(this.id);
|
|
153
213
|
// --------------------------------------------------------------------
|
|
154
214
|
// WARNING - MAKE SURE WE DON'T HAVE PROBLEM W/ THAT
|
|
155
215
|
// --------------------------------------------------------------------
|
|
156
216
|
// @todo - propagate server-side last, because if a subscription function sends a
|
|
157
217
|
// message to a client, network messages order are kept coherent
|
|
158
218
|
// this._subscriptions.forEach(func => func(updated));
|
|
159
|
-
for (let [remoteId,
|
|
219
|
+
for (let [remoteId, clientInfos] of this._attachedClients) {
|
|
220
|
+
const attached = clientInfos.client;
|
|
160
221
|
this._detachClient(remoteId, attached);
|
|
161
222
|
|
|
162
223
|
if (remoteId === this._creatorRemoteId) {
|
|
@@ -28,6 +28,8 @@ import {
|
|
|
28
28
|
const generateStateId = idGenerator();
|
|
29
29
|
const generateRemoteId = idGenerator();
|
|
30
30
|
|
|
31
|
+
const kIsObservableState = Symbol('StateManager::isObservableState');
|
|
32
|
+
|
|
31
33
|
/**
|
|
32
34
|
* @typedef {object} server.StateManager~schema
|
|
33
35
|
*
|
|
@@ -315,7 +317,7 @@ class StateManager extends BaseStateManager {
|
|
|
315
317
|
super(localClientId, localTransport);
|
|
316
318
|
|
|
317
319
|
this._clientByNodeId = new Map();
|
|
318
|
-
this.
|
|
320
|
+
this._sharedStatePrivateById = new Map();
|
|
319
321
|
this._schemas = new Map();
|
|
320
322
|
this._observers = new Set();
|
|
321
323
|
this._hooksBySchemaName = new Map(); // protected
|
|
@@ -323,6 +325,12 @@ class StateManager extends BaseStateManager {
|
|
|
323
325
|
this.addClient(localClientId, localTransport);
|
|
324
326
|
}
|
|
325
327
|
|
|
328
|
+
[kIsObservableState](state) {
|
|
329
|
+
const { schemaName, isCollectionController } = state;
|
|
330
|
+
// is observable only if not in private state and not a controller state
|
|
331
|
+
return !PRIVATE_STATES.includes(schemaName) && !isCollectionController;
|
|
332
|
+
}
|
|
333
|
+
|
|
326
334
|
/**
|
|
327
335
|
* Add a client to the manager.
|
|
328
336
|
*
|
|
@@ -342,90 +350,107 @@ class StateManager extends BaseStateManager {
|
|
|
342
350
|
// ---------------------------------------------
|
|
343
351
|
// CREATE
|
|
344
352
|
// ---------------------------------------------
|
|
345
|
-
client.transport.addListener(
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
353
|
+
client.transport.addListener(
|
|
354
|
+
CREATE_REQUEST,
|
|
355
|
+
(reqId, schemaName, requireSchema, initValues = {}, inCollection = false) => {
|
|
356
|
+
if (this._schemas.has(schemaName)) {
|
|
357
|
+
try {
|
|
358
|
+
const schema = this._schemas.get(schemaName);
|
|
359
|
+
const stateId = generateStateId.next().value;
|
|
360
|
+
const remoteId = generateRemoteId.next().value;
|
|
361
|
+
const state = new SharedStatePrivate(stateId, schemaName, schema, this, initValues);
|
|
362
|
+
|
|
363
|
+
// attach client to the state as owner
|
|
364
|
+
const isOwner = true;
|
|
365
|
+
state._attachClient(remoteId, client, isOwner, inCollection);
|
|
366
|
+
|
|
367
|
+
this._sharedStatePrivateById.set(stateId, state);
|
|
368
|
+
|
|
369
|
+
const currentValues = state._parameters.getValues();
|
|
370
|
+
const schemaOption = requireSchema ? schema : null;
|
|
371
|
+
|
|
372
|
+
client.transport.emit(
|
|
373
|
+
CREATE_RESPONSE,
|
|
374
|
+
reqId, stateId, remoteId, schemaName, schemaOption, currentValues
|
|
375
|
+
);
|
|
376
|
+
|
|
377
|
+
const isObservable = this[kIsObservableState](state);
|
|
378
|
+
|
|
379
|
+
if (isObservable) {
|
|
380
|
+
this._observers.forEach(observer => {
|
|
381
|
+
observer.transport.emit(OBSERVE_NOTIFICATION, schemaName, stateId, nodeId);
|
|
382
|
+
});
|
|
366
383
|
}
|
|
367
|
-
})
|
|
368
|
-
|
|
369
|
-
|
|
384
|
+
} catch (err) {
|
|
385
|
+
const msg = `Cannot create state "${schemaName}", ${err.message}`;
|
|
386
|
+
console.error(msg);
|
|
387
|
+
|
|
388
|
+
client.transport.emit(CREATE_ERROR, reqId, msg);
|
|
389
|
+
}
|
|
390
|
+
} else {
|
|
391
|
+
const msg = `Cannot create state "${schemaName}", schema does not exists`;
|
|
370
392
|
console.error(msg);
|
|
371
393
|
|
|
372
394
|
client.transport.emit(CREATE_ERROR, reqId, msg);
|
|
373
395
|
}
|
|
374
|
-
} else {
|
|
375
|
-
const msg = `Cannot create state "${schemaName}", schema does not exists`;
|
|
376
|
-
console.error(msg);
|
|
377
|
-
|
|
378
|
-
client.transport.emit(CREATE_ERROR, reqId, msg);
|
|
379
396
|
}
|
|
380
|
-
|
|
397
|
+
);
|
|
381
398
|
|
|
382
399
|
// ---------------------------------------------
|
|
383
400
|
// ATTACH (when creator, is attached by default)
|
|
384
401
|
// ---------------------------------------------
|
|
385
|
-
client.transport.addListener(
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
402
|
+
client.transport.addListener(
|
|
403
|
+
ATTACH_REQUEST,
|
|
404
|
+
(reqId, schemaName, stateId = null, requireSchema = true, inCollection = false) => {
|
|
405
|
+
if (this._schemas.has(schemaName)) {
|
|
406
|
+
let state = null;
|
|
407
|
+
|
|
408
|
+
if (stateId !== null && this._sharedStatePrivateById.has(stateId)) {
|
|
409
|
+
state = this._sharedStatePrivateById.get(stateId);
|
|
410
|
+
} else if (stateId === null) {
|
|
411
|
+
// if no `stateId` given, we try to find the first state with the given
|
|
412
|
+
// `schemaName` in the list, this allow a client to attach to a global
|
|
413
|
+
// state created by the server (or some persistant client) without
|
|
414
|
+
// having to know the `stateId` (e.g. some global state...)
|
|
415
|
+
for (let existingState of this._sharedStatePrivateById.values()) {
|
|
416
|
+
if (existingState.schemaName === schemaName) {
|
|
417
|
+
state = existingState;
|
|
418
|
+
break;
|
|
419
|
+
}
|
|
400
420
|
}
|
|
401
421
|
}
|
|
402
|
-
}
|
|
403
422
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
423
|
+
if (state !== null) {
|
|
424
|
+
// @note - we use a unique remote id to allow a client to attach
|
|
425
|
+
// several times to the same state.
|
|
426
|
+
// i.e. same state -> several remote attach on the same node
|
|
427
|
+
const remoteId = generateRemoteId.next().value;
|
|
428
|
+
const isOwner = false;
|
|
429
|
+
state._attachClient(remoteId, client, isOwner, inCollection);
|
|
430
|
+
|
|
431
|
+
const currentValues = state._parameters.getValues();
|
|
432
|
+
const schema = this._schemas.get(schemaName);
|
|
433
|
+
const schemaOption = requireSchema ? schema : null;
|
|
434
|
+
|
|
435
|
+
client.transport.emit(
|
|
436
|
+
ATTACH_RESPONSE,
|
|
437
|
+
reqId, state.id, remoteId, schemaName, schemaOption, currentValues
|
|
438
|
+
);
|
|
439
|
+
|
|
440
|
+
} else {
|
|
441
|
+
const msg = `Cannot attach, no existing state for schema "${schemaName}" with stateId: "${stateId}"`;
|
|
442
|
+
console.error(msg);
|
|
443
|
+
|
|
444
|
+
client.transport.emit(ATTACH_ERROR, reqId, msg);
|
|
445
|
+
}
|
|
416
446
|
} else {
|
|
417
|
-
const msg = `Cannot attach,
|
|
447
|
+
const msg = `Cannot attach, schema "${schemaName}" does not exists`;
|
|
418
448
|
console.error(msg);
|
|
419
449
|
|
|
420
450
|
client.transport.emit(ATTACH_ERROR, reqId, msg);
|
|
421
451
|
}
|
|
422
|
-
} else {
|
|
423
|
-
const msg = `Cannot attach, schema "${schemaName}" does not exists`;
|
|
424
|
-
console.error(msg);
|
|
425
|
-
|
|
426
|
-
client.transport.emit(ATTACH_ERROR, reqId, msg);
|
|
427
452
|
}
|
|
428
|
-
|
|
453
|
+
);
|
|
429
454
|
|
|
430
455
|
// ---------------------------------------------
|
|
431
456
|
// OBSERVE PEERS (be notified when a state is created, lazy)
|
|
@@ -434,11 +459,11 @@ class StateManager extends BaseStateManager {
|
|
|
434
459
|
if (observedSchemaName === null || this._schemas.has(observedSchemaName)) {
|
|
435
460
|
const statesInfos = [];
|
|
436
461
|
|
|
437
|
-
this.
|
|
438
|
-
const
|
|
462
|
+
this._sharedStatePrivateById.forEach(state => {
|
|
463
|
+
const isObservable = this[kIsObservableState](state);
|
|
439
464
|
|
|
440
|
-
|
|
441
|
-
|
|
465
|
+
if (isObservable) {
|
|
466
|
+
const { schemaName, id, _creatorId } = state;
|
|
442
467
|
statesInfos.push([schemaName, id, _creatorId]);
|
|
443
468
|
}
|
|
444
469
|
});
|
|
@@ -470,18 +495,22 @@ class StateManager extends BaseStateManager {
|
|
|
470
495
|
* @private
|
|
471
496
|
*/
|
|
472
497
|
removeClient(nodeId) {
|
|
473
|
-
for (let [_id, state] of this.
|
|
498
|
+
for (let [_id, state] of this._sharedStatePrivateById.entries()) {
|
|
474
499
|
let deleteState = false;
|
|
475
500
|
|
|
476
501
|
// define if the client is the creator of the state, in which case
|
|
477
502
|
// everybody must delete it
|
|
478
|
-
for (let [remoteId,
|
|
503
|
+
for (let [remoteId, clientInfos] of state._attachedClients) {
|
|
504
|
+
const attachedClient = clientInfos.client;
|
|
505
|
+
|
|
479
506
|
if (nodeId === attachedClient.id && remoteId === state._creatorRemoteId) {
|
|
480
507
|
deleteState = true;
|
|
481
508
|
}
|
|
482
509
|
}
|
|
483
510
|
|
|
484
|
-
for (let [remoteId,
|
|
511
|
+
for (let [remoteId, clientInfos] of state._attachedClients) {
|
|
512
|
+
const attachedClient = clientInfos.client;
|
|
513
|
+
|
|
485
514
|
if (nodeId === attachedClient.id) {
|
|
486
515
|
state._detachClient(remoteId, attachedClient);
|
|
487
516
|
}
|
|
@@ -492,7 +521,7 @@ class StateManager extends BaseStateManager {
|
|
|
492
521
|
attachedClient.transport.emit(`${DELETE_NOTIFICATION}-${state.id}-${remoteId}`);
|
|
493
522
|
}
|
|
494
523
|
|
|
495
|
-
this.
|
|
524
|
+
this._sharedStatePrivateById.delete(state.id);
|
|
496
525
|
}
|
|
497
526
|
}
|
|
498
527
|
}
|
|
@@ -559,14 +588,15 @@ class StateManager extends BaseStateManager {
|
|
|
559
588
|
*/
|
|
560
589
|
deleteSchema(schemaName) {
|
|
561
590
|
// @note: deleting schema
|
|
562
|
-
for (let [_id, state] of this.
|
|
591
|
+
for (let [_id, state] of this._sharedStatePrivateById) {
|
|
563
592
|
if (state.schemaName === schemaName) {
|
|
564
|
-
for (let [remoteId,
|
|
593
|
+
for (let [remoteId, clientInfos] of state._attachedClients) {
|
|
594
|
+
const attached = clientInfos.client;
|
|
565
595
|
state._detachClient(remoteId, attached);
|
|
566
596
|
attached.transport.emit(`${DELETE_NOTIFICATION}-${state.id}-${remoteId}`);
|
|
567
597
|
}
|
|
568
598
|
|
|
569
|
-
this.
|
|
599
|
+
this._sharedStatePrivateById.delete(state.id);
|
|
570
600
|
}
|
|
571
601
|
}
|
|
572
602
|
|