@soundworks/core 4.0.0-alpha.7 → 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 +16 -50
- package/package.json +26 -44
- 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 +636 -143
- 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 +559 -821
- 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 -517
- 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,74 @@
|
|
|
1
|
+
import { idGenerator } from '@ircam/sc-utils';
|
|
2
|
+
import { v4 as uuid } from 'uuid';
|
|
3
|
+
|
|
4
|
+
const generateId = idGenerator();
|
|
5
|
+
|
|
6
|
+
export const kServerClientToken = Symbol('soundworks:server-client-token');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Server-side representation of a client.
|
|
10
|
+
*
|
|
11
|
+
* @hideconstructor
|
|
12
|
+
* @see {@link Client}
|
|
13
|
+
*/
|
|
14
|
+
class ServerClient {
|
|
15
|
+
#role = null;
|
|
16
|
+
#id = null;
|
|
17
|
+
#uuid = null;
|
|
18
|
+
#socket = null;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {String} role - Role of the client
|
|
22
|
+
* @param {ServerSocket} socket - Socket connection with the client
|
|
23
|
+
*/
|
|
24
|
+
constructor(role, socket) {
|
|
25
|
+
this.#role = role;
|
|
26
|
+
this.#id = generateId.next().value;
|
|
27
|
+
this.#uuid = uuid();
|
|
28
|
+
this.#socket = socket;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Set in `server[kServerOnSocketConnection]`
|
|
32
|
+
* @private
|
|
33
|
+
*/
|
|
34
|
+
this[kServerClientToken] = null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Client role, as specified in client side config {@link Client}.
|
|
39
|
+
*
|
|
40
|
+
* @type {String}
|
|
41
|
+
*/
|
|
42
|
+
get role() {
|
|
43
|
+
return this.#role;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Session Id (incremented positive number).
|
|
48
|
+
*
|
|
49
|
+
* @type {Number}
|
|
50
|
+
*/
|
|
51
|
+
get id() {
|
|
52
|
+
return this.#id;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Unique session Id (uuidv4).
|
|
57
|
+
*
|
|
58
|
+
* @type {String}
|
|
59
|
+
*/
|
|
60
|
+
get uuid() {
|
|
61
|
+
return this.#uuid;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Socket connection with the remote {@link Client}.
|
|
66
|
+
*
|
|
67
|
+
* @type {ServerSocket}
|
|
68
|
+
*/
|
|
69
|
+
get socket() {
|
|
70
|
+
return this.#socket;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export default ServerClient;
|
|
@@ -1,31 +1,38 @@
|
|
|
1
|
-
import
|
|
1
|
+
import ServerClient from './ServerClient.js';
|
|
2
2
|
import Server from './Server.js';
|
|
3
3
|
|
|
4
|
+
import {
|
|
5
|
+
kServerContextManagerRegister,
|
|
6
|
+
} from './ServerContextManager.js';
|
|
7
|
+
|
|
8
|
+
export const kServerContextStatus = Symbol('soundworks:server-context-status');
|
|
9
|
+
|
|
4
10
|
/**
|
|
5
|
-
* Base class to extend in order to
|
|
6
|
-
* of a {@link
|
|
11
|
+
* Base class to extend in order to implement the optional server-side counterpart
|
|
12
|
+
* of a {@link ClientContext}. If not defined, a default context will be created
|
|
7
13
|
* and used by the server.
|
|
8
14
|
*
|
|
9
15
|
* In the `soundworks` paradigm, a client has a "role" (e.g. _player_, _controller_)
|
|
10
|
-
* see {@link
|
|
16
|
+
* see {@link Client#role}) and can be in different "contexts" (e.g. different
|
|
11
17
|
* part of the experience such as sections of a music piece, etc.). The
|
|
12
|
-
* {@link
|
|
13
|
-
* a simple and unified way to model these
|
|
18
|
+
* {@link ClientContext} and optional {@link ServerContext} abstractions provide
|
|
19
|
+
* a simple and unified way to model these recurring aspects of an application.
|
|
14
20
|
*
|
|
15
|
-
* If a `
|
|
16
|
-
* {@link
|
|
17
|
-
* and {@link
|
|
18
|
-
* by the
|
|
19
|
-
* {@link
|
|
20
|
-
* show how soundwords handles (and guarantees) the order of the `enter()` steps
|
|
21
|
-
* between the client-side and the server-side parts of the context. The same goes
|
|
22
|
-
* for the `exit()` method.
|
|
21
|
+
* If a `ServerContext` is recognized as the server-side counterpart of a
|
|
22
|
+
* {@link ClientContext}, based on their respective `name` (see {@link ClientContext#name}
|
|
23
|
+
* and {@link ServerContext#name}), `soundworks` will ensure the logic defined
|
|
24
|
+
* by the ServerContext will be executed at the beginning of the
|
|
25
|
+
* {@link ClientContext#enter} and {@link ClientContext#exit} methods.
|
|
23
26
|
*
|
|
24
|
-
*
|
|
27
|
+
* The example above shows how the order of the `enter()` steps between the client-side
|
|
28
|
+
* and the server-side parts of a `context` is handled and guaranteed. The same goes for
|
|
29
|
+
* the `exit()` method.
|
|
30
|
+
*
|
|
31
|
+
* ```js
|
|
25
32
|
* // client-side
|
|
26
|
-
* import {
|
|
33
|
+
* import { ClientContext } from '@soundworks/core/client.js';
|
|
27
34
|
*
|
|
28
|
-
* class MyContext extends
|
|
35
|
+
* class MyContext extends ClientContext {
|
|
29
36
|
* async enter() {
|
|
30
37
|
* // 1. client side context enter() starts
|
|
31
38
|
* // server-side logic is triggered first
|
|
@@ -37,20 +44,20 @@ import Server from './Server.js';
|
|
|
37
44
|
* }
|
|
38
45
|
* }
|
|
39
46
|
*
|
|
40
|
-
* //
|
|
47
|
+
* // Instantiate the context (assuming the `client.role` is 'test')
|
|
41
48
|
* const myContext = new MyContext(client);
|
|
42
49
|
*
|
|
43
|
-
* // At some point in the application, the client enters the context
|
|
50
|
+
* // At some point in the application, the client enters the context triggering
|
|
44
51
|
* // the steps 1 to 5 described in the client-side and server-side `enter()`
|
|
45
52
|
* // implementations. Note that the server-side `enter()` is never called manually.
|
|
46
53
|
* await myContext.enter();
|
|
47
54
|
* ```
|
|
48
55
|
*
|
|
49
|
-
* ```
|
|
56
|
+
* ```js
|
|
50
57
|
* // server-side
|
|
51
|
-
* import {
|
|
58
|
+
* import { ServerContext } from '@soundworks/core/server.js';
|
|
52
59
|
*
|
|
53
|
-
* class MyContext extends
|
|
60
|
+
* class MyContext extends ServerContext {
|
|
54
61
|
* async enter(client) {
|
|
55
62
|
* // 2. server-side context enter() starts
|
|
56
63
|
* await super.enter(client);
|
|
@@ -61,16 +68,18 @@ import Server from './Server.js';
|
|
|
61
68
|
* }
|
|
62
69
|
*
|
|
63
70
|
* // Instantiate the context
|
|
64
|
-
* const myContext = new
|
|
71
|
+
* const myContext = new MyContext(server);
|
|
65
72
|
* ```
|
|
66
|
-
*
|
|
67
|
-
* @memberof server
|
|
68
73
|
*/
|
|
69
|
-
class
|
|
74
|
+
class ServerContext {
|
|
75
|
+
#server = null;
|
|
76
|
+
#clients = new Set();
|
|
77
|
+
#roles = null;
|
|
78
|
+
|
|
70
79
|
/**
|
|
71
|
-
* @param {
|
|
72
|
-
* @param {string|string[]} [roles=[]] -
|
|
73
|
-
* use this context. In large applications, this may be
|
|
80
|
+
* @param {Server} server - The soundworks server instance.
|
|
81
|
+
* @param {string|string[]} [roles=[]] - Optional list of client roles that can
|
|
82
|
+
* use this context. In large applications, this may be useful to guarantee
|
|
74
83
|
* that a context can be consumed only by specific client roles, throwing an
|
|
75
84
|
* error if any other client role tries to use it. If empty, no access policy
|
|
76
85
|
* will be used.
|
|
@@ -78,55 +87,65 @@ class Context {
|
|
|
78
87
|
*/
|
|
79
88
|
constructor(server, roles = []) {
|
|
80
89
|
if (!(server instanceof Server)) {
|
|
81
|
-
throw new
|
|
90
|
+
throw new TypeError(`Cannot construct '${this.constructor.name}': argument 1 must be an instance of Server`);
|
|
82
91
|
}
|
|
83
92
|
|
|
84
|
-
/**
|
|
85
|
-
* soundworks server
|
|
86
|
-
* @type {server.Server}
|
|
87
|
-
* @readonly
|
|
88
|
-
*/
|
|
89
|
-
this.server = server;
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* List of clients that are currently in this context
|
|
93
|
-
* @type {server.Client[]}
|
|
94
|
-
*/
|
|
95
|
-
this.clients = new Set();
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Status of the context ('idle', 'inited', 'started' or 'errored')
|
|
99
|
-
* @type {string}
|
|
100
|
-
* @readonly
|
|
101
|
-
*/
|
|
102
|
-
this.status = 'idle';
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* List of client roles that can use this context.
|
|
106
|
-
* @type {string[]}
|
|
107
|
-
* @readonly
|
|
108
|
-
*/
|
|
109
93
|
roles = Array.isArray(roles) ? roles : [roles];
|
|
110
94
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
this.roles = new Set(roles);
|
|
95
|
+
this.#server = server;
|
|
96
|
+
this.#roles = new Set(roles);
|
|
116
97
|
|
|
98
|
+
/** @private */
|
|
99
|
+
this[kServerContextStatus] = 'idle';
|
|
117
100
|
// register in context manager
|
|
118
|
-
this
|
|
101
|
+
this.#server.contextManager[kServerContextManagerRegister](this);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The soundworks server instance.
|
|
106
|
+
*
|
|
107
|
+
* @type {Server}
|
|
108
|
+
*/
|
|
109
|
+
get server() {
|
|
110
|
+
return this.#server;
|
|
119
111
|
}
|
|
120
112
|
|
|
121
113
|
/**
|
|
122
|
-
*
|
|
114
|
+
* List of clients that are currently in this context.
|
|
115
|
+
*
|
|
116
|
+
* @type {Set<ServerClient>}
|
|
117
|
+
*/
|
|
118
|
+
get clients() {
|
|
119
|
+
return this.#clients;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* List of client roles that can use this context. No access policy if empty.
|
|
124
|
+
*
|
|
125
|
+
* @type {Set<string>}
|
|
126
|
+
*/
|
|
127
|
+
get roles() {
|
|
128
|
+
return this.#roles;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Status of the context ('idle', 'inited', 'started' or 'errored')
|
|
133
|
+
*
|
|
134
|
+
* @type {string}
|
|
135
|
+
*/
|
|
136
|
+
get status() {
|
|
137
|
+
return this[kServerContextStatus];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Optional user-defined name of the context (defaults to the class name).
|
|
123
142
|
*
|
|
124
143
|
* The context manager will match the client-side and server-side contexts based
|
|
125
|
-
* on this name. If the {@link
|
|
144
|
+
* on this name. If the {@link ServerContextManager} don't find a corresponding
|
|
126
145
|
* user-defined context with the same name, it will use a default (noop) context.
|
|
127
146
|
*
|
|
128
|
-
* @readonly
|
|
129
147
|
* @type {string}
|
|
148
|
+
*
|
|
130
149
|
* @example
|
|
131
150
|
* // server-side and client-side contexts are matched based on their respective `name`
|
|
132
151
|
* class MyContext extends Context {
|
|
@@ -140,11 +159,11 @@ class Context {
|
|
|
140
159
|
}
|
|
141
160
|
|
|
142
161
|
/**
|
|
143
|
-
* Start the context. This method is
|
|
144
|
-
* context for the first time (cf. ${
|
|
162
|
+
* Start the context. This method is lazily called when a client enters the
|
|
163
|
+
* context for the first time (cf. ${ServerContext#enter}). If you know some
|
|
145
164
|
* some heavy and/or potentially long job has to be done when starting the context
|
|
146
165
|
* (e.g. connect to a database, parsing a long file) it may be a good practice
|
|
147
|
-
* to call it
|
|
166
|
+
* to call it explicitly.
|
|
148
167
|
*
|
|
149
168
|
* This method should be implemented to perform operations that are valid for the
|
|
150
169
|
* whole lifetime of the context, regardless a client enters or exits the context.
|
|
@@ -181,11 +200,11 @@ class Context {
|
|
|
181
200
|
* Enter the context. Implement this method to define the logic that should be
|
|
182
201
|
* done (e.g. creating a shared state, etc.) when a client enters the context.
|
|
183
202
|
*
|
|
184
|
-
* If the context has not been started yet, the `start` method is
|
|
203
|
+
* If the context has not been started yet, the `start` method is implicitly executed.
|
|
185
204
|
*
|
|
186
205
|
* _WARNING: this method should never be called manually._
|
|
187
206
|
*
|
|
188
|
-
* @param {
|
|
207
|
+
* @param {ServerClient} client - Server-side representation of the client
|
|
189
208
|
* that enters the context.
|
|
190
209
|
* @returns {Promise} - Promise that resolves when the context is entered.
|
|
191
210
|
* @example
|
|
@@ -202,11 +221,11 @@ class Context {
|
|
|
202
221
|
* }
|
|
203
222
|
*/
|
|
204
223
|
async enter(client) {
|
|
205
|
-
if (!(client instanceof
|
|
206
|
-
throw new
|
|
224
|
+
if (!(client instanceof ServerClient)) {
|
|
225
|
+
throw new TypeError(`Cannot execute 'enter' on ${this.constructor.name}: argument 1 must be an instance of ServerClient`);
|
|
207
226
|
}
|
|
208
227
|
|
|
209
|
-
this
|
|
228
|
+
this.#clients.add(client);
|
|
210
229
|
}
|
|
211
230
|
|
|
212
231
|
/**
|
|
@@ -215,7 +234,7 @@ class Context {
|
|
|
215
234
|
*
|
|
216
235
|
* * _WARNING: this method should never be called manually._
|
|
217
236
|
*
|
|
218
|
-
* @param {
|
|
237
|
+
* @param {ServerClient} client - Server-side representation of the client
|
|
219
238
|
* that exits the context.
|
|
220
239
|
* @returns {Promise} - Promise that resolves when the context is exited.
|
|
221
240
|
* @example
|
|
@@ -232,12 +251,12 @@ class Context {
|
|
|
232
251
|
* }
|
|
233
252
|
*/
|
|
234
253
|
async exit(client) {
|
|
235
|
-
if (!(client instanceof
|
|
236
|
-
throw new
|
|
254
|
+
if (!(client instanceof ServerClient)) {
|
|
255
|
+
throw new TypeError(`Cannot execute 'exit' on ${this.constructor.name}: argument 1 must be an instance of ServerClient`);
|
|
237
256
|
}
|
|
238
257
|
|
|
239
|
-
this
|
|
258
|
+
this.#clients.delete(client);
|
|
240
259
|
}
|
|
241
260
|
}
|
|
242
261
|
|
|
243
|
-
export default
|
|
262
|
+
export default ServerContext;
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import
|
|
1
|
+
import Server from './Server.js';
|
|
2
|
+
import ServerContext, {
|
|
3
|
+
kServerContextStatus,
|
|
4
|
+
} from './ServerContext.js';
|
|
2
5
|
import {
|
|
3
6
|
CONTEXT_ENTER_REQUEST,
|
|
4
7
|
CONTEXT_ENTER_RESPONSE,
|
|
@@ -8,15 +11,22 @@ import {
|
|
|
8
11
|
CONTEXT_EXIT_ERROR,
|
|
9
12
|
} from '../common/constants.js';
|
|
10
13
|
|
|
14
|
+
export const kServerContextManagerStart = Symbol('soundworks:server-context-manager-start');
|
|
15
|
+
export const kServerContextManagerStop = Symbol('soundworks:server-context-manager-stop');
|
|
16
|
+
export const kServerContextManagerAddClient = Symbol('soundworks:server-context-manager-add-client');
|
|
17
|
+
export const kServerContextManagerRemoveClient = Symbol('soundworks:server-context-manager-remove-client');
|
|
18
|
+
export const kServerContextManagerRegister = Symbol('soundworks:server-context-manager-register');
|
|
19
|
+
// for testing purposes
|
|
20
|
+
export const kServerContextManagerContexts = Symbol('soundworks:server-context-manager-contexts');
|
|
21
|
+
|
|
11
22
|
/**
|
|
12
23
|
* Create a dummy server side context if a proper server-side context has not
|
|
13
|
-
* been declared and registered
|
|
24
|
+
* been declared and registered. Ane DefaultContext is created per unknown
|
|
14
25
|
* contextName and associated to all known client types.
|
|
15
|
-
*
|
|
16
26
|
* @private
|
|
17
27
|
*/
|
|
18
28
|
function createNamedContextClass(contextName) {
|
|
19
|
-
return class DefaultContext extends
|
|
29
|
+
return class DefaultContext extends ServerContext {
|
|
20
30
|
get name() {
|
|
21
31
|
return contextName;
|
|
22
32
|
}
|
|
@@ -25,122 +35,118 @@ function createNamedContextClass(contextName) {
|
|
|
25
35
|
|
|
26
36
|
/** @private */
|
|
27
37
|
class ContextCollection {
|
|
28
|
-
|
|
29
|
-
|
|
38
|
+
#inner = [];
|
|
39
|
+
|
|
40
|
+
// for testing purposes
|
|
41
|
+
get length() {
|
|
42
|
+
return this.#inner.length;
|
|
30
43
|
}
|
|
31
44
|
|
|
32
45
|
add(context) {
|
|
33
|
-
this
|
|
46
|
+
this.#inner.push(context);
|
|
34
47
|
}
|
|
35
48
|
|
|
36
49
|
has(name) {
|
|
37
|
-
return this
|
|
50
|
+
return this.#inner.find(c => c.name === name) !== undefined;
|
|
38
51
|
}
|
|
39
52
|
|
|
40
53
|
get(name) {
|
|
41
|
-
return this
|
|
54
|
+
return this.#inner.find(c => c.name === name);
|
|
42
55
|
}
|
|
43
56
|
|
|
44
57
|
map(func) {
|
|
45
|
-
return this
|
|
58
|
+
return this.#inner.map(func);
|
|
46
59
|
}
|
|
47
60
|
|
|
48
61
|
filter(func) {
|
|
49
|
-
return this
|
|
62
|
+
return this.#inner.filter(func);
|
|
50
63
|
}
|
|
51
64
|
}
|
|
52
65
|
|
|
53
66
|
/**
|
|
54
|
-
* Manage the different server-side contexts and their lifecycle.
|
|
55
|
-
*
|
|
67
|
+
* Manage the different server-side contexts and their lifecycle.
|
|
68
|
+
*
|
|
69
|
+
* The `ServerContextManager` is automatically instantiated by the {@link Server}.
|
|
56
70
|
*
|
|
57
71
|
* _WARNING: Most of the time, you should not have to manipulate the context manager directly._
|
|
58
72
|
*
|
|
59
|
-
* @
|
|
60
|
-
* @hideconstructor
|
|
73
|
+
* @template T
|
|
61
74
|
*/
|
|
62
|
-
class
|
|
75
|
+
class ServerContextManager {
|
|
76
|
+
#server = null;
|
|
77
|
+
|
|
78
|
+
#contextStartPromises = new Map();
|
|
79
|
+
|
|
63
80
|
/**
|
|
64
|
-
* @param {
|
|
81
|
+
* @param {Server} server - Instance of the soundworks server.
|
|
82
|
+
* @hideconstructor
|
|
65
83
|
*/
|
|
66
84
|
constructor(server) {
|
|
85
|
+
if (!(server instanceof Server)) {
|
|
86
|
+
throw new TypeError(`Cannot construct 'ServerContextManager': argument 1 must be an instance of Server`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
this.#server = server;
|
|
67
90
|
/** @private */
|
|
68
|
-
this
|
|
69
|
-
/** @private */
|
|
70
|
-
this._contexts = new ContextCollection();
|
|
71
|
-
/** @private */
|
|
72
|
-
this._contextStartPromises = new Map();
|
|
91
|
+
this[kServerContextManagerContexts] = new ContextCollection();
|
|
73
92
|
}
|
|
74
93
|
|
|
75
94
|
/**
|
|
76
95
|
* Register a context in the manager.
|
|
77
|
-
* This method is called in the {@link
|
|
96
|
+
* This method is called in the {@link ServerContext} constructor
|
|
78
97
|
*
|
|
79
|
-
* @param {
|
|
98
|
+
* @param {ServerContext} context - Context instance to register.
|
|
80
99
|
*
|
|
81
100
|
* @private
|
|
82
101
|
*/
|
|
83
|
-
|
|
84
|
-
// we must await the
|
|
85
|
-
if (this.
|
|
86
|
-
throw new
|
|
102
|
+
[kServerContextManagerRegister](context) {
|
|
103
|
+
// we must await the constructor initialization end to check the name and throw
|
|
104
|
+
if (this[kServerContextManagerContexts].has(context.name)) {
|
|
105
|
+
throw new DOMException(`Cannot register '${context.name}': a Context with same name has already been registered`, 'NotSupportedError');
|
|
87
106
|
}
|
|
88
107
|
|
|
89
|
-
this.
|
|
108
|
+
this[kServerContextManagerContexts].add(context);
|
|
90
109
|
}
|
|
91
110
|
|
|
92
111
|
/**
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
* _WARNING: Most of the time, you should not have to call this method manually._
|
|
112
|
+
* Start all contexts registered before `await server.start()`.
|
|
113
|
+
* Called on {@link Server#start}
|
|
96
114
|
*
|
|
97
|
-
* @
|
|
115
|
+
* @private
|
|
98
116
|
*/
|
|
99
|
-
async
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const context = this._contexts.get(contextName);
|
|
105
|
-
|
|
106
|
-
if (this._contextStartPromises.has(contextName)) {
|
|
107
|
-
const startPromise = this._contextStartPromises.get(contextName);
|
|
108
|
-
await startPromise;
|
|
109
|
-
} else {
|
|
110
|
-
context.status = 'inited';
|
|
111
|
-
|
|
112
|
-
try {
|
|
113
|
-
const startPromise = context.start();
|
|
114
|
-
this._contextStartPromises.set(contextName, startPromise);
|
|
115
|
-
await startPromise;
|
|
116
|
-
context.status = 'started';
|
|
117
|
-
} catch (err) {
|
|
118
|
-
context.status = 'errored';
|
|
119
|
-
throw new Error(err);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
117
|
+
async [kServerContextManagerStart]() {
|
|
118
|
+
const promises = this[kServerContextManagerContexts].map(context => this.get(context.name));
|
|
119
|
+
await Promise.all(promises);
|
|
120
|
+
}
|
|
122
121
|
|
|
123
|
-
|
|
122
|
+
/**
|
|
123
|
+
* Stop all contexts. Called on {@link Server#stop}
|
|
124
|
+
*
|
|
125
|
+
* @private
|
|
126
|
+
*/
|
|
127
|
+
async [kServerContextManagerStop]() {
|
|
128
|
+
const promises = this[kServerContextManagerContexts].map(context => context.stop());
|
|
129
|
+
await Promise.all(promises);
|
|
124
130
|
}
|
|
125
131
|
|
|
126
132
|
/**
|
|
127
133
|
* Called when a client connects to the server (websocket handshake)
|
|
128
134
|
*
|
|
129
|
-
* @param {
|
|
135
|
+
* @param {ServerClient} client
|
|
130
136
|
*
|
|
131
137
|
* @private
|
|
132
138
|
*/
|
|
133
|
-
|
|
139
|
+
[kServerContextManagerAddClient](client) {
|
|
134
140
|
client.socket.addListener(CONTEXT_ENTER_REQUEST, async (reqId, contextName) => {
|
|
135
141
|
// if no context found, create a DefaultContext on the fly
|
|
136
|
-
if (!this.
|
|
142
|
+
if (!this[kServerContextManagerContexts].has(contextName)) {
|
|
137
143
|
// create default context for all client types
|
|
138
144
|
const ctor = createNamedContextClass(contextName);
|
|
139
145
|
// this will automatically register the context in the context manager
|
|
140
|
-
new ctor(this
|
|
146
|
+
new ctor(this.#server);
|
|
141
147
|
}
|
|
142
148
|
|
|
143
|
-
// we ensure context is started, even
|
|
149
|
+
// we ensure context is started, even lazily after server.start()
|
|
144
150
|
let context;
|
|
145
151
|
try {
|
|
146
152
|
context = await this.get(contextName);
|
|
@@ -161,7 +167,7 @@ class ContextManager {
|
|
|
161
167
|
CONTEXT_ENTER_ERROR,
|
|
162
168
|
reqId,
|
|
163
169
|
contextName,
|
|
164
|
-
`
|
|
170
|
+
`Client already in context: if only one context is created 'enter' is called automatically)`,
|
|
165
171
|
);
|
|
166
172
|
return;
|
|
167
173
|
}
|
|
@@ -171,12 +177,12 @@ class ContextManager {
|
|
|
171
177
|
context = this.defaultContext;
|
|
172
178
|
}
|
|
173
179
|
|
|
174
|
-
if (!context.roles.has(client.role)) {
|
|
180
|
+
if (context.roles.size > 0 && !context.roles.has(client.role)) {
|
|
175
181
|
client.socket.send(
|
|
176
182
|
CONTEXT_ENTER_ERROR,
|
|
177
183
|
reqId,
|
|
178
184
|
contextName,
|
|
179
|
-
`
|
|
185
|
+
`Clients with role "${client.role}" are not declared as possible consumers of "${contextName}"`,
|
|
180
186
|
);
|
|
181
187
|
return;
|
|
182
188
|
}
|
|
@@ -191,12 +197,12 @@ class ContextManager {
|
|
|
191
197
|
});
|
|
192
198
|
|
|
193
199
|
client.socket.addListener(CONTEXT_EXIT_REQUEST, async (reqId, contextName) => {
|
|
194
|
-
if (!this.
|
|
200
|
+
if (!this[kServerContextManagerContexts].has(contextName)) {
|
|
195
201
|
client.socket.send(
|
|
196
202
|
CONTEXT_EXIT_ERROR,
|
|
197
203
|
reqId,
|
|
198
204
|
contextName,
|
|
199
|
-
`
|
|
205
|
+
`Context ${contextName} does not exists`,
|
|
200
206
|
);
|
|
201
207
|
return;
|
|
202
208
|
}
|
|
@@ -215,7 +221,7 @@ class ContextManager {
|
|
|
215
221
|
CONTEXT_EXIT_ERROR,
|
|
216
222
|
reqId,
|
|
217
223
|
contextName,
|
|
218
|
-
`
|
|
224
|
+
`Client with role "${client.role}" is not entered in context "${contextName}"`,
|
|
219
225
|
);
|
|
220
226
|
}
|
|
221
227
|
});
|
|
@@ -224,16 +230,16 @@ class ContextManager {
|
|
|
224
230
|
/**
|
|
225
231
|
* Called when a client connects to the server (websocket 'close' event)
|
|
226
232
|
*
|
|
227
|
-
* @param {
|
|
233
|
+
* @param {ServerClient} client
|
|
228
234
|
*
|
|
229
235
|
* @private
|
|
230
236
|
*/
|
|
231
|
-
async
|
|
237
|
+
async [kServerContextManagerRemoveClient](client) {
|
|
232
238
|
client.socket.removeAllListeners(CONTEXT_ENTER_REQUEST);
|
|
233
239
|
client.socket.removeAllListeners(CONTEXT_EXIT_REQUEST);
|
|
234
240
|
|
|
235
241
|
// exit from all contexts
|
|
236
|
-
const promises = this
|
|
242
|
+
const promises = this[kServerContextManagerContexts]
|
|
237
243
|
.filter(context => context.clients.has(client))
|
|
238
244
|
.map(context => context.exit(client));
|
|
239
245
|
|
|
@@ -241,25 +247,39 @@ class ContextManager {
|
|
|
241
247
|
}
|
|
242
248
|
|
|
243
249
|
/**
|
|
244
|
-
*
|
|
245
|
-
* Called on {@link server.Server#start}
|
|
250
|
+
* Retrieve a started context from its name.
|
|
246
251
|
*
|
|
247
|
-
*
|
|
248
|
-
*/
|
|
249
|
-
async start() {
|
|
250
|
-
const promises = this._contexts.map(context => this.get(context.name));
|
|
251
|
-
await Promise.all(promises);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
/**
|
|
255
|
-
* Stop all contexts. Called on {@link server.Server#stop}
|
|
252
|
+
* _WARNING: Most of the time, you should not have to call this method manually._
|
|
256
253
|
*
|
|
257
|
-
* @
|
|
254
|
+
* @param {string} contextName - Name of the context.
|
|
255
|
+
* @return {Promise<T<ServerContext>>}
|
|
258
256
|
*/
|
|
259
|
-
async
|
|
260
|
-
|
|
261
|
-
|
|
257
|
+
async get(contextName) {
|
|
258
|
+
if (!this[kServerContextManagerContexts].has(contextName)) {
|
|
259
|
+
throw new ReferenceError(`Cannot execute 'get' on ServerContextManager: Context '${contextName}' is not defined`);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const context = this[kServerContextManagerContexts].get(contextName);
|
|
263
|
+
|
|
264
|
+
if (this.#contextStartPromises.has(contextName)) {
|
|
265
|
+
const startPromise = this.#contextStartPromises.get(contextName);
|
|
266
|
+
await startPromise;
|
|
267
|
+
} else {
|
|
268
|
+
context[kServerContextStatus] = 'inited';
|
|
269
|
+
|
|
270
|
+
try {
|
|
271
|
+
const startPromise = context.start();
|
|
272
|
+
this.#contextStartPromises.set(contextName, startPromise);
|
|
273
|
+
await startPromise;
|
|
274
|
+
context[kServerContextStatus] = 'started';
|
|
275
|
+
} catch (err) {
|
|
276
|
+
context[kServerContextStatus] = 'errored';
|
|
277
|
+
throw new Error(err);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return context;
|
|
262
282
|
}
|
|
263
283
|
}
|
|
264
284
|
|
|
265
|
-
export default
|
|
285
|
+
export default ServerContextManager;
|