From ea6e5e602acae5c06a706e33773848c6e22414c9 Mon Sep 17 00:00:00 2001 From: b-ma Date: Sat, 25 May 2024 09:11:45 +0200 Subject: [PATCH] refactor: move Server to es2022 --- .jsdoc | 2 +- src/client/Client.js | 10 +- src/client/ClientPluginManager.js | 16 - src/common/BasePluginManager.js | 210 +++++---- src/common/BaseStateManager.js | 13 +- src/server/Server.js | 422 +++++++++--------- ...textManager.js => ServerContextManager.js} | 107 ++--- ...luginManager.js => ServerPluginManager.js} | 29 +- src/server/ServerSockets.js | 21 +- ...{StateManager.js => ServerStateManager.js} | 67 ++- tests/contexts/Context.spec.js | 9 +- tests/essentials/Client.spec.js | 5 +- tests/essentials/Server.spec.js | 23 +- ...er.spec.js => ClientPluginManager.spec.js} | 0 ...er.spec.js => ServerPluginManager.spec.js} | 6 +- types/client/Client.d.ts | 2 + types/client/ClientPluginManager.d.ts | 22 +- types/common/BasePluginManager.d.ts | 75 ++-- types/common/BaseStateManager.d.ts | 12 +- types/server/Server.d.ts | 190 +++----- ...Manager.d.ts => ServerContextManager.d.ts} | 45 +- ...nManager.d.ts => ServerPluginManager.d.ts} | 37 +- types/server/ServerSockets.d.ts | 22 +- ...teManager.d.ts => ServerStateManager.d.ts} | 111 +++-- 24 files changed, 702 insertions(+), 754 deletions(-) rename src/server/{ContextManager.js => ServerContextManager.js} (69%) rename src/server/{PluginManager.js => ServerPluginManager.js} (85%) rename src/server/{StateManager.js => ServerStateManager.js} (93%) rename tests/plugins/{client.PluginManager.spec.js => ClientPluginManager.spec.js} (100%) rename tests/plugins/{server.PluginManager.spec.js => ServerPluginManager.spec.js} (99%) rename types/server/{ContextManager.d.ts => ServerContextManager.d.ts} (74%) rename types/server/{PluginManager.d.ts => ServerPluginManager.d.ts} (73%) rename types/server/{StateManager.d.ts => ServerStateManager.d.ts} (84%) diff --git a/.jsdoc b/.jsdoc index 04d46ef6..8daac724 100644 --- a/.jsdoc +++ b/.jsdoc @@ -27,8 +27,8 @@ "typedefs": true, "sectionOrder": [ "Namespaces", - "Global", "Classes", + "Global", "Modules", "Externals", "Events", diff --git a/src/client/Client.js b/src/client/Client.js index ff757af7..a7437f58 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -18,6 +18,7 @@ import VERSION from '../common/version.js'; // for testing purposes export const kClientVersionTest = Symbol('soundworks:client-version-test'); +export const kClientOnStatusChangeCallbacks = Symbol('soundworks:client-on-status-change-callbacks'); /** * Configuration object for a client running in a browser runtime. @@ -74,7 +75,6 @@ class Client { #status = 'idle'; // Token of the client if connected through HTTP authentication. #token = null; - #onStatusChangeCallbacks = new Set(); #auditState = null; /** @@ -135,6 +135,8 @@ class Client { this.#stateManager = new ClientStateManager(); this.#status = 'idle'; + this[kClientOnStatusChangeCallbacks] = new Set(); + logger.configure(!!config.env.verbose); } @@ -264,7 +266,7 @@ class Client { // execute all callbacks in parallel const promises = []; - for (let callback of this.#onStatusChangeCallbacks) { + for (let callback of this[kClientOnStatusChangeCallbacks]) { promises.push(callback(status)); } @@ -455,8 +457,8 @@ class Client { * @returns {Function} Function that delete the listener when executed. */ onStatusChange(callback) { - this.#onStatusChangeCallbacks.add(callback); - return () => this.#onStatusChangeCallbacks.delete(callback); + this[kClientOnStatusChangeCallbacks].add(callback); + return () => this[kClientOnStatusChangeCallbacks].delete(callback); } } diff --git a/src/client/ClientPluginManager.js b/src/client/ClientPluginManager.js index 4ad7de11..8300c016 100644 --- a/src/client/ClientPluginManager.js +++ b/src/client/ClientPluginManager.js @@ -2,22 +2,6 @@ import BasePluginManager from '../common/BasePluginManager.js'; import ClientPlugin from './ClientPlugin.js'; import Client from './Client.js'; -/** - * Callback executed when a plugin internal state is updated. - * - * @callback ClientPluginManager~onStateChangeCallback - * @param {Object.} fullState - List of all plugins. - * @param {ClientPlugin|null} initiator - Plugin that initiated the update or `null` - * if the change was initiated by the state manager (i.e. when the initialization - * of the plugins starts). - */ - -/** - * Delete the registered {@link ClientPluginManager~onStateChangeCallback}. - * - * @callback ClientPluginManager~deleteOnStateChangeCallback - */ - /** * The `PluginManager` allows to register and retrieve `soundworks` plugins. * diff --git a/src/common/BasePluginManager.js b/src/common/BasePluginManager.js index 5264d165..6062ec5e 100644 --- a/src/common/BasePluginManager.js +++ b/src/common/BasePluginManager.js @@ -2,6 +2,22 @@ import { isPlainObject, isString } from '@ircam/sc-utils'; import logger from './logger.js'; +/** + * Callback executed when a plugin internal state is updated. + * + * @callback pluginManagerOnStateChangeCallback + * @param {object} fullState - List of all plugins. + * @param {server.Plugin|null} initiator - Plugin that initiated the update or `null` + * if the change was initiated by the state manager (i.e. when the initialization + * of the plugins starts). + */ + +/** + * Delete the registered {@link pluginManagerOnStateChangeCallback}. + * + * @callback pluginManagerDeleteOnStateChangeCallback + */ + /** * Shared functionnality between server-side and client-size plugin manager * @@ -24,83 +40,23 @@ class BasePluginManager { this.status = 'idle'; } - /** - * Register a plugin into soundworks. - * - * _A plugin must always be registered both on client-side and on server-side_ - * - * Refer to the plugin documentation to check its options and proper way of - * registering it. - * - * @param {string} id - Unique id of the plugin. Enables the registration of the - * same plugin factory under different ids. - * @param {Function} factory - Factory function that returns the Plugin class. - * @param {object} [options={}] - Options to configure the plugin. - * @param {array} [deps=[]] - List of plugins' names the plugin depends on, i.e. - * the plugin initialization will start only after the plugins it depends on are - * fully started themselves. - * @see {@link client.PluginManager#register} - * @see {@link server.PluginManager#register} - * @example - * // client-side - * client.pluginManager.register('user-defined-id', pluginFactory); - * // server-side - * server.pluginManager.register('user-defined-id', pluginFactory); - */ - register(id, ctor, options = {}, deps = []) { - // For now we don't allow to register a plugin after `client|server.init()`. - // This is subject to change in the future as we may want to dynamically - // register new plugins during application lifetime. - if (this._node.status === 'inited') { - throw new Error(`[soundworks.PluginManager] Cannot register plugin (${id}) after "client.init()"`); - } - - if (!isString(id)) { - throw new Error(`[soundworks.PluginManager] Invalid argument, "pluginManager.register" first argument should be a string`); - } - - if (!isPlainObject(options)) { - throw new Error(`[soundworks.PluginManager] Invalid argument, "pluginManager.register" third optionnal argument should be an object`); - } - - if (!Array.isArray(deps)) { - throw new Error(`[soundworks.PluginManager] Invalid argument, "pluginManager.register" fourth optionnal argument should be an array`); - } + /** @private */ + #propagateStateChange(instance = null, status = null) { + if (instance !== null) { + // status is null if wew forward some inner state change from the instance + if (status !== null) { + instance.status = status; + } - if (this._instances.has(id)) { - throw new Error(`[soundworks:PluginManager] Plugin "${id}" already registered`); + const fullState = Object.fromEntries(this._instances); + this._onStateChangeCallbacks.forEach(callback => callback(fullState, instance)); + } else { + const fullState = Object.fromEntries(this._instances); + this._onStateChangeCallbacks.forEach(callback => callback(fullState, null)); } - - // we instanciate the plugin here, so that a plugin can register another one - // in its own constructor. - // - // the dependencies must be created first, so that the instance can call - // addDependency in its constructor - this._dependencies.set(id, deps); - - const instance = new ctor(this._node, id, options); - this._instances.set(id, instance); } - /** - * Manually add a dependency to a given plugin. Usefull to require a plugin - * within a plugin - * - */ - addDependency(pluginId, dependencyId) { - const deps = this._dependencies.get(pluginId); - deps.push(dependencyId); - } - - /** - * Returns the list of the registered plugins ids - * @returns {string[]} - */ - getRegisteredPlugins() { - return Array.from(this._instances.keys()); - } - - /** + /** * Initialize all the registered plugin. Executed during the `Client.init()` or * `Server.init()` initialization step. * @private @@ -115,11 +71,11 @@ class BasePluginManager { this.status = 'inited'; // instanciate all plugins for (let [_id, instance] of this._instances.entries()) { - instance.onStateChange(_values => this._propagateStateChange(instance)); + instance.onStateChange(_values => this.#propagateStateChange(instance)); } // propagate all 'idle' statuses before start - this._propagateStateChange(); + this.#propagateStateChange(); const promises = Array.from(this._instances.keys()).map(id => this.unsafeGet(id)); @@ -177,7 +133,7 @@ class BasePluginManager { if (this._instanceStartPromises.has(id)) { await this._instanceStartPromises.get(id); } else { - this._propagateStateChange(instance, 'inited'); + this.#propagateStateChange(instance, 'inited'); let errored = false; try { @@ -187,20 +143,96 @@ class BasePluginManager { await startPromise; } catch (err) { errored = true; - this._propagateStateChange(instance, 'errored'); + this.#propagateStateChange(instance, 'errored'); throw err; } // this looks silly but it prevents the try / catch to catch errors that could // be triggered by the propagate status callback, putting the plugin in errored state if (!errored) { - this._propagateStateChange(instance, 'started'); + this.#propagateStateChange(instance, 'started'); } } return instance; } + /** + * Register a plugin into the manager. + * + * _A plugin must always be registered both on client-side and on server-side_ + * + * Refer to the plugin documentation to check its options and proper way of + * registering it. + * + * @param {string} id - Unique id of the plugin. Enables the registration of the + * same plugin factory under different ids. + * @param {Function} factory - Factory function that returns the Plugin class. + * @param {object} [options={}] - Options to configure the plugin. + * @param {array} [deps=[]] - List of plugins' names the plugin depends on, i.e. + * the plugin initialization will start only after the plugins it depends on are + * fully started themselves. + * @see {@link ClientPluginManager#register} + * @see {@link ServerPluginManager#register} + * @example + * // client-side + * client.pluginManager.register('user-defined-id', pluginFactory); + * // server-side + * server.pluginManager.register('user-defined-id', pluginFactory); + */ + register(id, ctor, options = {}, deps = []) { + // For now we don't allow to register a plugin after `client|server.init()`. + // This is subject to change in the future as we may want to dynamically + // register new plugins during application lifetime. + if (this._node.status === 'inited') { + throw new Error(`[soundworks.PluginManager] Cannot register plugin (${id}) after "client.init()"`); + } + + if (!isString(id)) { + throw new Error(`[soundworks.PluginManager] Invalid argument, "pluginManager.register" first argument should be a string`); + } + + if (!isPlainObject(options)) { + throw new Error(`[soundworks.PluginManager] Invalid argument, "pluginManager.register" third optionnal argument should be an object`); + } + + if (!Array.isArray(deps)) { + throw new Error(`[soundworks.PluginManager] Invalid argument, "pluginManager.register" fourth optionnal argument should be an array`); + } + + if (this._instances.has(id)) { + throw new Error(`[soundworks:PluginManager] Plugin "${id}" already registered`); + } + + // we instanciate the plugin here, so that a plugin can register another one + // in its own constructor. + // + // the dependencies must be created first, so that the instance can call + // addDependency in its constructor + this._dependencies.set(id, deps); + + const instance = new ctor(this._node, id, options); + this._instances.set(id, instance); + } + + /** + * Manually add a dependency to a given plugin. + * + * Usefull to require a plugin within a plugin + */ + addDependency(pluginId, dependencyId) { + const deps = this._dependencies.get(pluginId); + deps.push(dependencyId); + } + + /** + * Returns the list of the registered plugins ids + * @returns {string[]} + */ + getRegisteredPlugins() { + return Array.from(this._instances.keys()); + } + /** * Propagate a notification each time a plugin is updated (status or inner state). * The callback will receive the list of all plugins as first parameter, and the @@ -208,10 +240,8 @@ class BasePluginManager { * * _In most cases, you should not have to rely on this method._ * - * @param {client.PluginManager~onStateChangeCallback|server.PluginManager~onStateChangeCallback} callback - * Callback to be executed on state change - * @param {client.PluginManager~deleteOnStateChangeCallback|client.PluginManager~deleteOnStateChangeCallback} - * Function to execute to listening for changes. + * @param {pluginManagerOnStateChangeCallback} callback - Callback to execute on state change + * @returns {pluginManagerDeleteOnStateChangeCallback} - Clear the subscription when executed * @example * const unsubscribe = client.pluginManager.onStateChange(pluginList, initiator => { * // log the current status of all plugins @@ -230,22 +260,6 @@ class BasePluginManager { this._onStateChangeCallbacks.add(callback); return () => this._onStateChangeCallbacks.delete(callback); } - - /** @private */ - _propagateStateChange(instance = null, status = null) { - if (instance !== null) { - // status is null if wew forward some inner state change from the instance - if (status !== null) { - instance.status = status; - } - - const fullState = Object.fromEntries(this._instances); - this._onStateChangeCallbacks.forEach(callback => callback(fullState, instance)); - } else { - const fullState = Object.fromEntries(this._instances); - this._onStateChangeCallbacks.forEach(callback => callback(fullState, null)); - } - } } export default BasePluginManager; diff --git a/src/common/BaseStateManager.js b/src/common/BaseStateManager.js index 9055802e..a6eb587c 100644 --- a/src/common/BaseStateManager.js +++ b/src/common/BaseStateManager.js @@ -26,6 +26,15 @@ export const kStateManagerDeleteState = Symbol('soundworks:state-manager-delete- // for testing purposes export const kStateManagerClient = Symbol('soundworks:state-manager-client'); + +/** + * @callback stateManagerObserveCallback + * @async + * @param {string} schemaName - name of the schema + * @param {number} stateId - id of the state + * @param {number} nodeId - id of the node that created the state + */ + /** @private */ class BaseStateManager { #client = null; @@ -321,11 +330,11 @@ class BaseStateManager { * * @param {string} [schemaName] - optionnal schema name to filter the observed * states. - * @param {server.StateManager~ObserveCallback|client.StateManager~ObserveCallback} + * @param {stateManagerObserveCallback} * callback - Function to be called when a new state is created on the network. * @param {object} options - Options. * @param {boolean} [options.excludeLocal = false] - If set to true, exclude states - * created locallly, i.e. by the same node, from the collection. + * created locally, i.e. by the same node, from the collection. * @returns {Promise} - Returns a Promise that resolves when the given * callback as been executed on each existing states. The promise value is a * function which allows to stop observing the states on the network. diff --git a/src/server/Server.js b/src/server/Server.js index 396b4b6b..b9d37205 100644 --- a/src/server/Server.js +++ b/src/server/Server.js @@ -30,9 +30,9 @@ import { decryptData, } from './crypto.js'; import Client from './Client.js'; -import ContextManager from './ContextManager.js'; -import PluginManager from './PluginManager.js'; -import StateManager from './StateManager.js'; +import ServerContextManager from './ServerContextManager.js'; +import ServerPluginManager from './ServerPluginManager.js'; +import ServerStateManager from './ServerStateManager.js'; import { kSocketClientId, kSocketTerminate, @@ -93,13 +93,18 @@ const DEFAULT_CONFIG = { const TOKEN_VALID_DURATION = 20; // sec export const kServerOnSocketConnection = Symbol('soundworks:server-on-socket-connection'); +export const kServerIsProtectedRole = Symbol('soundworks:server-is-protected-role'); +export const kServerIsValidConnectionToken = Symbol('soundworks:server-is-valid-connection-token'); +// for testing purposes +export const kServerOnStatusChangeCallbacks = Symbol('soundworks:server-on-status-change-callbacks'); +export const kServerApplicationTemplateOptions = Symbol('soundworks:server-application-template-options'); /** * The `Server` class is the main entry point for the server-side of a soundworks * application. * - * The `Server` instance allows to access soundworks components such as {@link server.StateManager}, - * {@link server.PluginManager},{@link server.Socket} or {@link server.ContextManager}. + * The `Server` instance allows to access soundworks components such as {@link ServerStateManager}, + * {@link ServerPluginManager}, {@link ServerSocket} or {@link ServerContextManager}. * Its is also responsible for handling the initialization lifecycles of the different * soundworks components. * @@ -142,9 +147,24 @@ export const kServerOnSocketConnection = Symbol('soundworks:server-on-socket-con class Server { #config = null; #version = null; + #status = null; #router = null; #httpServer = null; + #db = null; + #sockets = null; + #pluginManager = null; + #stateManager = null; + #contextManager = null; + + // If `https` is required, hold informations about the certificates, e.g. if + // self-signed, the dates of validity of the certificates, etc. + #httpsInfos = null; + #onClientConnectCallbacks = new Set(); + #onClientDisconnectCallbacks = new Set(); + #auditState = null; + #pendingConnectionTokens = new Set(); + #trustedClients = new Set(); /** * @param {ServerConfig} config - Configuration object for the server. @@ -202,79 +222,25 @@ class Server { this.#version = VERSION; // @note: we use express() instead of express.Router() because all 404 and // error stuff is handled by default + // compression must be set before `express.static()` this.#router = express(); - // compression - must be set before express.static() this.#router.use(compression()); this.#sockets = new ServerSockets(this, { path: 'socket' }); - - /** - * Instance of the {@link server.PluginManager} class. - * - * @see {@link server.PluginManager} - * @type {server.PluginManager} - */ - this.pluginManager = new PluginManager(this); - - /** - * Instance of the {@link server.StateManager} class. - * - * @see {@link server.StateManager} - * @type {server.StateManager} - */ - this.stateManager = new StateManager(); - - /** - * Instance of the {@link server.ContextManager} class. - * - * @see {@link server.ContextManager} - * @type {server.ContextManager} - */ - this.contextManager = new ContextManager(this); - - /** - * If `https` is required, hold informations about the certificates, e.g. if - * self-signed, the dates of validity of the certificates, etc. - */ - this.httpsInfos = null; - - /** - * Status of the server, 'idle', 'inited', 'started' or 'errored'. - * - * @type {string} - */ - this.status = 'idle'; - - /** - * Simple key / value database with Promise based Map API store on filesystem, - * basically a tiny wrapper around the `kvey` package. - * - * @private - * @see {@link https://github.com/lukechilds/keyv} - */ - this.db = this.createNamespacedDb('core'); - - /** @private */ - this._applicationTemplateOptions = { + this.#pluginManager = new ServerPluginManager(this); + this.#stateManager = new ServerStateManager(); + this.#contextManager = new ServerContextManager(this); + this.#status = 'idle'; + this.#db = this.createNamespacedDb('core'); + + this[kServerOnStatusChangeCallbacks] = new Set(); + this[kServerApplicationTemplateOptions] = { templateEngine: null, templatePath: null, clientConfigFunction: null, }; - /** @private */ - this._onStatusChangeCallbacks = new Set(); - /** @private */ - this._onClientConnectCallbacks = new Set(); - /** @private */ - this._onClientDisconnectCallbacks = new Set(); - /** @private */ - this._auditState = null; - /** @private */ - this._pendingConnectionTokens = new Set(); - /** @private */ - this._trustedClients = new Set(); - // register audit state schema - this.stateManager.registerSchema(AUDIT_STATE_NAME, auditSchema); + this.#stateManager.registerSchema(AUDIT_STATE_NAME, auditSchema); logger.configure(this.#config.env.verbose); } @@ -322,6 +288,15 @@ class Server { return SERVER_ID; } + /** + * Status of the server. + * + * @type {'idle'|'inited'|'started'|'errored'} + */ + get status() { + return this.#status; + } + /** * Instance of the express router. * @@ -353,6 +328,16 @@ class Server { return this.#httpServer; } + + /** + * Simple key / value filesystem database with Promise based Map API. + * + * Basically a tiny wrapper around the {@link https://github.com/lukechilds/keyv} package. + */ + get db() { + return this.#db; + } + /** * Instance of the {@link ServerSockets} class. * @@ -362,13 +347,93 @@ class Server { return this.#sockets; } + /** + * Instance of the {@link ServerPluginManager} class. + * + * @type {ServerPluginManager} + */ + get pluginManager() { + return this.#pluginManager; + } + + /** + * Instance of the {@link ServerStateManager} class. + * + * @type {ServerStateManager} + */ + get stateManager() { + return this.#stateManager; + } + + /** + * Instance of the {@link ServerContextManager} class. + * + * @type {ServerContextManager} + */ + get contextManager() { + return this.#contextManager; + } + + /** @private */ + async #dispatchStatus(status) { + this.#status = status; + + // if launched in a child process, forward status to parent process + if (process.send !== undefined) { + process.send(`soundworks:server:${status}`); + } + + // execute all callbacks in parallel + const promises = []; + + for (let callback of this[kServerOnStatusChangeCallbacks]) { + promises.push(callback(status)); + } + + await Promise.all(promises); + } + + /** + * Register a callback to execute when status change + * + * @param {function} callback + */ + onStatusChange(callback) { + this[kServerOnStatusChangeCallbacks].add(callback); + return () => this[kServerOnStatusChangeCallbacks].delete(callback); + } + + /** + * Attach and retrieve the global audit state of the application. + * + * The audit state is a {@link SharedState} instance that keeps track of + * global informations about the application such as, the number of connected + * clients, network latency estimation, etc. + * + * The audit state is created by the server on start up. + * + * @returns {Promise} + * @throws Will throw if called before `server.init()` + * + * @example + * const auditState = await server.getAuditState(); + * auditState.onUpdate(() => console.log(auditState.getValues()), true); + */ + async getAuditState() { + if (this.#status === 'idle') { + throw new Error(`[soundworks.Server] Cannot access audit state before init`); + } + + return this.#auditState; + } + /** * The `init` method is part of the initialization lifecycle of the `soundworks` * server. Most of the time, the `init` method will be implicitly called by the - * {@link server.Server#start} method. + * {@link Server#start} method. * * In some situations you might want to call this method manually, in such cases - * the method should be called before the {@link server.Server#start} method. + * the method should be called before the {@link Server#start} method. * * What it does: * - create the audit state @@ -376,7 +441,7 @@ class Server { * declared in `config.app.clients` * - initialize all registered plugins * - * After `await server.init()` is fulfilled, the {@link server.Server#stateManager} + * After `await server.init()` is fulfilled, the {@link Server#stateManager} * and all registered plugins can be safely used. * * @example @@ -388,17 +453,14 @@ class Server { * await server.start(); // init is called implicitely */ async init() { - // ------------------------------------------------------------ - // INIT STATE MANAGER - // ------------------------------------------------------------ - this.stateManager.init(SERVER_ID, new EventEmitter()); + this.#stateManager.init(SERVER_ID, new EventEmitter()); const numClients = {}; for (let name in this.#config.app.clients) { numClients[name] = 0; } /** @private */ - this._auditState = await this.stateManager.create(AUDIT_STATE_NAME, { numClients }); + this.#auditState = await this.#stateManager.create(AUDIT_STATE_NAME, { numClients }); // basic http authentication if (this.#config.env.auth) { @@ -419,7 +481,7 @@ class Server { return; } - const isProtected = this.isProtected(role); + const isProtected = this[kServerIsProtectedRole](role); if (isProtected) { // authentication middleware @@ -438,10 +500,10 @@ class Server { const token = { id, ip, time }; const encryptedToken = encryptData(token); - this._pendingConnectionTokens.add(encryptedToken); + this.#pendingConnectionTokens.add(encryptedToken); setTimeout(() => { - this._pendingConnectionTokens.delete(encryptedToken); + this.#pendingConnectionTokens.delete(encryptedToken); }, TOKEN_VALID_DURATION * 1000); // pass to the response object to be send to the client @@ -492,7 +554,7 @@ class Server { try { x509 = new X509Certificate(cert); } catch (err) { - this._dispatchStatus('errored'); + this.#dispatchStatus('errored'); throw new Error(`[soundworks:Server] Invalid https cert file`); } @@ -500,11 +562,11 @@ class Server { const keyObj = createPrivateKey(key); if (!x509.checkPrivateKey(keyObj)) { - this._dispatchStatus('errored'); + this.#dispatchStatus('errored'); throw new Error(`[soundworks:Server] Invalid https key file`); } } catch (err) { - this._dispatchStatus('errored'); + this.#dispatchStatus('errored'); throw new Error(`[soundworks:Server] Invalid https key file`); } @@ -516,7 +578,7 @@ class Server { const diff = certExpire - now; const daysRemaining = Math.round(diff / 1000 / 60 / 60 / 24); - this.httpsInfos = { + this.#httpsInfos = { selfSigned: false, CN: x509.subject.split('=')[1], altNames: x509.subjectAltName.split(',').map(e => e.trim().split(':')[1]), @@ -534,17 +596,17 @@ Invalid certificate files, please check your: - cert file: ${httpsInfos.cert} `); - this._dispatchStatus('errored'); + this.#dispatchStatus('errored'); throw err; } } else { // generate certs // -------------------------------------------------------- - const cert = await this.db.get('httpsCert'); - const key = await this.db.get('httpsKey'); + const cert = await this.#db.get('httpsCert'); + const key = await this.#db.get('httpsKey'); if (key && cert) { - this.httpsInfos = { selfSigned: true }; + this.#httpsInfos = { selfSigned: true }; this.#httpServer = https.createServer({ cert, key }, this.#router); } else { this.#httpServer = await new Promise((resolve, reject) => { @@ -552,7 +614,7 @@ Invalid certificate files, please check your: pem.createCertificate({ days: 1, selfSigned: true }, async (err, keys) => { if (err) { logger.error(err.stack); - this._dispatchStatus('errored'); + this.#dispatchStatus('errored'); reject(err); return; @@ -561,11 +623,11 @@ Invalid certificate files, please check your: const cert = keys.certificate; const key = keys.serviceKey; - this.httpsInfos = { selfSigned: true }; + this.#httpsInfos = { selfSigned: true }; // we store the generated cert so that we don't have to re-accept // the cert each time the server restarts in development - await this.db.set('httpsCert', cert); - await this.db.set('httpsKey', key); + await this.#db.set('httpsCert', cert); + await this.#db.set('httpsKey', key); const httpsServer = https.createServer({ cert, key }, this.#router); @@ -586,9 +648,9 @@ Invalid certificate files, please check your: } if (!nodeOnly) { - if (this._applicationTemplateOptions.templateEngine === null - || this._applicationTemplateOptions.templatePath === null - || this._applicationTemplateOptions.clientConfigFunction === null + if (this[kServerApplicationTemplateOptions].templateEngine === null + || this[kServerApplicationTemplateOptions].templatePath === null + || this[kServerApplicationTemplateOptions].clientConfigFunction === null ) { 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()`'); } @@ -612,7 +674,7 @@ Invalid certificate files, please check your: clientsConfig .sort(a => a.default === true ? 1 : -1) .forEach(config => { - const path = this._openClientRoute(this.#router, config); + const path = this.#openClientRoute(this.#router, config); routes.push({ role: config.role, path }); }); @@ -621,20 +683,20 @@ Invalid certificate files, please check your: // ------------------------------------------------------------ // START PLUGIN MANAGER // ------------------------------------------------------------ - await this.pluginManager.start(); + await this.#pluginManager.start(); - await this._dispatchStatus('inited'); + await this.#dispatchStatus('inited'); return Promise.resolve(); } /** * The `start` method is part of the initialization lifecycle of the `soundworks` - * server. The `start` method will implicitly call the {@link server.Server#init} + * server. The `start` method will implicitly call the {@link Server#init} * method if it has not been called manually. * * What it does: - * - implicitely call {@link server.Server#init} if not done manually + * - implicitely call {@link Server#init} if not done manually * - launch the HTTP and WebSocket servers * - start all created contexts. To this end, you will have to call `server.init` * manually and instantiate the contexts between `server.init()` and `server.start()` @@ -648,22 +710,22 @@ Invalid certificate files, please check your: * await server.start(); */ async start() { - if (this.status === 'idle') { + if (this.#status === 'idle') { await this.init(); } - if (this.status === 'started') { + if (this.#status === 'started') { throw new Error(`[soundworks:Server] Cannot call "server.start()" twice`); } - if (this.status !== 'inited') { + if (this.#status !== 'inited') { throw new Error(`[soundworks:Server] Cannot "server.start()" before "server.init()"`); } // ------------------------------------------------------------ // START CONTEXT MANAGER // ------------------------------------------------------------ - await this.contextManager.start(); + await this.#contextManager.start(); // ------------------------------------------------------------ // START SOCKET SERVER @@ -690,39 +752,39 @@ Invalid certificate files, please check your: }); }); - if (this.httpsInfos !== null) { + if (this.#httpsInfos !== null) { logger.title(`https certificates infos`); - // this.httpsInfos.selfSigned = true; - if (this.httpsInfos.selfSigned) { - logger.log(` self-signed: ${this.httpsInfos.selfSigned ? 'true' : 'false'}`); + // this.#httpsInfos.selfSigned = true; + if (this.#httpsInfos.selfSigned) { + logger.log(` self-signed: ${this.#httpsInfos.selfSigned ? 'true' : 'false'}`); logger.log(chalk.yellow` > INVALID CERTIFICATE (self-signed)`); } else { - logger.log(` valid from: ${this.httpsInfos.validFrom}`); - logger.log(` valid to: ${this.httpsInfos.validTo}`); + logger.log(` valid from: ${this.#httpsInfos.validFrom}`); + logger.log(` valid to: ${this.#httpsInfos.validTo}`); - // this.httpsInfos.isValid = false; // for testing - if (!this.httpsInfos.isValid) { + // this.#httpsInfos.isValid = false; // for testing + if (!this.#httpsInfos.isValid) { logger.error(chalk.red` -------------------------------------------`); logger.error(chalk.red` > INVALID CERTIFICATE `); logger.error(chalk.red` i.e. you pretend to be safe but you are not`); logger.error(chalk.red` -------------------------------------------`); } else { - // this.httpsInfos.daysRemaining = 2; // for testing - if (this.httpsInfos.daysRemaining < 5) { - logger.log(chalk.red` > CERTIFICATE IS VALID... BUT ONLY ${this.httpsInfos.daysRemaining} DAYS LEFT, PLEASE CONSIDER UPDATING YOUR CERTS!`); - } else if (this.httpsInfos.daysRemaining < 15) { - logger.log(chalk.yellow` > CERTIFICATE IS VALID - only ${this.httpsInfos.daysRemaining} days left, be careful...`); + // this.#httpsInfos.daysRemaining = 2; // for testing + if (this.#httpsInfos.daysRemaining < 5) { + logger.log(chalk.red` > CERTIFICATE IS VALID... BUT ONLY ${this.#httpsInfos.daysRemaining} DAYS LEFT, PLEASE CONSIDER UPDATING YOUR CERTS!`); + } else if (this.#httpsInfos.daysRemaining < 15) { + logger.log(chalk.yellow` > CERTIFICATE IS VALID - only ${this.#httpsInfos.daysRemaining} days left, be careful...`); } else { - logger.log(chalk.green` > CERTIFICATE IS VALID (${this.httpsInfos.daysRemaining} days left)`); + logger.log(chalk.green` > CERTIFICATE IS VALID (${this.#httpsInfos.daysRemaining} days left)`); } } } } - await this._dispatchStatus('started'); + await this.#dispatchStatus('started'); if (this.#config.env.type === 'development') { logger.log(`\n> press "${chalk.bold('Ctrl + C')}" to exit`); @@ -751,12 +813,12 @@ Invalid certificate files, please check your: * await server.stop(); */ async stop() { - if (this.status !== 'started') { + if (this.#status !== 'started') { throw new Error(`[soundworks:Server] Cannot stop() before start()`); } - await this.contextManager.stop(); - await this.pluginManager.stop(); + await this.#contextManager.stop(); + await this.#pluginManager.stop(); this.#sockets[kSocketsStop](); @@ -766,14 +828,14 @@ Invalid certificate files, please check your: } }); - await this._dispatchStatus('stopped'); + await this.#dispatchStatus('stopped'); } /** * Open the route for a given client. * @private */ - _openClientRoute(router, config) { + #openClientRoute(router, config) { const { role, target } = config; const isDefault = (config.default === true); // only browser targets need a route @@ -794,7 +856,7 @@ Invalid certificate files, please check your: templatePath, templateEngine, clientConfigFunction, - } = this._applicationTemplateOptions; + } = this[kServerApplicationTemplateOptions]; const clientTmpl = path.join(templatePath, `${role}.tmpl`); const defaultTmpl = path.join(templatePath, `default.tmpl`); @@ -851,15 +913,15 @@ Invalid certificate files, please check your: } onClientConnect(callback) { - this._onClientConnectCallbacks.add(callback); + this.#onClientConnectCallbacks.add(callback); - return () => this._onClientConnectCallbacks.delete(callback); + return () => this.#onClientConnectCallbacks.delete(callback); } onClientDisconnect(callback) { - this._onClientDisconnectCallbacks.add(callback); + this.#onClientDisconnectCallbacks.add(callback); - return () => this._onClientDisconnectCallbacks.delete(callback); + return () => this.#onClientDisconnectCallbacks.delete(callback); } /** @@ -872,41 +934,41 @@ Invalid certificate files, please check your: const roles = Object.keys(this.#config.app.clients); // this has been validated - if (this.isProtected(role) && this.isValidConnectionToken(connectionToken)) { + if (this[kServerIsProtectedRole](role) && this[kServerIsValidConnectionToken](connectionToken)) { const { ip } = decryptData(connectionToken); const newData = { ip, id: client.id }; const newToken = encryptData(newData); client.token = newToken; - this._pendingConnectionTokens.delete(connectionToken); - this._trustedClients.add(client); + this.#pendingConnectionTokens.delete(connectionToken); + this.#trustedClients.add(client); } socket.addListener('close', async () => { // do nothing if client role is invalid if (roles.includes(role)) { // decrement audit state counter - const numClients = this._auditState.get('numClients'); + const numClients = this.#auditState.get('numClients'); numClients[role] -= 1; - this._auditState.set({ numClients }); + this.#auditState.set({ numClients }); // delete token - if (this._trustedClients.has(client)) { - this._trustedClients.delete(client); + if (this.#trustedClients.has(client)) { + this.#trustedClients.delete(client); } // if something goes wrong here, the 'close' event is called again and // again and again... let's just log the error and terminate the socket try { // clean context manager, await before cleaning state manager - await this.contextManager.removeClient(client); + await this.#contextManager.removeClient(client); // remove client from pluginManager - await this.pluginManager.removeClient(client); + await this.#pluginManager.removeClient(client); // clean state manager - await this.stateManager.removeClient(client.id); + await this.#stateManager.removeClient(client.id); - this._onClientDisconnectCallbacks.forEach(callback => callback(client)); + this.#onClientDisconnectCallbacks.forEach(callback => callback(client)); } catch (err) { console.error(err); } @@ -934,7 +996,7 @@ Invalid certificate files, please check your: } try { - this.pluginManager.checkRegisteredPlugins(registeredPlugins); + this.#pluginManager.checkRegisteredPlugins(registeredPlugins); } catch (err) { socket.send(CLIENT_HANDSHAKE_ERROR, { type: 'invalid-plugin-list', @@ -944,23 +1006,23 @@ Invalid certificate files, please check your: } // increment audit state - const numClients = this._auditState.get('numClients'); + const numClients = this.#auditState.get('numClients'); numClients[role] += 1; - this._auditState.set({ numClients }); + this.#auditState.set({ numClients }); // add client to state manager - await this.stateManager.addClient(client.id, { + await this.#stateManager.addClient(client.id, { emit: client.socket.send.bind(client.socket), addListener: client.socket.addListener.bind(client.socket), removeAllListeners: client.socket.removeAllListeners.bind(client.socket), }); // add client to plugin manager // server-side, all plugins are active for the lifetime of the client - await this.pluginManager.addClient(client, registeredPlugins); + await this.#pluginManager.addClient(client, registeredPlugins); // add client to context manager - await this.contextManager.addClient(client); + await this.#contextManager.addClient(client); - this._onClientConnectCallbacks.forEach(callback => callback(client)); + this.#onClientConnectCallbacks.forEach(callback => callback(client)); const { id, uuid, token } = client; socket.send(CLIENT_HANDSHAKE_RESPONSE, { id, uuid, token, version: this.#version }); @@ -996,31 +1058,6 @@ Invalid certificate files, please check your: return db; } - onStatusChange(callback) { - this._onStatusChangeCallbacks.add(callback); - - return () => this._onStatusChangeCallbacks.delete(callback); - } - - /** @private */ - async _dispatchStatus(status) { - this.status = status; - - // if launched in a child process, forward status to parent process - if (process.send !== undefined) { - process.send(`soundworks:server:${status}`); - } - - // execute all callbacks in parallel - const promises = []; - - for (let callback of this._onStatusChangeCallbacks) { - promises.push(callback(status)); - } - - await Promise.all(promises); - } - /** * Configure the server to work _out-of-the-box_ within the soundworks application * template provided by `@soundworks/create. @@ -1061,7 +1098,7 @@ Invalid certificate files, please check your: } }); - this._applicationTemplateOptions = { + this[kServerApplicationTemplateOptions] = { templateEngine: { compile }, templatePath: path.join('.build', 'server', 'tmpl'), clientConfigFunction: (role, config, _httpRequest) => { @@ -1098,35 +1135,14 @@ Invalid certificate files, please check your: * first to explain your use-case :) */ setCustomApplicationTemplateOptions(options) { - Object.assign(this._applicationTemplateOptions, options); + Object.assign(this[kServerApplicationTemplateOptions], options); } - /** - * Attach and retrieve the global audit state of the application. - * - * The audit state is a {@link server.SharedState} instance that keeps track of - * global informations about the application such as, the number of connected - * clients, network latency estimation, etc. - * - * The audit state is created by the server on start up. - * - * @returns {Promise} - * @throws Will throw if called before `server.init()` - * @see {@link server.SharedState} - * @example - * const auditState = await server.getAuditState(); - * auditState.onUpdate(() => console.log(auditState.getValues()), true); - */ - async getAuditState() { - if (this.status === 'idle') { - throw new Error(`[soundworks.Server] Cannot access audit state before init`); - } - return this._auditState; - } +// /** @private */ - isProtected(role) { + [kServerIsProtectedRole](role) { if (this.#config.env.auth && Array.isArray(this.#config.env.auth.clients)) { return this.#config.env.auth.clients.includes(role); } @@ -1135,9 +1151,9 @@ Invalid certificate files, please check your: } /** @private */ - isValidConnectionToken(token) { + [kServerIsValidConnectionToken](token) { // token should be in pending token list - if (!this._pendingConnectionTokens.has(token)) { + if (!this.#pendingConnectionTokens.has(token)) { return false; } @@ -1148,7 +1164,7 @@ Invalid certificate files, please check your: // token is valid only for 30 seconds (this is arbitrary) if (now > data.time + TOKEN_VALID_DURATION) { // delete the token, is too old - this._pendingConnectionTokens.delete(token); + this.#pendingConnectionTokens.delete(token); return false; } else { return true; @@ -1159,14 +1175,14 @@ Invalid certificate files, please check your: * Check if the given client is trusted, i.e. config.env.type == 'production' * and the client is protected behind a password. * - * @param {server.Client} client - Client to be tested + * @param {ServerClient} client - Client to be tested * @returns {Boolean} */ isTrustedClient(client) { if (this.#config.env.type !== 'production') { return true; } else { - return this._trustedClients.has(client); + return this.#trustedClients.has(client); } } @@ -1184,7 +1200,7 @@ Invalid certificate files, please check your: if (this.#config.env.type !== 'production') { return true; } else { - for (let client of this._trustedClients) { + for (let client of this.#trustedClients) { if (client.id === clientId && client.token === token) { // check that given token is consistent with client ip and id const { id, ip } = decryptData(client.token); diff --git a/src/server/ContextManager.js b/src/server/ServerContextManager.js similarity index 69% rename from src/server/ContextManager.js rename to src/server/ServerContextManager.js index a8098606..b4859477 100644 --- a/src/server/ContextManager.js +++ b/src/server/ServerContextManager.js @@ -8,6 +8,9 @@ import { CONTEXT_EXIT_ERROR, } from '../common/constants.js'; +// for testing purposes +export const kContextManagerContexts = Symbol('soundworks:context-manager-contexts'); + /** * Create a dummy server side context if a proper server-side context has not * been declared and registered, one DefaultContext is created per unknown @@ -25,68 +28,55 @@ function createNamedContextClass(contextName) { /** @private */ class ContextCollection { - constructor() { - this.inner = []; + #inner = []; + + // for testing purposes + get length() { + return this.#inner.length; } add(context) { - this.inner.push(context); + this.#inner.push(context); } has(name) { - return this.inner.find(c => c.name === name) !== undefined; + return this.#inner.find(c => c.name === name) !== undefined; } get(name) { - return this.inner.find(c => c.name === name); + return this.#inner.find(c => c.name === name); } map(func) { - return this.inner.map(func); + return this.#inner.map(func); } filter(func) { - return this.inner.filter(func); + return this.#inner.filter(func); } } /** - * Manage the different server-side contexts and their lifecycle. The `ContextManager` - * is automatically instantiated by the {@link server.Server}. + * Manage the different server-side contexts and their lifecycle. + * + * The `ServerContextManager` is automatically instantiated by the {@link server.Server}. * * _WARNING: Most of the time, you should not have to manipulate the context manager directly._ * - * @memberof server * @hideconstructor */ -class ContextManager { +class ServerContextManager { + #server = null; + + #contextStartPromises = new Map(); + /** * @param {server.Server} server - Instance of the soundworks server. */ constructor(server) { - /** @private */ - this.server = server; - /** @private */ - this._contexts = new ContextCollection(); - /** @private */ - this._contextStartPromises = new Map(); - } - - /** - * Register a context in the manager. - * This method is called in the {@link server.Context} constructor - * - * @param {server.Context} context - Context instance to register. - * - * @private - */ - register(context) { - // we must await the contructor initialization end to check the name and throw - if (this._contexts.has(context.name)) { - throw new Error(`[soundworks:ContextManager] Context "${context.name}" already registered`); - } + this.#server = server; - this._contexts.add(context); + this[kContextManagerContexts] = new ContextCollection(); } /** @@ -97,21 +87,21 @@ class ContextManager { * @param {server.Context#name} contextName - Name of the context. */ async get(contextName) { - if (!this._contexts.has(contextName)) { - throw new Error(`[soundworks:ContextManager] Can't get context "${contextName}", not registered`); + if (!this[kContextManagerContexts].has(contextName)) { + throw new Error(`[soundworks:ServerContextManager] Can't get context "${contextName}", not registered`); } - const context = this._contexts.get(contextName); + const context = this[kContextManagerContexts].get(contextName); - if (this._contextStartPromises.has(contextName)) { - const startPromise = this._contextStartPromises.get(contextName); + if (this.#contextStartPromises.has(contextName)) { + const startPromise = this.#contextStartPromises.get(contextName); await startPromise; } else { context.status = 'inited'; try { const startPromise = context.start(); - this._contextStartPromises.set(contextName, startPromise); + this.#contextStartPromises.set(contextName, startPromise); await startPromise; context.status = 'started'; } catch (err) { @@ -123,6 +113,23 @@ class ContextManager { return context; } + /** + * Register a context in the manager. + * This method is called in the {@link server.Context} constructor + * + * @param {server.Context} context - Context instance to register. + * + * @private + */ + register(context) { + // we must await the contructor initialization end to check the name and throw + if (this[kContextManagerContexts].has(context.name)) { + throw new Error(`[soundworks:ServerContextManager] Context "${context.name}" already registered`); + } + + this[kContextManagerContexts].add(context); + } + /** * Called when a client connects to the server (websocket handshake) * @@ -133,11 +140,11 @@ class ContextManager { addClient(client) { client.socket.addListener(CONTEXT_ENTER_REQUEST, async (reqId, contextName) => { // if no context found, create a DefaultContext on the fly - if (!this._contexts.has(contextName)) { + if (!this[kContextManagerContexts].has(contextName)) { // create default context for all client types const ctor = createNamedContextClass(contextName); // this will automatically register the context in the context manager - new ctor(this.server); + new ctor(this.#server); } // we ensure context is started, even lazilly after server.start() @@ -161,7 +168,7 @@ class ContextManager { CONTEXT_ENTER_ERROR, reqId, contextName, - `[soundworks:ContextManager] Client already in context (if only one context is created .enter() has been called automatically)`, + `[soundworks:ServerContextManager] Client already in context (if only one context is created .enter() has been called automatically)`, ); return; } @@ -176,7 +183,7 @@ class ContextManager { CONTEXT_ENTER_ERROR, reqId, contextName, - `[soundworks:ContextManager] Clients with role "${client.role}" are not declared as possible consumers of context "${contextName}"`, + `[soundworks:ServerContextManager] Clients with role "${client.role}" are not declared as possible consumers of context "${contextName}"`, ); return; } @@ -191,12 +198,12 @@ class ContextManager { }); client.socket.addListener(CONTEXT_EXIT_REQUEST, async (reqId, contextName) => { - if (!this._contexts.has(contextName)) { + if (!this[kContextManagerContexts].has(contextName)) { client.socket.send( CONTEXT_EXIT_ERROR, reqId, contextName, - `[soundworks:ContextManager] Cannot exit(), context ${contextName} does not exists`, + `[soundworks:ServerContextManager] Cannot exit(), context ${contextName} does not exists`, ); return; } @@ -215,7 +222,7 @@ class ContextManager { CONTEXT_EXIT_ERROR, reqId, contextName, - `[soundworks:ContextManager] Client with role "${client.role}" is not in context "${contextName}"`, + `[soundworks:ServerContextManager] Client with role "${client.role}" is not in context "${contextName}"`, ); } }); @@ -233,7 +240,7 @@ class ContextManager { client.socket.removeAllListeners(CONTEXT_EXIT_REQUEST); // exit from all contexts - const promises = this._contexts + const promises = this[kContextManagerContexts] .filter(context => context.clients.has(client)) .map(context => context.exit(client)); @@ -247,7 +254,7 @@ class ContextManager { * @private */ async start() { - const promises = this._contexts.map(context => this.get(context.name)); + const promises = this[kContextManagerContexts].map(context => this.get(context.name)); await Promise.all(promises); } @@ -257,9 +264,9 @@ class ContextManager { * @private */ async stop() { - const promises = this._contexts.map(context => context.stop()); + const promises = this[kContextManagerContexts].map(context => context.stop()); await Promise.all(promises); } } -export default ContextManager; +export default ServerContextManager; diff --git a/src/server/PluginManager.js b/src/server/ServerPluginManager.js similarity index 85% rename from src/server/PluginManager.js rename to src/server/ServerPluginManager.js index 843b3108..47c36737 100644 --- a/src/server/PluginManager.js +++ b/src/server/ServerPluginManager.js @@ -2,22 +2,6 @@ import BasePluginManager from '../common/BasePluginManager.js'; import Plugin from './Plugin.js'; import Server from './Server.js'; -/** - * Callback executed when a plugin internal state is updated. - * - * @callback server.PluginManager~onStateChangeCallback - * @param {object} fullState - List of all plugins. - * @param {server.Plugin|null} initiator - Plugin that initiated the update or `null` - * if the change was initiated by the state manager (i.e. when the initialization - * of the plugins starts). - */ - -/** - * Delete the registered {@link server.PluginManager~onStateChangeCallback}. - * - * @callback server.PluginManager~deleteOnStateChangeCallback - */ - /** * The `PluginManager` allows to register and retrieve `soundworks` plugins. * @@ -70,12 +54,11 @@ import Server from './Server.js'; * }, 1000); * ``` * - * @memberof server * @extends BasePluginManager * @inheritdoc * @hideconstructor */ -class PluginManager extends BasePluginManager { +class ServerPluginManager extends BasePluginManager { constructor(server) { if (!(server instanceof Server)) { throw new Error(`[soundworks.PluginManager] Invalid argument, "new PluginManager(server)" should receive an instance of "soundworks.Server" as argument`); @@ -95,7 +78,7 @@ class PluginManager extends BasePluginManager { } /** - * Retrieve an fully started instance of a registered plugin. + * Retrieve a fully started instance of a registered plugin. * * Be aware that the `get` method resolves only when the plugin is fully 'started', * which is what we want 99.99% of the time. As such, and to prevent the application @@ -111,9 +94,7 @@ class PluginManager extends BasePluginManager { * * @param {server.Plugin#id} id - Id of the plugin as defined when registered. * @returns {server.Plugin} - * @see {@link server.PluginManager#onStateChange} - * - * @private + * @see {@link ServerPluginManager#onStateChange} */ async get(id) { if (this.status !== 'started') { @@ -125,7 +106,7 @@ class PluginManager extends BasePluginManager { // server only methods - /** private */ + /** @private */ checkRegisteredPlugins(registeredPlugins) { let missingPlugins = []; @@ -167,4 +148,4 @@ class PluginManager extends BasePluginManager { } } -export default PluginManager; +export default ServerPluginManager; diff --git a/src/server/ServerSockets.js b/src/server/ServerSockets.js index 457ce0e9..3950eecb 100644 --- a/src/server/ServerSockets.js +++ b/src/server/ServerSockets.js @@ -10,6 +10,7 @@ import { import { kServerOnSocketConnection, + kServerIsProtectedRole, } from './Server.js'; import Socket, { kSocketTerminate, @@ -26,9 +27,9 @@ export const kSocketsLatencyStatsWorker = Symbol('soundworks:sockets-latency-sta export const kSocketsDebugPreventHeartBeat = Symbol('soundworks:sockets-debug-prevent-heartbeat'); /** - * Manage all {@link server.Socket} instances. + * Manage all {@link ServerSocket} instances. * - * _Important: In most cases, you should consider using a {@link client.SharedState} + * _Important: In most cases, you should consider using a {@link SharedState} * rather than directly using the Socket instance._ */ class ServerSockets { @@ -98,10 +99,10 @@ class ServerSockets { this.#server.httpServer.on('upgrade', async (req, socket, head) => { const { role, token } = querystring.parse(req.url.split('?')[1]); - if (this.#server.isProtected(role)) { + if (this.#server[kServerIsProtectedRole](role)) { // we don't have any IP in the upgrade request object, // so we just check the connection token is pending and valid - if (!this.#server.isValidConnectionToken(token)) { + if (!this.#server[kServerIsValidConnectionToken](token)) { socket.destroy('not allowed'); } } @@ -137,9 +138,9 @@ class ServerSockets { /** * Add a socket to a room. * - * _Note that in most cases, you should use a shared state instead_ + * _Note that in most cases, you should use a {@link SharedState} instead_ * - * @param {server.Socket} socket - Socket to add to the room. + * @param {ServerSocket} socket - Socket to add to the room. * @param {String} roomId - Id of the room. */ addToRoom(socket, roomId) { @@ -154,9 +155,9 @@ class ServerSockets { /** * Remove a socket from a room. * - * _Note that in most cases, you should use a shared state instead_ + * _Note that in most cases, you should use a {@link SharedState} instead_ * - * @param {server.Socket} socket - Socket to remove from the room. + * @param {ServerSocket} socket - Socket to remove from the room. * @param {String} roomId - Id of the room. */ removeFromRoom(socket, roomId) { @@ -170,11 +171,11 @@ class ServerSockets { * Send a message to all clients os given room(s). If no room is specified, * the message is sent to all clients. * - * _Note that in most cases, you should use a shared state instead_ + * _Note that in most cases, you should use a {@link SharedState} instead_ * * @param {String|Array} roomsIds - Ids of the rooms that must receive * the message. If `null` the message is sent to all clients. - * @param {server.Socket} excludeSocket - Optionnal socket to ignore when + * @param {ServerSocket} excludeSocket - Optionnal socket to ignore when * broadcasting the message, typically the client at the origin of the message. * @param {String} channel - Channel name. * @param {...*} args - Payload of the message. As many arguments as needed, of diff --git a/src/server/StateManager.js b/src/server/ServerStateManager.js similarity index 93% rename from src/server/StateManager.js rename to src/server/ServerStateManager.js index af8673ea..086c898f 100644 --- a/src/server/StateManager.js +++ b/src/server/ServerStateManager.js @@ -33,9 +33,9 @@ const generateStateId = idGenerator(); const generateRemoteId = idGenerator(); /** - * @typedef {object} server.StateManager~schema + * @typedef {object} ServerStateManager~schema * - * Description of a schema to be registered by the {@link server.StateManager#registerSchema} + * Description of a schema to be registered by the {@link ServerStateManager#registerSchema} * * A schema is the blueprint, or definition from which shared states can be created. * @@ -43,12 +43,12 @@ const generateRemoteId = idGenerator(); * the parameter, and the value is an object describing the parameter. * * The value can be of any of the foolowing types: - * - {@link server.StateManager~schemaBooleanDefinition} - * - {@link server.StateManager~schemaStringDefinition} - * - {@link server.StateManager~schemaIntegerDefinition} - * - {@link server.StateManager~schemaFloatDefinition} - * - {@link server.StateManager~schemaEnumDefinition} - * - {@link server.StateManager~schemaAnyDefinition} + * - {@link ServerStateManager~schemaBooleanDefinition} + * - {@link ServerStateManager~schemaStringDefinition} + * - {@link ServerStateManager~schemaIntegerDefinition} + * - {@link ServerStateManager~schemaFloatDefinition} + * - {@link ServerStateManager~schemaEnumDefinition} + * - {@link ServerStateManager~schemaAnyDefinition} * * @example * const mySchema = { @@ -67,9 +67,9 @@ const generateRemoteId = idGenerator(); * server.stateManager.registerSchema('my-schema-name', mySchema); */ /** - * Describe a {@link server.StateManager~schema} entry of "boolean" type. + * Describe a {@link ServerStateManager~schema} entry of "boolean" type. * - * @typedef {object} server.StateManager~schemaBooleanDefinition + * @typedef {object} ServerStateManager~schemaBooleanDefinition * @property {string} type='boolean' - Define a boolean parameter. * @property {boolean} default - Default value of the parameter. * @property {boolean} [nullable=false] - Define if the parameter is nullable. If @@ -93,9 +93,9 @@ const generateRemoteId = idGenerator(); * @property {object} [metas={}] - Optionnal metadata of the parameter. */ /** - * Describe a {@link server.StateManager~schema} entry of "string" type. + * Describe a {@link ServerStateManager~schema} entry of "string" type. * - * @typedef {object} server.StateManager~schemaStringDefinition + * @typedef {object} ServerStateManager~schemaStringDefinition * @property {string} type='string' - Define a boolean parameter. * @property {string} default - Default value of the parameter. * @property {boolean} [nullable=false] - Define if the parameter is nullable. If @@ -119,9 +119,9 @@ const generateRemoteId = idGenerator(); * @property {object} [metas={}] - Optionnal metadata of the parameter. */ /** - * Describe a {@link server.StateManager~schema} entry of "integer" type. + * Describe a {@link ServerStateManager~schema} entry of "integer" type. * - * @typedef {object} server.StateManager~schemaIntegerDefinition + * @typedef {object} ServerStateManager~schemaIntegerDefinition * @property {string} type='integer' - Define a boolean parameter. * @property {number} default - Default value of the parameter. * @property {number} [min=-Infinity] - Minimum value of the parameter. @@ -147,9 +147,9 @@ const generateRemoteId = idGenerator(); * @property {object} [metas={}] - Optionnal metadata of the parameter. */ /** - * Describe a {@link server.StateManager~schema} entry of "float" type. + * Describe a {@link ServerStateManager~schema} entry of "float" type. * - * @typedef {object} server.StateManager~schemaFloatDefinition + * @typedef {object} ServerStateManager~schemaFloatDefinition * @property {string} [type='float'] - Float parameter. * @property {number} default - Default value. * @property {number} [min=-Infinity] - Minimum value. @@ -175,9 +175,9 @@ const generateRemoteId = idGenerator(); * @property {object} [metas={}] - Optionnal metadata of the parameter. */ /** - * Describe a {@link server.StateManager~schema} entry of "enum" type. + * Describe a {@link ServerStateManager~schema} entry of "enum" type. * - * @typedef {object} server.StateManager~schemaEnumDefinition + * @typedef {object} ServerStateManager~schemaEnumDefinition * @property {string} [type='enum'] - Enum parameter. * @property {string} default - Default value of the parameter. * @property {Array} list - Possible values of the parameter. @@ -202,12 +202,12 @@ const generateRemoteId = idGenerator(); * @property {object} [metas={}] - Optionnal metadata of the parameter. */ /** - * Describe a {@link server.StateManager~schema} entry of "any" type. + * Describe a {@link ServerStateManager~schema} entry of "any" type. * * Note that the `any` type always return a shallow copy of the state internal * value. Mutating the returned value will therefore not modify the internal state. * - * @typedef {object} server.StateManager~schemaAnyDefinition + * @typedef {object} ServerStateManager~schemaAnyDefinition * @property {string} [type='any'] - Parameter of any type. * @property {*} default - Default value of the parameter. * @property {boolean} [nullable=false] - Define if the parameter is nullable. If @@ -232,15 +232,7 @@ const generateRemoteId = idGenerator(); */ /** - * @callback server.StateManager~ObserveCallback - * @async - * @param {string} schemaName - name of the schema - * @param {number} stateId - id of the state - * @param {number} nodeId - id of the node that created the state - */ - -/** - * @callback server.StateManager~updateHook + * @callback serverStateManagerUpdateHook * @async * * @param {object} updates - Update object as given on a set callback, or @@ -249,7 +241,7 @@ const generateRemoteId = idGenerator(); * @param {object} [context=null] - Optionnal context passed by the creator * of the update. * - * @return {object} The "real" updates to be applied on the state. + * @returns {object} The "real" updates to be applied on the state. */ /** @@ -260,7 +252,7 @@ const generateRemoteId = idGenerator(); * An instance of `StateManager` is automatically created by the `soundworks.Server` * at initialization (cf. {@link server.Server#stateManager}). * - * Compared to the {@link client.StateManager}, the `server.StateManager` can also + * Compared to the {@link client.StateManager}, the `ServerStateManager` can also * create and delete schemas, as well as register update hook that are executed when * a state is updated. * @@ -305,12 +297,11 @@ const generateRemoteId = idGenerator(); * }, 1000); * ``` * - * @memberof server * @extends BaseStateManager * @inheritdoc * @hideconstructor */ -class StateManager extends BaseStateManager { +class ServerStateManager extends BaseStateManager { constructor() { super(); @@ -572,11 +563,11 @@ class StateManager extends BaseStateManager { * can be instanciated. * * @param {string} schemaName - Name of the schema. - * @param {server.StateManager~schema} schema - Data structure + * @param {ServerStateManagerSchema} schema - Data structure * describing the states that will be created from this schema. * - * @see {@link server.StateManager#create} - * @see {@link client.StateManager#create} + * @see {@link ServerStateManager#create} + * @see {@link ClientStateManager#create} * * @example * server.stateManager.registerSchema('my-schema', { @@ -658,7 +649,7 @@ class StateManager extends BaseStateManager { * the "actual" update of the state (e.g. before the call of `onUpdate`). * * @param {string} schemaName - Kind of states on which applying the hook. - * @param {server.StateManager~updateHook} updateHook - Function + * @param {serverStateManagerUpdateHook} updateHook - Function * called between the `set` call and the actual update. * * @returns {Fuction} deleteHook - Handler that deletes the hook when executed. @@ -694,4 +685,4 @@ class StateManager extends BaseStateManager { } } -export default StateManager; +export default ServerStateManager; diff --git a/tests/contexts/Context.spec.js b/tests/contexts/Context.spec.js index 206de055..882adc4a 100644 --- a/tests/contexts/Context.spec.js +++ b/tests/contexts/Context.spec.js @@ -3,6 +3,10 @@ import { assert } from 'chai'; import { Server, Context as ServerContext } from '../../src/server/index.js'; import { Client, ClientContext } from '../../src/client/index.js'; +import { + kContextManagerContexts, +} from '../../src/server/ServerContextManager.js' + import config from '../utils/config.js'; describe('# Context', () => { @@ -41,7 +45,6 @@ describe('# Context', () => { it(`[client] should throw if first argument is not instance of Client`, async () => { // server const server = new Server(config); - await server.init(); await server.start(); // client @@ -290,8 +293,8 @@ describe('# Context', () => { await client.start(); await clientTestContext.enter(); - assert.equal(entered, true, 'enter() was not called'); - assert.equal(server.contextManager._contexts.inner.length, 1, 'default context was not created'); + assert.isTrue(entered); + assert.equal(server.contextManager[kContextManagerContexts].length, 1); await server.stop(); }); diff --git a/tests/essentials/Client.spec.js b/tests/essentials/Client.spec.js index e93b3efe..d7e87f4b 100644 --- a/tests/essentials/Client.spec.js +++ b/tests/essentials/Client.spec.js @@ -2,6 +2,9 @@ import { assert } from 'chai'; import { Server, Context as ServerContext } from '../../src/server/index.js'; import { Client, ClientContext } from '../../src/client/index.js'; +import { + kClientOnStatusChangeCallbacks, +} from '../../src/client/Client.js'; import pluginDelayClient from '../utils/PluginDelayClient.js'; import config from '../utils/config.js'; @@ -331,7 +334,7 @@ describe('# client::Client', () => { const unsubscribe = client.onStatusChange(async () => {}); unsubscribe(); - assert.equal(server._onStatusChangeCallbacks.size, 0) + assert.equal(client[kClientOnStatusChangeCallbacks].size, 0) }); it('should receive "inited" events', async () => { diff --git a/tests/essentials/Server.spec.js b/tests/essentials/Server.spec.js index 2f8e5d17..d18f04ad 100644 --- a/tests/essentials/Server.spec.js +++ b/tests/essentials/Server.spec.js @@ -11,6 +11,11 @@ import tcpp from 'tcp-ping'; import { Server, Context as ServerContext } from '../../src/server/index.js'; import { Client } from '../../src/client/index.js'; +import { + kServerOnStatusChangeCallbacks, + kServerApplicationTemplateOptions, +} from '../../src/server/Server.js'; + import config from '../utils/config.js'; const __filename = url.fileURLToPath(import.meta.url); @@ -509,7 +514,7 @@ describe('# server::Server', () => { const unsubscribe = server.onStatusChange(async () => {}); unsubscribe(); - assert.equal(server._onStatusChangeCallbacks.size, 0) + assert.equal(server[kServerOnStatusChangeCallbacks].size, 0) }); it('should receive "inited" events', async () => { @@ -604,18 +609,18 @@ describe('# server::Server', () => { }); describe(`## server.useDefaultApplicationTemplate()`, () => { - it(`should populate server._applicationTemplateOptions`, () => { + it(`should populate server[kServerApplicationTemplateOptions]`, () => { const server = new Server(config); server.useDefaultApplicationTemplate(); - assert.notEqual(server._applicationTemplateOptions.templateEngine, null); - assert.notEqual(server._applicationTemplateOptions.templatePath, null); - assert.notEqual(server._applicationTemplateOptions.clientConfigFunction, null); + assert.notEqual(server[kServerApplicationTemplateOptions].templateEngine, null); + assert.notEqual(server[kServerApplicationTemplateOptions].templatePath, null); + assert.notEqual(server[kServerApplicationTemplateOptions].clientConfigFunction, null); }); }); describe(`## server.setCustomApplicationTemplateOptions(options)`, () => { - it(`should override server._applicationTemplateOptions`, () => { + it(`should override server[kServerApplicationTemplateOptions]`, () => { const server = new Server(config); server.useDefaultApplicationTemplate(); // returns the config to be sent to the client @@ -637,9 +642,9 @@ describe('# server::Server', () => { server.setCustomApplicationTemplateOptions({ clientConfigFunction }); - assert.notEqual(server._applicationTemplateOptions.templateEngine, null); - assert.notEqual(server._applicationTemplateOptions.templatePath, null); - assert.equal(server._applicationTemplateOptions.clientConfigFunction, clientConfigFunction); + assert.notEqual(server[kServerApplicationTemplateOptions].templateEngine, null); + assert.notEqual(server[kServerApplicationTemplateOptions].templatePath, null); + assert.equal(server[kServerApplicationTemplateOptions].clientConfigFunction, clientConfigFunction); }); }); diff --git a/tests/plugins/client.PluginManager.spec.js b/tests/plugins/ClientPluginManager.spec.js similarity index 100% rename from tests/plugins/client.PluginManager.spec.js rename to tests/plugins/ClientPluginManager.spec.js diff --git a/tests/plugins/server.PluginManager.spec.js b/tests/plugins/ServerPluginManager.spec.js similarity index 99% rename from tests/plugins/server.PluginManager.spec.js rename to tests/plugins/ServerPluginManager.spec.js index 357f2bff..fd5e30ca 100644 --- a/tests/plugins/server.PluginManager.spec.js +++ b/tests/plugins/ServerPluginManager.spec.js @@ -3,17 +3,17 @@ import { assert } from 'chai'; import { Server } from '../../src/server/index.js'; import { Client } from '../../src/client/index.js'; import Plugin from '../../src/server/Plugin.js'; -import PluginManager from '../../src/server/PluginManager.js'; +import ServerPluginManager from '../../src/server/ServerPluginManager.js'; import pluginDelayServer from '../utils/PluginDelayServer.js'; import config from '../utils/config.js'; -describe(`# PluginManagerServer`, () => { +describe(`# ServerPluginManager`, () => { describe(`## [private] constructor(server)`, () => { it(`should throw if argument is not instance of Server`, () => { let errored = false; try { - new PluginManager({}); + new ServerPluginManager({}); } catch(err) { console.log(err.message); errored = true; diff --git a/types/client/Client.d.ts b/types/client/Client.d.ts index 1a72e5af..1339aeab 100644 --- a/types/client/Client.d.ts +++ b/types/client/Client.d.ts @@ -1,4 +1,5 @@ export const kClientVersionTest: unique symbol; +export const kClientOnStatusChangeCallbacks: unique symbol; export default Client; /** * Configuration object for a client running in a browser runtime. @@ -237,6 +238,7 @@ declare class Client { * @returns {Function} Function that delete the listener when executed. */ onStatusChange(callback: Function): Function; + [kClientOnStatusChangeCallbacks]: Set; #private; } import ClientSocket from './ClientSocket.js'; diff --git a/types/client/ClientPluginManager.d.ts b/types/client/ClientPluginManager.d.ts index bf9c77f4..d3e28081 100644 --- a/types/client/ClientPluginManager.d.ts +++ b/types/client/ClientPluginManager.d.ts @@ -1,24 +1,4 @@ export default ClientPluginManager; -/** - * ~onStateChangeCallback - */ -export type ClientPluginManager = (fullState: { - [x: string]: ClientPlugin; -}, initiator: ClientPlugin | null) => any; -/** - * Callback executed when a plugin internal state is updated. - * - * @callback ClientPluginManager~onStateChangeCallback - * @param {Object.} fullState - List of all plugins. - * @param {ClientPlugin|null} initiator - Plugin that initiated the update or `null` - * if the change was initiated by the state manager (i.e. when the initialization - * of the plugins starts). - */ -/** - * Delete the registered {@link ClientPluginManager~onStateChangeCallback}. - * - * @callback ClientPluginManager~deleteOnStateChangeCallback - */ /** * The `PluginManager` allows to register and retrieve `soundworks` plugins. * @@ -114,6 +94,6 @@ declare class ClientPluginManager extends BasePluginManager { */ get(id: string): Promise; } -import ClientPlugin from './ClientPlugin.js'; import BasePluginManager from '../common/BasePluginManager.js'; +import ClientPlugin from './ClientPlugin.js'; import Client from './Client.js'; diff --git a/types/common/BasePluginManager.d.ts b/types/common/BasePluginManager.d.ts index 21750463..be681784 100644 --- a/types/common/BasePluginManager.d.ts +++ b/types/common/BasePluginManager.d.ts @@ -1,4 +1,26 @@ export default BasePluginManager; +/** + * Callback executed when a plugin internal state is updated. + */ +export type pluginManagerOnStateChangeCallback = (: object, initiator: server.Plugin | null) => any; +/** + * Delete the registered {@link pluginManagerOnStateChangeCallback }. + */ +export type pluginManagerDeleteOnStateChangeCallback = () => any; +/** + * Callback executed when a plugin internal state is updated. + * + * @callback pluginManagerOnStateChangeCallback + * @param {object} fullState - List of all plugins. + * @param {server.Plugin|null} initiator - Plugin that initiated the update or `null` + * if the change was initiated by the state manager (i.e. when the initialization + * of the plugins starts). + */ +/** + * Delete the registered {@link pluginManagerOnStateChangeCallback}. + * + * @callback pluginManagerDeleteOnStateChangeCallback + */ /** * Shared functionnality between server-side and client-size plugin manager * @@ -18,7 +40,23 @@ declare class BasePluginManager { private _onStateChangeCallbacks; status: string; /** - * Register a plugin into soundworks. + * Initialize all the registered plugin. Executed during the `Client.init()` or + * `Server.init()` initialization step. + * @private + */ + private start; + /** @private */ + private stop; + /** + * Retrieve an fully started instance of a registered plugin, without checking + * that the pluginManager has started. This is required for starting the plugin + * manager itself and to require a plugin from within another plugin + * + * @private + */ + private unsafeGet; + /** + * Register a plugin into the manager. * * _A plugin must always be registered both on client-side and on server-side_ * @@ -32,8 +70,8 @@ declare class BasePluginManager { * @param {array} [deps=[]] - List of plugins' names the plugin depends on, i.e. * the plugin initialization will start only after the plugins it depends on are * fully started themselves. - * @see {@link client.PluginManager#register} - * @see {@link server.PluginManager#register} + * @see {@link ClientPluginManager#register} + * @see {@link ServerPluginManager#register} * @example * // client-side * client.pluginManager.register('user-defined-id', pluginFactory); @@ -42,9 +80,9 @@ declare class BasePluginManager { */ register(id: string, ctor: any, options?: object, deps?: any[]): void; /** - * Manually add a dependency to a given plugin. Usefull to require a plugin - * within a plugin + * Manually add a dependency to a given plugin. * + * Usefull to require a plugin within a plugin */ addDependency(pluginId: any, dependencyId: any): void; /** @@ -52,22 +90,6 @@ declare class BasePluginManager { * @returns {string[]} */ getRegisteredPlugins(): string[]; - /** - * Initialize all the registered plugin. Executed during the `Client.init()` or - * `Server.init()` initialization step. - * @private - */ - private start; - /** @private */ - private stop; - /** - * Retrieve an fully started instance of a registered plugin, without checking - * that the pluginManager has started. This is required for starting the plugin - * manager itself and to require a plugin from within another plugin - * - * @private - */ - private unsafeGet; /** * Propagate a notification each time a plugin is updated (status or inner state). * The callback will receive the list of all plugins as first parameter, and the @@ -75,10 +97,8 @@ declare class BasePluginManager { * * _In most cases, you should not have to rely on this method._ * - * @param {client.PluginManager~onStateChangeCallback|server.PluginManager~onStateChangeCallback} callback - * Callback to be executed on state change - * @param {client.PluginManager~deleteOnStateChangeCallback|client.PluginManager~deleteOnStateChangeCallback} - * Function to execute to listening for changes. + * @param {pluginManagerOnStateChangeCallback} callback - Callback to execute on state change + * @returns {pluginManagerDeleteOnStateChangeCallback} - Clear the subscription when executed * @example * const unsubscribe = client.pluginManager.onStateChange(pluginList, initiator => { * // log the current status of all plugins @@ -93,7 +113,6 @@ declare class BasePluginManager { * // stop listening for updates later * unsubscribe(); */ - onStateChange(callback: any): () => boolean; - /** @private */ - private _propagateStateChange; + onStateChange(callback: pluginManagerOnStateChangeCallback): pluginManagerDeleteOnStateChangeCallback; + #private; } diff --git a/types/common/BaseStateManager.d.ts b/types/common/BaseStateManager.d.ts index c7c0b9c5..3f7fbb9b 100644 --- a/types/common/BaseStateManager.d.ts +++ b/types/common/BaseStateManager.d.ts @@ -1,6 +1,14 @@ export const kStateManagerDeleteState: unique symbol; export const kStateManagerClient: unique symbol; export default BaseStateManager; +export type stateManagerObserveCallback = () => any; +/** + * @callback stateManagerObserveCallback + * @async + * @param {string} schemaName - name of the schema + * @param {number} stateId - id of the state + * @param {number} nodeId - id of the node that created the state + */ /** @private */ declare class BaseStateManager { /** @@ -78,11 +86,11 @@ declare class BaseStateManager { * * @param {string} [schemaName] - optionnal schema name to filter the observed * states. - * @param {server.StateManager~ObserveCallback|client.StateManager~ObserveCallback} + * @param {stateManagerObserveCallback} * callback - Function to be called when a new state is created on the network. * @param {object} options - Options. * @param {boolean} [options.excludeLocal = false] - If set to true, exclude states - * created locallly, i.e. by the same node, from the collection. + * created locally, i.e. by the same node, from the collection. * @returns {Promise} - Returns a Promise that resolves when the given * callback as been executed on each existing states. The promise value is a * function which allows to stop observing the states on the network. diff --git a/types/server/Server.d.ts b/types/server/Server.d.ts index 148867b9..8d4a2ba5 100644 --- a/types/server/Server.d.ts +++ b/types/server/Server.d.ts @@ -1,4 +1,8 @@ export const kServerOnSocketConnection: unique symbol; +export const kServerIsProtectedRole: unique symbol; +export const kServerIsValidConnectionToken: unique symbol; +export const kServerOnStatusChangeCallbacks: unique symbol; +export const kServerApplicationTemplateOptions: unique symbol; export default Server; /** * Configuration object for the server. @@ -27,8 +31,8 @@ export type ServerConfig = { * The `Server` class is the main entry point for the server-side of a soundworks * application. * - * The `Server` instance allows to access soundworks components such as {@link server.StateManager}, - * {@link server.PluginManager},{@link server.Socket} or {@link server.ContextManager}. + * The `Server` instance allows to access soundworks components such as {@link ServerStateManager}, + * {@link ServerPluginManager}, {@link ServerSocket} or {@link ServerContextManager}. * Its is also responsible for handling the initialization lifecycles of the different * soundworks components. * @@ -79,84 +83,6 @@ declare class Server { * `config.env.httpsInfos.key` should point to valid cert files. */ constructor(config: ServerConfig); - /** - * Instance of the {@link server.PluginManager} class. - * - * @see {@link server.PluginManager} - * @type {server.PluginManager} - */ - pluginManager: server.PluginManager; - /** - * Instance of the {@link server.StateManager} class. - * - * @see {@link server.StateManager} - * @type {server.StateManager} - */ - stateManager: server.StateManager; - /** - * Instance of the {@link server.ContextManager} class. - * - * @see {@link server.ContextManager} - * @type {server.ContextManager} - */ - contextManager: server.ContextManager; - /** - * If `https` is required, hold informations about the certificates, e.g. if - * self-signed, the dates of validity of the certificates, etc. - */ - httpsInfos: { - selfSigned: boolean; - CN: string; - altNames: string[]; - validFrom: string; - validTo: string; - isValid: boolean; - daysRemaining: number; - } | { - selfSigned: boolean; - CN?: undefined; - altNames?: undefined; - validFrom?: undefined; - validTo?: undefined; - isValid?: undefined; - daysRemaining?: undefined; - } | { - selfSigned: boolean; - CN?: undefined; - altNames?: undefined; - validFrom?: undefined; - validTo?: undefined; - isValid?: undefined; - daysRemaining?: undefined; - }; - /** - * Status of the server, 'idle', 'inited', 'started' or 'errored'. - * - * @type {string} - */ - status: string; - /** - * Simple key / value database with Promise based Map API store on filesystem, - * basically a tiny wrapper around the `kvey` package. - * - * @private - * @see {@link https://github.com/lukechilds/keyv} - */ - private db; - /** @private */ - private _applicationTemplateOptions; - /** @private */ - private _onStatusChangeCallbacks; - /** @private */ - private _onClientConnectCallbacks; - /** @private */ - private _onClientDisconnectCallbacks; - /** @private */ - private _auditState; - /** @private */ - private _pendingConnectionTokens; - /** @private */ - private _trustedClients; /** * Given config object merged with the following defaults: * @example @@ -191,6 +117,12 @@ declare class Server { * @readonly */ readonly get id(): number; + /** + * Status of the server. + * + * @type {'idle'|'inited'|'started'|'errored'} + */ + get status(): "idle" | "inited" | "started" | "errored"; /** * Instance of the express router. * @@ -216,19 +148,66 @@ declare class Server { * @see {@link https://nodejs.org/api/https.html} */ get httpServer(): any; + /** + * Simple key / value filesystem database with Promise based Map API. + * + * Basically a tiny wrapper around the {@link https://github.com/lukechilds/keyv} package. + */ + get db(): any; /** * Instance of the {@link ServerSockets} class. * * @type {ServerSockets} */ get sockets(): ServerSockets; + /** + * Instance of the {@link ServerPluginManager} class. + * + * @type {ServerPluginManager} + */ + get pluginManager(): ServerPluginManager; + /** + * Instance of the {@link ServerStateManager} class. + * + * @type {ServerStateManager} + */ + get stateManager(): ServerStateManager; + /** + * Instance of the {@link ServerContextManager} class. + * + * @type {ServerContextManager} + */ + get contextManager(): ServerContextManager; + /** + * Register a callback to execute when status change + * + * @param {function} callback + */ + onStatusChange(callback: Function): () => any; + /** + * Attach and retrieve the global audit state of the application. + * + * The audit state is a {@link SharedState} instance that keeps track of + * global informations about the application such as, the number of connected + * clients, network latency estimation, etc. + * + * The audit state is created by the server on start up. + * + * @returns {Promise} + * @throws Will throw if called before `server.init()` + * + * @example + * const auditState = await server.getAuditState(); + * auditState.onUpdate(() => console.log(auditState.getValues()), true); + */ + getAuditState(): Promise; /** * The `init` method is part of the initialization lifecycle of the `soundworks` * server. Most of the time, the `init` method will be implicitly called by the - * {@link server.Server#start} method. + * {@link Server#start} method. * * In some situations you might want to call this method manually, in such cases - * the method should be called before the {@link server.Server#start} method. + * the method should be called before the {@link Server#start} method. * * What it does: * - create the audit state @@ -236,7 +215,7 @@ declare class Server { * declared in `config.app.clients` * - initialize all registered plugins * - * After `await server.init()` is fulfilled, the {@link server.Server#stateManager} + * After `await server.init()` is fulfilled, the {@link Server#stateManager} * and all registered plugins can be safely used. * * @example @@ -250,11 +229,11 @@ declare class Server { init(): Promise; /** * The `start` method is part of the initialization lifecycle of the `soundworks` - * server. The `start` method will implicitly call the {@link server.Server#init} + * server. The `start` method will implicitly call the {@link Server#init} * method if it has not been called manually. * * What it does: - * - implicitely call {@link server.Server#init} if not done manually + * - implicitely call {@link Server#init} if not done manually * - launch the HTTP and WebSocket servers * - start all created contexts. To this end, you will have to call `server.init` * manually and instantiate the contexts between `server.init()` and `server.start()` @@ -286,11 +265,6 @@ declare class Server { * await server.stop(); */ stop(): Promise; - /** - * Open the route for a given client. - * @private - */ - private _openClientRoute; onClientConnect(callback: any): () => boolean; onClientDisconnect(callback: any): () => boolean; /** @@ -302,9 +276,6 @@ declare class Server { * @private */ private createNamespacedDb; - onStatusChange(callback: any): () => boolean; - /** @private */ - private _dispatchStatus; /** * Configure the server to work _out-of-the-box_ within the soundworks application * template provided by `@soundworks/create. @@ -331,35 +302,14 @@ declare class Server { * first to explain your use-case :) */ setCustomApplicationTemplateOptions(options: any): void; - /** - * Attach and retrieve the global audit state of the application. - * - * The audit state is a {@link server.SharedState} instance that keeps track of - * global informations about the application such as, the number of connected - * clients, network latency estimation, etc. - * - * The audit state is created by the server on start up. - * - * @returns {Promise} - * @throws Will throw if called before `server.init()` - * @see {@link server.SharedState} - * @example - * const auditState = await server.getAuditState(); - * auditState.onUpdate(() => console.log(auditState.getValues()), true); - */ - getAuditState(): Promise; - /** @private */ - private isProtected; - /** @private */ - private isValidConnectionToken; /** * Check if the given client is trusted, i.e. config.env.type == 'production' * and the client is protected behind a password. * - * @param {server.Client} client - Client to be tested + * @param {ServerClient} client - Client to be tested * @returns {Boolean} */ - isTrustedClient(client: server.Client): boolean; + isTrustedClient(client: ServerClient): boolean; /** * Check if the token from a client is trusted, i.e. config.env.type == 'production' * and the client is protected behind a password. @@ -370,11 +320,9 @@ declare class Server { * @returns {Boolean} */ isTrustedToken(clientId: number, clientIp: number, token: string): boolean; - /** - * Socket connection callback. - * @private - */ - private [kServerOnSocketConnection]; #private; } import ServerSockets from './ServerSockets.js'; +import ServerPluginManager from './ServerPluginManager.js'; +import ServerStateManager from './ServerStateManager.js'; +import ServerContextManager from './ServerContextManager.js'; diff --git a/types/server/ContextManager.d.ts b/types/server/ServerContextManager.d.ts similarity index 74% rename from types/server/ContextManager.d.ts rename to types/server/ServerContextManager.d.ts index b11afc3a..413afbd0 100644 --- a/types/server/ContextManager.d.ts +++ b/types/server/ServerContextManager.d.ts @@ -1,24 +1,27 @@ -export default ContextManager; +export const kContextManagerContexts: unique symbol; +export default ServerContextManager; /** - * Manage the different server-side contexts and their lifecycle. The `ContextManager` - * is automatically instantiated by the {@link server.Server}. + * Manage the different server-side contexts and their lifecycle. + * + * The `ServerContextManager` is automatically instantiated by the {@link server.Server}. * * _WARNING: Most of the time, you should not have to manipulate the context manager directly._ * - * @memberof server * @hideconstructor */ -declare class ContextManager { +declare class ServerContextManager { /** * @param {server.Server} server - Instance of the soundworks server. */ constructor(server: server.Server); - /** @private */ - private server; - /** @private */ - private _contexts; - /** @private */ - private _contextStartPromises; + /** + * Retrieve a started context from its name. + * + * _WARNING: Most of the time, you should not have to call this method manually._ + * + * @param {server.Context#name} contextName - Name of the context. + */ + get(contextName: any): Promise; /** * Register a context in the manager. * This method is called in the {@link server.Context} constructor @@ -28,14 +31,6 @@ declare class ContextManager { * @private */ private register; - /** - * Retrieve a started context from its name. - * - * _WARNING: Most of the time, you should not have to call this method manually._ - * - * @param {server.Context#name} contextName - Name of the context. - */ - get(contextName: any): Promise; /** * Called when a client connects to the server (websocket handshake) * @@ -65,4 +60,16 @@ declare class ContextManager { * @private */ private stop; + [kContextManagerContexts]: ContextCollection; + #private; +} +/** @private */ +declare class ContextCollection { + get length(): number; + add(context: any): void; + has(name: any): boolean; + get(name: any): any; + map(func: any): any[]; + filter(func: any): any[]; + #private; } diff --git a/types/server/PluginManager.d.ts b/types/server/ServerPluginManager.d.ts similarity index 73% rename from types/server/PluginManager.d.ts rename to types/server/ServerPluginManager.d.ts index 3aaf7591..63cc37d0 100644 --- a/types/server/PluginManager.d.ts +++ b/types/server/ServerPluginManager.d.ts @@ -1,24 +1,4 @@ -export default PluginManager; -export namespace server { - /** - * ~onStateChangeCallback - */ - type PluginManager = (: object, initiator: server.Plugin | null) => any; -} -/** - * Callback executed when a plugin internal state is updated. - * - * @callback server.PluginManager~onStateChangeCallback - * @param {object} fullState - List of all plugins. - * @param {server.Plugin|null} initiator - Plugin that initiated the update or `null` - * if the change was initiated by the state manager (i.e. when the initialization - * of the plugins starts). - */ -/** - * Delete the registered {@link server.PluginManager~onStateChangeCallback}. - * - * @callback server.PluginManager~deleteOnStateChangeCallback - */ +export default ServerPluginManager; /** * The `PluginManager` allows to register and retrieve `soundworks` plugins. * @@ -71,15 +51,14 @@ export namespace server { * }, 1000); * ``` * - * @memberof server * @extends BasePluginManager * @inheritdoc * @hideconstructor */ -declare class PluginManager extends BasePluginManager { +declare class ServerPluginManager extends BasePluginManager { register(id: any, factory?: any, options?: {}, deps?: any[]): void; /** - * Retrieve an fully started instance of a registered plugin. + * Retrieve a fully started instance of a registered plugin. * * Be aware that the `get` method resolves only when the plugin is fully 'started', * which is what we want 99.99% of the time. As such, and to prevent the application @@ -95,13 +74,11 @@ declare class PluginManager extends BasePluginManager { * * @param {server.Plugin#id} id - Id of the plugin as defined when registered. * @returns {server.Plugin} - * @see {@link server.PluginManager#onStateChange} - * - * @private + * @see {@link ServerPluginManager#onStateChange} */ - private get; - /** private */ - checkRegisteredPlugins(registeredPlugins: any): void; + get(id: any): server.Plugin; + /** @private */ + private checkRegisteredPlugins; /** @private */ private addClient; /** @private */ diff --git a/types/server/ServerSockets.d.ts b/types/server/ServerSockets.d.ts index 71f944ab..8588d5cb 100644 --- a/types/server/ServerSockets.d.ts +++ b/types/server/ServerSockets.d.ts @@ -5,9 +5,9 @@ export const kSocketsLatencyStatsWorker: unique symbol; export const kSocketsDebugPreventHeartBeat: unique symbol; export default ServerSockets; /** - * Manage all {@link server.Socket} instances. + * Manage all {@link ServerSocket} instances. * - * _Important: In most cases, you should consider using a {@link client.SharedState} + * _Important: In most cases, you should consider using a {@link SharedState} * rather than directly using the Socket instance._ */ declare class ServerSockets { @@ -15,35 +15,35 @@ declare class ServerSockets { /** * Add a socket to a room. * - * _Note that in most cases, you should use a shared state instead_ + * _Note that in most cases, you should use a {@link SharedState} instead_ * - * @param {server.Socket} socket - Socket to add to the room. + * @param {ServerSocket} socket - Socket to add to the room. * @param {String} roomId - Id of the room. */ - addToRoom(socket: server.Socket, roomId: string): void; + addToRoom(socket: ServerSocket, roomId: string): void; /** * Remove a socket from a room. * - * _Note that in most cases, you should use a shared state instead_ + * _Note that in most cases, you should use a {@link SharedState} instead_ * - * @param {server.Socket} socket - Socket to remove from the room. + * @param {ServerSocket} socket - Socket to remove from the room. * @param {String} roomId - Id of the room. */ - removeFromRoom(socket: server.Socket, roomId: string): void; + removeFromRoom(socket: ServerSocket, roomId: string): void; /** * Send a message to all clients os given room(s). If no room is specified, * the message is sent to all clients. * - * _Note that in most cases, you should use a shared state instead_ + * _Note that in most cases, you should use a {@link SharedState} instead_ * * @param {String|Array} roomsIds - Ids of the rooms that must receive * the message. If `null` the message is sent to all clients. - * @param {server.Socket} excludeSocket - Optionnal socket to ignore when + * @param {ServerSocket} excludeSocket - Optionnal socket to ignore when * broadcasting the message, typically the client at the origin of the message. * @param {String} channel - Channel name. * @param {...*} args - Payload of the message. As many arguments as needed, of * JSON compatible data types (i.e. string, number, boolean, object, array and null). */ - broadcast(roomIds: any, excludeSocket: server.Socket, channel: string, ...args: any[]): void; + broadcast(roomIds: any, excludeSocket: ServerSocket, channel: string, ...args: any[]): void; #private; } diff --git a/types/server/StateManager.d.ts b/types/server/ServerStateManager.d.ts similarity index 84% rename from types/server/StateManager.d.ts rename to types/server/ServerStateManager.d.ts index 5ac2428f..53e73bce 100644 --- a/types/server/StateManager.d.ts +++ b/types/server/ServerStateManager.d.ts @@ -1,29 +1,28 @@ -export default StateManager; -export namespace server { - /** - * ~schema - * - * Description of a schema to be registered by the {@link server.StateManagerregisterSchema } - * - * A schema is the blueprint, or definition from which shared states can be created. - * - * It consists of a set of key / value pairs where the key is the name of - * the parameter, and the value is an object describing the parameter. - * - * The value can be of any of the foolowing types: - * - {@link server.StateManager ~schemaBooleanDefinition} - * - {@link server.StateManager ~schemaStringDefinition} - * - {@link server.StateManager ~schemaIntegerDefinition} - * - {@link server.StateManager ~schemaFloatDefinition} - * - {@link server.StateManager ~schemaEnumDefinition} - * - {@link server.StateManager ~schemaAnyDefinition} - */ - type StateManager = object; -} +export default ServerStateManager; +/** + * ~schema + * + * Description of a schema to be registered by the {@link ServerStateManagerregisterSchema } + * + * A schema is the blueprint, or definition from which shared states can be created. + * + * It consists of a set of key / value pairs where the key is the name of + * the parameter, and the value is an object describing the parameter. + * + * The value can be of any of the foolowing types: + * - {@link ServerStateManager ~schemaBooleanDefinition} + * - {@link ServerStateManager ~schemaStringDefinition} + * - {@link ServerStateManager ~schemaIntegerDefinition} + * - {@link ServerStateManager ~schemaFloatDefinition} + * - {@link ServerStateManager ~schemaEnumDefinition} + * - {@link ServerStateManager ~schemaAnyDefinition} + */ +export type ServerStateManager = object; +export type serverStateManagerUpdateHook = () => any; /** - * @typedef {object} server.StateManager~schema + * @typedef {object} ServerStateManager~schema * - * Description of a schema to be registered by the {@link server.StateManager#registerSchema} + * Description of a schema to be registered by the {@link ServerStateManager#registerSchema} * * A schema is the blueprint, or definition from which shared states can be created. * @@ -31,12 +30,12 @@ export namespace server { * the parameter, and the value is an object describing the parameter. * * The value can be of any of the foolowing types: - * - {@link server.StateManager~schemaBooleanDefinition} - * - {@link server.StateManager~schemaStringDefinition} - * - {@link server.StateManager~schemaIntegerDefinition} - * - {@link server.StateManager~schemaFloatDefinition} - * - {@link server.StateManager~schemaEnumDefinition} - * - {@link server.StateManager~schemaAnyDefinition} + * - {@link ServerStateManager~schemaBooleanDefinition} + * - {@link ServerStateManager~schemaStringDefinition} + * - {@link ServerStateManager~schemaIntegerDefinition} + * - {@link ServerStateManager~schemaFloatDefinition} + * - {@link ServerStateManager~schemaEnumDefinition} + * - {@link ServerStateManager~schemaAnyDefinition} * * @example * const mySchema = { @@ -55,9 +54,9 @@ export namespace server { * server.stateManager.registerSchema('my-schema-name', mySchema); */ /** - * Describe a {@link server.StateManager~schema} entry of "boolean" type. + * Describe a {@link ServerStateManager~schema} entry of "boolean" type. * - * @typedef {object} server.StateManager~schemaBooleanDefinition + * @typedef {object} ServerStateManager~schemaBooleanDefinition * @property {string} type='boolean' - Define a boolean parameter. * @property {boolean} default - Default value of the parameter. * @property {boolean} [nullable=false] - Define if the parameter is nullable. If @@ -81,9 +80,9 @@ export namespace server { * @property {object} [metas={}] - Optionnal metadata of the parameter. */ /** - * Describe a {@link server.StateManager~schema} entry of "string" type. + * Describe a {@link ServerStateManager~schema} entry of "string" type. * - * @typedef {object} server.StateManager~schemaStringDefinition + * @typedef {object} ServerStateManager~schemaStringDefinition * @property {string} type='string' - Define a boolean parameter. * @property {string} default - Default value of the parameter. * @property {boolean} [nullable=false] - Define if the parameter is nullable. If @@ -107,9 +106,9 @@ export namespace server { * @property {object} [metas={}] - Optionnal metadata of the parameter. */ /** - * Describe a {@link server.StateManager~schema} entry of "integer" type. + * Describe a {@link ServerStateManager~schema} entry of "integer" type. * - * @typedef {object} server.StateManager~schemaIntegerDefinition + * @typedef {object} ServerStateManager~schemaIntegerDefinition * @property {string} type='integer' - Define a boolean parameter. * @property {number} default - Default value of the parameter. * @property {number} [min=-Infinity] - Minimum value of the parameter. @@ -135,9 +134,9 @@ export namespace server { * @property {object} [metas={}] - Optionnal metadata of the parameter. */ /** - * Describe a {@link server.StateManager~schema} entry of "float" type. + * Describe a {@link ServerStateManager~schema} entry of "float" type. * - * @typedef {object} server.StateManager~schemaFloatDefinition + * @typedef {object} ServerStateManager~schemaFloatDefinition * @property {string} [type='float'] - Float parameter. * @property {number} default - Default value. * @property {number} [min=-Infinity] - Minimum value. @@ -163,9 +162,9 @@ export namespace server { * @property {object} [metas={}] - Optionnal metadata of the parameter. */ /** - * Describe a {@link server.StateManager~schema} entry of "enum" type. + * Describe a {@link ServerStateManager~schema} entry of "enum" type. * - * @typedef {object} server.StateManager~schemaEnumDefinition + * @typedef {object} ServerStateManager~schemaEnumDefinition * @property {string} [type='enum'] - Enum parameter. * @property {string} default - Default value of the parameter. * @property {Array} list - Possible values of the parameter. @@ -190,12 +189,12 @@ export namespace server { * @property {object} [metas={}] - Optionnal metadata of the parameter. */ /** - * Describe a {@link server.StateManager~schema} entry of "any" type. + * Describe a {@link ServerStateManager~schema} entry of "any" type. * * Note that the `any` type always return a shallow copy of the state internal * value. Mutating the returned value will therefore not modify the internal state. * - * @typedef {object} server.StateManager~schemaAnyDefinition + * @typedef {object} ServerStateManager~schemaAnyDefinition * @property {string} [type='any'] - Parameter of any type. * @property {*} default - Default value of the parameter. * @property {boolean} [nullable=false] - Define if the parameter is nullable. If @@ -219,14 +218,7 @@ export namespace server { * @property {object} [metas={}] - Optionnal metadata of the parameter. */ /** - * @callback server.StateManager~ObserveCallback - * @async - * @param {string} schemaName - name of the schema - * @param {number} stateId - id of the state - * @param {number} nodeId - id of the node that created the state - */ -/** - * @callback server.StateManager~updateHook + * @callback serverStateManagerUpdateHook * @async * * @param {object} updates - Update object as given on a set callback, or @@ -235,7 +227,7 @@ export namespace server { * @param {object} [context=null] - Optionnal context passed by the creator * of the update. * - * @return {object} The "real" updates to be applied on the state. + * @returns {object} The "real" updates to be applied on the state. */ /** * The `StateManager` allows to create new {@link server.SharedState}s, or attach @@ -245,7 +237,7 @@ export namespace server { * An instance of `StateManager` is automatically created by the `soundworks.Server` * at initialization (cf. {@link server.Server#stateManager}). * - * Compared to the {@link client.StateManager}, the `server.StateManager` can also + * Compared to the {@link client.StateManager}, the `ServerStateManager` can also * create and delete schemas, as well as register update hook that are executed when * a state is updated. * @@ -290,12 +282,11 @@ export namespace server { * }, 1000); * ``` * - * @memberof server * @extends BaseStateManager * @inheritdoc * @hideconstructor */ -declare class StateManager extends BaseStateManager { +declare class ServerStateManager extends BaseStateManager { _clientByNodeId: Map; _sharedStatePrivateById: Map; _schemas: Map; @@ -331,11 +322,11 @@ declare class StateManager extends BaseStateManager { * can be instanciated. * * @param {string} schemaName - Name of the schema. - * @param {server.StateManager~schema} schema - Data structure + * @param {ServerStateManagerSchema} schema - Data structure * describing the states that will be created from this schema. * - * @see {@link server.StateManager#create} - * @see {@link client.StateManager#create} + * @see {@link ServerStateManager#create} + * @see {@link ClientStateManager#create} * * @example * server.stateManager.registerSchema('my-schema', { @@ -351,7 +342,7 @@ declare class StateManager extends BaseStateManager { * } * }) */ - registerSchema(schemaName: string, schema: any): void; + registerSchema(schemaName: string, schema: ServerStateManagerSchema): void; /** * Delete a schema and all associated states. * @@ -375,7 +366,7 @@ declare class StateManager extends BaseStateManager { * the "actual" update of the state (e.g. before the call of `onUpdate`). * * @param {string} schemaName - Kind of states on which applying the hook. - * @param {server.StateManager~updateHook} updateHook - Function + * @param {serverStateManagerUpdateHook} updateHook - Function * called between the `set` call and the actual update. * * @returns {Fuction} deleteHook - Handler that deletes the hook when executed. @@ -398,7 +389,7 @@ declare class StateManager extends BaseStateManager { * const values = state.getValues(); * assert.deepEqual(result, { name: 'test', numUpdates: 1 }); */ - registerUpdateHook(schemaName: string, updateHook: any): Fuction; + registerUpdateHook(schemaName: string, updateHook: serverStateManagerUpdateHook): Fuction; #private; } import BaseStateManager from '../common/BaseStateManager.js';