@soundworks/core 4.0.0-alpha.9 → 5.0.0
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/CHANGELOG.md +102 -0
- package/README.md +14 -29
- package/package.json +24 -42
- package/src/client/Client.js +306 -256
- package/src/client/{Context.js → ClientContext.js} +63 -54
- package/src/client/ClientContextManager.js +115 -0
- package/src/client/{Plugin.js → ClientPlugin.js} +23 -19
- package/src/client/{PluginManager.js → ClientPluginManager.js} +43 -40
- package/src/client/ClientSocket.js +270 -0
- package/src/client/{StateManager.js → ClientStateManager.js} +13 -13
- package/src/client/index.js +36 -39
- package/src/common/BasePlugin.js +63 -58
- package/src/common/BasePluginManager.js +191 -142
- package/src/common/BaseStateManager.js +635 -145
- package/src/common/BatchedTransport.js +73 -0
- package/src/common/ParameterBag.js +90 -93
- package/src/common/PromiseStore.js +54 -0
- package/src/common/SharedState.js +821 -0
- package/src/common/SharedStateCollection.js +492 -0
- package/src/common/constants.js +14 -2
- package/src/common/logger.js +64 -68
- package/src/common/shared-state-types.js +82 -0
- package/src/common/sockets-utils.js +0 -64
- package/src/common/version.js +2 -0
- package/src/server/Server.js +552 -824
- package/src/server/ServerClient.js +74 -0
- package/src/server/{Context.js → ServerContext.js} +96 -77
- package/src/server/{ContextManager.js → ServerContextManager.js} +107 -87
- package/src/server/{Plugin.js → ServerPlugin.js} +30 -32
- package/src/server/ServerPluginManager.js +177 -0
- package/src/server/ServerSocket.js +256 -0
- package/src/server/ServerSockets.js +150 -0
- package/src/server/ServerStateManager.js +706 -0
- package/src/server/SharedStatePrivate.js +277 -0
- package/src/server/audit-network-latency.worker.js +19 -4
- package/src/server/{audit-schema.js → audit-state-class-description.js} +3 -2
- package/src/server/create-http-server.js +127 -0
- package/src/server/crypto.js +8 -7
- package/src/server/index.js +60 -17
- package/types/client/Client.d.ts +95 -70
- package/types/client/{Context.d.ts → ClientContext.d.ts} +59 -34
- package/types/client/{ContextManager.d.ts → ClientContextManager.d.ts} +19 -22
- package/types/client/ClientPlugin.d.ts +51 -0
- package/types/client/ClientPluginManager.d.ts +112 -0
- package/types/client/ClientSocket.d.ts +76 -0
- package/types/client/{StateManager.d.ts → ClientStateManager.d.ts} +18 -21
- package/types/client/index.d.ts +33 -6
- package/types/common/BasePlugin.d.ts +64 -53
- package/types/common/BasePluginManager.d.ts +84 -39
- package/types/common/BaseStateManager.d.ts +227 -49
- package/types/common/BatchedTransport.d.ts +17 -0
- package/types/common/ParameterBag.d.ts +73 -76
- package/types/common/PromiseStore.d.ts +11 -0
- package/types/common/SharedState.d.ts +291 -0
- package/types/common/SharedStateCollection.d.ts +311 -0
- package/types/common/constants.d.ts +10 -3
- package/types/common/logger.d.ts +8 -6
- package/types/common/shared-state-types.d.ts +95 -0
- package/types/common/sockets-utils.d.ts +0 -5
- package/types/common/version.d.ts +2 -0
- package/types/server/Server.d.ts +215 -136
- package/types/server/ServerClient.d.ts +45 -0
- package/types/server/ServerContext.d.ts +209 -0
- package/types/server/ServerContextManager.d.ts +75 -0
- package/types/server/ServerPlugin.d.ts +57 -0
- package/types/server/ServerPluginManager.d.ts +115 -0
- package/types/server/ServerSocket.d.ts +69 -0
- package/types/server/ServerSockets.d.ts +42 -0
- package/types/server/ServerStateManager.d.ts +284 -0
- package/types/server/SharedStatePrivate.d.ts +23 -0
- package/types/server/audit-network-latency.worker.d.ts +2 -2
- package/types/server/{audit-schema.d.ts → audit-state-class-description.d.ts} +8 -9
- package/types/server/create-http-server.d.ts +5 -0
- package/types/server/crypto.d.ts +4 -0
- package/types/server/index.d.ts +51 -3
- package/.eslintrc +0 -15
- package/.github/workflows/gh-pages.yml +0 -31
- package/.github/workflows/unit-tests.yml +0 -18
- package/src/client/ContextManager.js +0 -111
- package/src/client/SharedState.js +0 -84
- package/src/client/SharedStateCollection.js +0 -35
- package/src/client/Socket.js +0 -402
- package/src/common/BaseSharedState.js +0 -583
- package/src/common/BaseSharedStateCollection.js +0 -179
- package/src/common/SharedStatePrivate.js +0 -192
- package/src/common/promise-store.js +0 -25
- package/src/server/Client.js +0 -55
- package/src/server/PluginManager.js +0 -170
- package/src/server/SharedState.js +0 -85
- package/src/server/SharedStateCollection.js +0 -36
- package/src/server/Socket.js +0 -396
- package/src/server/Sockets.js +0 -224
- package/src/server/StateManager.js +0 -620
- package/tsconfig.json +0 -21
- 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 +0 -53
- package/types/client/Plugin.d.ts.map +0 -1
- package/types/client/PluginManager.d.ts +0 -96
- 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/Socket.d.ts +0 -138
- 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 -173
- package/types/common/BaseSharedState.d.ts.map +0 -1
- package/types/common/BaseStateManager.d.ts.map +0 -1
- package/types/common/ParameterBag.d.ts.map +0 -1
- package/types/common/SharedStatePrivate.d.ts +0 -20
- 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/promise-store.d.ts +0 -4
- package/types/common/promise-store.d.ts.map +0 -1
- package/types/common/sockets-utils.d.ts.map +0 -1
- package/types/common/utils.d.ts +0 -5
- package/types/common/utils.d.ts.map +0 -1
- package/types/server/Client.d.ts +0 -40
- package/types/server/Client.d.ts.map +0 -1
- package/types/server/Context.d.ts +0 -59
- 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 +0 -70
- package/types/server/Plugin.d.ts.map +0 -1
- package/types/server/PluginManager.d.ts +0 -91
- 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/Socket.d.ts +0 -128
- package/types/server/Socket.d.ts.map +0 -1
- package/types/server/Sockets.d.ts +0 -73
- package/types/server/Sockets.d.ts.map +0 -1
- package/types/server/StateManager.d.ts +0 -190
- 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/index.d.ts.map +0 -1
|
@@ -0,0 +1,706 @@
|
|
|
1
|
+
import { counter, isString, isPlainObject, isFunction } 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_SHARED_STATE_CLASS,
|
|
23
|
+
GET_CLASS_DESCRIPTION_REQUEST,
|
|
24
|
+
GET_CLASS_DESCRIPTION_RESPONSE,
|
|
25
|
+
GET_CLASS_DESCRIPTION_ERROR,
|
|
26
|
+
PRIVATE_STATES,
|
|
27
|
+
} from '../common/constants.js';
|
|
28
|
+
|
|
29
|
+
import SharedStatePrivate, {
|
|
30
|
+
kSharedStatePrivateAttachClient,
|
|
31
|
+
kSharedStatePrivateDetachClient,
|
|
32
|
+
kSharedStatePrivateGetValues,
|
|
33
|
+
} from './SharedStatePrivate.js';
|
|
34
|
+
|
|
35
|
+
import logger from '../common/logger.js';
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
const generateStateId = counter();
|
|
39
|
+
const instanceIdGenerator = counter();
|
|
40
|
+
|
|
41
|
+
export const kServerStateManagerAddClient = Symbol('soundworks:server-state-manager-add-client');
|
|
42
|
+
export const kServerStateManagerRemoveClient = Symbol('soundworks:server-state-manager-remove-client');
|
|
43
|
+
export const kServerStateManagerHasClient = Symbol('soundworks:server-state-manager-has-client');
|
|
44
|
+
export const kServerStateManagerDeletePrivateState = Symbol('soundworks:server-state-manager-delete-private-state');
|
|
45
|
+
export const kServerStateManagerGetUpdateHooks = Symbol('soundworks:server-state-manager-get-update-hooks');
|
|
46
|
+
// for testing purposes
|
|
47
|
+
export const kStateManagerClientsByNodeId = Symbol('soundworks:server-state-clients-by-node-id');
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @callback serverStateManagerCreateHook
|
|
52
|
+
* @async
|
|
53
|
+
*
|
|
54
|
+
* @param {object} initValues - Initialization values object as given when the
|
|
55
|
+
* shared state is created
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @callback serverStateManagerUpdateHook
|
|
60
|
+
* @async
|
|
61
|
+
*
|
|
62
|
+
* @param {object} updates - Update object as given on a `set` callback, or
|
|
63
|
+
* result of the previous hook.
|
|
64
|
+
* @param {object} currentValues - Current values of the state.
|
|
65
|
+
* @returns {object} The "real" updates to be applied on the state.
|
|
66
|
+
*/
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @callback serverStateManagerDeleteHook
|
|
70
|
+
* @async
|
|
71
|
+
*
|
|
72
|
+
* @param {object} currentValues - Update object as given on a `set` callback, or
|
|
73
|
+
* result of the previous hook.
|
|
74
|
+
*/
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The `StateManager` allows to create new {@link SharedState}s, or attach
|
|
78
|
+
* to {@link SharedState}s created by other nodes (clients or server). It
|
|
79
|
+
* can also track all the {@link SharedState}s created by other nodes.
|
|
80
|
+
*
|
|
81
|
+
* An instance of `StateManager` is automatically created by the `soundworks.Server`
|
|
82
|
+
* at initialization (cf. {@link Server#stateManager}).
|
|
83
|
+
*
|
|
84
|
+
* Compared to the {@link ClientStateManager}, the `ServerStateManager` can also
|
|
85
|
+
* define and delete shared state classes, as well as register hooks executed at
|
|
86
|
+
* lifecycle phases of a shared state
|
|
87
|
+
*
|
|
88
|
+
* See {@link Server#stateManager}
|
|
89
|
+
*
|
|
90
|
+
* Tutorial: {@link https://soundworks.dev/guide/state-manager.html}
|
|
91
|
+
*
|
|
92
|
+
* ```
|
|
93
|
+
* // server-side
|
|
94
|
+
* import { Server } from '@soundworks/server/index.js';
|
|
95
|
+
*
|
|
96
|
+
* const server = new Server(config);
|
|
97
|
+
* // declare and register the class of a shared state.
|
|
98
|
+
* server.stateManager.defineClass('some-global-state', {
|
|
99
|
+
* myRandom: {
|
|
100
|
+
* type: 'float',
|
|
101
|
+
* default: 0,
|
|
102
|
+
* }
|
|
103
|
+
* });
|
|
104
|
+
*
|
|
105
|
+
* await server.start();
|
|
106
|
+
*
|
|
107
|
+
* // create a global state server-side
|
|
108
|
+
* const globalState = await server.stateManager.create('some-global-state');
|
|
109
|
+
* // listen and react to the changes made by the clients
|
|
110
|
+
* globalState.onUpdate(updates => console.log(updates));
|
|
111
|
+
* ```
|
|
112
|
+
*
|
|
113
|
+
* ```
|
|
114
|
+
* // client-side
|
|
115
|
+
* import { Client } from '@soundworks/client.index.js';
|
|
116
|
+
*
|
|
117
|
+
* const client = new Client(config);
|
|
118
|
+
* await client.start();
|
|
119
|
+
*
|
|
120
|
+
* // attach to the global state created by the server
|
|
121
|
+
* const globalState = await client.stateManager.attach('some-global-state');
|
|
122
|
+
*
|
|
123
|
+
* // update the value of a `myRandom` parameter every seconds
|
|
124
|
+
* setInterval(() => {
|
|
125
|
+
* globalState.set({ myRandom: Math.random() });
|
|
126
|
+
* }, 1000);
|
|
127
|
+
* ```
|
|
128
|
+
*
|
|
129
|
+
* @extends BaseStateManager
|
|
130
|
+
* @inheritdoc
|
|
131
|
+
* @hideconstructor
|
|
132
|
+
*/
|
|
133
|
+
class ServerStateManager extends BaseStateManager {
|
|
134
|
+
#sharedStatePrivateById = new Map();
|
|
135
|
+
#classes = new Map();
|
|
136
|
+
#observers = new Set();
|
|
137
|
+
#createHooksByClassName = new Map();
|
|
138
|
+
#updateHooksByClassName = new Map();
|
|
139
|
+
#deleteHooksByClassName = new Map();
|
|
140
|
+
|
|
141
|
+
constructor() {
|
|
142
|
+
super();
|
|
143
|
+
/** @private */
|
|
144
|
+
this[kStateManagerClientsByNodeId] = new Map();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** @private */
|
|
148
|
+
[kStateManagerInit](id, transport) {
|
|
149
|
+
super[kStateManagerInit](id, transport);
|
|
150
|
+
// add itself as client of the state manager server
|
|
151
|
+
this[kServerStateManagerAddClient](id, transport);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** @private */
|
|
155
|
+
async [kServerStateManagerDeletePrivateState](state) {
|
|
156
|
+
this.#sharedStatePrivateById.delete(state.id);
|
|
157
|
+
// @todo - could use getValuesUnsafe instead
|
|
158
|
+
let currentValues = state[kSharedStatePrivateGetValues]();
|
|
159
|
+
const hooks = this.#deleteHooksByClassName.get(state.className);
|
|
160
|
+
|
|
161
|
+
for (let hook of hooks.values()) {
|
|
162
|
+
const result = await hook(currentValues);
|
|
163
|
+
|
|
164
|
+
if (result === null) { // explicit abort
|
|
165
|
+
break;
|
|
166
|
+
} else if (result === undefined) { // implicit continue
|
|
167
|
+
continue;
|
|
168
|
+
} else {
|
|
169
|
+
currentValues = result;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** @private */
|
|
175
|
+
[kServerStateManagerGetUpdateHooks](className) {
|
|
176
|
+
return this.#updateHooksByClassName.get(className);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** @private */
|
|
180
|
+
[kServerStateManagerHasClient](nodeId) {
|
|
181
|
+
return this[kStateManagerClientsByNodeId].has(nodeId);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Add a client to the manager.
|
|
186
|
+
*
|
|
187
|
+
* This is automatically handled by the {@link Server} when a client connects.
|
|
188
|
+
*
|
|
189
|
+
* @param {number} nodeId - Unique id of the client node
|
|
190
|
+
* @param {object} transport - Transport mechanism to communicate with the
|
|
191
|
+
* client. Must implement a basic EventEmitter API.
|
|
192
|
+
*
|
|
193
|
+
* @private
|
|
194
|
+
*/
|
|
195
|
+
[kServerStateManagerAddClient](nodeId, transport) {
|
|
196
|
+
const batchedTransport = new BatchedTransport(transport);
|
|
197
|
+
const client = {
|
|
198
|
+
id: nodeId,
|
|
199
|
+
transport: batchedTransport,
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
this[kStateManagerClientsByNodeId].set(nodeId, client);
|
|
203
|
+
|
|
204
|
+
// ---------------------------------------------
|
|
205
|
+
// CREATE
|
|
206
|
+
// ---------------------------------------------
|
|
207
|
+
client.transport.addListener(
|
|
208
|
+
CREATE_REQUEST,
|
|
209
|
+
async (reqId, className, requireDescription, initValues = {}) => {
|
|
210
|
+
if (this.#classes.has(className)) {
|
|
211
|
+
try {
|
|
212
|
+
const classDescription = this.#classes.get(className);
|
|
213
|
+
const stateId = generateStateId();
|
|
214
|
+
const instanceId = instanceIdGenerator();
|
|
215
|
+
|
|
216
|
+
// apply create hooks on init values
|
|
217
|
+
const hooks = this.#createHooksByClassName.get(className);
|
|
218
|
+
let hookAborted = false;
|
|
219
|
+
|
|
220
|
+
for (let hook of hooks.values()) {
|
|
221
|
+
const result = await hook(initValues);
|
|
222
|
+
|
|
223
|
+
if (result === null) { // explicit abort
|
|
224
|
+
hookAborted = true;
|
|
225
|
+
break;
|
|
226
|
+
} else if (result === undefined) { // implicit continue
|
|
227
|
+
continue;
|
|
228
|
+
} else {
|
|
229
|
+
initValues = result;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (hookAborted) {
|
|
234
|
+
throw new Error(`A 'serverStateManagerCreateHook' explicitly aborted state creation of class '${className}' by returning 'null'`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const state = new SharedStatePrivate(this, className, classDescription, stateId, initValues);
|
|
238
|
+
// attach client to the state as owner
|
|
239
|
+
const isOwner = true;
|
|
240
|
+
const filter = null;
|
|
241
|
+
state[kSharedStatePrivateAttachClient](instanceId, client, isOwner, filter);
|
|
242
|
+
|
|
243
|
+
this.#sharedStatePrivateById.set(stateId, state);
|
|
244
|
+
|
|
245
|
+
const currentValues = state.parameters.getValues();
|
|
246
|
+
const classDescriptionOption = requireDescription ? classDescription : null;
|
|
247
|
+
|
|
248
|
+
client.transport.emit(
|
|
249
|
+
CREATE_RESPONSE,
|
|
250
|
+
reqId,
|
|
251
|
+
stateId,
|
|
252
|
+
instanceId,
|
|
253
|
+
className,
|
|
254
|
+
classDescriptionOption,
|
|
255
|
+
currentValues,
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
const isObservable = this.#isObservableState(state);
|
|
259
|
+
|
|
260
|
+
if (isObservable) {
|
|
261
|
+
this.#observers.forEach(observer => {
|
|
262
|
+
observer.transport.emit(OBSERVE_NOTIFICATION, className, stateId, nodeId);
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
} catch (err) {
|
|
266
|
+
const msg = `${err.message}`;
|
|
267
|
+
client.transport.emit(CREATE_ERROR, reqId, msg);
|
|
268
|
+
}
|
|
269
|
+
} else {
|
|
270
|
+
const msg = `Undefined SharedStateClassName '${className}'`;
|
|
271
|
+
client.transport.emit(CREATE_ERROR, reqId, msg);
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
// ---------------------------------------------
|
|
277
|
+
// ATTACH (when creator, is attached by default)
|
|
278
|
+
// ---------------------------------------------
|
|
279
|
+
client.transport.addListener(
|
|
280
|
+
ATTACH_REQUEST,
|
|
281
|
+
(reqId, className, stateId = null, requireDescription = true, filter = null) => {
|
|
282
|
+
if (this.#classes.has(className)) {
|
|
283
|
+
let state = null;
|
|
284
|
+
|
|
285
|
+
if (stateId !== null && this.#sharedStatePrivateById.has(stateId)) {
|
|
286
|
+
state = this.#sharedStatePrivateById.get(stateId);
|
|
287
|
+
} else if (stateId === null) {
|
|
288
|
+
// if no `stateId` given, we try to find the first state with the given
|
|
289
|
+
// `className` in the list, this allow a client to attach to a global
|
|
290
|
+
// state created by the server (or some persistent client) without
|
|
291
|
+
// having to know the `stateId` (e.g. some global state...)
|
|
292
|
+
for (let existingState of this.#sharedStatePrivateById.values()) {
|
|
293
|
+
if (existingState.className === className) {
|
|
294
|
+
state = existingState;
|
|
295
|
+
break;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (state !== null) {
|
|
301
|
+
// @note - we use a unique remote id to allow a client to attach
|
|
302
|
+
// several times to the same state.
|
|
303
|
+
// i.e. same state -> several remote attach on the same node
|
|
304
|
+
const instanceId = instanceIdGenerator();
|
|
305
|
+
const isOwner = false;
|
|
306
|
+
const currentValues = state.parameters.getValues();
|
|
307
|
+
const classDescription = this.#classes.get(className);
|
|
308
|
+
const classDescriptionOption = requireDescription ? classDescription : null;
|
|
309
|
+
|
|
310
|
+
// if filter given, check that all filter entries are valid class keys
|
|
311
|
+
// @todo - improve error reporting: report invalid filters
|
|
312
|
+
if (filter !== null) {
|
|
313
|
+
const keys = Object.keys(classDescription);
|
|
314
|
+
const isValid = filter.reduce((acc, key) => acc && keys.includes(key), true);
|
|
315
|
+
|
|
316
|
+
if (!isValid) {
|
|
317
|
+
const msg = `Invalid filter (${filter.join(', ')}) for class '${className}'`;
|
|
318
|
+
return client.transport.emit(ATTACH_ERROR, reqId, msg);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
state[kSharedStatePrivateAttachClient](instanceId, client, isOwner, filter);
|
|
323
|
+
|
|
324
|
+
client.transport.emit(
|
|
325
|
+
ATTACH_RESPONSE,
|
|
326
|
+
reqId,
|
|
327
|
+
state.id,
|
|
328
|
+
instanceId,
|
|
329
|
+
className,
|
|
330
|
+
classDescriptionOption,
|
|
331
|
+
currentValues,
|
|
332
|
+
filter,
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
} else {
|
|
336
|
+
const msg = `No existing state for class "${className}" with stateId: "${stateId}"`;
|
|
337
|
+
client.transport.emit(ATTACH_ERROR, reqId, msg);
|
|
338
|
+
}
|
|
339
|
+
} else {
|
|
340
|
+
const msg = `Undefined SharedStateClassName '${className}'`;
|
|
341
|
+
client.transport.emit(ATTACH_ERROR, reqId, msg);
|
|
342
|
+
}
|
|
343
|
+
},
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
// ---------------------------------------------
|
|
347
|
+
// OBSERVE PEERS (be notified when a state is created, lazy)
|
|
348
|
+
// ---------------------------------------------
|
|
349
|
+
client.transport.addListener(OBSERVE_REQUEST, (reqId, observedClassName) => {
|
|
350
|
+
if (observedClassName === null || this.#classes.has(observedClassName)) {
|
|
351
|
+
const list = [];
|
|
352
|
+
|
|
353
|
+
this.#sharedStatePrivateById.forEach(state => {
|
|
354
|
+
const isObservable = this.#isObservableState(state);
|
|
355
|
+
|
|
356
|
+
if (isObservable) {
|
|
357
|
+
const { className, id, creatorId } = state;
|
|
358
|
+
list.push([className, id, creatorId]);
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
// add client to observers first because if some synchronous server side
|
|
363
|
+
// callback throws, the client would never be added to the list
|
|
364
|
+
this.#observers.add(client);
|
|
365
|
+
|
|
366
|
+
client.transport.emit(OBSERVE_RESPONSE, reqId, ...list);
|
|
367
|
+
} else {
|
|
368
|
+
const msg = `Undefined SharedStateClassName '${observedClassName}'`;
|
|
369
|
+
client.transport.emit(OBSERVE_ERROR, reqId, msg);
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
client.transport.addListener(UNOBSERVE_NOTIFICATION, () => {
|
|
374
|
+
this.#observers.delete(client);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
// ---------------------------------------------
|
|
378
|
+
// GET CLASS DESCRIPTION
|
|
379
|
+
// ---------------------------------------------
|
|
380
|
+
client.transport.addListener(GET_CLASS_DESCRIPTION_REQUEST, (reqId, className) => {
|
|
381
|
+
if (this.#classes.has(className)) {
|
|
382
|
+
const classDescription = this.#classes.get(className);
|
|
383
|
+
client.transport.emit(
|
|
384
|
+
GET_CLASS_DESCRIPTION_RESPONSE,
|
|
385
|
+
reqId,
|
|
386
|
+
className,
|
|
387
|
+
classDescription,
|
|
388
|
+
);
|
|
389
|
+
} else {
|
|
390
|
+
const msg = `Undefined SharedStateClassName '${className}'`;
|
|
391
|
+
client.transport.emit(GET_CLASS_DESCRIPTION_ERROR, reqId, msg);
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Remove a client from the manager. Clean all created or attached states.
|
|
398
|
+
*
|
|
399
|
+
* This is automatically handled by the {@link Server} when a client disconnects.
|
|
400
|
+
*
|
|
401
|
+
* @param {number} nodeId - Id of the client node, as given in
|
|
402
|
+
* {@link client.StateManager}
|
|
403
|
+
*
|
|
404
|
+
* @private
|
|
405
|
+
*/
|
|
406
|
+
[kServerStateManagerRemoveClient](nodeId) {
|
|
407
|
+
for (let [_id, state] of this.#sharedStatePrivateById.entries()) {
|
|
408
|
+
let deleteState = false;
|
|
409
|
+
|
|
410
|
+
// define if the client is the creator of the state, in which case
|
|
411
|
+
// everybody must delete it
|
|
412
|
+
for (let [instanceId, clientInfos] of state.attachedClients) {
|
|
413
|
+
const attachedClient = clientInfos.client;
|
|
414
|
+
|
|
415
|
+
if (nodeId === attachedClient.id && instanceId === state.creatorInstanceId) {
|
|
416
|
+
deleteState = true;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
for (let [instanceId, clientInfos] of state.attachedClients) {
|
|
421
|
+
const attachedClient = clientInfos.client;
|
|
422
|
+
|
|
423
|
+
if (nodeId === attachedClient.id) {
|
|
424
|
+
state[kSharedStatePrivateDetachClient](instanceId, attachedClient);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (deleteState) {
|
|
428
|
+
if (instanceId !== state.creatorInstanceId) {
|
|
429
|
+
// send notification to other attached nodes
|
|
430
|
+
attachedClient.transport.emit(`${DELETE_NOTIFICATION}-${state.id}-${instanceId}`);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
this[kServerStateManagerDeletePrivateState](state);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// if is an observer, delete it
|
|
439
|
+
const client = this[kStateManagerClientsByNodeId].get(nodeId);
|
|
440
|
+
this.#observers.delete(client);
|
|
441
|
+
this[kStateManagerClientsByNodeId].delete(nodeId);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
#isObservableState(state) {
|
|
445
|
+
// is observable if not in private states list
|
|
446
|
+
return !PRIVATE_STATES.includes(state.className);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Define a generic class from which {@link SharedState}s can be created.
|
|
451
|
+
*
|
|
452
|
+
* @param {SharedStateClassName} className - Name of the class.
|
|
453
|
+
* @param {SharedStateClassDescription} classDescription - Description of the class.
|
|
454
|
+
*
|
|
455
|
+
* @see {@link ServerStateManager#create}
|
|
456
|
+
* @see {@link ClientStateManager#create}
|
|
457
|
+
*
|
|
458
|
+
* @example
|
|
459
|
+
* server.stateManager.defineClass('my-class', {
|
|
460
|
+
* myBoolean: {
|
|
461
|
+
* type: 'boolean'
|
|
462
|
+
* default: false,
|
|
463
|
+
* },
|
|
464
|
+
* myFloat: {
|
|
465
|
+
* type: 'float'
|
|
466
|
+
* default: 0.1,
|
|
467
|
+
* min: -1,
|
|
468
|
+
* max: 1
|
|
469
|
+
* }
|
|
470
|
+
* });
|
|
471
|
+
*/
|
|
472
|
+
defineClass(className, classDescription) {
|
|
473
|
+
if (!isString(className)) {
|
|
474
|
+
throw new TypeError(`Cannot execute 'defineClass' on ServerStateManager: argument 1 must be of type SharedStateClassName`);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
if (!isPlainObject(classDescription)) {
|
|
478
|
+
throw new TypeError(`Cannot execute 'defineClass' on ServerStateManager: argument 2 must be of type SharedStateClassDescription`);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (this.#classes.has(className)) {
|
|
482
|
+
throw new DOMException(`Cannot execute 'defineClass' on ServerStateManager: SharedState class '${className}' is already defined`, 'NotSupportedError');
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
try {
|
|
486
|
+
ParameterBag.validateDescription(classDescription);
|
|
487
|
+
} catch (err) {
|
|
488
|
+
throw new TypeError(`Cannot execute 'defineClass' on ServerStateManager: ${err.message}`);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
this.#classes.set(className, clonedeep(classDescription));
|
|
492
|
+
// create hooks list
|
|
493
|
+
this.#createHooksByClassName.set(className, new Set());
|
|
494
|
+
this.#updateHooksByClassName.set(className, new Set());
|
|
495
|
+
this.#deleteHooksByClassName.set(className, new Set());
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* @deprecated Use {@link ServerStateManager#defineClass} instead.
|
|
500
|
+
*/
|
|
501
|
+
registerSchema(className, classDescription) {
|
|
502
|
+
logger.deprecated('ServerStateManager#registerSchema', 'ServerStateManager#defineClass', '4.0.0-alpha.29');
|
|
503
|
+
this.defineClass(className, classDescription);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Delete a whole class of {@link SharedState}.
|
|
508
|
+
*
|
|
509
|
+
* All {@link SharedState} instances created from this class will be deleted
|
|
510
|
+
* as well, triggering their eventual `onDetach` and `onDelete` callbacks.
|
|
511
|
+
*
|
|
512
|
+
* @param {SharedStateClassName} className - Name of the shared state class to delete.
|
|
513
|
+
*/
|
|
514
|
+
deleteClass(className) {
|
|
515
|
+
// @note: deleting schema
|
|
516
|
+
for (let [_, state] of this.#sharedStatePrivateById) {
|
|
517
|
+
if (state.className === className) {
|
|
518
|
+
for (let [instanceId, clientInfos] of state.attachedClients) {
|
|
519
|
+
const attached = clientInfos.client;
|
|
520
|
+
state[kSharedStatePrivateDetachClient](instanceId, attached);
|
|
521
|
+
attached.transport.emit(`${DELETE_NOTIFICATION}-${state.id}-${instanceId}`);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
this[kServerStateManagerDeletePrivateState](state);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// clear class cache of all connected clients
|
|
529
|
+
for (let client of this[kStateManagerClientsByNodeId].values()) {
|
|
530
|
+
client.transport.emit(`${DELETE_SHARED_STATE_CLASS}`, className);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
this.#classes.delete(className);
|
|
534
|
+
// delete registered hooks
|
|
535
|
+
this.#createHooksByClassName.delete(className);
|
|
536
|
+
this.#updateHooksByClassName.delete(className);
|
|
537
|
+
this.#deleteHooksByClassName.delete(className);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* @deprecated Use {@link ServerStateManager#defineClass} instead.
|
|
542
|
+
*/
|
|
543
|
+
deleteSchema(className) {
|
|
544
|
+
logger.deprecated('ServerStateManager#deleteSchema', 'ServerStateManager#deleteClass', '4.0.0-alpha.29');
|
|
545
|
+
this.deleteClass(className);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Register a function for a given class of shared state class to be executed
|
|
550
|
+
* when a state is created.
|
|
551
|
+
*
|
|
552
|
+
* For example, this can be useful to retrieve some initialization values stored
|
|
553
|
+
* in the filesystem, given the value (e.g. a hostname) of one the parameters.
|
|
554
|
+
*
|
|
555
|
+
* The hook is associated to each states created from the given class name.
|
|
556
|
+
* Note that the hooks are executed server-side regardless the node on which
|
|
557
|
+
* `create` has been called.
|
|
558
|
+
*
|
|
559
|
+
* Multiple hook can be added to the same `className`, they will be executed in
|
|
560
|
+
* order of registration.
|
|
561
|
+
*
|
|
562
|
+
* @param {string} className - Kind of states on which applying the hook.
|
|
563
|
+
* @param {serverStateManagerUpdateHook} createHook - Function called on when
|
|
564
|
+
* a state of `className` is created on the network.
|
|
565
|
+
*
|
|
566
|
+
* @returns {function} deleteHook - Handler that deletes the hook when executed.
|
|
567
|
+
*
|
|
568
|
+
* @example
|
|
569
|
+
* server.stateManager.defineClass('hooked', {
|
|
570
|
+
* name: { type: 'string', required: true },
|
|
571
|
+
* hookTriggered: { type: 'boolean', default: false },
|
|
572
|
+
* });
|
|
573
|
+
* server.stateManager.registerCreateHook('hooked', initValues => {
|
|
574
|
+
* return {
|
|
575
|
+
* ...initValues
|
|
576
|
+
* hookTriggered: true,
|
|
577
|
+
* };
|
|
578
|
+
* });
|
|
579
|
+
*
|
|
580
|
+
* const state = await server.stateManager.create('hooked', {
|
|
581
|
+
* name: 'coucou',
|
|
582
|
+
* });
|
|
583
|
+
*
|
|
584
|
+
* const values = state.getValues();
|
|
585
|
+
* assert.deepEqual(result, { value: 'coucou', hookTriggered: true });
|
|
586
|
+
*/
|
|
587
|
+
registerCreateHook(className, createHook) {
|
|
588
|
+
if (!this.#classes.has(className)) {
|
|
589
|
+
throw new TypeError(`Cannot execute 'registerCreateHook' on BaseStateManager: SharedState class '${className}' is not defined`);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
if (!isFunction(createHook)) {
|
|
593
|
+
throw new TypeError(`Cannot execute 'registerCreateHook' on BaseStateManager: argument 2 must be a function`);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
const hooks = this.#createHooksByClassName.get(className);
|
|
597
|
+
hooks.add(createHook);
|
|
598
|
+
|
|
599
|
+
return () => hooks.delete(createHook);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Register a function for a given class of shared state class to be executed
|
|
604
|
+
* when a state is deleted.
|
|
605
|
+
*
|
|
606
|
+
* For example, this can be useful to store the values of a given shared state
|
|
607
|
+
* in the filesystem.
|
|
608
|
+
*
|
|
609
|
+
* The hook is associated to each states created from the given class name.
|
|
610
|
+
* Note that the hooks are executed server-side regardless the node on which
|
|
611
|
+
* `delete` has been called.
|
|
612
|
+
*
|
|
613
|
+
* Multiple hook can be added to the same `className`, they will be executed in
|
|
614
|
+
* order of registration.
|
|
615
|
+
*
|
|
616
|
+
* @param {string} className - Kind of states on which applying the hook.
|
|
617
|
+
* @param {serverStateManagerUpdateHook} createHook - Function called on when
|
|
618
|
+
* a state of `className` is created on the network.
|
|
619
|
+
*
|
|
620
|
+
* @returns {function} deleteHook - Handler that deletes the hook when executed.
|
|
621
|
+
*
|
|
622
|
+
* @example
|
|
623
|
+
* server.stateManager.defineClass('hooked', {
|
|
624
|
+
* name: { type: 'string', required: true },
|
|
625
|
+
* hookTriggered: { type: 'boolean', default: false },
|
|
626
|
+
* });
|
|
627
|
+
* server.stateManager.registerDeleteHook('hooked', async currentValues => {
|
|
628
|
+
* await doSomethingWithValues(currentValues)
|
|
629
|
+
* });
|
|
630
|
+
*
|
|
631
|
+
* const state = await server.stateManager.create('hooked');
|
|
632
|
+
* // later
|
|
633
|
+
* await state.delete();
|
|
634
|
+
*/
|
|
635
|
+
registerDeleteHook(className, deleteHook) {
|
|
636
|
+
if (!this.#classes.has(className)) {
|
|
637
|
+
throw new TypeError(`Cannot execute 'registerDeleteHook' on BaseStateManager: SharedState class '${className}' is not defined`);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
if (!isFunction(deleteHook)) {
|
|
641
|
+
throw new TypeError(`Cannot execute 'registerDeleteHook' on BaseStateManager: argument 2 must be a function`);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
const hooks = this.#deleteHooksByClassName.get(className);
|
|
645
|
+
hooks.add(deleteHook);
|
|
646
|
+
|
|
647
|
+
return () => hooks.delete(deleteHook);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Register a function for a given class of shared state to be executed between
|
|
652
|
+
* `set` instructions and `onUpdate` callback(s).
|
|
653
|
+
*
|
|
654
|
+
* For example, this can be used to implement a preset system where all the values
|
|
655
|
+
* of the state are updated from e.g. some data stored in filesystem while the
|
|
656
|
+
* consumer of the state only want to update the preset name.
|
|
657
|
+
*
|
|
658
|
+
* The hook is associated to each states created from the given class name and
|
|
659
|
+
* executed on each update (i.e. `state.set(updates)`). Note that the hooks are
|
|
660
|
+
* executed server-side regardless the node on which `set` has been called and
|
|
661
|
+
* before the call of the `onUpdate` callback of the shared state.
|
|
662
|
+
*
|
|
663
|
+
* Multiple hook can be added to the same `className`, they will be executed in
|
|
664
|
+
* order of registration.
|
|
665
|
+
*
|
|
666
|
+
* @param {string} className - Kind of states on which applying the hook.
|
|
667
|
+
* @param {serverStateManagerUpdateHook} updateHook - Function called on each update,
|
|
668
|
+
* to eventually modify the updates before they are actually applied.
|
|
669
|
+
*
|
|
670
|
+
* @returns {function} deleteHook - Handler that deletes the hook when executed.
|
|
671
|
+
*
|
|
672
|
+
* @example
|
|
673
|
+
* server.stateManager.defineClass('hooked', {
|
|
674
|
+
* value: { type: 'string', default: null, nullable: true },
|
|
675
|
+
* numUpdates: { type: 'integer', default: 0 },
|
|
676
|
+
* });
|
|
677
|
+
* server.stateManager.registerUpdateHook('hooked', updates => {
|
|
678
|
+
* return {
|
|
679
|
+
* ...updates
|
|
680
|
+
* numUpdates: currentValues.numUpdates + 1,
|
|
681
|
+
* };
|
|
682
|
+
* });
|
|
683
|
+
*
|
|
684
|
+
* const state = await server.stateManager.create('hooked');
|
|
685
|
+
*
|
|
686
|
+
* await state.set({ value: 'test' });
|
|
687
|
+
* const values = state.getValues();
|
|
688
|
+
* assert.deepEqual(result, { value: 'test', numUpdates: 1 });
|
|
689
|
+
*/
|
|
690
|
+
registerUpdateHook(className, updateHook) {
|
|
691
|
+
if (!this.#classes.has(className)) {
|
|
692
|
+
throw new TypeError(`Cannot execute 'registerUpdateHook' on BaseStateManager: SharedState class '${className}' is not defined`);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
if (!isFunction(updateHook)) {
|
|
696
|
+
throw new TypeError(`Cannot execute 'registerUpdateHook' on BaseStateManager: argument 2 must be a function`);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
const hooks = this.#updateHooksByClassName.get(className);
|
|
700
|
+
hooks.add(updateHook);
|
|
701
|
+
|
|
702
|
+
return () => hooks.delete(updateHook);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
export default ServerStateManager;
|