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

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.
@@ -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
  *
@@ -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
@@ -31,6 +31,27 @@ import {
31
31
 
32
32
  let _dbNamespaces = new Set();
33
33
 
34
+ /**
35
+ * Configuration object for the server.
36
+ *
37
+ * @typedef ServerConfig
38
+ * @memberof server
39
+ * @type {object}
40
+ * @property {object} [app] - Application configration object.
41
+ * @property {object} app.clients - Definition of the application clients.
42
+ * @property {string} [app.name=''] - Name of the application.
43
+ * @property {string} [app.author=''] - Name of the author.
44
+ * @property {object} [env] - Environment configration object.
45
+ * @property {boolean} env.port - Port on which the server is listening.
46
+ * @property {boolean} env.useHttps - Define is the server run in http or in https.
47
+ * @property {boolean} [env.httpsInfos={}] - Path to cert files for https.
48
+ * @property {boolean} env.serverAddress - Domain name or IP of the server.
49
+ * Mandatory if node clients are defined
50
+ * @property {string} [env.websockets={}] - Configuration options for websockets.
51
+ * @property {string} [env.subpath=''] - If running behind a proxy, path to the application.
52
+ */
53
+
54
+ /** @private */
34
55
  const DEFAULT_CONFIG = {
35
56
  env: {
36
57
  type: 'development',
@@ -74,50 +95,88 @@ function setTerminalTitle(server) {
74
95
  }
75
96
 
76
97
  /**
77
- * Server side entry point for a `soundworks` application.
98
+ * The `Server` class is the main entry point for the server-side of a soundworks
99
+ * application.
78
100
  *
79
- * This object hosts configuration informations, as well as methods to
80
- * initialize and start the application. It is also responsible for creating
81
- * the static file (http) server as well as the socket server.
82
- *
83
- * @memberof server
101
+ * The `Server` instance allows to access soundworks components such as {@link server.StateManager},
102
+ * {@link server.PluginManager},{@link server.Socket} or {@link server.ContextManager}.
103
+ * Its is also responsible for handling the initialization lifecycles of the different
104
+ * soundworks components.
84
105
  *
85
- * @param {server.ServerConfig} config
86
- * @example
106
+ * ```
87
107
  * import { Server } from '@soundworks/core/server';
108
+ *
88
109
  * const server = new Server({
89
110
  * app: {
90
111
  * name: 'my-example-app',
91
- * clients: { myClient: { target: 'node' } },
112
+ * clients: {
113
+ * player: { target: 'browser', default: true },
114
+ * controller: { target: 'browser' },
115
+ * thing: { target: 'node' }
116
+ * },
92
117
  * },
93
118
  * env: {
94
- * port: 8888,
119
+ * port: 8000,
95
120
  * },
96
121
  * });
97
- * await server.init();
122
+ *
98
123
  * await server.start();
124
+ * ```
125
+ *
126
+ * According to the clients definitions provided in `config.app.clients`, the
127
+ * server will automatically create a dedicated route for each browser client role.
128
+ * For example, given the config object of the example above that defines two
129
+ * different client roles for browser targets (i.e. `player` and `controller`):
130
+ *
131
+ * ```
132
+ * config.app.clients = {
133
+ * player: { target: 'browser', default: true },
134
+ * controller: { target: 'browser' },
135
+ * }
136
+ * ```
137
+ *
138
+ * The server will listen to the following URLs:
139
+ * - `http://127.0.0.1:8000/` for the `player` role, which is defined as the default client.
140
+ * - `http://127.0.0.1:8000/controller` for the `controller` role.
141
+ *
142
+ * @memberof server
99
143
  */
100
144
  class Server {
145
+ /**
146
+ * @param {server.ServerConfig} config - Configuration object for the server.
147
+ * @throws
148
+ * - If `config.app.clients` is empty.
149
+ * - If a `node` client is defined but `config.env.serverAddress` is not defined.
150
+ * - if `config.env.useHttps` is `true` and `config.env.httpsInfos` is not `null`
151
+ * (which generates self signed certificated), `config.env.httpsInfos.cert` and
152
+ * `config.env.httpsInfos.key` should point to valid cert files.
153
+ */
101
154
  constructor(config) {
102
155
  if (!isPlainObject(config)) {
103
156
  throw new Error(`[soundworks:Server] Invalid argument for Server constructor, config should be an object`);
104
157
  }
105
158
  /**
106
- * Configuration informations.
107
- * Defaults to:
159
+ * Given config object merged with the following defaults:
108
160
  * ```
109
161
  * {
110
162
  * env: {
111
163
  * type: 'development',
112
164
  * port: 8000,
113
- * subfolder: '',
165
+ * serverAddress: null,
166
+ * subpath: '',
167
+ * websockets: {
168
+ * path: 'socket',
169
+ * pingInterval: 5000,
170
+ * },
114
171
  * useHttps: false,
115
172
  * httpsInfos: null,
173
+ * crossOriginIsolated: true,
174
+ * verbose: true,
116
175
  * },
117
176
  * app: {
118
177
  * name: 'soundworks',
119
178
  * clients: {},
120
- * },
179
+ * }
121
180
  * }
122
181
  * ```
123
182
  */
@@ -161,67 +220,90 @@ class Server {
161
220
  }
162
221
 
163
222
  /**
164
- * Router. Internally use polka.
165
- * (cf. {@link https://github.com/lukeed/polka})
223
+ * Instance of the express router.
224
+ *
225
+ * The router can be used to open new route, for example to expose a directory
226
+ * of static assets (in default soundworks applications only the `public` is exposed).
227
+ *
228
+ * @see {@link https://github.com/expressjs/express}
229
+ * @example
230
+ * import { Server } from '@soundworks/core/server.js';
231
+ * import express from 'express';
232
+ *
233
+ * // create the soundworks server instance
234
+ * const server = new Server(config);
235
+ *
236
+ * // expose assets located in the `soundfiles` directory on the network
237
+ * server.router.use('/soundfiles', express.static('soundfiles')));
166
238
  */
167
239
  this.router = express();
168
240
  // compression (must be set before serve-static)
169
241
  this.router.use(compression());
170
242
 
171
243
  /**
172
- * Http(s) server instance. The node `http` or `https` module instance
173
- * (cf. {@link https://nodejs.org/api/http.html})
244
+ * Raw Node.js `http` or `https` instance
245
+ *
246
+ * @see {@link https://nodejs.org/api/http.html}
247
+ * @see {@link https://nodejs.org/api/https.html}
174
248
  */
175
249
  this.httpServer = null;
176
250
 
177
251
  /**
178
- * Key / value storage with Promise based Map API
179
- * basically a wrapper around kvey (cf. {@link https://github.com/lukechilds/keyv})
180
- * @private
181
- */
182
- this.db = this.createNamespacedDb('core');
183
-
184
- /**
185
- * The {@link server.Sockets} instance. A small wrapper around
186
- * [`ws`](https://github.com/websockets/ws) server.
252
+ * Instance of the {@link server.Sockets} class.
253
+ *
187
254
  * @see {@link server.Sockets}
188
255
  * @type {server.Sockets}
189
256
  */
190
257
  this.sockets = new Sockets();
191
258
 
192
259
  /**
193
- * The {@link server.PluginManager} instance.
260
+ * Instance of the {@link server.PluginManager} class.
261
+ *
194
262
  * @see {@link server.PluginManager}
195
263
  * @type {server.PluginManager}
196
264
  */
197
265
  this.pluginManager = new PluginManager(this);
198
266
 
199
267
  /**
200
- * The {@link server.StateManager} instance.
268
+ * Instance of the {@link server.StateManager} class.
269
+ *
201
270
  * @see {@link server.StateManager}
202
271
  * @type {server.StateManager}
203
272
  */
204
273
  this.stateManager = new StateManager();
205
274
 
206
275
  /**
207
- * The {@link server.ContextManager} instance.
276
+ * Instance of the {@link server.ContextManager} class.
208
277
  *
278
+ * @see {@link server.ContextManager}
279
+ * @type {server.ContextManager}
209
280
  */
210
281
  this.contextManager = new ContextManager(this);
211
282
 
212
283
  /**
213
- * If https is required, will contain informations about the certificates
214
- * (self-signed, validity dates, etc.)
284
+ * If `https` is required, hold informations about the certificates, e.g. if
285
+ * self-signed, the dates of validity of the certificates, etc.
215
286
  */
216
287
  this.httpsInfos = null;
217
288
 
218
289
  /**
219
- * Current status of the server ['idle', 'inited', 'started']
290
+ * Status of the server, 'idle', 'inited', 'started' or 'errored'.
291
+ *
292
+ * @type {string}
220
293
  */
221
294
  this.status = 'idle';
222
295
 
296
+ /**
297
+ * Simple key / value database with Promise based Map API store on filesystem,
298
+ * basically a tiny wrapper around the `kvey` package.
299
+ *
300
+ * @private
301
+ * @see {@link https://github.com/lukechilds/keyv}
302
+ */
303
+ this.db = this.createNamespacedDb('core');
304
+
223
305
  /** @private */
224
- this._applicationTemplateConfig = {
306
+ this._applicationTemplateOptions = {
225
307
  templateEngine: null,
226
308
  templatePath: null,
227
309
  clientConfigFunction: null,
@@ -232,7 +314,7 @@ class Server {
232
314
  /** @private */
233
315
  this._auditState = null;
234
316
 
235
- // create audit state
317
+ // register audit state schema
236
318
  this.stateManager.registerSchema(AUDIT_STATE_NAME, auditSchema);
237
319
 
238
320
  logger.configure(this.config.env.verbose);
@@ -240,18 +322,21 @@ class Server {
240
322
  }
241
323
 
242
324
  /**
243
- * Method to be called before `start` in the initialization lifecycle of the
244
- * soundworks server. Note that if `init()`` is not explicitely called, `start()`
245
- * will call it implicitely.
325
+ * The `init` method is part of the initialization lifecycle of the `soundworks`
326
+ * server. Most of the time, the `init` method will be implicitly called by the
327
+ * {@link server.Server#start} method.
328
+ *
329
+ * In some situations you might want to call this method manually, in such cases
330
+ * the method should be called before the {@link server.Server#start} method.
246
331
  *
247
332
  * What it does:
248
333
  * - create the audit state
249
334
  * - prepapre http(s) server and routing according to the informations
250
335
  * declared in `config.app.clients`
251
- * - starts all registered plugins
336
+ * - initialize all registered plugins
252
337
  *
253
- * After `await server.init()`, you can safely use the StateManager, as well
254
- * as any registered Plugins.
338
+ * After `await server.init()` is fulfilled, the {@link server.Server#stateManager}
339
+ * and all registered plugins can be safely used.
255
340
  *
256
341
  * @example
257
342
  * const server = new Server(config);
@@ -421,9 +506,9 @@ Invalid certificate files, please check your:
421
506
  }
422
507
 
423
508
  if (!nodeOnly) {
424
- if (this._applicationTemplateConfig.templateEngine === null
425
- || this._applicationTemplateConfig.templatePath === null
426
- || this._applicationTemplateConfig.clientConfigFunction === null
509
+ if (this._applicationTemplateOptions.templateEngine === null
510
+ || this._applicationTemplateOptions.templatePath === null
511
+ || this._applicationTemplateOptions.clientConfigFunction === null
427
512
  ) {
428
513
  throw new Error('[soundworks:Server] A browser client has been found in "config.app.clients" but configuration for html templating is missing. You should probably call `server.setDefaultTemplateConfig()` if you use the soundworks-template and/or refer (at your own risks) to the documentation of `setCustomTemplateConfig()`');
429
514
  }
@@ -464,17 +549,23 @@ Invalid certificate files, please check your:
464
549
  }
465
550
 
466
551
  /**
467
- * Method to be called when `init` step is done in the initialization
468
- * lifecycle of the soundworks server. If `server.init()` has not been called
469
- * explicitely, `server.start()` will call it automatically.
552
+ * The `start` method is part of the initialization lifecycle of the `soundworks`
553
+ * server. The `start` method will implicitly call the {@link server.Server#init}
554
+ * method if it has not been called manually.
470
555
  *
471
556
  * What it does:
472
- * - starts all registered contexts (context are automatically registered
473
- * when instantiated)
474
- * - start the web socket server
475
- * - launch the HTTP server on given port
557
+ * - implicitely call {@link server.Server#init} if not done manually
558
+ * - launch the HTTP and WebSocket servers
559
+ * - start all created contexts. To this end, you will have to call `server.init`
560
+ * manually and instantiate the contexts between `server.init()` and `server.start()`
476
561
  *
477
- * After `await server.start()` the server is ready to accept incoming connexions
562
+ * After `await server.start()` the server is ready to accept incoming connections
563
+ *
564
+ * @example
565
+ * import { Server } from '@soundworks/core/server.js'
566
+ *
567
+ * const server = new Server(config);
568
+ * await server.start();
478
569
  */
479
570
  async start() {
480
571
  if (this.status === 'idle') {
@@ -557,15 +648,31 @@ Invalid certificate files, please check your:
557
648
 
558
649
  await this._dispatchStatus('started');
559
650
 
651
+ if (this.config.env.type === 'development') {
652
+ logger.log(`\n> press "${chalk.bold('Ctrl + C')}" to exit`);
653
+ }
654
+
560
655
  resolve();
561
656
  });
562
657
  });
563
658
  }
564
659
 
565
- // @todo - handle gracefull close of the server (but define what it means first...)
566
660
  /**
567
- * Stop the server, close all existing WebSocket connections.
568
- * Mainly usefull for test.
661
+ * Stops all started contexts, plugins, close all the socket connections and
662
+ * the http(s) server.
663
+ *
664
+ * In most situations, you might not need to call this method. However, it can
665
+ * be usefull for unit testing or similar situations where you want to create
666
+ * and delete several servers in the same process.
667
+ *
668
+ * @example
669
+ * import { Server } from '@soundworks/core/server.js'
670
+ *
671
+ * const server = new Server(config);
672
+ * await server.start();
673
+ *
674
+ * await new Promise(resolve => setTimeout(resolve, 1000));
675
+ * await server.stop();
569
676
  */
570
677
  async stop() {
571
678
  if (this.status !== 'started') {
@@ -608,7 +715,7 @@ Invalid certificate files, please check your:
608
715
  templatePath,
609
716
  templateEngine,
610
717
  clientConfigFunction,
611
- } = this._applicationTemplateConfig;
718
+ } = this._applicationTemplateOptions;
612
719
 
613
720
  const clientTmpl = path.join(templatePath, `${role}.tmpl`);
614
721
  const defaultTmpl = path.join(templatePath, `default.tmpl`);
@@ -783,8 +890,8 @@ Invalid certificate files, please check your:
783
890
  }
784
891
 
785
892
  /**
786
- * Configure the server to work out-of-the box with the soundworks-template
787
- * directory tree structure.
893
+ * Configure the server to work _out-of-the-box_ within the soundworks application
894
+ * template provided by `@soundworks/create.
788
895
  *
789
896
  * - uses [template-literal](https://www.npmjs.com/package/template-literal) package
790
897
  * as html templateEngine
@@ -796,9 +903,12 @@ Invalid certificate files, please check your:
796
903
  * Also expose two public directory:
797
904
  * - the `public` directory which is exposed behind the root path
798
905
  * - the `./.build/public` directory which is exposed behind the `build` path
906
+ *
907
+ * _Note: except in very rare cases (so rare that they are quite difficult to imagine),
908
+ * you should rely on these defaults._
799
909
  */
800
- setDefaultTemplateConfig() {
801
- this._applicationTemplateConfig = {
910
+ useDefaultApplicationTemplate() {
911
+ this._applicationTemplateOptions = {
802
912
  templateEngine: { compile },
803
913
  templatePath: path.join('.build', 'server', 'tmpl'),
804
914
  clientConfigFunction: (role, config, _httpRequest) => {
@@ -822,20 +932,30 @@ Invalid certificate files, please check your:
822
932
  }
823
933
 
824
934
  /**
825
- * Define your own template path, template engine, and clientConfig function.
826
- * This method is for very advanced use-cases and only be used if you know what
827
- * you are doing. As such its behavior could probably be improved a lot...
828
- *
829
- * If you end up using this, please contact me to explain your use-case :)
935
+ * Define custom template path, template engine, and clientConfig function.
936
+ * This method is proposed for very advanced use-cases and should very probably
937
+ * be improved. If you consider using this for some reason, please get in touch
938
+ * first to explain your use-case :)
830
939
  */
831
- setCustomTemplateConfig(options) {
832
- Object.assign(this._applicationTemplateConfig, options);
940
+ setCustomApplicationTemplateOptions(options) {
941
+ Object.assign(this._applicationTemplateOptions, options);
833
942
  }
834
943
 
835
944
  /**
836
- * Get the global audit state of the application.
945
+ * Attach and retrieve the global audit state of the application.
837
946
  *
947
+ * The audit state is a {@link server.SharedState} instance that keeps track of
948
+ * global informations about the application such as, the number of connected
949
+ * clients, network latency estimation, etc.
950
+ *
951
+ * The audit state is created by the server on start up.
952
+ *
953
+ * @returns {Promise<server.SharedState>}
838
954
  * @throws Will throw if called before `server.init()`
955
+ * @see {@link server.SharedState}
956
+ * @example
957
+ * const auditState = await server.getAuditState();
958
+ * auditState.onUpdate(() => console.log(auditState.getValues()), true);
839
959
  */
840
960
  async getAuditState() {
841
961
  if (this.status === 'idle') {