@soundworks/core 4.0.0-alpha.27 → 4.0.0-alpha.29
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 +3 -1
- package/CHANGELOG.md +15 -5
- package/package.json +5 -5
- package/src/client/Client.js +78 -83
- package/src/client/{Context.js → ClientContext.js} +48 -38
- package/src/client/ClientContextManager.js +115 -0
- package/src/client/{Plugin.js → ClientPlugin.js} +22 -18
- package/src/client/{PluginManager.js → ClientPluginManager.js} +39 -35
- package/src/client/{Socket.js → ClientSocket.js} +54 -65
- package/src/client/{StateManager.js → ClientStateManager.js} +7 -8
- package/src/client/index.js +17 -41
- package/src/common/BasePlugin.js +56 -51
- package/src/common/BasePluginManager.js +175 -139
- package/src/common/BaseStateManager.js +316 -120
- package/src/common/BatchedTransport.js +24 -24
- package/src/common/ParameterBag.js +18 -6
- package/src/common/PromiseStore.js +1 -0
- package/src/common/{BaseSharedState.js → SharedState.js} +271 -199
- package/src/common/{BaseSharedStateCollection.js → SharedStateCollection.js} +92 -61
- package/src/common/logger.js +5 -0
- package/src/common/shared-state-types.js +76 -0
- package/src/common/version.js +1 -1
- package/src/server/Server.js +391 -354
- package/src/server/ServerClient.js +73 -0
- package/src/server/{Context.js → ServerContext.js} +85 -66
- package/src/server/{ContextManager.js → ServerContextManager.js} +98 -85
- package/src/server/{Plugin.js → ServerPlugin.js} +10 -24
- package/src/server/{PluginManager.js → ServerPluginManager.js} +76 -69
- package/src/server/{Socket.js → ServerSocket.js} +19 -18
- package/src/server/{Sockets.js → ServerSockets.js} +36 -27
- package/src/server/ServerStateManager.js +515 -0
- package/src/{common → server}/SharedStatePrivate.js +61 -26
- package/src/server/audit-network-latency.worker.js +9 -7
- package/src/server/audit-schema.js +1 -1
- package/src/server/crypto.js +2 -0
- package/src/server/index.js +18 -21
- package/types/client/Client.d.ts +74 -95
- package/types/client/{Context.d.ts → ClientContext.d.ts} +25 -26
- package/types/client/{ContextManager.d.ts → ClientContextManager.d.ts} +19 -22
- package/types/client/{Plugin.d.ts → ClientPlugin.d.ts} +17 -20
- package/types/client/{PluginManager.d.ts → ClientPluginManager.d.ts} +13 -36
- package/types/client/ClientSocket.d.ts +74 -0
- package/types/client/{StateManager.d.ts → ClientStateManager.d.ts} +11 -15
- package/types/client/index.d.ts +26 -2
- package/types/common/BasePlugin.d.ts +52 -46
- package/types/common/BasePluginManager.d.ts +68 -44
- package/types/common/BaseStateManager.d.ts +207 -97
- package/types/common/BatchedTransport.d.ts +1 -5
- package/types/common/ParameterBag.d.ts +16 -18
- package/types/common/PromiseStore.d.ts +1 -1
- package/types/common/SharedState.d.ts +297 -0
- package/types/common/{BaseSharedStateCollection.d.ts → SharedStateCollection.d.ts} +34 -18
- package/types/common/constants.d.ts +0 -2
- package/types/common/logger.d.ts +2 -1
- package/types/common/shared-state-types.d.ts +83 -0
- package/types/common/sockets-utils.d.ts +0 -5
- package/types/common/version.d.ts +1 -2
- package/types/server/Server.d.ts +78 -132
- package/types/server/ServerClient.d.ts +45 -0
- package/types/server/{Context.d.ts → ServerContext.d.ts} +53 -43
- package/types/server/ServerContextManager.d.ts +80 -0
- package/types/server/{Plugin.d.ts → ServerPlugin.d.ts} +19 -33
- package/types/server/{PluginManager.d.ts → ServerPluginManager.d.ts} +39 -37
- package/types/server/ServerSocket.d.ts +85 -0
- package/types/server/ServerSockets.d.ts +50 -0
- package/types/server/ServerStateManager.d.ts +189 -0
- package/types/server/SharedStatePrivate.d.ts +21 -0
- package/types/server/audit-network-latency.worker.d.ts +1 -2
- package/types/server/audit-schema.d.ts +0 -1
- package/types/server/crypto.d.ts +2 -1
- package/types/server/index.d.ts +24 -2
- package/src/client/ContextManager.js +0 -111
- package/src/client/SharedState.js +0 -84
- package/src/client/SharedStateCollection.js +0 -38
- package/src/server/Client.js +0 -55
- package/src/server/SharedState.js +0 -85
- package/src/server/SharedStateCollection.js +0 -39
- package/src/server/StateManager.js +0 -696
- package/types/client/Client.d.ts.map +0 -1
- package/types/client/Context.d.ts.map +0 -1
- package/types/client/ContextManager.d.ts.map +0 -1
- package/types/client/Plugin.d.ts.map +0 -1
- package/types/client/PluginManager.d.ts.map +0 -1
- package/types/client/SharedState.d.ts +0 -85
- package/types/client/SharedState.d.ts.map +0 -1
- package/types/client/SharedStateCollection.d.ts +0 -41
- package/types/client/SharedStateCollection.d.ts.map +0 -1
- package/types/client/Socket.d.ts +0 -149
- package/types/client/Socket.d.ts.map +0 -1
- package/types/client/StateManager.d.ts.map +0 -1
- package/types/client/index.d.ts.map +0 -1
- package/types/common/BasePlugin.d.ts.map +0 -1
- package/types/common/BasePluginManager.d.ts.map +0 -1
- package/types/common/BaseSharedState.d.ts +0 -244
- package/types/common/BaseSharedState.d.ts.map +0 -1
- package/types/common/BaseSharedStateCollection.d.ts.map +0 -1
- package/types/common/BaseStateManager.d.ts.map +0 -1
- package/types/common/BatchedTransport.d.ts.map +0 -1
- package/types/common/ParameterBag.d.ts.map +0 -1
- package/types/common/PromiseStore.d.ts.map +0 -1
- package/types/common/SharedStatePrivate.d.ts +0 -21
- package/types/common/SharedStatePrivate.d.ts.map +0 -1
- package/types/common/constants.d.ts.map +0 -1
- package/types/common/logger.d.ts.map +0 -1
- package/types/common/sockets-utils.d.ts.map +0 -1
- package/types/common/version.d.ts.map +0 -1
- package/types/server/Client.d.ts +0 -45
- package/types/server/Client.d.ts.map +0 -1
- package/types/server/Context.d.ts.map +0 -1
- package/types/server/ContextManager.d.ts +0 -69
- package/types/server/ContextManager.d.ts.map +0 -1
- package/types/server/Plugin.d.ts.map +0 -1
- package/types/server/PluginManager.d.ts.map +0 -1
- package/types/server/Server.d.ts.map +0 -1
- package/types/server/SharedState.d.ts +0 -86
- package/types/server/SharedState.d.ts.map +0 -1
- package/types/server/SharedStateCollection.d.ts +0 -41
- package/types/server/SharedStateCollection.d.ts.map +0 -1
- package/types/server/Socket.d.ts +0 -164
- package/types/server/Socket.d.ts.map +0 -1
- package/types/server/Sockets.d.ts +0 -80
- package/types/server/Sockets.d.ts.map +0 -1
- package/types/server/StateManager.d.ts +0 -405
- package/types/server/StateManager.d.ts.map +0 -1
- package/types/server/audit-network-latency.worker.d.ts.map +0 -1
- package/types/server/audit-schema.d.ts.map +0 -1
- package/types/server/crypto.d.ts.map +0 -1
- package/types/server/index.d.ts.map +0 -1
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
import { idGenerator, isString, isPlainObject } from '@ircam/sc-utils';
|
|
2
|
+
import clonedeep from 'lodash/cloneDeep.js';
|
|
3
|
+
|
|
4
|
+
import BaseStateManager, {
|
|
5
|
+
kStateManagerInit,
|
|
6
|
+
} from '../common/BaseStateManager.js';
|
|
7
|
+
import BatchedTransport from '../common/BatchedTransport.js';
|
|
8
|
+
import ParameterBag from '../common/ParameterBag.js';
|
|
9
|
+
import {
|
|
10
|
+
CREATE_REQUEST,
|
|
11
|
+
CREATE_RESPONSE,
|
|
12
|
+
CREATE_ERROR,
|
|
13
|
+
DELETE_NOTIFICATION,
|
|
14
|
+
ATTACH_REQUEST,
|
|
15
|
+
ATTACH_RESPONSE,
|
|
16
|
+
ATTACH_ERROR,
|
|
17
|
+
OBSERVE_REQUEST,
|
|
18
|
+
OBSERVE_RESPONSE,
|
|
19
|
+
OBSERVE_ERROR,
|
|
20
|
+
OBSERVE_NOTIFICATION,
|
|
21
|
+
UNOBSERVE_NOTIFICATION,
|
|
22
|
+
DELETE_SCHEMA,
|
|
23
|
+
GET_SCHEMA_REQUEST,
|
|
24
|
+
GET_SCHEMA_RESPONSE,
|
|
25
|
+
GET_SCHEMA_ERROR,
|
|
26
|
+
PRIVATE_STATES,
|
|
27
|
+
} from '../common/constants.js';
|
|
28
|
+
|
|
29
|
+
import SharedStatePrivate, {
|
|
30
|
+
kSharedStatePrivateAttachClient,
|
|
31
|
+
kSharedStatePrivateDetachClient,
|
|
32
|
+
} from './SharedStatePrivate.js';
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
const generateStateId = idGenerator();
|
|
36
|
+
const generateRemoteId = idGenerator();
|
|
37
|
+
|
|
38
|
+
export const kServerStateManagerAddClient = Symbol('soundworks:server-state-manager-add-client');
|
|
39
|
+
export const kServerStateManagerRemoveClient = Symbol('soundworks:server-state-manager-remove-client');
|
|
40
|
+
export const kServerStateManagerDeletePrivateState = Symbol('soundworks:server-state-manager-delete-private-state');
|
|
41
|
+
export const kServerStateManagerGetHooks = Symbol('soundworks:server-state-manager-get-hooks');
|
|
42
|
+
// for testing purposes
|
|
43
|
+
export const kStateManagerClientsByNodeId = Symbol('soundworks:server-state-clients-by-node-id');
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @callback serverStateManagerUpdateHook
|
|
48
|
+
* @async
|
|
49
|
+
*
|
|
50
|
+
* @param {object} updates - Update object as given on a set callback, or
|
|
51
|
+
* result of the previous hook
|
|
52
|
+
* @param {object} currentValues - Current values of the state.
|
|
53
|
+
* @param {object} [context=null] - Optionnal context passed by the creator
|
|
54
|
+
* of the update.
|
|
55
|
+
*
|
|
56
|
+
* @returns {object} The "real" updates to be applied on the state.
|
|
57
|
+
*/
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The `StateManager` allows to create new {@link SharedState}s, or attach
|
|
61
|
+
* to {@link SharedState}s created by other nodes (clients or server). It
|
|
62
|
+
* can also track all the {@link SharedState}s created by other nodes.
|
|
63
|
+
*
|
|
64
|
+
* An instance of `StateManager` is automatically created by the `soundworks.Server`
|
|
65
|
+
* at initialization (cf. {@link Server#stateManager}).
|
|
66
|
+
*
|
|
67
|
+
* Compared to the {@link ClientStateManager}, the `ServerStateManager` can also
|
|
68
|
+
* register and delete schemas, as well as register update hook that are executed when
|
|
69
|
+
* a state is updated.
|
|
70
|
+
*
|
|
71
|
+
* See {@link Server#stateManager}
|
|
72
|
+
*
|
|
73
|
+
* Tutorial: {@link https://soundworks.dev/guide/state-manager.html}
|
|
74
|
+
*
|
|
75
|
+
* ```
|
|
76
|
+
* // server-side
|
|
77
|
+
* import { Server } from '@soundworks/server/index.js';
|
|
78
|
+
*
|
|
79
|
+
* const server = new Server(config);
|
|
80
|
+
* // declare and register the schema of a shared state.
|
|
81
|
+
* server.stateManager.registerSchema('some-global-state', {
|
|
82
|
+
* myRandom: {
|
|
83
|
+
* type: 'float',
|
|
84
|
+
* default: 0,
|
|
85
|
+
* }
|
|
86
|
+
* });
|
|
87
|
+
*
|
|
88
|
+
* await server.start();
|
|
89
|
+
*
|
|
90
|
+
* // create a global state server-side
|
|
91
|
+
* const globalState = await server.stateManager.create('some-global-state');
|
|
92
|
+
* // listen and react to the changes made by the clients
|
|
93
|
+
* globalState.onUpdate(updates => console.log(updates));
|
|
94
|
+
* ```
|
|
95
|
+
*
|
|
96
|
+
* ```
|
|
97
|
+
* // client-side
|
|
98
|
+
* import { Client } from '@soundworks/client.index.js';
|
|
99
|
+
*
|
|
100
|
+
* const client = new Client(config);
|
|
101
|
+
* await client.start();
|
|
102
|
+
*
|
|
103
|
+
* // attach to the global state created by the server
|
|
104
|
+
* const globalState = await client.stateManager.attach('some-global-state');
|
|
105
|
+
*
|
|
106
|
+
* // update the value of a `myRandom` parameter every seconds
|
|
107
|
+
* setInterval(() => {
|
|
108
|
+
* globalState.set({ myRandom: Math.random() });
|
|
109
|
+
* }, 1000);
|
|
110
|
+
* ```
|
|
111
|
+
*
|
|
112
|
+
* @extends BaseStateManager
|
|
113
|
+
* @inheritdoc
|
|
114
|
+
* @hideconstructor
|
|
115
|
+
*/
|
|
116
|
+
class ServerStateManager extends BaseStateManager {
|
|
117
|
+
#sharedStatePrivateById = new Map();
|
|
118
|
+
#schemas = new Map();
|
|
119
|
+
#observers = new Set();
|
|
120
|
+
#hooksBySchemaName = new Map(); // protected
|
|
121
|
+
|
|
122
|
+
constructor() {
|
|
123
|
+
super();
|
|
124
|
+
/** @private */
|
|
125
|
+
this[kStateManagerClientsByNodeId] = new Map();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
#isObservableState(state) {
|
|
129
|
+
const { schemaName, isCollectionController } = state;
|
|
130
|
+
// is observable only if not in private state and not a controller state
|
|
131
|
+
return !PRIVATE_STATES.includes(schemaName) && !isCollectionController;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** @private */
|
|
135
|
+
[kServerStateManagerDeletePrivateState](id) {
|
|
136
|
+
this.#sharedStatePrivateById.delete(id);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** @private */
|
|
140
|
+
[kServerStateManagerGetHooks](schemaName) {
|
|
141
|
+
return this.#hooksBySchemaName.get(schemaName);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** @private */
|
|
145
|
+
[kStateManagerInit](id, transport) {
|
|
146
|
+
super[kStateManagerInit](id, transport);
|
|
147
|
+
// add itself as client of the state manager server
|
|
148
|
+
this[kServerStateManagerAddClient](id, transport);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Add a client to the manager.
|
|
153
|
+
*
|
|
154
|
+
* This is automatically handled by the {@link Server} when a client connects.
|
|
155
|
+
*
|
|
156
|
+
* @param {number} nodeId - Id of the client node, as given in
|
|
157
|
+
* {@link client.StateManager}
|
|
158
|
+
* @param {object} transport - Transport mecanism to communicate with the
|
|
159
|
+
* client. Must implement a basic EventEmitter API.
|
|
160
|
+
*
|
|
161
|
+
* @private
|
|
162
|
+
*/
|
|
163
|
+
[kServerStateManagerAddClient](nodeId, transport) {
|
|
164
|
+
const batchedTransport = new BatchedTransport(transport);
|
|
165
|
+
const client = {
|
|
166
|
+
id: nodeId,
|
|
167
|
+
transport: batchedTransport,
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
this[kStateManagerClientsByNodeId].set(nodeId, client);
|
|
171
|
+
|
|
172
|
+
// ---------------------------------------------
|
|
173
|
+
// CREATE
|
|
174
|
+
// ---------------------------------------------
|
|
175
|
+
client.transport.addListener(
|
|
176
|
+
CREATE_REQUEST,
|
|
177
|
+
(reqId, schemaName, requireSchema, initValues = {}) => {
|
|
178
|
+
if (this.#schemas.has(schemaName)) {
|
|
179
|
+
try {
|
|
180
|
+
const schema = this.#schemas.get(schemaName);
|
|
181
|
+
const stateId = generateStateId.next().value;
|
|
182
|
+
const remoteId = generateRemoteId.next().value;
|
|
183
|
+
const state = new SharedStatePrivate(stateId, schemaName, schema, this, initValues);
|
|
184
|
+
|
|
185
|
+
// attach client to the state as owner
|
|
186
|
+
const isOwner = true;
|
|
187
|
+
const filter = null;
|
|
188
|
+
state[kSharedStatePrivateAttachClient](remoteId, client, isOwner, filter);
|
|
189
|
+
|
|
190
|
+
this.#sharedStatePrivateById.set(stateId, state);
|
|
191
|
+
|
|
192
|
+
const currentValues = state.parameters.getValues();
|
|
193
|
+
const schemaOption = requireSchema ? schema : null;
|
|
194
|
+
|
|
195
|
+
client.transport.emit(
|
|
196
|
+
CREATE_RESPONSE,
|
|
197
|
+
reqId, stateId, remoteId, schemaName, schemaOption, currentValues,
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
const isObservable = this.#isObservableState(state);
|
|
201
|
+
|
|
202
|
+
if (isObservable) {
|
|
203
|
+
this.#observers.forEach(observer => {
|
|
204
|
+
observer.transport.emit(OBSERVE_NOTIFICATION, schemaName, stateId, nodeId);
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
} catch (err) {
|
|
208
|
+
const msg = `[stateManager] Cannot create state "${schemaName}", ${err.message}`;
|
|
209
|
+
console.error(msg);
|
|
210
|
+
|
|
211
|
+
client.transport.emit(CREATE_ERROR, reqId, msg);
|
|
212
|
+
}
|
|
213
|
+
} else {
|
|
214
|
+
const msg = `[stateManager] Cannot create state "${schemaName}", schema does not exists`;
|
|
215
|
+
console.error(msg);
|
|
216
|
+
|
|
217
|
+
client.transport.emit(CREATE_ERROR, reqId, msg);
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
// ---------------------------------------------
|
|
223
|
+
// ATTACH (when creator, is attached by default)
|
|
224
|
+
// ---------------------------------------------
|
|
225
|
+
client.transport.addListener(
|
|
226
|
+
ATTACH_REQUEST,
|
|
227
|
+
(reqId, schemaName, stateId = null, requireSchema = true, filter = null) => {
|
|
228
|
+
if (this.#schemas.has(schemaName)) {
|
|
229
|
+
let state = null;
|
|
230
|
+
|
|
231
|
+
if (stateId !== null && this.#sharedStatePrivateById.has(stateId)) {
|
|
232
|
+
state = this.#sharedStatePrivateById.get(stateId);
|
|
233
|
+
} else if (stateId === null) {
|
|
234
|
+
// if no `stateId` given, we try to find the first state with the given
|
|
235
|
+
// `schemaName` in the list, this allow a client to attach to a global
|
|
236
|
+
// state created by the server (or some persistant client) without
|
|
237
|
+
// having to know the `stateId` (e.g. some global state...)
|
|
238
|
+
for (let existingState of this.#sharedStatePrivateById.values()) {
|
|
239
|
+
if (existingState.schemaName === schemaName) {
|
|
240
|
+
state = existingState;
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (state !== null) {
|
|
247
|
+
// @note - we use a unique remote id to allow a client to attach
|
|
248
|
+
// several times to the same state.
|
|
249
|
+
// i.e. same state -> several remote attach on the same node
|
|
250
|
+
const remoteId = generateRemoteId.next().value;
|
|
251
|
+
const isOwner = false;
|
|
252
|
+
const currentValues = state.parameters.getValues();
|
|
253
|
+
const schema = this.#schemas.get(schemaName);
|
|
254
|
+
const schemaOption = requireSchema ? schema : null;
|
|
255
|
+
|
|
256
|
+
// if filter given, check that all filter entries are valid schema keys
|
|
257
|
+
// @todo - improve error reportin: report invalid filters
|
|
258
|
+
if (filter !== null) {
|
|
259
|
+
const keys = Object.keys(schema);
|
|
260
|
+
const isValid = filter.reduce((acc, key) => acc && keys.includes(key), true);
|
|
261
|
+
|
|
262
|
+
if (!isValid) {
|
|
263
|
+
const msg = `[stateManager] Cannot attach, invalid filter (${filter.join(', ')}) for schema "${schemaName}"`;
|
|
264
|
+
console.error(msg);
|
|
265
|
+
|
|
266
|
+
return client.transport.emit(ATTACH_ERROR, reqId, msg);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
state[kSharedStatePrivateAttachClient](remoteId, client, isOwner, filter);
|
|
271
|
+
|
|
272
|
+
client.transport.emit(
|
|
273
|
+
ATTACH_RESPONSE,
|
|
274
|
+
reqId, state.id, remoteId, schemaName, schemaOption, currentValues, filter,
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
} else {
|
|
278
|
+
const msg = `[stateManager] Cannot attach, no existing state for schema "${schemaName}" with stateId: "${stateId}"`;
|
|
279
|
+
console.error(msg);
|
|
280
|
+
|
|
281
|
+
client.transport.emit(ATTACH_ERROR, reqId, msg);
|
|
282
|
+
}
|
|
283
|
+
} else {
|
|
284
|
+
const msg = `[stateManager] Cannot attach, schema "${schemaName}" does not exists`;
|
|
285
|
+
console.error(msg);
|
|
286
|
+
|
|
287
|
+
client.transport.emit(ATTACH_ERROR, reqId, msg);
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
// ---------------------------------------------
|
|
293
|
+
// OBSERVE PEERS (be notified when a state is created, lazy)
|
|
294
|
+
// ---------------------------------------------
|
|
295
|
+
client.transport.addListener(OBSERVE_REQUEST, (reqId, observedSchemaName) => {
|
|
296
|
+
if (observedSchemaName === null || this.#schemas.has(observedSchemaName)) {
|
|
297
|
+
const statesInfos = [];
|
|
298
|
+
|
|
299
|
+
this.#sharedStatePrivateById.forEach(state => {
|
|
300
|
+
const isObservable = this.#isObservableState(state);
|
|
301
|
+
|
|
302
|
+
if (isObservable) {
|
|
303
|
+
const { schemaName, id, creatorId } = state;
|
|
304
|
+
statesInfos.push([schemaName, id, creatorId]);
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
// add client to observers first because if some synchronous server side
|
|
309
|
+
// callback throws, the client would never be added to the list
|
|
310
|
+
this.#observers.add(client);
|
|
311
|
+
|
|
312
|
+
client.transport.emit(OBSERVE_RESPONSE, reqId, ...statesInfos);
|
|
313
|
+
} else {
|
|
314
|
+
const msg = `[stateManager] Cannot observe, schema "${observedSchemaName}" does not exists`;
|
|
315
|
+
client.transport.emit(OBSERVE_ERROR, reqId, msg);
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
client.transport.addListener(UNOBSERVE_NOTIFICATION, () => {
|
|
320
|
+
this.#observers.delete(client);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
// ---------------------------------------------
|
|
324
|
+
// GET SCHEMA
|
|
325
|
+
// ---------------------------------------------
|
|
326
|
+
client.transport.addListener(GET_SCHEMA_REQUEST, (reqId, schemaName) => {
|
|
327
|
+
if (this.#schemas.has(schemaName)) {
|
|
328
|
+
const schema = this.#schemas.get(schemaName);
|
|
329
|
+
client.transport.emit(GET_SCHEMA_RESPONSE, reqId, schemaName, schema);
|
|
330
|
+
} else {
|
|
331
|
+
const msg = `[stateManager] Cannot get schema, schema "${schemaName}" does not exists`;
|
|
332
|
+
client.transport.emit(GET_SCHEMA_ERROR, reqId, msg);
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Remove a client from the manager. Clean all created or attached states.
|
|
339
|
+
*
|
|
340
|
+
* This is automatically handled by the {@link Server} when a client disconnects.
|
|
341
|
+
*
|
|
342
|
+
* @param {number} nodeId - Id of the client node, as given in
|
|
343
|
+
* {@link client.StateManager}
|
|
344
|
+
*
|
|
345
|
+
* @private
|
|
346
|
+
*/
|
|
347
|
+
[kServerStateManagerRemoveClient](nodeId) {
|
|
348
|
+
for (let [_id, state] of this.#sharedStatePrivateById.entries()) {
|
|
349
|
+
let deleteState = false;
|
|
350
|
+
|
|
351
|
+
// define if the client is the creator of the state, in which case
|
|
352
|
+
// everybody must delete it
|
|
353
|
+
for (let [remoteId, clientInfos] of state.attachedClients) {
|
|
354
|
+
const attachedClient = clientInfos.client;
|
|
355
|
+
|
|
356
|
+
if (nodeId === attachedClient.id && remoteId === state.creatorRemoteId) {
|
|
357
|
+
deleteState = true;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
for (let [remoteId, clientInfos] of state.attachedClients) {
|
|
362
|
+
const attachedClient = clientInfos.client;
|
|
363
|
+
|
|
364
|
+
if (nodeId === attachedClient.id) {
|
|
365
|
+
state[kSharedStatePrivateDetachClient](remoteId, attachedClient);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (deleteState) {
|
|
369
|
+
if (remoteId !== state.creatorRemoteId) {
|
|
370
|
+
// send notification to other attached nodes
|
|
371
|
+
attachedClient.transport.emit(`${DELETE_NOTIFICATION}-${state.id}-${remoteId}`);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
this.#sharedStatePrivateById.delete(state.id);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// if is an observer, delete it
|
|
380
|
+
const client = this[kStateManagerClientsByNodeId].get(nodeId);
|
|
381
|
+
this.#observers.delete(client);
|
|
382
|
+
this[kStateManagerClientsByNodeId].delete(nodeId);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Define a class of data structure from which {@link SharedState} can be instanciated.
|
|
387
|
+
*
|
|
388
|
+
* _In a future revision, this method and its arguments will be renamed_
|
|
389
|
+
*
|
|
390
|
+
* @param {SharedStateClassName} schemaName - Name of the schema.
|
|
391
|
+
* @param {SharedStateSchema} schema - Data structure
|
|
392
|
+
* describing the states that will be created from this schema.
|
|
393
|
+
*
|
|
394
|
+
* @see {@link ServerStateManager#create}
|
|
395
|
+
* @see {@link ClientStateManager#create}
|
|
396
|
+
*
|
|
397
|
+
* @example
|
|
398
|
+
* server.stateManager.registerSchema('my-schema', {
|
|
399
|
+
* myBoolean: {
|
|
400
|
+
* type: 'boolean'
|
|
401
|
+
* default: false,
|
|
402
|
+
* },
|
|
403
|
+
* myFloat: {
|
|
404
|
+
* type: 'float'
|
|
405
|
+
* default: 0.1,
|
|
406
|
+
* min: -1,
|
|
407
|
+
* max: 1
|
|
408
|
+
* }
|
|
409
|
+
* })
|
|
410
|
+
*/
|
|
411
|
+
registerSchema(schemaName, schema) {
|
|
412
|
+
if (!isString(schemaName)) {
|
|
413
|
+
throw new Error(`[stateManager.registerSchema] Invalid schema name "${schemaName}", should be a string`);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (this.#schemas.has(schemaName)) {
|
|
417
|
+
throw new Error(`[stateManager.registerSchema] cannot register schema with name: "${schemaName}", schema name already exists`);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (!isPlainObject(schema)) {
|
|
421
|
+
throw new Error(`[stateManager.registerSchema] Invalid schema, should be an object`);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
ParameterBag.validateSchema(schema);
|
|
425
|
+
|
|
426
|
+
this.#schemas.set(schemaName, clonedeep(schema));
|
|
427
|
+
// create hooks list
|
|
428
|
+
this.#hooksBySchemaName.set(schemaName, new Set());
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Delete a whole class of {@link ShareState}.
|
|
433
|
+
*
|
|
434
|
+
* All {@link SharedState} instance that belong to this class are deleted
|
|
435
|
+
* as well, triggering the `onDetach` and `onDelete` callbacks are called on
|
|
436
|
+
* the actual {@link SharedState} instances.
|
|
437
|
+
*
|
|
438
|
+
* _In a future revision, this method and its arguments will be renamed_
|
|
439
|
+
*
|
|
440
|
+
* @param {SharedStateClassName} schemaName - Name of the schema.
|
|
441
|
+
*/
|
|
442
|
+
deleteSchema(schemaName) {
|
|
443
|
+
// @note: deleting schema
|
|
444
|
+
for (let [_, state] of this.#sharedStatePrivateById) {
|
|
445
|
+
if (state.schemaName === schemaName) {
|
|
446
|
+
for (let [remoteId, clientInfos] of state.attachedClients) {
|
|
447
|
+
const attached = clientInfos.client;
|
|
448
|
+
state[kSharedStatePrivateDetachClient](remoteId, attached);
|
|
449
|
+
attached.transport.emit(`${DELETE_NOTIFICATION}-${state.id}-${remoteId}`);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
this.#sharedStatePrivateById.delete(state.id);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// clear schema cache of all connected clients
|
|
457
|
+
for (let client of this[kStateManagerClientsByNodeId].values()) {
|
|
458
|
+
client.transport.emit(`${DELETE_SCHEMA}`, schemaName);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
this.#schemas.delete(schemaName);
|
|
462
|
+
// delete registered hooks
|
|
463
|
+
this.#hooksBySchemaName.delete(schemaName);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Register a function for a given schema (e.g. will be applied on all states
|
|
468
|
+
* created from this schema) that will be executed before the update values
|
|
469
|
+
* are propagated. For example, this could be used to implement a preset system
|
|
470
|
+
* where all the values of the state are updated from e.g. some data stored in
|
|
471
|
+
* filesystem while the consumer of the state only want to update the preset name.
|
|
472
|
+
*
|
|
473
|
+
* The hook is associated to every state of its kind (i.e. schemaName) and
|
|
474
|
+
* executed on every update (call of `set`). Note that the hooks are executed
|
|
475
|
+
* server-side regarless the node on which `set` has been called and before
|
|
476
|
+
* the "actual" update of the state (e.g. before the call of `onUpdate`).
|
|
477
|
+
*
|
|
478
|
+
* @param {string} schemaName - Kind of states on which applying the hook.
|
|
479
|
+
* @param {serverStateManagerUpdateHook} updateHook - Function
|
|
480
|
+
* called between the `set` call and the actual update.
|
|
481
|
+
*
|
|
482
|
+
* @returns {Fuction} deleteHook - Handler that deletes the hook when executed.
|
|
483
|
+
*
|
|
484
|
+
* @example
|
|
485
|
+
* server.stateManager.registerSchema('hooked', {
|
|
486
|
+
* value: { type: 'string', default: null, nullable: true },
|
|
487
|
+
* numUpdates: { type: 'integer', default: 0 },
|
|
488
|
+
* });
|
|
489
|
+
* server.stateManager.registerUpdateHook('hooked', updates => {
|
|
490
|
+
* return {
|
|
491
|
+
* ...updates
|
|
492
|
+
* numUpdates: currentValues.numUpdates + 1,
|
|
493
|
+
* };
|
|
494
|
+
* });
|
|
495
|
+
*
|
|
496
|
+
* const state = await server.stateManager.create('hooked');
|
|
497
|
+
*
|
|
498
|
+
* await state.set({ value: 'test' });
|
|
499
|
+
* const values = state.getValues();
|
|
500
|
+
* assert.deepEqual(result, { value: 'test', numUpdates: 1 });
|
|
501
|
+
*/
|
|
502
|
+
registerUpdateHook(schemaName, updateHook) {
|
|
503
|
+
// throw error if schemaName has not been registered
|
|
504
|
+
if (!this.#schemas.has(schemaName)) {
|
|
505
|
+
throw new Error(`[stateManager.registerUpdateHook] cannot register update hook for schema name "${schemaName}", schema name does not exists`);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const hooks = this.#hooksBySchemaName.get(schemaName);
|
|
509
|
+
hooks.add(updateHook);
|
|
510
|
+
|
|
511
|
+
return () => hooks.delete(updateHook);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
export default ServerStateManager;
|
|
@@ -10,12 +10,17 @@ import {
|
|
|
10
10
|
UPDATE_ABORT,
|
|
11
11
|
UPDATE_NOTIFICATION,
|
|
12
12
|
} from '../common/constants.js';
|
|
13
|
+
import {
|
|
14
|
+
kServerStateManagerDeletePrivateState,
|
|
15
|
+
kServerStateManagerGetHooks,
|
|
16
|
+
} from '../server/ServerStateManager.js';
|
|
13
17
|
|
|
14
18
|
/**
|
|
15
19
|
* Filter update object according to filter list.
|
|
16
20
|
* Return identity is filter is null
|
|
17
21
|
* @param {object} updates
|
|
18
22
|
* @param {array|null} filter
|
|
23
|
+
* @private
|
|
19
24
|
*/
|
|
20
25
|
function filterUpdates(updates, filter) {
|
|
21
26
|
if (filter === null) {
|
|
@@ -30,6 +35,9 @@ function filterUpdates(updates, filter) {
|
|
|
30
35
|
}, {});
|
|
31
36
|
}
|
|
32
37
|
|
|
38
|
+
export const kSharedStatePrivateAttachClient = Symbol('soundworks:shared-state-private-attach-client');
|
|
39
|
+
export const kSharedStatePrivateDetachClient = Symbol('soundworks:shared-state-private-detach-client');
|
|
40
|
+
|
|
33
41
|
/**
|
|
34
42
|
* The "real" state, this instance is kept private by the server.StateManager.
|
|
35
43
|
* It can only be accessed through a SharedState proxy.
|
|
@@ -37,32 +45,59 @@ function filterUpdates(updates, filter) {
|
|
|
37
45
|
* @private
|
|
38
46
|
*/
|
|
39
47
|
class SharedStatePrivate {
|
|
48
|
+
#id = null;
|
|
49
|
+
#schemaName = null;
|
|
50
|
+
#manager = null;
|
|
51
|
+
#parameters = null;
|
|
52
|
+
#creatorId = null;
|
|
53
|
+
#creatorRemoteId = null;
|
|
54
|
+
#attachedClients = new Map();
|
|
55
|
+
|
|
40
56
|
constructor(id, schemaName, schema, manager, initValues = {}) {
|
|
41
|
-
this
|
|
42
|
-
this
|
|
57
|
+
this.#id = id;
|
|
58
|
+
this.#schemaName = schemaName;
|
|
59
|
+
this.#manager = manager;
|
|
60
|
+
this.#parameters = new ParameterBag(schema, initValues);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
get id() {
|
|
64
|
+
return this.#id;
|
|
65
|
+
}
|
|
43
66
|
|
|
44
|
-
|
|
45
|
-
this
|
|
46
|
-
|
|
67
|
+
get schemaName() {
|
|
68
|
+
return this.#schemaName;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
get creatorId() {
|
|
72
|
+
return this.#creatorId;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
get creatorRemoteId() {
|
|
76
|
+
return this.#creatorRemoteId;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
get attachedClients() {
|
|
80
|
+
return this.#attachedClients;
|
|
81
|
+
}
|
|
47
82
|
|
|
48
|
-
|
|
49
|
-
this
|
|
83
|
+
get parameters() {
|
|
84
|
+
return this.#parameters;
|
|
50
85
|
}
|
|
51
86
|
|
|
52
|
-
|
|
87
|
+
[kSharedStatePrivateAttachClient](remoteId, client, isOwner, filter) {
|
|
53
88
|
const clientInfos = { client, isOwner, filter };
|
|
54
|
-
this.
|
|
89
|
+
this.#attachedClients.set(remoteId, clientInfos);
|
|
55
90
|
|
|
56
91
|
if (isOwner) {
|
|
57
|
-
this
|
|
58
|
-
this
|
|
92
|
+
this.#creatorId = client.id;
|
|
93
|
+
this.#creatorRemoteId = remoteId;
|
|
59
94
|
}
|
|
60
95
|
|
|
61
96
|
// attach client listeners
|
|
62
97
|
client.transport.addListener(`${UPDATE_REQUEST}-${this.id}-${remoteId}`, async (reqId, updates, context) => {
|
|
63
98
|
// apply registered hooks
|
|
64
|
-
const hooks = this
|
|
65
|
-
const values = this.
|
|
99
|
+
const hooks = this.#manager[kServerStateManagerGetHooks](this.schemaName);
|
|
100
|
+
const values = this.#parameters.getValues();
|
|
66
101
|
let hookAborted = false;
|
|
67
102
|
|
|
68
103
|
// cf. https://github.com/collective-soundworks/soundworks/issues/45
|
|
@@ -84,14 +119,14 @@ class SharedStatePrivate {
|
|
|
84
119
|
let hasUpdates = false;
|
|
85
120
|
|
|
86
121
|
for (let name in updates) {
|
|
87
|
-
const [newValue, changed] = this.
|
|
122
|
+
const [newValue, changed] = this.#parameters.set(name, updates[name]);
|
|
88
123
|
// if `filterChange` is set to `false` we don't check if the value
|
|
89
124
|
// has been changed or not, it is always propagated to client states
|
|
90
|
-
const { event, filterChange } = this.
|
|
125
|
+
const { event, filterChange } = this.#parameters.getSchema(name);
|
|
91
126
|
|
|
92
127
|
// if event type reset internal value to null
|
|
93
128
|
if (event) {
|
|
94
|
-
this.
|
|
129
|
+
this.#parameters.set(name, null);
|
|
95
130
|
}
|
|
96
131
|
|
|
97
132
|
if ((filterChange && changed) || !filterChange) {
|
|
@@ -130,7 +165,7 @@ class SharedStatePrivate {
|
|
|
130
165
|
}
|
|
131
166
|
|
|
132
167
|
// propagate NOTIFICATION to all attached clients except server
|
|
133
|
-
for (let [peerRemoteId, clientInfos] of this
|
|
168
|
+
for (let [peerRemoteId, clientInfos] of this.#attachedClients) {
|
|
134
169
|
const peer = clientInfos.client;
|
|
135
170
|
|
|
136
171
|
if (remoteId !== peerRemoteId && peer.id !== -1) {
|
|
@@ -157,7 +192,7 @@ class SharedStatePrivate {
|
|
|
157
192
|
}
|
|
158
193
|
|
|
159
194
|
// propagate NOTIFICATION to other state attached on the server
|
|
160
|
-
for (let [peerRemoteId, clientInfos] of this
|
|
195
|
+
for (let [peerRemoteId, clientInfos] of this.#attachedClients) {
|
|
161
196
|
const peer = clientInfos.client;
|
|
162
197
|
|
|
163
198
|
if (remoteId !== peerRemoteId && peer.id === -1) {
|
|
@@ -182,7 +217,7 @@ class SharedStatePrivate {
|
|
|
182
217
|
const oldValues = {};
|
|
183
218
|
|
|
184
219
|
for (let name in updates) {
|
|
185
|
-
oldValues[name] = this.
|
|
220
|
+
oldValues[name] = this.#parameters.get(name);
|
|
186
221
|
}
|
|
187
222
|
// aborted by hook (updates have been overriden to {})
|
|
188
223
|
client.transport.emit(`${UPDATE_ABORT}-${this.id}-${remoteId}`, reqId, oldValues, context);
|
|
@@ -192,18 +227,18 @@ class SharedStatePrivate {
|
|
|
192
227
|
if (isOwner) {
|
|
193
228
|
// delete only if creator
|
|
194
229
|
client.transport.addListener(`${DELETE_REQUEST}-${this.id}-${remoteId}`, (reqId) => {
|
|
195
|
-
this
|
|
230
|
+
this.#manager[kServerStateManagerDeletePrivateState](this.id);
|
|
196
231
|
// --------------------------------------------------------------------
|
|
197
232
|
// WARNING - MAKE SURE WE DON'T HAVE PROBLEM W/ THAT
|
|
198
233
|
// --------------------------------------------------------------------
|
|
199
234
|
// @todo - propagate server-side last, because if a subscription function sends a
|
|
200
235
|
// message to a client, network messages order are kept coherent
|
|
201
236
|
// this._subscriptions.forEach(func => func(updated));
|
|
202
|
-
for (let [remoteId, clientInfos] of this
|
|
237
|
+
for (let [remoteId, clientInfos] of this.#attachedClients) {
|
|
203
238
|
const attached = clientInfos.client;
|
|
204
|
-
this
|
|
239
|
+
this[kSharedStatePrivateDetachClient](remoteId, attached);
|
|
205
240
|
|
|
206
|
-
if (remoteId === this
|
|
241
|
+
if (remoteId === this.#creatorRemoteId) {
|
|
207
242
|
attached.transport.emit(`${DELETE_RESPONSE}-${this.id}-${remoteId}`, reqId);
|
|
208
243
|
} else {
|
|
209
244
|
attached.transport.emit(`${DELETE_NOTIFICATION}-${this.id}-${remoteId}`);
|
|
@@ -213,14 +248,14 @@ class SharedStatePrivate {
|
|
|
213
248
|
} else {
|
|
214
249
|
// detach only if not creator
|
|
215
250
|
client.transport.addListener(`${DETACH_REQUEST}-${this.id}-${remoteId}`, (reqId) => {
|
|
216
|
-
this
|
|
251
|
+
this[kSharedStatePrivateDetachClient](remoteId, client);
|
|
217
252
|
client.transport.emit(`${DETACH_RESPONSE}-${this.id}-${remoteId}`, reqId);
|
|
218
253
|
});
|
|
219
254
|
}
|
|
220
255
|
}
|
|
221
256
|
|
|
222
|
-
|
|
223
|
-
this.
|
|
257
|
+
[kSharedStatePrivateDetachClient](remoteId, client) {
|
|
258
|
+
this.#attachedClients.delete(remoteId);
|
|
224
259
|
// delete listeners
|
|
225
260
|
client.transport.removeAllListeners(`${UPDATE_REQUEST}-${this.id}-${remoteId}`);
|
|
226
261
|
client.transport.removeAllListeners(`${DELETE_REQUEST}-${this.id}-${remoteId}`);
|