@soundworks/core 4.0.0-alpha.1 → 4.0.0-alpha.11

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.
@@ -2,44 +2,116 @@ import Client from './Client.js';
2
2
  import Server from './Server.js';
3
3
 
4
4
  /**
5
- * Base class to extend in order to create the server-side counterpart of
6
- * a {@link client.Context}. If not defined, a default context will be created
5
+ * Base class to extend in order to implment the optionnal server-side counterpart
6
+ * of a {@link client.Context}. If not defined, a default context will be created
7
7
  * and used by the server.
8
8
  *
9
+ * In the `soundworks` paradigm, a client has a "role" (e.g. _player_, _controller_)
10
+ * see {@link client.Client#role}) and can be in different "contexts" (e.g. different
11
+ * part of the experience such as sections of a music piece, etc.). The
12
+ * {@link client.Context} and optionnal {@link serverContext} abstractions provide
13
+ * a simple and unified way to model these reccuring aspects of an application.
14
+ *
15
+ * If a `server.Context` is recognized as the server-side counterpart of a
16
+ * {@link client.Context}, based on their respective `name` (see {@link client.Context#name}
17
+ * and {@link server.Context#name}), `soundworks` will ensure the logic defined
18
+ * by the server-side Context will be executed at the beginning of the
19
+ * {@link client.Client#enter} and {@link client.Client#exit} methods. The example
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.
23
+ *
24
+ * ```
25
+ * // client-side
26
+ * import { Context } from '@soundworks/core/client.js';
27
+ *
28
+ * class MyContext extends Context {
29
+ * async enter() {
30
+ * // 1. client side context enter() starts
31
+ * // server-side logic is triggered first
32
+ * await super.enter();
33
+ * // 4. server-side context enter() is fully done
34
+ * // some async job can be done here
35
+ * await new Promise(resolve => setTimeout(resolve, 1000));
36
+ * // 5. client-side context enter() ends
37
+ * }
38
+ * }
39
+ *
40
+ * // Instanciate the context (assuming the `client.role` is 'test')
41
+ * const myContext = new MyContext(client);
42
+ *
43
+ * // At some point in the application, the client enters the context trigerring
44
+ * // the steps 1 to 5 described in the client-side and server-side `enter()`
45
+ * // implementations. Note that the server-side `enter()` is never called manually.
46
+ * await myContext.enter();
47
+ * ```
48
+ *
49
+ * ```
50
+ * // server-side
51
+ * import { Context } from '@soundworks/core/server.js';
52
+ *
53
+ * class MyContext extends Context {
54
+ * async enter(client) {
55
+ * // 2. server-side context enter() starts
56
+ * await super.enter(client);
57
+ * // some async job can be done here
58
+ * await new Promise(resolve => setTimeout(resolve, 1000));
59
+ * // 3. server-side context enter() ends
60
+ * }
61
+ * }
62
+ *
63
+ * // Instantiate the context
64
+ * const myContext = new Context(server);
65
+ * ```
66
+ *
9
67
  * @memberof server
10
68
  */
11
69
  class Context {
70
+ /**
71
+ * @param {client.Client} client - The soundworks client instance.
72
+ * @param {string|string[]} [roles=[]] - Optionnal list of client roles that can
73
+ * use this context. In large applications, this may be usefull to guarantee
74
+ * that a context can be consumed only by specific client roles, throwing an
75
+ * error if any other client role tries to use it. If empty, no access policy
76
+ * will be used.
77
+ * @throws Will throw if the first argument is not a soundworks server instance.
78
+ */
12
79
  constructor(server, roles = []) {
13
80
  if (!(server instanceof Server)) {
14
81
  throw new Error(`[soundworks:Context] Invalid argument, context "${this.constructor.name}" should receive a "soundworks.Server" instance as first argument`);
15
82
  }
16
83
 
17
- if (roles === null) {
18
- throw new Error('Invalid client types');
19
- }
20
-
21
84
  /**
22
85
  * soundworks server
23
86
  * @type {server.Server}
87
+ * @readonly
24
88
  */
25
89
  this.server = server;
26
90
 
27
91
  /**
28
92
  * List of clients that are currently in this context
29
- * @type {Client[]}
93
+ * @type {server.Client[]}
30
94
  */
31
95
  this.clients = new Set();
32
96
 
33
97
  /**
34
98
  * Status of the context ('idle', 'inited', 'started' or 'errored')
35
- * @type {String}
99
+ * @type {string}
100
+ * @readonly
36
101
  */
37
102
  this.status = 'idle';
38
103
 
39
104
  /**
40
- * List of client role associated with this context.
105
+ * List of client roles that can use this context.
106
+ * @type {string[]}
107
+ * @readonly
41
108
  */
42
109
  roles = Array.isArray(roles) ? roles : [roles];
110
+
111
+ if (roles.length === 0) {
112
+ roles = Object.keys(server.config.app.clients);
113
+ }
114
+
43
115
  this.roles = new Set(roles);
44
116
 
45
117
  // register in context manager
@@ -47,32 +119,87 @@ class Context {
47
119
  }
48
120
 
49
121
  /**
50
- * Name of the context, default to the class name.
51
- * Override the `get name()` getter to use a user-defined context name.
122
+ * Optionnal user-defined name of the context (defaults to the class name).
123
+ *
124
+ * The context manager will match the client-side and server-side contexts based
125
+ * on this name. If the {@link server.ContextManager} don't find a corresponding
126
+ * user-defined context with the same name, it will use a default (noop) context.
127
+ *
128
+ * @readonly
129
+ * @type {string}
130
+ * @example
131
+ * // server-side and client-side contexts are matched based on their respective `name`
132
+ * class MyContext extends Context {
133
+ * get name() {
134
+ * return 'my-user-defined-context-name';
135
+ * }
136
+ * }
52
137
  */
53
138
  get name() {
54
139
  return this.constructor.name;
55
140
  }
56
141
 
57
142
  /**
58
- * Method automatically called when the server starts, or lazilly called if
59
- * the context is created after `server.start()`
143
+ * Start the context. This method is lazilly called when a client enters the
144
+ * context for the first time (cf. ${server.Context#enter}). If you know some
145
+ * some heavy and/or potentially long job has to be done when starting the context
146
+ * (e.g. connect to a database, parsing a long file) it may be a good practice
147
+ * to call it explicitely.
60
148
  *
61
- * _WARNING: this method should never be called manually._
149
+ * This method should be implemented to perform operations that are valid for the
150
+ * whole lifetime of the context, regardless a client enters or exits the context.
151
+ *
152
+ * @example
153
+ * import { Context } from '@soundworks/core/server.js';
154
+ *
155
+ * class MyContext extends Context {
156
+ * async start() {
157
+ * await super.start();
158
+ * await this.doSomeLongJob();
159
+ * }
160
+ * }
161
+ *
162
+ * // Instantiate the context
163
+ * const myContext = new Context(server, ['test']);
164
+ * // manually start the context to perform the long operation before the first
165
+ * // client enters the context
166
+ * await myContext.start();
167
+ * ```
62
168
  */
63
169
  async start() {}
64
170
 
65
171
  /**
66
- * Method automatically called when the server stops.
172
+ * Stop the context. The method that is automatically called when the server
173
+ * stops. It should be used to cleanup context wise operations made in `start()`
174
+ * (e.g. disconnect from a database, release a file handle).
67
175
  *
68
176
  * _WARNING: this method should never be called manually._
69
177
  */
70
178
  async stop() {}
71
179
 
72
180
  /**
73
- * Method automatically called when the client enters the context.
181
+ * Enter the context. Implement this method to define the logic that should be
182
+ * done (e.g. creating a shared state, etc.) when a client enters the context.
183
+ *
184
+ * If the context has not been started yet, the `start` method is implicitely executed.
74
185
  *
75
186
  * _WARNING: this method should never be called manually._
187
+ *
188
+ * @param {server.Client} client - Server-side representation of the client
189
+ * that enters the context.
190
+ * @returns {Promise} - Promise that resolves when the context is entered.
191
+ * @example
192
+ * class MyContext extends Context {
193
+ * async enter(client) {
194
+ * await super.enter(client);
195
+ * registerTheClientSomewhere(client);
196
+ * }
197
+ *
198
+ * async exit(client) {
199
+ * await super.exit(client);
200
+ * unregisterTheClientSomewhere(client);
201
+ * }
202
+ * }
76
203
  */
77
204
  async enter(client) {
78
205
  if (!(client instanceof Client)) {
@@ -83,9 +210,26 @@ class Context {
83
210
  }
84
211
 
85
212
  /**
86
- * Method automatically called when the client exits the context.
213
+ * Exit the context. Implement this method to define the logic that should be
214
+ * done (e.g. delete a shared state, etc.) when a client exits the context.
87
215
  *
88
- * _WARNING: this method should never be called manually._
216
+ * * _WARNING: this method should never be called manually._
217
+ *
218
+ * @param {server.Client} client - Server-side representation of the client
219
+ * that exits the context.
220
+ * @returns {Promise} - Promise that resolves when the context is exited.
221
+ * @example
222
+ * class MyContext extends Context {
223
+ * async enter(client) {
224
+ * await super.enter(client);
225
+ * this.state = await this.client.stateManager.create('my-context-state');
226
+ * }
227
+ *
228
+ * async exit(client) {
229
+ * await super.exit(client);
230
+ * await this.state.delete();
231
+ * }
232
+ * }
89
233
  */
90
234
  async exit(client) {
91
235
  if (!(client instanceof Client)) {
@@ -57,6 +57,7 @@ class ContextCollection {
57
57
  * _WARNING: Most of the time, you should not have to manipulate the context manager directly._
58
58
  *
59
59
  * @memberof server
60
+ * @hideconstructor
60
61
  */
61
62
  class ContextManager {
62
63
  /**
@@ -91,10 +92,9 @@ class ContextManager {
91
92
  /**
92
93
  * Retrieve a started context from its name.
93
94
  *
94
- * @see {server.Context#name}
95
- * @param {String} contextName - Name of the context.
95
+ * _WARNING: Most of the time, you should not have to call this method manually._
96
96
  *
97
- * @private
97
+ * @param {server.Context#name} contextName - Name of the context.
98
98
  */
99
99
  async get(contextName) {
100
100
  if (!this._contexts.has(contextName)) {
@@ -114,7 +114,7 @@ class ContextManager {
114
114
  this._contextStartPromises.set(contextName, startPromise);
115
115
  await startPromise;
116
116
  context.status = 'started';
117
- } catch(err) {
117
+ } catch (err) {
118
118
  context.status = 'errored';
119
119
  throw new Error(err);
120
120
  }
@@ -136,16 +136,15 @@ class ContextManager {
136
136
  if (!this._contexts.has(contextName)) {
137
137
  // create default context for all client types
138
138
  const ctor = createNamedContextClass(contextName);
139
- const roles = Object.keys(this.server.config.app.clients);
140
139
  // this will automatically register the context in the context manager
141
- new ctor(this.server, roles);
140
+ new ctor(this.server);
142
141
  }
143
142
 
144
143
  // we ensure context is started, even lazilly after server.start()
145
144
  let context;
146
145
  try {
147
146
  context = await this.get(contextName);
148
- } catch(err) {
147
+ } catch (err) {
149
148
  client.socket.send(
150
149
  CONTEXT_ENTER_ERROR,
151
150
  reqId,
@@ -186,7 +185,7 @@ class ContextManager {
186
185
  await context.enter(client);
187
186
 
188
187
  client.socket.send(CONTEXT_ENTER_RESPONSE, reqId, contextName);
189
- } catch(err) {
188
+ } catch (err) {
190
189
  client.socket.send(CONTEXT_ENTER_ERROR, reqId, contextName, err.message);
191
190
  }
192
191
  });
@@ -208,7 +207,7 @@ class ContextManager {
208
207
  try {
209
208
  await context.exit(client);
210
209
  client.socket.send(CONTEXT_EXIT_RESPONSE, reqId, contextName);
211
- } catch(err) {
210
+ } catch (err) {
212
211
  client.socket.send(CONTEXT_EXIT_ERROR, reqId, contextName, err.message);
213
212
  }
214
213
  } else {
@@ -14,7 +14,8 @@ import BasePlugin from '../common/BasePlugin.js';
14
14
  */
15
15
 
16
16
  /**
17
- * Base class to extend in order to create new `soundworks` plugins.
17
+ * Base class to extend in order to create the server-side counterpart of a
18
+ * `soundworks` plugin.
18
19
  *
19
20
  * In the `soundworks` paradigm, a plugin is a component that allows to extend
20
21
  * the framework capabilities by encapsulating common and reusable logic in
@@ -23,7 +23,7 @@ import Server from './Server.js';
23
23
  *
24
24
  * Plugins should always be registered both client-side and server-side,
25
25
  * and before {@link client.Client#start} or {@link server.Server#start}
26
- * are called for proper initialization.
26
+ * to be properly initialized.
27
27
  *
28
28
  * In some sitautions, you might want to register the same plugin factory several times
29
29
  * using different ids (e.g. for watching several parts of the file system, etc.).
@@ -33,12 +33,32 @@ import Server from './Server.js';
33
33
  * for more informations on the available plugins.
34
34
  *
35
35
  * ```
36
+ * // client-side
37
+ * import { Client } from '@soundworks/core/client.js';
38
+ * import syncPlugin from '@soundworks/plugin-sync/client.js';
39
+ *
40
+ * const client = new Client(config);
41
+ * // register the plugin before `client.start()`
42
+ * client.pluginManager.register('sync', syncPlugin);
43
+ *
44
+ * await client.start();
45
+ *
46
+ * const sync = await client.pluginManager.get('sync');
47
+ *
48
+ * setInterval(() => {
49
+ * // log the estimated global synced clock alongside the local clock.
50
+ * console.log(sync.getSyncTime(), sync.getLocalTime());
51
+ * }, 1000);
52
+ * ```
53
+ *
54
+ * ```
55
+ * // server-side
36
56
  * import { Server } from '@soundworks/core/server.js';
37
- * import platformPlugin from '@soundworks/plugin-sync/server.js';
57
+ * import syncPlugin from '@soundworks/plugin-sync/server.js';
38
58
  *
39
59
  * const server = new Server(config);
40
60
  * // register the plugin before `server.start()`
41
- * server.pluginManager.register('sync', pluginSync);
61
+ * server.pluginManager.register('sync', syncPlugin);
42
62
  *
43
63
  * await server.start();
44
64
  *
@@ -58,7 +78,7 @@ import Server from './Server.js';
58
78
  class PluginManager extends BasePluginManager {
59
79
  constructor(server) {
60
80
  if (!(server instanceof Server)) {
61
- throw new Error(`[soundworks.PluginManager] Invalid argument, "new PluginManager(server)" should receive an instance of "soundworks.Server as argument"`);
81
+ throw new Error(`[soundworks.PluginManager] Invalid argument, "new PluginManager(server)" should receive an instance of "soundworks.Server" as argument`);
62
82
  }
63
83
 
64
84
  super(server);
@@ -86,8 +106,8 @@ class PluginManager extends BasePluginManager {
86
106
  * initialization (e.g. to display initialization screen, etc.), you should rely
87
107
  * on the `onStateChange` method.
88
108
  *
89
- * _Note: the async API is designed to enable the dynamic creation of plugins (hopefully
90
- * without brealing changes) in a future release._
109
+ * _Note: the async API is designed to enable the dynamic creation of plugins
110
+ * (hopefully without brealing changes) in a future release._
91
111
  *
92
112
  * @param {server.Plugin#id} id - Id of the plugin as defined when registered.
93
113
  * @returns {server.Plugin}
@@ -100,7 +120,7 @@ class PluginManager extends BasePluginManager {
100
120
  throw new Error(`[soundworks.PluginManager] Cannot get plugin before "server.init()"`);
101
121
  }
102
122
 
103
- return super.getUnsafe(id);
123
+ return super.unsafeGet(id);
104
124
  }
105
125
 
106
126
  // server only methods