From b3b3a4adef2f2898278b6a220ce2d90f80389f4b Mon Sep 17 00:00:00 2001 From: b-ma Date: Mon, 20 Jan 2025 16:34:08 +0100 Subject: [PATCH] refactor: harmonize error messages + spelling --- src/client/Client.js | 50 ++++----- src/client/ClientContext.js | 16 +-- src/client/ClientContextManager.js | 6 +- src/client/ClientPlugin.js | 2 +- src/client/ClientPluginManager.js | 17 +-- src/client/ClientSocket.js | 8 +- src/common/BasePlugin.js | 10 +- src/common/BasePluginManager.js | 35 +++++-- src/common/BaseStateManager.js | 77 ++++++++------ src/common/ParameterBag.js | 50 ++++----- src/common/PromiseStore.js | 6 +- src/common/SharedState.js | 51 ++++----- src/common/SharedStateCollection.js | 18 ++-- src/common/shared-state-types.js | 8 +- src/server/Server.js | 1 - src/server/ServerClient.js | 5 +- src/server/ServerContext.js | 34 +++--- src/server/ServerContextManager.js | 21 ++-- src/server/ServerPlugin.js | 2 +- src/server/ServerPluginManager.js | 15 ++- src/server/ServerSocket.js | 8 +- src/server/ServerStateManager.js | 62 +++++------ src/server/SharedStatePrivate.js | 13 +-- src/server/audit-network-latency.worker.js | 2 +- src/server/audit-state-class-description.js | 3 +- src/server/create-http-server.js | 6 +- tests/plugins/ClientPluginManager.spec.js | 20 +--- tests/plugins/ServerPluginManager.spec.js | 16 --- tests/states/ParameterBag.spec.js | 108 ++++++++++---------- tests/states/StateManager.spec.js | 2 +- 30 files changed, 343 insertions(+), 329 deletions(-) diff --git a/src/client/Client.js b/src/client/Client.js index a831d6fa..6a51566f 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -74,17 +74,17 @@ class Client { */ constructor(config) { if (!isPlainObject(config)) { - throw new Error(`[soundworks:Client] Cannot instanciate Client: argument 1 should be an object`); + throw new Error(`Cannot construct Client: argument 1 must be an object`); } if (!('role' in config) || !isString(config.role)) { - throw new Error('[soundworks:Client] Cannot instanciate Client: Invalid ClientConfig object: property "role" is not a string defined'); + throw new Error('Cannot construct Client: Invalid ClientConfig object: Property "role" must be a string'); } // for node clients env.https is required to open the websocket if (!isBrowser()) { if (!('env' in config)) { - throw new Error('[soundworks:Client] Cannot instanciate Client: `config.env: ClientEnvConfig { useHttps, serverAddress, port }` must be defined'); + throw new Error(`Cannot construct Client: The 'env' property with type 'ClientEnvConfig' must be defined`); } let missing = []; @@ -102,7 +102,7 @@ class Client { } if (missing.length) { - throw new Error(`[soundworks:Client] Invalid config object, "config.env" is missing: ${missing.join(', ')}`); + throw new Error(`Cannot construct Client: Invalid 'env' property of type 'ClientEnvConfig': Fields ${missing.join(', ')} are missing`); } } @@ -164,8 +164,8 @@ class Client { /** * Session id of the client. * - * Incremeted positive integer generated and retrieved by the server during - * `client.init`. The counter is reset when the server restarts. + * Incremented positive integer generated and retrieved by the server during + * {@link Client#init}. The counter is reset when the server restarts. * * @type {number} */ @@ -296,11 +296,15 @@ class Client { * import { Client } from '@soundworks/core/client.js' * * const client = new Client(config); - * // optionnal explicit call of `init` before `start` + * // optional explicit call of `init` before `start` * await client.init(); * await client.start(); */ async init() { + if (this.#status !== 'idle') { + throw new DOMException(`Cannot execute 'init' on Client: Lifecycle methods must be called in following order: init, start, stop`, 'InvalidAccessError'); + } + // init socket communications await this.#socket.init(); @@ -320,22 +324,9 @@ class Client { }); this.#socket.addListener(CLIENT_HANDSHAKE_ERROR, (err) => { - let msg = ``; - - switch (err.type) { - case 'invalid-client-type': - msg = `[soundworks:Client] ${err.message}`; - break; - case 'invalid-plugin-list': - msg = `[soundworks:Client] ${err.message}`; - break; - default: - msg = `[soundworks:Client] Undefined error: ${err.message}`; - break; - } - + let msg = `Cannot execute 'init' on Client: ${err.message}`; // These are development errors, we can just hang. If we terminate the - // socket, a reload is triggered by the launcher which is bad in terms of DX + // socket, a reload is triggered by the launcher which is not good DX. reject(msg); }); @@ -381,12 +372,13 @@ class Client { * await client.start(); */ async start() { + // lazily call init for convenience if (this.#status === 'idle') { await this.init(); } - if (this.#status === 'started') { - throw new Error(`[soundworks:Server] Cannot call "client.start()" twice`); + if (this.#status !== 'inited') { + throw new DOMException(`Cannot execute 'start' on Client: Lifecycle methods must be called in following order: init, start, stop`, 'InvalidAccessError'); } await this.#contextManager[kClientContextManagerStart](); @@ -397,7 +389,7 @@ class Client { * Stops all started contexts, plugins and terminates the socket connections. * * In most situations, you might not need to call this method. However, it can - * be usefull for unit testing or similar situations where you want to create + * be useful for unit testing or similar situations where you want to create * and delete several clients in the same process. * * @example @@ -411,7 +403,7 @@ class Client { */ async stop() { if (this.#status !== 'started') { - throw new Error(`[soundworks:Client] Cannot "client.stop()" before "client.start()"`); + throw new DOMException(`Cannot execute 'stop' on Client: Lifecycle methods must be called in following order: init, start, stop`, 'InvalidAccessError'); } await this.#contextManager[kClientContextManagerStop](); @@ -425,8 +417,8 @@ class Client { * 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. It is usefull for controller client + * global information about the application such as, the number of connected + * clients, network latency estimation, etc. It is useful for controller client * roles to give the user an overview about the state of the application. * * The audit state is lazily attached to the client only if this method is called. @@ -440,7 +432,7 @@ class Client { */ async getAuditState() { if (this.#status === 'idle') { - throw new Error(`[soundworks.Client] Cannot access audit state before "client.init()"`); + throw new DOMException(`Cannot execute 'getAuditState' on Server: 'init' must be called first`, 'InvalidAccessError'); } if (this.#auditState === null) { diff --git a/src/client/ClientContext.js b/src/client/ClientContext.js index 750321f0..b897106e 100644 --- a/src/client/ClientContext.js +++ b/src/client/ClientContext.js @@ -23,7 +23,7 @@ const promiseStore = new PromiseStore('Context'); * In the `soundworks` paradigm, a client has a "role" (e.g. _player_, _controller_) * see {@link Client#role}) and can be in different "contexts" (e.g. different * part of the experience such as sections of a music piece, etc.). This class - * provides a simple and unified way to model these reccuring aspects of an application. + * provides a simple and unified way to model these recurring aspects of an application. * * You can also think of a `Context` as a state of a state machine from which a * client can `enter()` or `exit()` (be aware that `soundworks` does not provide @@ -68,7 +68,7 @@ class ClientContext { */ constructor(client) { if (!(client instanceof Client)) { - throw new Error(`[soundworks:ClientContext] Invalid argument, context "${this.name}" should receive a "soundworks.Client" instance as first argument`); + throw new TypeError(`Cannot create 'ClientContext' (${this.name}): Argument 1 must be an instance of Client`); } this.#client = client; @@ -87,7 +87,7 @@ class ClientContext { return; } - promiseStore.reject(reqId, msg); + promiseStore.reject(reqId, `Cannot execute 'enter' on Context: ${msg}`); }); this.#client.socket.addListener(CONTEXT_EXIT_RESPONSE, (reqId, contextName) => { @@ -103,7 +103,7 @@ class ClientContext { return; } - promiseStore.reject(reqId, msg); + promiseStore.reject(reqId, `Cannot execute 'exit' on Context: ${msg}`); }); this.#client.contextManager[kClientContextManagerRegister](this); @@ -126,7 +126,7 @@ class ClientContext { } /** - * Optionnal user-defined name of the context (defaults to the class name). + * Optional user-defined name of the context (defaults to the class name). * * The context manager will match the client-side and server-side contexts based * on this name. If the {@link ServerContextManager} don't find a corresponding @@ -147,10 +147,10 @@ class ClientContext { } /** - * Start the context. This method is lazilly called when the client enters the + * Start the context. This method is lazily called when the client enters the * context for the first time (cf. ${ClientContext#enter}). If you know some * some heavy and/or potentially long job has to be done when starting the context - * (e.g. call to a web service) it may be a good practice to call it explicitely. + * (e.g. call to a web service) it may be a good practice to call it explicitly. * * This method should be implemented to perform operations that are valid for the * whole lifetime of the context, regardless the client enters or exits the context. @@ -190,7 +190,7 @@ class ClientContext { * {@link ClientContext#name}), the corresponding server-side `enter()` method * will be executed before the returned Promise is fulfilled. * - * If the context has not been started yet, the `start` method is implicitely executed. + * If the context has not been started yet, the `start` method is implicitly executed. * * @returns {Promise} - Promise that resolves when the context is entered. * @example diff --git a/src/client/ClientContextManager.js b/src/client/ClientContextManager.js index 06e34f63..5cf24445 100644 --- a/src/client/ClientContextManager.js +++ b/src/client/ClientContextManager.js @@ -52,7 +52,7 @@ class ClientContextManager { */ [kClientContextManagerRegister](context) { if (this.#contexts.has(context.name)) { - throw new Error(`[soundworks:context-manager] Context "${context.name}" already registered`); + throw new DOMException(`Cannot register '${context.name}': a Context with same name has already been registered`, 'NotSupportedError'); } this.#contexts.add(context); @@ -86,7 +86,7 @@ class ClientContextManager { */ async get(contextName) { if (!this.#contexts.has(contextName)) { - throw new Error(`[soundworks:ClientContextManager] Can't get context "${contextName}", not registered`); + throw new ReferenceError(`Cannot execute 'get' on ClientContextManager: Context '${contextName}' is not registered`); } const context = this.#contexts.get(contextName); @@ -104,7 +104,7 @@ class ClientContextManager { context[kClientContextStatus] = 'started'; } catch (err) { context[kClientContextStatus] = 'errored'; - throw new Error(err); + throw new Error(`Cannot execute 'get' on ClientContextManager: ${err.message}`); } } diff --git a/src/client/ClientPlugin.js b/src/client/ClientPlugin.js index 4c087f3b..86a3baf0 100644 --- a/src/client/ClientPlugin.js +++ b/src/client/ClientPlugin.js @@ -24,7 +24,7 @@ import BasePlugin from '../common/BasePlugin.js'; * have both a client-side and a server-side part. * * See [https://soundworks.dev/guide/ecosystem](https://soundworks.dev/guide/ecosystem) - * for more informations on the available plugins. + * for more information on the available plugins. * * _Creating new plugins should be considered an advanced usage._ * diff --git a/src/client/ClientPluginManager.js b/src/client/ClientPluginManager.js index 932cadfb..b168ef02 100644 --- a/src/client/ClientPluginManager.js +++ b/src/client/ClientPluginManager.js @@ -9,12 +9,12 @@ import Client from './Client.js'; * and before {@link Client#start} or {@link Server#start} * to be properly initialized. * - * In some sitautions, you might want to register the same plugin factory several times + * In some situations, you might want to register the same plugin factory several times * using different ids (e.g. for watching several parts of the file system, etc.). * * Refer to the plugins' documentation for more precise examples, and the specific API * they expose. See [https://soundworks.dev/guide/ecosystem](https://soundworks.dev/guide/ecosystem) - * for more informations on the available plugins. + * for more information on the available plugins. * * See {@link Client#pluginManager} * @@ -66,7 +66,7 @@ class ClientPluginManager extends BasePluginManager { */ constructor(client) { if (!(client instanceof Client)) { - throw new Error(`[soundworks.ClientPluginManager] Invalid argument, "new ClientPluginManager(client)" should receive an instance of "soundworks.Client" as argument`); + throw new TypeError(`Cannot construct ClientPluginManager: Argument 1 must be an instance of Client`); } super(client); @@ -96,16 +96,21 @@ class ClientPluginManager extends BasePluginManager { * server.pluginManager.register('user-defined-id', pluginFactory); */ register(id, factory, options = {}, deps = []) { + // Note that additional argument checks are done on the BasePluginManager + + // @todo - review all this const ctor = factory(ClientPlugin); if (!(ctor.prototype instanceof ClientPlugin)) { - throw new Error(`[ClientPluginManager] Invalid argument, "pluginManager.register" second argument should be a factory function returning a class extending the "Plugin" base class`); + throw new TypeError(`Cannot execute 'register' on ClientPluginManager: argument 2 must be a factory function returning a class extending the "ClientPlugin" base class`); } if (ctor.target === undefined || ctor.target !== 'client') { - throw new Error(`[ClientPluginManager] Invalid argument, The plugin class should implement a 'target' static field with value 'client'`); + throw new TypeError(`Cannot execute 'register' on ClientPluginManager: The plugin class must implement a 'target' static field with value 'client'`); } + // @todo - check deps + super.register(id, ctor, options, deps); } @@ -130,7 +135,7 @@ class ClientPluginManager extends BasePluginManager { */ async get(id) { if (this.status !== 'started') { - throw new Error(`[soundworks.ClientPluginManager] Cannot get plugin before "client.init()"`); + throw new DOMException(`Cannot execute 'get' on ClientPluginManager: 'Client#init' has not been called yet`, 'NotSupportedError'); } return super.getUnsafe(id); diff --git a/src/client/ClientSocket.js b/src/client/ClientSocket.js index a677e9f1..de33c68a 100644 --- a/src/client/ClientSocket.js +++ b/src/client/ClientSocket.js @@ -85,7 +85,7 @@ class ClientSocket { } if (this.#config.env.useHttps && window.location.hostname !== serverAddress) { - console.warn(`The WebSocket will try to connect at ${serverAddress} while the page is accessed from ${hostname}. This can lead to socket errors, e.g. If you run the application with self-signed certificates as the certificate may not have been accepted for ${serverAddress}. In such case you should rather access the page from ${serverAddress}.`); + console.warn(`ClientSocket will try to connect at ${serverAddress} while the page is accessed from ${hostname}. This can lead to socket errors, e.g. If you run the application with self-signed certificates as the certificate may not have been accepted for ${serverAddress}. In such case you should rather access the page from ${serverAddress}.`); } webSocketOptions = []; @@ -121,7 +121,7 @@ class ClientSocket { // WebSocket "native" events: // - `close`: Fired when a connection with a websocket is closed. // - `error`: Fired when a connection with a websocket has been closed - // because of an error, such as whensome data couldn't be sent. + // because of an error, such as when some data couldn't be sent. // - `message`: Fired when data is received through a websocket. // - `open`: Fired when a connection with a websocket is opened. @@ -157,7 +157,7 @@ class ClientSocket { ws.terminate ? ws.terminate() : ws.close(); if (e.error) { - const msg = `[Socket Error] code: ${e.error.code}, message: ${e.error.message}`; + const msg = `ClientSocket error - code: ${e.error.code}, message: ${e.error.message}`; logger.log(msg); } @@ -177,7 +177,7 @@ class ClientSocket { * * Is also called when a disconnection is detected by the heartbeat (note that * in this case, the launcher will call `client.stop()` but the listeners are - * already cleared so the event will be trigerred only once. + * already cleared so the event will be triggered only once. * * @private */ diff --git a/src/common/BasePlugin.js b/src/common/BasePlugin.js index f838b3ca..84122a0b 100644 --- a/src/common/BasePlugin.js +++ b/src/common/BasePlugin.js @@ -55,7 +55,7 @@ class BasePlugin { /** * Type of the plugin, i.e. the ClassName. * - * Usefull to do perform some logic based on certain types of plugins without + * Useful to do perform some logic based on certain types of plugins without * knowing under which `id` they have been registered. (e.g. creating some generic * views, etc.) * @@ -78,11 +78,11 @@ class BasePlugin { /** * Start the plugin. * - * This method is automatically called during the client or server `init()` lifecyle + * This method is automatically called during the client or server `init()` lifecycle * step. After `start()` is fulfilled the plugin should be ready to use. * * @example - * // server-side couterpart of a plugin that creates a dedicated global shared + * // server-side counterpart of a plugin that creates a dedicated global shared * // state on which the server-side part can attach. * class MyPlugin extends ServerPlugin { * constructor(server, id) { @@ -112,10 +112,10 @@ class BasePlugin { /** * Stop the plugin. * - * This method is automatically called during the client or server `stop()` lifecyle step. + * This method is automatically called during the client or server `stop()` lifecycle step. * * @example - * // server-side couterpart of a plugin that creates a dedicated global shared + * // server-side counterpart of a plugin that creates a dedicated global shared * // state on which the client-side part can attach. * class MyPlugin extends ServerPlugin { * constructor(server, id) { diff --git a/src/common/BasePluginManager.js b/src/common/BasePluginManager.js index 1ed6de3b..0709f5fd 100644 --- a/src/common/BasePluginManager.js +++ b/src/common/BasePluginManager.js @@ -59,7 +59,7 @@ class BasePluginManager { /** * Initialize all registered plugins. * - * Executed during the `Client.init()` or `Server.init()` initialization step. + * Executed during the {@link Client#init} or {@link Client#stop} initialization step. * * @private */ @@ -67,7 +67,7 @@ class BasePluginManager { logger.title('starting registered plugins'); if (this.#status !== 'idle') { - throw new Error(`[soundworks:PluginManager] Cannot call "pluginManager.init()" twice`); + throw new DOMException(`Cannot execute 'kPluginManagerStart' on BasePluginManager: Lifecycle methods must be called in following order: kPluginManagerStart, kPluginManagerStop`, 'InvalidAccessError'); } this.#status = 'inited'; @@ -92,6 +92,10 @@ class BasePluginManager { /** @private */ async [kPluginManagerStop]() { + if (this.#status !== 'started') { + throw new DOMException(`Cannot execute 'kPluginManagerStop' on BasePluginManager: Lifecycle methods must be called in following order: kPluginManagerStart, kPluginManagerStop`, 'InvalidAccessError'); + } + for (let instance of this[kPluginManagerInstances].values()) { await instance.stop(); } @@ -109,6 +113,7 @@ class BasePluginManager { /** * Alias for existing plugins (i.e. plugin-scripting), remove once updated * @private + * @deprecated */ async unsafeGet(id) { return this.getUnsafe(id); @@ -126,11 +131,11 @@ class BasePluginManager { */ async getUnsafe(id) { if (!isString(id)) { - throw new Error(`[soundworks.PluginManager] Invalid argument, "pluginManager.get(name)" argument should be a string`); + throw new TypeError(`Cannot execute 'get' on BasePluginManager: Argument 1 must be of type string`); } if (!this[kPluginManagerInstances].has(id)) { - throw new Error(`[soundworks:PluginManager] Cannot get plugin "${id}", plugin is not registered`); + throw new ReferenceError(`Cannot execute 'get' on BasePluginManager: Plugin '${id}' is not registered`); } // @note - For now, all instances are created at the beginning of `start()` @@ -184,8 +189,16 @@ class BasePluginManager { * * _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. + * Plugins must be registered between the instantiation of {@link Client} and {@link Server}, + * and their respective initialization, i.e.: + * + * ```js + * const client = new Client(config); + * client.pluginManager.register('my-plugin', plugin); + * await client.start(); + * ``` + * + * Refer to the plugins documentation to check their configuration options and API. * * @param {string} id - Unique id of the plugin. Enables the registration of the * same plugin factory under different ids. @@ -207,23 +220,23 @@ class BasePluginManager { // 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()"`); + throw new DOMException(`Cannot execute 'register' on BasePluginManager: Host (client or server) has already been initialized`, 'InvalidAccessError'); } if (!isString(id)) { - throw new Error(`[soundworks.PluginManager] Invalid argument, "pluginManager.register" first argument should be a string`); + throw new TypeError(`Cannot execute 'register' on BasePluginManager: Argument 1 must be a string`); } if (!isPlainObject(options)) { - throw new Error(`[soundworks.PluginManager] Invalid argument, "pluginManager.register" third optional argument should be an object`); + throw new TypeError(`Cannot execute 'register' on BasePluginManager: Argument 3 must be an object`); } if (!Array.isArray(deps)) { - throw new Error(`[soundworks.PluginManager] Invalid argument, "pluginManager.register" fourth optional argument should be an array`); + throw new TypeError(`Cannot execute 'register' on BasePluginManager: Argument 3 must be an array`); } if (this[kPluginManagerInstances].has(id)) { - throw new Error(`[soundworks:PluginManager] Plugin "${id}" already registered`); + throw new DOMException(`Cannot execute 'register' on BasePluginManager: A plugin with same id (${id}) has already registered`, 'NotSupportedError'); } // We instantiate the plugin here, so that a plugin can register another one diff --git a/src/common/BaseStateManager.js b/src/common/BaseStateManager.js index 9f611871..b71bb01a 100644 --- a/src/common/BaseStateManager.js +++ b/src/common/BaseStateManager.js @@ -1,6 +1,8 @@ import { isString, isFunction, isPlainObject } from '@ircam/sc-utils'; import SharedState from './SharedState.js'; -import SharedStateCollection from './SharedStateCollection.js'; +import SharedStateCollection, { + kSharedStateCollectionInit, +} from './SharedStateCollection.js'; import BatchedTransport from './BatchedTransport.js'; import ParameterBag from './ParameterBag.js'; import PromiseStore from './PromiseStore.js'; @@ -47,7 +49,7 @@ export const kStateManagerClient = Symbol('soundworks:state-manager-client'); /** @private */ class BaseStateManager { #statesById = new Map(); - #cachedClasses = new Map(); // + #cachedClasses = new Map(); // #observeListeners = new Set(); // Set <[observedClassName, callback, options]> #observeRequestCallbacks = new Map(); // Map #promiseStore = null; @@ -99,7 +101,7 @@ class BaseStateManager { CREATE_RESPONSE, (reqId, stateId, instanceId, className, classDescription, initValues) => { // cache class description to save some bandwidth - // @note: when we make the class dynamic, we will need some mecanism to + // @note: when we make the class dynamic, we will need some mechanism to // invalidate the cached description if (!this.#cachedClasses.has(className)) { this.#cachedClasses.set(className, classDescription); @@ -115,7 +117,7 @@ class BaseStateManager { instanceId, isOwner: true, initValues, - filter: null, // owner cannot filter paramters + filter: null, // owner cannot filter parameters }); this.#statesById.set(state.id, state); @@ -124,6 +126,7 @@ class BaseStateManager { ); this[kStateManagerClient].transport.addListener(CREATE_ERROR, (reqId, msg) => { + msg = `Cannot execute 'create' on BaseStateManager: ${msg}`; this.#promiseStore.reject(reqId, msg); }); @@ -134,7 +137,7 @@ class BaseStateManager { ATTACH_RESPONSE, (reqId, stateId, instanceId, className, classDescription, currentValues, filter) => { // cache class description to save some bandwidth - // @note: when we make the class dynamic, we will need some mecanism to + // @note: when we make the class dynamic, we will need some mechanism to // invalidate the cached description if (!this.#cachedClasses.has(className)) { this.#cachedClasses.set(className, classDescription); @@ -159,6 +162,7 @@ class BaseStateManager { ); this[kStateManagerClient].transport.addListener(ATTACH_ERROR, (reqId, msg) => { + msg = `Cannot execute 'attach' on BaseStateManager: ${msg}`; this.#promiseStore.reject(reqId, msg); }); @@ -200,6 +204,7 @@ class BaseStateManager { // Observe error occur if observed class name does not exists this[kStateManagerClient].transport.addListener(OBSERVE_ERROR, (reqId, msg) => { + msg = `Cannot execute 'observe' on BaseStateManager: ${msg}`; this.#observeRequestCallbacks.delete(reqId); this.#promiseStore.reject(reqId, msg); }); @@ -234,13 +239,14 @@ class BaseStateManager { if (!this.#cachedClasses.has(className)) { this.#cachedClasses.set(className, classDescription); } - // return a full class description + // This cannot throw as the SharedState class has already been registered const parameterBag = new ParameterBag(classDescription); this.#promiseStore.resolve(reqId, parameterBag.getDescription()); }, ); this[kStateManagerClient].transport.addListener(GET_CLASS_DESCRIPTION_ERROR, (reqId, msg) => { + msg = `Cannot execute 'getClassDescription' on BaseStateManager: ${msg}`; this.#promiseStore.reject(reqId, msg); }); @@ -258,12 +264,12 @@ class BaseStateManager { */ async getClassDescription(className) { if (this.#status !== 'inited') { - throw new DOMException(`Cannot execute 'getClassDescription' on 'StateManager': state manager is not inited. This method can be safely called only once 'client' or 'server' is inited itself`, 'InvalidStateError'); + throw new DOMException(`Cannot execute 'getClassDescription' on BaseStateManager: BaseStateManager is not inited`, 'InvalidStateError'); } if (this.#cachedClasses.has(className)) { const classDescription = this.#cachedClasses.get(className); - // return a full class description + // This cannot throw as the SharedState class has already been registered const parameterBag = new ParameterBag(classDescription); return parameterBag.getDescription(); } @@ -293,7 +299,7 @@ class BaseStateManager { */ async create(className, initValues = {}) { if (this.#status !== 'inited') { - throw new DOMException(`Cannot execute 'create' on 'StateManager': state manager is not inited. This method can be safely called only once 'client' or 'server' is inited itself`, 'InvalidStateError'); + throw new DOMException(`Cannot execute 'create' on BaseStateManager: BaseStateManager is not inited`, 'InvalidStateError'); } return new Promise((resolve, reject) => { @@ -358,7 +364,7 @@ class BaseStateManager { * * @param {SharedStateClassName} className - Name of the class. * @param {number|string[]} [stateIdOrFilter] - Id of the state to attach to. If `null`, - * attach to the first state found with the given class name (usefull for + * attach to the first state found with the given class name (useful for * globally shared states owned by the server). * @param {string[]} [filter] - List of parameters of interest in the * returned state. If set to `null`, no filter is applied. @@ -369,13 +375,13 @@ class BaseStateManager { */ async attach(className, stateIdOrFilter = null, filter = null) { if (this.#status !== 'inited') { - throw new DOMException(`Cannot execute 'attach' on 'StateManager': state manager is not inited. This method can be safely called only once 'client' or 'server' is inited itself`, 'InvalidStateError'); + throw new DOMException(`Cannot execute 'attach' on BaseStateManager: BaseStateManager is not inited`, 'InvalidStateError'); } let stateId = null; if (!isString(className)) { - throw new TypeError(`Cannot execute 'attach' on 'StateManager': argument 1 should be either a number or an array`); + throw new TypeError(`Cannot execute 'attach' on BaseStateManager: argument 1 must be a string`); } if (arguments.length === 2) { @@ -389,7 +395,7 @@ class BaseStateManager { stateId = null; filter = stateIdOrFilter; } else { - throw new TypeError(`Cannot execute 'attach' on 'StateManager': argument 2 should be either null, a number or an array`); + throw new TypeError(`Cannot execute 'attach' on BaseStateManager: argument 2 must be either null, a number or an array`); } } @@ -397,11 +403,11 @@ class BaseStateManager { stateId = stateIdOrFilter; if (stateId !== null && !Number.isFinite(stateId)) { - throw new TypeError(`Cannot execute 'attach' on 'StateManager': argument 2 should be either null or a number`); + throw new TypeError(`Cannot execute 'attach' on BaseStateManager: argument 2 must be either null or a number`); } if (filter !== null && !Array.isArray(filter)) { - throw new TypeError(`Cannot execute 'attach' on 'StateManager': argument 3 should be either null or an array`); + throw new TypeError(`Cannot execute 'attach' on BaseStateManager: argument 3 must be either null or an array`); } } @@ -477,7 +483,7 @@ class BaseStateManager { * Observe all the {@link SharedState} instances that are created on the network. * * Notes: - * - The order of execution is not guaranted between nodes, i.e. a state attached + * - The order of execution is not guaranteed between nodes, i.e. a state attached * in the `observe` callback can be instantiated before the `async create` method * resolves on the creator node. * - Filtering, i.e. `observedClassName` and `options.excludeLocal` are handled @@ -491,7 +497,7 @@ class BaseStateManager { * - `stateManager.observe(callback, options)` * - `stateManager.observe(className, callback, options)` * - * @param {SharedStateClassName} [className] - Optionnal class name to filter the observed + * @param {SharedStateClassName} [className] - Optional class name to filter the observed * states. * @param {stateManagerObserveCallback} * callback - Function to be called when a new state is created on the network. @@ -511,7 +517,7 @@ class BaseStateManager { */ async observe(...args) { if (this.#status !== 'inited') { - throw new DOMException(`Cannot execute 'observe' on 'StateManager': state manager is not inited. This method can be safely called only once 'client' or 'server' is inited itself`, 'InvalidStateError'); + throw new DOMException(`Cannot execute 'observe' on BaseStateManager: BaseStateManager is not inited`, 'InvalidStateError'); } const defaultOptions = { @@ -526,7 +532,7 @@ class BaseStateManager { case 1: { // variation: .observe(callback) if (!isFunction(args[0])) { - throw new TypeError(`[stateManager] Invalid arguments, argument 1 should be a function"`); + throw new TypeError(`Cannot execute 'observe' on BaseStateManager: Argument 1 should be a function`); } observedClassName = null; @@ -538,7 +544,7 @@ class BaseStateManager { // variation: .observe(className, callback) if (isString(args[0])) { if (!isFunction(args[1])) { - throw new TypeError(`[stateManager] Invalid arguments, argument 2 should be a function"`); + throw new TypeError(`Cannot execute 'observe' on BaseStateManager: Argument 2 should be a function`); } observedClassName = args[0]; @@ -548,7 +554,7 @@ class BaseStateManager { // variation: .observe(callback, options) } else if (isFunction(args[0])) { if (!isPlainObject(args[1])) { - throw new TypeError(`[stateManager] Invalid arguments, argument 2 should be an object"`); + throw new TypeError(`Cannot execute 'observe' on BaseStateManager: Argument 2 should be an object`); } observedClassName = null; @@ -556,7 +562,7 @@ class BaseStateManager { options = Object.assign(defaultOptions, args[1]); } else { - throw new TypeError(`[stateManager] Invalid signature, refer to the StateManager.observe documentation"`); + throw new TypeError(`Cannot execute 'observe' on BaseStateManager: Invalid signature, refer to the documentation`); } break; @@ -564,15 +570,15 @@ class BaseStateManager { case 3: { // variation: .observe(className, callback, options) if (!isString(args[0])) { - throw new TypeError(`[stateManager] Invalid arguments, argument 1 should be a string"`); + throw new TypeError(`Cannot execute 'observe' on BaseStateManager: Argument 1 should be a string`); } if (!isFunction(args[1])) { - throw new TypeError(`[stateManager] Invalid arguments, argument 2 should be a function"`); + throw new TypeError(`Cannot execute 'observe' on BaseStateManager: Argument 2 should be a function`); } if (!isPlainObject(args[2])) { - throw new TypeError(`[stateManager] Invalid arguments, argument 2 should be an object"`); + throw new TypeError(`Cannot execute 'observe' on BaseStateManager: Argument 3 should be an object`); } observedClassName = args[0]; @@ -583,7 +589,7 @@ class BaseStateManager { } // throw in all other cases default: { - throw new Error(`[stateManager] Invalid signature, refer to the StateManager.observe documentation"`); + throw new TypeError(`Cannot execute 'observe' on BaseStateManager: Invalid signature, refer to the documentation`); } } @@ -591,7 +597,7 @@ class BaseStateManager { return new Promise((resolve, reject) => { const reqId = this.#promiseStore.add(resolve, reject, 'BaseStateManager#observe'); // store the callback for execution on the response. the returned Promise - // is fullfiled once callback has been executed with each existing states + // is fulfilled once callback has been executed with each existing states const observeInfos = [observedClassName, callback, options]; this.#observeRequestCallbacks.set(reqId, observeInfos); @@ -689,11 +695,11 @@ class BaseStateManager { */ async getCollection(className, filterOrOptions = null, options = {}) { if (this.#status !== 'inited') { - throw new DOMException(`Cannot execute 'getCollection' on 'StateManager': state manager is not inited. This method can be safely called only once 'client' or 'server' is inited itself`, 'InvalidStateError'); + throw new DOMException(`Cannot execute 'getCollection' on BaseStateManager: BaseStateManager is not inited`, 'InvalidStateError'); } if (!isString(className)) { - throw new TypeError(`Cannot execute 'getCollection' on 'StateManager': 'className' should be a string"`); + throw new TypeError(`Cannot execute 'getCollection' on BaseStateManager: Argument 1 should be a string"`); } let filter; @@ -709,7 +715,7 @@ class BaseStateManager { filter = null; options = filterOrOptions; } else { - throw new TypeError(`Cannot execute 'getCollection' on 'StateManager': argument 2 should be either null, an array or an object"`); + throw new TypeError(`Cannot execute 'getCollection' on BaseStateManager: Argument 2 should be either null, an array or an object"`); } } @@ -717,16 +723,21 @@ class BaseStateManager { filter = filterOrOptions; if (filter !== null && !Array.isArray(filter)) { - throw new TypeError(`Cannot execute 'getCollection' on 'StateManager': 'filter' should be either an array or null"`); + throw new TypeError(`Cannot execute 'getCollection' on BaseStateManager: Argument 2 should be either an array or null"`); } if (options === null || typeof options !== 'object') { - throw new TypeError(`Cannot execute 'getCollection' on 'StateManager': 'options' should be an object"`); + throw new TypeError(`Cannot execute 'getCollection' on BaseStateManager: Argument 3 should be either an object"`); } } const collection = new SharedStateCollection(this, className, filter, options); - await collection._init(); + + try { + await collection[kSharedStateCollectionInit](); + } catch (err) { + throw new ReferenceError(`Cannot execute 'getCollection' on BaseStateManager: ${err.message}`); + } return collection; } diff --git a/src/common/ParameterBag.js b/src/common/ParameterBag.js index d9ad5872..4ecd5062 100644 --- a/src/common/ParameterBag.js +++ b/src/common/ParameterBag.js @@ -1,6 +1,10 @@ import cloneDeep from 'lodash/cloneDeep.js'; import equal from 'fast-deep-equal'; +import { + isPlainObject, +} from '@ircam/sc-utils'; + export const sharedOptions = { nullable: false, event: false, // if event=true, nullable=true @@ -18,7 +22,7 @@ export const types = { }, coerceFunction: (name, def, value) => { if (typeof value !== 'boolean') { - throw new TypeError(`[SharedState] Invalid value "${value}" for boolean parameter "${name}"`); + throw new TypeError(`Invalid value (${value}) for boolean parameter '${name}'`); } return value; @@ -31,7 +35,7 @@ export const types = { }, coerceFunction: (name, def, value) => { if (typeof value !== 'string') { - throw new TypeError(`[SharedState] Invalid value "${value}" for string parameter "${name}"`); + throw new TypeError(`Invalid value (${value}) for string parameter '${name}'`); } return value; @@ -47,7 +51,7 @@ export const types = { }, sanitizeDescription: (def) => { // sanitize `null` values in received description, this prevent a bug when - // `min` and `max` are explicitely set to `±Infinity`, the description is stringified + // `min` and `max` are explicitly set to `±Infinity`, the description is stringified // when sent over the network and therefore Infinity is transformed to `null` // // JSON.parse({ a: Infinity }); @@ -64,7 +68,7 @@ export const types = { }, coerceFunction: (name, def, value) => { if (!(typeof value === 'number' && Math.floor(value) === value)) { - throw new TypeError(`[SharedState] Invalid value "${value}" for integer parameter "${name}"`); + throw new TypeError(`Invalid value (${value}) for integer parameter '${name}'`); } return Math.max(def.min, Math.min(def.max, value)); @@ -80,7 +84,7 @@ export const types = { }, sanitizeDescription: (def) => { // sanitize `null` values in received description, this prevent a bug when - // `min` and `max` are explicitely set to `±Infinity`, the description is stringified + // `min` and `max` are explicitly set to `±Infinity`, the description is stringified // when sent over the network and therefore Infinity is transformed to `null` // // JSON.parse({ a: Infinity }); @@ -97,7 +101,7 @@ export const types = { }, coerceFunction: (name, def, value) => { if (typeof value !== 'number' || value !== value) { // reject NaN - throw new TypeError(`[SharedState] Invalid value "${value}" for float parameter "${name}"`); + throw new TypeError(`Invalid value (${value}) for float parameter '${name}'`); } return Math.max(def.min, Math.min(def.max, value)); @@ -110,7 +114,7 @@ export const types = { }, coerceFunction: (name, def, value) => { if (def.list.indexOf(value) === -1) { - throw new TypeError(`[SharedState] Invalid value "${value}" for enum parameter "${name}"`); + throw new TypeError(`Invalid value (${value}) for enum parameter '${name}'`); } @@ -137,11 +141,11 @@ class ParameterBag { const def = description[name]; if (!Object.prototype.hasOwnProperty.call(def, 'type')) { - throw new TypeError(`[StateManager.defineClass] Invalid class description - param "${name}": "type" key is required`); + throw new TypeError(`Invalid ParameterDescription for param '${name}': 'type' key is required`); } if (!Object.prototype.hasOwnProperty.call(types, def.type)) { - throw new TypeError(`[StateManager.defineClass] Invalid class description - param "${name}": "{ type: '${def.type}' }" does not exists`); + throw new TypeError(`Invalid ParameterDescription for param '${name}': type '${def.type}' is not a valid type`); } const required = types[def.type].required; @@ -152,10 +156,10 @@ class ParameterBag { // - default is always null for `event` params // - default is always null for `required` params if ('default' in def && def.default !== null) { - throw new TypeError(`[StateManager.defineClass] Invalid class description for param ${name} - "default" propaerty is set and not null while the parameter definition is declared as "event" or "required"`); + throw new TypeError(`Invalid ParameterDescription for param ${name}: 'default' property is set and not null while the parameter definition is declared as 'event' or 'required'`); } } else if (!Object.prototype.hasOwnProperty.call(def, key)) { - throw new TypeError(`[StateManager.defineClass] Invalid class description - param "${name}" (type "${def.type}"): "${key}" key is required`); + throw new TypeError(`Invalid ParameterDescription for param "${name}"; property '${key}' key is required`); } }); } @@ -165,8 +169,8 @@ class ParameterBag { #values = {}; constructor(description, initValues = {}) { - if (!description) { - throw new Error(`[ParameterBag] description is mandatory`); + if (!isPlainObject(description)) { + throw new TypeError(`Cannot construct ParameterBag: argument 1 must be an object`); } description = cloneDeep(description); @@ -174,10 +178,10 @@ class ParameterBag { ParameterBag.validateDescription(description); - // make shure initValues make sens according to the given description + // make sure initValues make sens according to the given description for (let name in initValues) { if (!Object.prototype.hasOwnProperty.call(description, name)) { - throw new ReferenceError(`[StateManager.create] init value defined for undefined param "${name}"`); + throw new ReferenceError(`Invalid init value for parameter '${name}': Parameter does not exists`); } } @@ -198,7 +202,7 @@ class ParameterBag { if (def.required === true) { // throw if value is not given in init values if (initValues[name] === undefined || initValues[name] === null) { - throw new Error(`[SharedState.create] Invalid init value for required param "${name}", cannot be null or undefined`); + throw new TypeError(`Invalid init value for required param "${name}": Init value must be defined`); } def.default = initValues[name]; @@ -251,7 +255,7 @@ class ParameterBag { /** * Return values of all parameters as a flat object. Similar to `getValues` but * returns a reference to the underlying value in case of `any` type. May be - * usefull if the underlying value is big (e.g. sensors recordings, etc.) and + * useful if the underlying value is big (e.g. sensors recordings, etc.) and * deep cloning expensive. Be aware that if changes are made on the returned * object, the state of your application will become inconsistent. * @@ -276,7 +280,7 @@ class ParameterBag { */ get(name) { if (!this.has(name)) { - throw new ReferenceError(`[SharedState] Cannot get value of undefined parameter "${name}"`); + throw new ReferenceError(`Cannot get value of undefined parameter '${name}'`); } if (this.#description[name].type === 'any') { @@ -290,7 +294,7 @@ class ParameterBag { /** * Similar to `get` but returns a reference to the underlying value in case of - * `any` type. May be usefull if the underlying value is big (e.g. sensors + * `any` type. May be useful if the underlying value is big (e.g. sensors * recordings, etc.) and deep cloning expensive. Be aware that if changes are * made on the returned object, the state of your application will become * inconsistent. @@ -300,7 +304,7 @@ class ParameterBag { */ getUnsafe(name) { if (!this.has(name)) { - throw new ReferenceError(`[SharedState] Cannot get value of undefined parameter "${name}"`); + throw new ReferenceError(`Cannot get value of undefined parameter '${name}'`); } return this.#values[name]; @@ -314,13 +318,13 @@ class ParameterBag { */ coerceValue(name, value) { if (!this.has(name)) { - throw new ReferenceError(`[SharedState] Cannot set value of undefined parameter "${name}"`); + throw new ReferenceError(`Cannot set value of undefined parameter "${name}"`); } const def = this.#description[name]; if (value === null && def.nullable === false) { - throw new TypeError(`[SharedState] Invalid value for ${def.type} param "${name}": value is null and param is not nullable`); + throw new TypeError(`Invalid value for ${def.type} param "${name}": value is null and param is not nullable`); } else if (value === null && def.nullable === true) { value = null; } else { @@ -386,7 +390,7 @@ class ParameterBag { } if (!this.has(name)) { - throw new ReferenceError(`[SharedState] Cannot get description of undefined parameter "${name}"`); + throw new ReferenceError(`Cannot get description of undefined parameter "${name}"`); } return this.#description[name]; diff --git a/src/common/PromiseStore.js b/src/common/PromiseStore.js index 2580d116..fae1b226 100644 --- a/src/common/PromiseStore.js +++ b/src/common/PromiseStore.js @@ -27,7 +27,7 @@ export default class PromiseStore { resolve(data); } else { - throw new Error(`[${this.name}] cannot resolve request id (${reqId}), id does not exist`); + throw new ReferenceError(`Cannot resolve request id (${reqId}): id does not exist`); } } @@ -38,7 +38,7 @@ export default class PromiseStore { reject(new Error(msg)); } else { - throw new Error(`[${this.name}] cannot reject request id (${reqId}), id does not exist`); + throw new ReferenceError(`Cannot resolve request id (${reqId}): id does not exist`); } } @@ -46,7 +46,7 @@ export default class PromiseStore { flush() { for (let [_reqId, entry] of this.store) { const { reject, type } = entry; - reject(new Error(`[${this.name}] Discard promise "${type}", cannot resolve`)); + reject(new Error(`Discard promise '${type}'`)); } this.store.clear(); diff --git a/src/common/SharedState.js b/src/common/SharedState.js index 3afb8d0d..c924ea0a 100644 --- a/src/common/SharedState.js +++ b/src/common/SharedState.js @@ -44,7 +44,7 @@ export const kSharedStatePromiseStore = Symbol('soundworks:shared-state-promise- /** * The `SharedState` is one of the most important and versatile abstraction provided - * by `soundworks`. It represents a set of parameters that are synchronized accross + * by `soundworks`. It represents a set of parameters that are synchronized across * every nodes of the application (clients and server) that declared some interest * to the shared state. * @@ -134,10 +134,7 @@ class SharedState { try { this.#parameters = new ParameterBag(classDescription, initValues); } catch (err) { - console.error(err.stack); - - throw new Error(`Error creating or attaching state "${className}" w/ values:\n -${JSON.stringify(initValues, null, 2)}`); + throw new Error(`Cannot construct 'SharedState': ${err.message}`); } /** @private */ @@ -328,7 +325,7 @@ ${JSON.stringify(initValues, null, 2)}`); // @note - we don't need to check filterChange here because the value // has been updated in parameters on the `set` side so can rely on `changed` // to avoid retrigger listeners. - // If the value has been overriden by the server, `changed` will true + // If the value has been overridden by the server, `changed` will true // anyway so it should behave correctly. if (!changed || event) { continue; @@ -377,7 +374,11 @@ ${JSON.stringify(initValues, null, 2)}`); * const paramDescription = state.getDescription('my-param'); */ getDescription(paramName = null) { - return this.#parameters.getDescription(paramName); + try { + return this.#parameters.getDescription(paramName); + } catch (err) { + throw new ReferenceError(`Cannot execute 'getDescription' on SharedState: ${err.message}`); + } } /** @@ -459,7 +460,7 @@ ${JSON.stringify(initValues, null, 2)}`); if (arguments.length === 2 && isString(updates)) { updates = { [updates]: arguments[1] }; } else if (!isPlainObject(updates)) { - throw new TypeError(`Cannot execute 'set' on SharedState: 'updates' argument should be an object`); + throw new TypeError(`Cannot execute 'set' on SharedState (${this.#className}): 'updates' argument should be an object`); } const newValues = {}; @@ -475,12 +476,16 @@ ${JSON.stringify(initValues, null, 2)}`); // Try to coerce value early, so that eventual errors are triggered early // on the node requesting the update, and not only on the server side // This throws if name does not exists - this.#parameters.coerceValue(name, updates[name]); + try { + this.#parameters.coerceValue(name, updates[name]); + } catch (err) { + throw new TypeError(`Cannot execute 'set' on SharedState (${this.#className}): ${err.message}`); + } // Check that name is in filter list, if any if (this.#filter !== null) { if (!this.#filter.includes(name)) { - throw new DOMException(`[SharedState] State "${this.#className}": cannot set parameter '${name}', parameter is not in filter list`, 'NotSupportedError'); + throw new DOMException(`Cannot execute 'set' on SharedState (${this.#className}): Parameter '${name}' is not in white list`, 'NotSupportedError'); } } @@ -491,7 +496,7 @@ ${JSON.stringify(initValues, null, 2)}`); // - go through normal server path // - retrigger only if response from server is different from current value // If immediate=true && (filterChange=false || event=true) - // - call listeners with value regarless it changed + // - call listeners with value regardless it changed // - go through normal server path // - if the node is initiator of the update (UPDATE_RESPONSE), (re-)check // to prevent execute the listeners twice @@ -559,7 +564,7 @@ ${JSON.stringify(initValues, null, 2)}`); /** * Get the value of a parameter of the state. * - * Be aware that in case of 'any' typethe returned value is deeply copied. + * Be aware that in case of 'any' type, the returned value is deeply copied. * While this prevents from pollution of the state by mutating the reference, * this can also lead to performance issues when the parameter contains large * data. In such cases you should use the {@link SharedState#getUnsafe} method @@ -573,12 +578,12 @@ ${JSON.stringify(initValues, null, 2)}`); */ get(name) { if (!this.#parameters.has(name)) { - throw new ReferenceError(`[SharedState] State "${this.#className}": Cannot get value of undefined parameter "${name}"`); + throw new ReferenceError(`Cannot execute 'get' on SharedState (${this.#className}): Parameter '${name}' is not defined`); } if (this.#filter !== null) { if (!this.#filter.includes(name)) { - throw new DOMException(`[SharedState] State "${this.#className}": cannot get parameter '${name}', parameter is not in filter list`, 'NotSupportedError'); + throw new DOMException(`Cannot execute 'get' on SharedState (${this.#className}): Parameter '${name}' is not in white list`, 'NotSupportedError'); } } @@ -589,7 +594,7 @@ ${JSON.stringify(initValues, null, 2)}`); * Get an unsafe reference to the value of a parameter of the state. * * Similar to `get` but returns a reference to the underlying value in case of - * `any` type. Can be usefull if the underlying value is large (e.g. sensors + * `any` type. Can be useful if the underlying value is large (e.g. sensors * recordings, etc.) and deep cloning expensive. Be aware that if changes are * made on the returned object, the state of your application will become * inconsistent. @@ -602,12 +607,12 @@ ${JSON.stringify(initValues, null, 2)}`); */ getUnsafe(name) { if (!this.#parameters.has(name)) { - throw new ReferenceError(`[SharedState] State "${this.#className}": Cannot get value of undefined parameter "${name}"`); + throw new ReferenceError(`Cannot execute 'getUnsafe' on SharedState (${this.#className}): Parameter '${name}' is not defined`); } if (this.#filter !== null) { if (!this.#filter.includes(name)) { - throw new DOMException(`[SharedState] State "${this.#className}": cannot get parameter '${name}', parameter is not in filter list`, 'NotSupportedError'); + throw new DOMException(`Cannot execute 'getUnsafe' on SharedState (${this.#className}): Parameter '${name}' is not in white list`, 'NotSupportedError'); } } @@ -641,7 +646,7 @@ ${JSON.stringify(initValues, null, 2)}`); * Get all the key / value pairs of the state. * * Similar to `getValues` but returns a reference to the underlying value in - * case of `any` type. Can be usefull if the underlying value is big (e.g. + * case of `any` type. Can be useful if the underlying value is big (e.g. * sensors recordings, etc.) and deep cloning expensive. Be aware that if * changes are made on the returned object, the state of your application will * become inconsistent. @@ -698,7 +703,7 @@ ${JSON.stringify(initValues, null, 2)}`); */ async detach() { if (this.#detached) { - throw new Error(`[SharedState] State "${this.#className} (${this.#id})" already detached, cannot detach twice`); + throw new DOMException(`Cannot execute 'detach' on SharedState (${this.#className}): SharedState (${this.#id}) already detached`, 'NotSupportedError'); } this.#detached = true; // mark detached early @@ -722,25 +727,25 @@ ${JSON.stringify(initValues, null, 2)}`); * * All nodes attached to the state will be detached, triggering any registered * `onDetach` callbacks. The creator of the state will also have its own `onDelete` - * callback triggered. The local `onDeatch` and `onDelete` callbacks will be + * callback triggered. The local `onDetach` and `onDelete` callbacks will be * executed *before* the returned Promise resolves * * @throws Throws if the method is called by a node which is not the owner of * the state. * @example - * const state = await client.stateManaager.create('my-class-name'); + * const state = await client.stateManager.create('my-class-name'); * // later * await state.delete(); */ async delete() { if (this.#isOwner) { if (this.#detached) { - throw new Error(`[SharedState] State "${this.#className} (${this.#id})" already deleted, cannot delete twice`); + throw new DOMException(`Cannot execute 'delete' on SharedState (${this.#className}): SharedState (${this.#id}) already deleted`, 'NotSupportedError'); } return this.detach(); } else { - throw new Error(`[SharedState] Cannot delete state "${this.#className}", only the owner of the state (i.e. the node that created it) can delete the state. Use "detach" instead.`); + throw new DOMException(`Cannot execute 'delete' on SharedState (${this.#className}): SharedState (${this.#id}) is not owned by this node. Use 'SharedState#detach' instead`, 'NotSupportedError'); } } diff --git a/src/common/SharedStateCollection.js b/src/common/SharedStateCollection.js index 019738ef..7b01dc48 100644 --- a/src/common/SharedStateCollection.js +++ b/src/common/SharedStateCollection.js @@ -57,11 +57,13 @@ import logger from './logger.js'; * @callback sharedStateCollectionDeleteOnChangeCallback */ +export const kSharedStateCollectionInit = Symbol('soundworks:shared-state-collection-init'); + /** * The `SharedStateCollection` interface represent a collection of all states * created from a given class name on the network. * - * It can optionnaly exclude the states created by the current node. + * It can optionally exclude the states created by the current node. * * See {@link ClientStateManager#getCollection} and * {@link ServerStateManager#getCollection} for factory methods API @@ -96,7 +98,7 @@ class SharedStateCollection { } /** @private */ - async _init() { + async [kSharedStateCollectionInit]() { this.#classDescription = await this.#stateManager.getClassDescription(this.#className); // if filter is set, check that it contains only valid param names @@ -105,7 +107,7 @@ class SharedStateCollection { for (let filter of this.#filter) { if (!keys.includes(filter)) { - throw new ReferenceError(`[SharedStateCollection] Invalid filter key (${filter}) for class "${this.#className}"`); + throw new ReferenceError(`Invalid filter key (${filter}) for class "${this.#className}"`); } } } @@ -188,7 +190,7 @@ class SharedStateCollection { getDescription(paramName = null) { if (paramName) { if (!(paramName in this.#classDescription)) { - throw new ReferenceError(`Cannot execute "getDescription" on "SharedStateCollection": Parameter "${paramName}" does not exists`); + throw new ReferenceError(`Cannot execute 'getDescription' on SharedStateCollection: Parameter "${paramName}" does not exists`); } return this.#classDescription[paramName]; @@ -224,7 +226,7 @@ class SharedStateCollection { * Return the current values of all the states in the collection. * * Similar to `getValues` but returns a reference to the underlying value in - * case of `any` type. May be usefull if the underlying value is big (e.g. + * case of `any` type. May be useful if the underlying value is big (e.g. * sensors recordings, etc.) and deep cloning expensive. Be aware that if * changes are made on the returned object, the state of your application will * become inconsistent. @@ -249,7 +251,7 @@ class SharedStateCollection { /** * Similar to `get` but returns a reference to the underlying value in case of - * `any` type. May be usefull if the underlying value is big (e.g. sensors + * `any` type. May be useful if the underlying value is big (e.g. sensors * recordings, etc.) and deep cloning expensive. Be aware that if changes are * made on the returned object, the state of your application will become * inconsistent. @@ -441,8 +443,8 @@ class SharedStateCollection { * the estates that pass the test implemented by the provided function (see `Array.filter`). * * @param {Function} func - A function to execute for each element in the array. - * It should return a truthy to keep the element in the resulting array, and a f - * alsy value otherwise. + * It should return a truthy to keep the element in the resulting array, and a + * falsy value otherwise. */ filter(func) { return this.#states.filter(func); diff --git a/src/common/shared-state-types.js b/src/common/shared-state-types.js index b54fce56..97e702c0 100644 --- a/src/common/shared-state-types.js +++ b/src/common/shared-state-types.js @@ -54,22 +54,22 @@ * When `true`, `nullable` is automatically set to `true` and `default` to `null`. * @property {boolean} [filterChange=true] - When set to `false`, an update will * trigger the propagation of a parameter even when its value didn't change. - * This option provides a sort of middle ground between the default bahavior + * This option provides a sort of middle ground between the default behavior * (e.g. where only changed values are propagated) and the behavior of the `event` * option (which has no state per se). Hence, setting this options to `false` if * `event=true` makes no sens. * @property {boolean} [immediate=false] - When set to `true`, an update will * trigger the update listeners immediately on the node that generated the update, * before propagating the change on the network. - * This option is usefull in cases the network would introduce a noticeable + * This option is useful in cases the network would introduce a noticeable * latency on the client. - * If for some reason the value is overriden server-side (e.g. in an `updateHook`) + * If for some reason the value is overridden server-side (e.g. in an `updateHook`) * the listeners will be called again when the "real" value is received. * @property {boolean} [required=false] - When set to true, the parameter must be * provided in the initialization values when the state is created. * @property {boolean} [local=false] - When set to true, the parameter is never * propagated on the network (hence it is no longer a shared parameter :). This - * is usefull to declare some common parameter (e.g. some interface state) that + * is useful to declare some common parameter (e.g. some interface state) that * don't need to be shared but to stay in the shared state API paradigm. * @property {number} [min=-Number.MIN_VALUE] - Minimum value of the parameter. Only applies * for `integer` and `float` types. diff --git a/src/server/Server.js b/src/server/Server.js index f9a48948..ebf7d099 100644 --- a/src/server/Server.js +++ b/src/server/Server.js @@ -441,7 +441,6 @@ class Server { */ async getAuditState() { if (this.#status === 'idle') { - // DomException InvalidAccessError throw new DOMException(`Cannot execute 'getAuditState' on Server: 'init' must be called first`, 'InvalidAccessError'); } diff --git a/src/server/ServerClient.js b/src/server/ServerClient.js index fd0e84e9..91e25f60 100644 --- a/src/server/ServerClient.js +++ b/src/server/ServerClient.js @@ -6,7 +6,7 @@ const generateId = idGenerator(); export const kServerClientToken = Symbol('soundworks:server-client-token'); /** - * Server-side representation of a `soundworks` client. + * Server-side representation of a client. * * @hideconstructor * @see {@link Client} @@ -26,8 +26,9 @@ class ServerClient { this.#id = generateId.next().value; this.#uuid = uuid(); this.#socket = socket; + /** - * Is set in server[kServerOnSocketConnection] + * Set in `server[kServerOnSocketConnection]` * @private */ this[kServerClientToken] = null; diff --git a/src/server/ServerContext.js b/src/server/ServerContext.js index a2a4cf67..7fe5130e 100644 --- a/src/server/ServerContext.js +++ b/src/server/ServerContext.js @@ -8,15 +8,15 @@ import { export const kServerContextStatus = Symbol('soundworks:server-context-status'); /** - * Base class to extend in order to implment the optionnal server-side counterpart + * Base class to extend in order to implement the optional server-side counterpart * of a {@link ClientContext}. If not defined, a default context will be created * and used by the server. * * In the `soundworks` paradigm, a client has a "role" (e.g. _player_, _controller_) * see {@link Client#role}) and can be in different "contexts" (e.g. different * part of the experience such as sections of a music piece, etc.). The - * {@link ClientContext} and optionnal {@link ServerContext} abstractions provide - * a simple and unified way to model these reccuring aspects of an application. + * {@link ClientContext} and optional {@link ServerContext} abstractions provide + * a simple and unified way to model these recurring aspects of an application. * * If a `ServerContext` is recognized as the server-side counterpart of a * {@link ClientContext}, based on their respective `name` (see {@link ClientContext#name} @@ -24,9 +24,9 @@ export const kServerContextStatus = Symbol('soundworks:server-context-status'); * by the ServerContext will be executed at the beginning of the * {@link ClientContext#enter} and {@link ClientContext#exit} methods. * - * The example above shows how soundwords handles (and guarantees) the order of - * the `enter()` steps between the client-side and the server-side parts of the - * context. The same goes for the `exit()` method. + * The example above shows how the order of the `enter()` steps between the client-side + * and the server-side parts of a `context` is handled and guaranteed. The same goes for + * the `exit()` method. * * ```js * // client-side @@ -44,10 +44,10 @@ export const kServerContextStatus = Symbol('soundworks:server-context-status'); * } * } * - * // Instanciate the context (assuming the `client.role` is 'test') + * // Instantiate the context (assuming the `client.role` is 'test') * const myContext = new MyContext(client); * - * // At some point in the application, the client enters the context trigerring + * // At some point in the application, the client enters the context triggering * // the steps 1 to 5 described in the client-side and server-side `enter()` * // implementations. Note that the server-side `enter()` is never called manually. * await myContext.enter(); @@ -78,8 +78,8 @@ class ServerContext { /** * @param {Server} server - The soundworks server instance. - * @param {string|string[]} [roles=[]] - Optionnal list of client roles that can - * use this context. In large applications, this may be usefull to guarantee + * @param {string|string[]} [roles=[]] - Optional list of client roles that can + * use this context. In large applications, this may be useful to guarantee * that a context can be consumed only by specific client roles, throwing an * error if any other client role tries to use it. If empty, no access policy * will be used. @@ -87,7 +87,7 @@ class ServerContext { */ constructor(server, roles = []) { if (!(server instanceof Server)) { - throw new Error(`[soundworks:Context] Invalid argument, context "${this.constructor.name}" should receive a "soundworks.Server" instance as first argument`); + throw new TypeError(`Cannot construct '${this.constructor.name}': argument 1 must be an instance of Server`); } roles = Array.isArray(roles) ? roles : [roles]; @@ -138,7 +138,7 @@ class ServerContext { } /** - * Optionnal user-defined name of the context (defaults to the class name). + * Optional user-defined name of the context (defaults to the class name). * * The context manager will match the client-side and server-side contexts based * on this name. If the {@link ServerContextManager} don't find a corresponding @@ -159,11 +159,11 @@ class ServerContext { } /** - * Start the context. This method is lazilly called when a client enters the + * Start the context. This method is lazily called when a client enters the * context for the first time (cf. ${ServerContext#enter}). If you know some * some heavy and/or potentially long job has to be done when starting the context * (e.g. connect to a database, parsing a long file) it may be a good practice - * to call it explicitely. + * to call it explicitly. * * This method should be implemented to perform operations that are valid for the * whole lifetime of the context, regardless a client enters or exits the context. @@ -200,7 +200,7 @@ class ServerContext { * Enter the context. Implement this method to define the logic that should be * done (e.g. creating a shared state, etc.) when a client enters the context. * - * If the context has not been started yet, the `start` method is implicitely executed. + * If the context has not been started yet, the `start` method is implicitly executed. * * _WARNING: this method should never be called manually._ * @@ -222,7 +222,7 @@ class ServerContext { */ async enter(client) { if (!(client instanceof ServerClient)) { - throw new Error(`[soundworks.Context] Invalid argument, ${this.name} context ".enter()" method should receive a "ServerClient" instance argument`); + throw new TypeError(`Cannot execute 'enter' on ${this.constructor.name}: argument 1 must be an instance of ServerClient`); } this.#clients.add(client); @@ -252,7 +252,7 @@ class ServerContext { */ async exit(client) { if (!(client instanceof ServerClient)) { - throw new Error(`[soundworks.Context] Invalid argument, ${this.name}.exit() should receive a "ServerClient" instance argument`); + throw new TypeError(`Cannot execute 'exit' on ${this.constructor.name}: argument 1 must be an instance of ServerClient`); } this.#clients.delete(client); diff --git a/src/server/ServerContextManager.js b/src/server/ServerContextManager.js index ffcf0d97..f41e1f8e 100644 --- a/src/server/ServerContextManager.js +++ b/src/server/ServerContextManager.js @@ -1,3 +1,4 @@ +import Server from './Server.js'; import ServerContext, { kServerContextStatus, } from './ServerContext.js'; @@ -80,6 +81,10 @@ class ServerContextManager { * @param {Server} server - Instance of the soundworks server. */ constructor(server) { + if (!(server instanceof Server)) { + throw new TypeError(`Cannot construct 'ServerContextManager': argument 1 must be an instance of Server`); + } + this.#server = server; this[kServerContextManagerContexts] = new ContextCollection(); @@ -94,9 +99,9 @@ class ServerContextManager { * @private */ [kServerContextManagerRegister](context) { - // we must await the contructor initialization end to check the name and throw + // we must await the constructor initialization end to check the name and throw if (this[kServerContextManagerContexts].has(context.name)) { - throw new Error(`[soundworks:ServerContextManager] Context "${context.name}" already registered`); + throw new DOMException(`Cannot register '${context.name}': a Context with same name has already been registered`, 'NotSupportedError'); } this[kServerContextManagerContexts].add(context); @@ -140,7 +145,7 @@ class ServerContextManager { new ctor(this.#server); } - // we ensure context is started, even lazilly after server.start() + // we ensure context is started, even lazily after server.start() let context; try { context = await this.get(contextName); @@ -161,7 +166,7 @@ class ServerContextManager { CONTEXT_ENTER_ERROR, reqId, contextName, - `[soundworks:ServerContextManager] Client already in context (if only one context is created .enter() has been called automatically)`, + `Client already in context: if only one context is created 'enter' is called automatically)`, ); return; } @@ -176,7 +181,7 @@ class ServerContextManager { CONTEXT_ENTER_ERROR, reqId, contextName, - `[soundworks:ServerContextManager] Clients with role "${client.role}" are not declared as possible consumers of context "${contextName}"`, + `Clients with role "${client.role}" are not declared as possible consumers of "${contextName}"`, ); return; } @@ -196,7 +201,7 @@ class ServerContextManager { CONTEXT_EXIT_ERROR, reqId, contextName, - `[soundworks:ServerContextManager] Cannot exit(), context ${contextName} does not exists`, + `Context ${contextName} does not exists`, ); return; } @@ -215,7 +220,7 @@ class ServerContextManager { CONTEXT_EXIT_ERROR, reqId, contextName, - `[soundworks:ServerContextManager] Client with role "${client.role}" is not in context "${contextName}"`, + `Client with role "${client.role}" is not entered in context "${contextName}"`, ); } }); @@ -249,7 +254,7 @@ class ServerContextManager { */ async get(contextName) { if (!this[kServerContextManagerContexts].has(contextName)) { - throw new Error(`[soundworks:ServerContextManager] Can't get context "${contextName}", not registered`); + throw new ReferenceError(`Cannot execute 'get' on ServerContextManager: Context '${contextName}' is not defined`); } const context = this[kServerContextManagerContexts].get(contextName); diff --git a/src/server/ServerPlugin.js b/src/server/ServerPlugin.js index d8895443..ee4fbbb1 100644 --- a/src/server/ServerPlugin.js +++ b/src/server/ServerPlugin.js @@ -11,7 +11,7 @@ import BasePlugin from '../common/BasePlugin.js'; * have both a client-side and a server-side part. * * See [https://soundworks.dev/guide/ecosystem](https://soundworks.dev/guide/ecosystem) - * for more informations on the available plugins. + * for more information on the available plugins. * * _Creating new plugins should be considered an advanced usage._ * diff --git a/src/server/ServerPluginManager.js b/src/server/ServerPluginManager.js index 126ed36f..217f0de7 100644 --- a/src/server/ServerPluginManager.js +++ b/src/server/ServerPluginManager.js @@ -67,7 +67,7 @@ export const kServerPluginManagerRemoveClient = Symbol('soundworks:server-plugin 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`); + throw new TypeError(`Cannot construct 'ServerPluginManager': Argument 1 must be an instance of Server`); } super(server); @@ -84,7 +84,7 @@ class ServerPluginManager extends BasePluginManager { } if (missingPlugins.length > 0) { - throw new Error(`Invalid plugin list, the following plugins registered client-side: [${missingPlugins.join(', ')}] have not been registered server-side. Registered server-side plugins are: [${Array.from(this[kPluginManagerInstances].keys()).join(', ')}].`); + throw new DOMException(`Invalid 'ServerPluginManager' internal state: The following plugins registered on the client-side: [${missingPlugins.join(', ')}] have not been registered on the server-side. Plugins registered on the server-side are: [${Array.from(this[kPluginManagerInstances].keys()).join(', ')}].`, 'InvalidStateError'); } } @@ -138,16 +138,21 @@ class ServerPluginManager extends BasePluginManager { * server.pluginManager.register('user-defined-id', pluginFactory); */ register(id, factory = null, options = {}, deps = []) { + // Note that additional argument checks are done on the BasePluginManager + + // @todo - review all this const ctor = factory(ServerPlugin); if (!(ctor.prototype instanceof ServerPlugin)) { - throw new Error(`[ServerPluginManager] Invalid argument, "pluginManager.register" second argument should be a factory function returning a class extending the "ServerPlugin" base class`); + throw new TypeError(`Cannot execute 'register' on ServerPluginManager: argument 2 must be a factory function returning a class extending the "ServerPlugin" base class`); } if (ctor.target === undefined || ctor.target !== 'server') { - throw new Error(`[ServerPluginManager] Invalid argument, The plugin class should implement a 'target' static field with value 'server'`); + throw new TypeError(`Cannot execute 'register' on ServerPluginManager: The plugin class must implement a 'target' static field with value 'server'`); } + // @todo - check deps + super.register(id, ctor, options, deps); } @@ -171,7 +176,7 @@ class ServerPluginManager extends BasePluginManager { */ async get(id) { if (this.status !== 'started') { - throw new Error(`[soundworks.PluginManager] Cannot get plugin before "server.init()"`); + throw new DOMException(`Cannot execute 'get' on ServerPluginManager: 'Server#init' has not been called yet`, 'NotSupportedError'); } return super.getUnsafe(id); diff --git a/src/server/ServerSocket.js b/src/server/ServerSocket.js index 76be1c34..5609a2aa 100644 --- a/src/server/ServerSocket.js +++ b/src/server/ServerSocket.js @@ -87,7 +87,7 @@ class ServerSocket { // client is busy, e.g. when loading large sound files so let's just warn // until we gather more feedback // cf. https://making.close.com/posts/reliable-websockets/ - console.warn(`[ServerSocket] client (id: ${this[kSocketClientId]}) did not respond to ping message in time (missed: ${heartbeatMissed}, interval: ${PING_INTERVAL})`); + console.warn(`ClientSocket (id: ${this[kSocketClientId]}) did not respond to ping message in time (missed: ${heartbeatMissed}, interval: ${PING_INTERVAL})`); // this.#dispatchEvent('close'); // return; } @@ -121,7 +121,7 @@ class ServerSocket { } /** - * Dipatch an event to the listeners of the given channel. + * Dispatch an event to the listeners of the given channel. * @param {string} channel - Channel name. * @param {...*} args - Content of the message. */ @@ -172,7 +172,7 @@ class ServerSocket { } /** - * Reay state of the underlying socket instance. + * Ready state of the underlying socket instance. * * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/readyState} * @type {number} @@ -199,7 +199,7 @@ class ServerSocket { if (this.#socket.readyState === 1) { this.#socket.send(msg, (err) => { if (err) { - console.error('[Socket] error sending msg:', channel, args, err.message); + console.error(`ServerSocket failed to send message:`, channel, args, err.message); } }); } diff --git a/src/server/ServerStateManager.js b/src/server/ServerStateManager.js index ea8baea6..01ad9422 100644 --- a/src/server/ServerStateManager.js +++ b/src/server/ServerStateManager.js @@ -187,7 +187,7 @@ class ServerStateManager extends BaseStateManager { * This is automatically handled by the {@link Server} when a client connects. * * @param {number} nodeId - Unique id of the client node - * @param {object} transport - Transport mecanism to communicate with the + * @param {object} transport - Transport mechanism to communicate with the * client. Must implement a basic EventEmitter API. * * @private @@ -231,7 +231,7 @@ class ServerStateManager extends BaseStateManager { } if (hookAborted) { - throw new Error(`A create hook function explicitly aborted state creation by returninig 'null'`); + throw new Error(`A 'serverStateManagerCreateHook' explicitly aborted state creation of class '${className}' by returning 'null'`); } const state = new SharedStatePrivate(this, className, classDescription, stateId, initValues); @@ -263,11 +263,11 @@ class ServerStateManager extends BaseStateManager { }); } } catch (err) { - const msg = `Cannot execute 'create' on 'BaseStateManager' for class '${className}': ${err.message}`; + const msg = `${err.message}`; client.transport.emit(CREATE_ERROR, reqId, msg); } } else { - const msg = `Cannot execute 'create' on 'BaseStateManager' for class '${className}': class is not defined`; + const msg = `Undefined SharedStateClassName '${className}'`; client.transport.emit(CREATE_ERROR, reqId, msg); } }, @@ -287,7 +287,7 @@ class ServerStateManager extends BaseStateManager { } else if (stateId === null) { // if no `stateId` given, we try to find the first state with the given // `className` in the list, this allow a client to attach to a global - // state created by the server (or some persistant client) without + // state created by the server (or some persistent client) without // having to know the `stateId` (e.g. some global state...) for (let existingState of this.#sharedStatePrivateById.values()) { if (existingState.className === className) { @@ -314,7 +314,7 @@ class ServerStateManager extends BaseStateManager { const isValid = filter.reduce((acc, key) => acc && keys.includes(key), true); if (!isValid) { - const msg = `[stateManager] Cannot attach, invalid filter (${filter.join(', ')}) for class "${className}"`; + const msg = `Invalid filter (${filter.join(', ')}) for class '${className}'`; return client.transport.emit(ATTACH_ERROR, reqId, msg); } } @@ -333,11 +333,11 @@ class ServerStateManager extends BaseStateManager { ); } else { - const msg = `[stateManager] Cannot attach, no existing state for class "${className}" with stateId: "${stateId}"`; + const msg = `No existing state for class "${className}" with stateId: "${stateId}"`; client.transport.emit(ATTACH_ERROR, reqId, msg); } } else { - const msg = `[stateManager] Cannot attach, class "${className}" does not exists`; + const msg = `Undefined SharedStateClassName '${className}'`; client.transport.emit(ATTACH_ERROR, reqId, msg); } }, @@ -365,7 +365,7 @@ class ServerStateManager extends BaseStateManager { client.transport.emit(OBSERVE_RESPONSE, reqId, ...list); } else { - const msg = `[stateManager] Cannot observe class "${observedClassName}", class does not exists`; + const msg = `Undefined SharedStateClassName '${observedClassName}'`; client.transport.emit(OBSERVE_ERROR, reqId, msg); } }); @@ -387,7 +387,7 @@ class ServerStateManager extends BaseStateManager { classDescription, ); } else { - const msg = `[stateManager] Cannot get class "${className}", class does not exists`; + const msg = `Undefined SharedStateClassName '${className}'`; client.transport.emit(GET_CLASS_DESCRIPTION_ERROR, reqId, msg); } }); @@ -425,7 +425,7 @@ class ServerStateManager extends BaseStateManager { } if (deleteState) { - if (instanceId !== state.creatorinstanceId) { + if (instanceId !== state.creatorInstanceId) { // send notification to other attached nodes attachedClient.transport.emit(`${DELETE_NOTIFICATION}-${state.id}-${instanceId}`); } @@ -471,18 +471,22 @@ class ServerStateManager extends BaseStateManager { */ defineClass(className, classDescription) { if (!isString(className)) { - throw new Error(`[stateManager.defineClass] Invalid class name "${className}", should be a string`); + throw new TypeError(`Cannot execute 'defineClass' on ServerStateManager: argument 1 must be of type SharedStateClassName`); } - if (this.#classes.has(className)) { - throw new Error(`[stateManager.defineClass] Cannot define class with name: "${className}", class already exists`); + if (!isPlainObject(classDescription)) { + throw new TypeError(`Cannot execute 'defineClass' on ServerStateManager: argument 2 must be of type SharedStateClassDescription`); } - if (!isPlainObject(classDescription)) { - throw new Error(`[stateManager.defineClass] Invalid class description, should be an object`); + if (this.#classes.has(className)) { + throw new DOMException(`Cannot execute 'defineClass' on ServerStateManager: SharedState class '${className}' is already defined`, 'NotSupportedError'); } - ParameterBag.validateDescription(classDescription); + try { + ParameterBag.validateDescription(classDescription); + } catch (err) { + throw new TypeError(`Cannot execute 'defineClass' on ServerStateManager: ${err.message}`); + } this.#classes.set(className, clonedeep(classDescription)); // create hooks list @@ -500,7 +504,7 @@ class ServerStateManager extends BaseStateManager { } /** - * Delete a whole class of {@link ShareState}. + * Delete a whole class of {@link SharedState}. * * All {@link SharedState} instances created from this class will be deleted * as well, triggering their eventual `onDetach` and `onDelete` callbacks. @@ -545,11 +549,11 @@ class ServerStateManager extends BaseStateManager { * Register a function for a given class of shared state class to be executed * when a state is created. * - * For example, this can be usefull to retrieve some initialization values stored + * For example, this can be useful to retrieve some initialization values stored * in the filesystem, given the value (e.g. a hostname) of one the parameters. * * The hook is associated to each states created from the given class name. - * Note that the hooks are executed server-side regarless the node on which + * Note that the hooks are executed server-side regardless the node on which * `create` has been called. * * Multiple hook can be added to the same `className`, they will be executed in @@ -582,11 +586,11 @@ class ServerStateManager extends BaseStateManager { */ registerCreateHook(className, createHook) { if (!this.#classes.has(className)) { - throw new TypeError(`Cannot execute 'registerCreateHook' on 'BaseStateManager': SharedState class '${className}' does not exists`); + throw new TypeError(`Cannot execute 'registerCreateHook' on BaseStateManager: SharedState class '${className}' is not defined`); } if (!isFunction(createHook)) { - throw new TypeError(`Cannot execute 'registerCreateHook' on 'BaseStateManager': argument 2 must be a function`); + throw new TypeError(`Cannot execute 'registerCreateHook' on BaseStateManager: argument 2 must be a function`); } const hooks = this.#createHooksByClassName.get(className); @@ -599,11 +603,11 @@ class ServerStateManager extends BaseStateManager { * Register a function for a given class of shared state class to be executed * when a state is deleted. * - * For example, this can be usefull to store the values of a given shared state + * For example, this can be useful to store the values of a given shared state * in the filesystem. * * The hook is associated to each states created from the given class name. - * Note that the hooks are executed server-side regarless the node on which + * Note that the hooks are executed server-side regardless the node on which * `delete` has been called. * * Multiple hook can be added to the same `className`, they will be executed in @@ -630,11 +634,11 @@ class ServerStateManager extends BaseStateManager { */ registerDeleteHook(className, deleteHook) { if (!this.#classes.has(className)) { - throw new TypeError(`Cannot execute 'registerDeleteHook' on 'BaseStateManager': SharedState class '${className}' does not exists`); + throw new TypeError(`Cannot execute 'registerDeleteHook' on BaseStateManager: SharedState class '${className}' is not defined`); } if (!isFunction(deleteHook)) { - throw new TypeError(`Cannot execute 'registerDeleteHook' on 'BaseStateManager': argument 2 must be a function`); + throw new TypeError(`Cannot execute 'registerDeleteHook' on BaseStateManager: argument 2 must be a function`); } const hooks = this.#deleteHooksByClassName.get(className); @@ -653,7 +657,7 @@ class ServerStateManager extends BaseStateManager { * * The hook is associated to each states created from the given class name and * executed on each update (i.e. `state.set(updates)`). Note that the hooks are - * executed server-side regarless the node on which `set` has been called and + * executed server-side regardless the node on which `set` has been called and * before the call of the `onUpdate` callback of the shared state. * * Multiple hook can be added to the same `className`, they will be executed in @@ -685,11 +689,11 @@ class ServerStateManager extends BaseStateManager { */ registerUpdateHook(className, updateHook) { if (!this.#classes.has(className)) { - throw new TypeError(`Cannot execute 'registerUpdateHook' on 'BaseStateManager': SharedState class '${className}' does not exists`); + throw new TypeError(`Cannot execute 'registerUpdateHook' on BaseStateManager: SharedState class '${className}' is not defined`); } if (!isFunction(updateHook)) { - throw new TypeError(`Cannot execute 'registerUpdateHook' on 'BaseStateManager': argument 2 must be a function`); + throw new TypeError(`Cannot execute 'registerUpdateHook' on BaseStateManager: argument 2 must be a function`); } const hooks = this.#updateHooksByClassName.get(className); diff --git a/src/server/SharedStatePrivate.js b/src/server/SharedStatePrivate.js index 29c87a64..f38f1af9 100644 --- a/src/server/SharedStatePrivate.js +++ b/src/server/SharedStatePrivate.js @@ -58,6 +58,7 @@ class SharedStatePrivate { this.#manager = manager; this.#className = className; this.#id = id; + // This can throw but will be catch in ServerStateManager CREATE_REQUEST handler this.#parameters = new ParameterBag(classDefinition, initValues); } @@ -148,15 +149,15 @@ class SharedStatePrivate { // - server attached state (client.id: -1) spot a problem and overrides the value // We want the remote client (id: 2) to receive in the right order: // * 1. the value it requested, - // * 2. the value overriden by the server-side attached state (id: -1) + // * 2. the value overridden by the server-side attached state (id: -1) // - // such problem is now better solved with the the upateHook system, none - // nonetheway we don't want to introduce inconsistencies here + // This problem is now better solved with the the updateHook system, + // nevertheless we don't want to introduce inconsistencies here // // Then we propagate server-side last, because as the server transport // is synchronous it can break ordering if a subscription function makes // itself an update in reaction to an update. Propagating to server last - // alllows to maintain network messages order consistent. + // allows to maintain network messages order consistent. // // @note - instanceId correspond to unique remote state id @@ -220,13 +221,13 @@ class SharedStatePrivate { client.transport.emit(`${UPDATE_ABORT}-${this.id}-${instanceId}`, reqId, updates); } } else { - // retrieve values from inner state (also handle immediate approriately) + // retrieve values from inner state (also handle immediate appropriately) const oldValues = {}; for (let name in updates) { oldValues[name] = this.#parameters.get(name); } - // aborted by hook (updates have been overriden to {}) + // aborted by hook (updates have been overridden to {}) client.transport.emit(`${UPDATE_ABORT}-${this.id}-${instanceId}`, reqId, oldValues); } }); diff --git a/src/server/audit-network-latency.worker.js b/src/server/audit-network-latency.worker.js index 760f29d8..4861a832 100644 --- a/src/server/audit-network-latency.worker.js +++ b/src/server/audit-network-latency.worker.js @@ -13,7 +13,7 @@ let averageLatencyPeriod = 2; let intervalId = null; let meanLatency = 0; -// workaround that sc-utils is pure emascript module +// workaround that sc-utils is pure ecmascript module // 2024/09/06 - Just copy getTime implementation so that we don't even need the node_modules const start = hrtime.bigint(); diff --git a/src/server/audit-state-class-description.js b/src/server/audit-state-class-description.js index 8b4a2770..103a1021 100644 --- a/src/server/audit-state-class-description.js +++ b/src/server/audit-state-class-description.js @@ -1,5 +1,6 @@ /** * Internal shared state class used to audit the application. + * @private */ export default { /** @@ -17,7 +18,7 @@ export default { }, /** - * Average latency in seconds computed from ping/pong informations. + * Average latency in seconds computed from ping/pong information. */ averageNetworkLatency: { type: 'float', diff --git a/src/server/create-http-server.js b/src/server/create-http-server.js index cb139bfd..c4186312 100644 --- a/src/server/create-http-server.js +++ b/src/server/create-http-server.js @@ -44,17 +44,17 @@ export async function createHttpServer(server) { try { x509 = new X509Certificate(cert); } catch { - throw new Error(`[soundworks:Server] Invalid https cert file`); + throw new TypeError(`Cannot create https.Server: Invalid https cert file`); } try { const keyObj = createPrivateKey(key); if (!x509.checkPrivateKey(keyObj)) { - throw new Error(`[soundworks:Server] Invalid https key file`); + throw new TypeError(`Cannot create https.Server: Invalid https key file`); } } catch { - throw new Error(`[soundworks:Server] Invalid https key file`); + throw new TypeError(`Cannot create https.Server: Invalid https key file`); } // check is certificate is still valid diff --git a/tests/plugins/ClientPluginManager.spec.js b/tests/plugins/ClientPluginManager.spec.js index 61f34573..7aad66f7 100644 --- a/tests/plugins/ClientPluginManager.spec.js +++ b/tests/plugins/ClientPluginManager.spec.js @@ -2,8 +2,8 @@ import { assert } from 'chai'; import { Server } from '../../src/server/index.js'; import { Client } from '../../src/client/index.js'; +import ClientPluginManager from '../../src/client/ClientPluginManager.js'; import ClientPlugin from '../../src/client/ClientPlugin.js'; - import pluginDelayServer from '../utils/PluginDelayServer.js'; import pluginDelayClient from '../utils/PluginDelayClient.js'; import config from '../utils/config.js'; @@ -31,7 +31,7 @@ describe(`# PluginManagerClient`, () => { it(`should throw if argument is not instance of Client`, () => { let errored = false; try { - new PluginManager({}); + new ClientPluginManager({}); } catch(err) { console.log(err.message); errored = true; @@ -152,22 +152,6 @@ describe(`# PluginManagerClient`, () => { }); }); - describe(`## [private] async init()`, () => { - it(`should throw if started twice`, async () => { - const client = new Client({ role: 'test', ...config }); - await client.init(); // run pluginManager.init() - - let errored = false; - try { - await client.pluginManager.start(); - } catch(err) { - errored = true; - console.log(err.message); - } - if (!errored) { assert.fail('should throw'); } - }); - }); - describe(`## async get(id)`, () => { it(`should throw if called before server.init()`, async () => { const client = new Client({ role: 'test', ...config }); diff --git a/tests/plugins/ServerPluginManager.spec.js b/tests/plugins/ServerPluginManager.spec.js index 85950e99..c8844021 100644 --- a/tests/plugins/ServerPluginManager.spec.js +++ b/tests/plugins/ServerPluginManager.spec.js @@ -139,22 +139,6 @@ describe(`# ServerPluginManager`, () => { }); }); - describe(`## [private] async init()`, () => { - it(`should throw if started twice`, async () => { - const server = new Server(config); - await server.init(); // run pluginManager.start() - - let errored = false; - try { - await server.pluginManager.start(); - } catch(err) { - errored = true; - console.log(err.message); - } - if (!errored) { assert.fail('should throw'); } - }); - }); - describe(`## async get(id)`, () => { it(`should throw if called before server.init()`, async () => { const server = new Server(config); diff --git a/tests/states/ParameterBag.spec.js b/tests/states/ParameterBag.spec.js index d8c3ef55..04389c0b 100644 --- a/tests/states/ParameterBag.spec.js +++ b/tests/states/ParameterBag.spec.js @@ -14,15 +14,15 @@ describe('# [private] ParameterBag', () => { it('should check if schema is invalid', () => { assert.throw(() => ParameterBag.validateDescription({ noType: {} - }), TypeError, `[StateManager.defineClass] Invalid class description - param "noType": "type" key is required`); + })); assert.throw(() => ParameterBag.validateDescription({ invalidType: { type: 'invalid' } - }), TypeError, `[StateManager.defineClass] Invalid class description - param "invalidType": "{ type: 'invalid' }" does not exists`); + })); assert.throw(() => ParameterBag.validateDescription({ myBoolean: { type: 'boolean' } - }), TypeError, `[StateManager.defineClass] Invalid class description - param "myBoolean" (type "boolean"): "default" key is required`); + })); }); it(`should throw if "default" is declared when "event" is true`, () => { @@ -52,7 +52,7 @@ describe('# [private] ParameterBag', () => { it('should validate the given schema', () => { assert.throws(() => new ParameterBag({ invalidType: { type: 'invalid' } - }), TypeError, `[StateManager.defineClass] Invalid class description - param "invalidType": "{ type: 'invalid' }" does not exists`); + })); }); it('should check initValues consistency', () => { @@ -61,7 +61,7 @@ describe('# [private] ParameterBag', () => { { myBoolean: { type: 'boolean', default: 0 }}, { myFloat: 0.1 } ) - }, ReferenceError, `[StateManager.create] init value defined for undefined param "myFloat"`); + }); }); it('should throw if required param is not given at initialization', () => { @@ -183,7 +183,7 @@ describe('# [private] ParameterBag', () => { describe(`## get(name)`, () => { it(`should throw if name is undefined`, () => { - assert.throw(() => params.get('doNotExists'), ReferenceError, `[SharedState] Cannot get value of undefined parameter "doNotExists"`) + assert.throw(() => params.get('doNotExists')) }); it(`should return proper value`, () => { @@ -197,7 +197,7 @@ describe('# [private] ParameterBag', () => { describe(`## getUnsafe(name)`, () => { it(`should throw if name is undefined`, () => { - assert.throw(() => params.get('doNotExists'), ReferenceError, `[SharedState] Cannot get value of undefined parameter "doNotExists"`) + assert.throw(() => params.get('doNotExists')) }); it(`should return reference`, () => { @@ -209,11 +209,11 @@ describe('# [private] ParameterBag', () => { describe(`## set(name, value)`, () => { it(`should throw if name does not exists`, () => { - assert.throw(() => params.set('doNotExists', false), ReferenceError, `[SharedState] Cannot set value of undefined parameter "doNotExists"`) + assert.throw(() => params.set('doNotExists', false)); }); it(`should throw if not nullable and null given`, () => { - assert.throw(() => params.set('bool', null), TypeError, `[SharedState] Invalid value for boolean param "bool": value is null and param is not nullable`); + assert.throw(() => params.set('bool', null)); }); it(`should return [value, updated]`, () => { @@ -299,9 +299,7 @@ describe('# [private] ParameterBag', () => { }); it(`should throw if name does not exists`, () => { - assert.throw(() => params.getDescription('42'), ReferenceError, - `[SharedState] Cannot get description of undefined parameter "42"` - ); + assert.throw(() => params.getDescription('42')); }); }); @@ -328,17 +326,17 @@ describe('# [private] ParameterBag::types', () => { it('should coerce properly', () => { assert.doesNotThrow(() => coerce('b', {}, true)); assert.doesNotThrow(() => coerce('b', {}, false)); - assert.throws(() => coerce('b', {}, 0.1), TypeError, /\[SharedState\] Invalid value "(.*)" for boolean parameter "b"/); - assert.throws(() => coerce('b', {}, -100000), TypeError, /\[SharedState\] Invalid value "(.*)" for boolean parameter "b"/); - assert.throws(() => coerce('b', {}, Math.PI), TypeError, /\[SharedState\] Invalid value "(.*)" for boolean parameter "b"/); - assert.throws(() => coerce('b', {}, -Infinity), TypeError, /\[SharedState\] Invalid value "(.*)" for boolean parameter "b"/); - assert.throws(() => coerce('b', {}, NaN), TypeError, /\[SharedState\] Invalid value "(.*)" for boolean parameter "b"/); - assert.throws(() => coerce('b', {}, 0), TypeError, /\[SharedState\] Invalid value "(.*)" for boolean parameter "b"/); - assert.throws(() => coerce('b', {}, '10'), TypeError, /\[SharedState\] Invalid value "(.*)" for boolean parameter "b"/); - assert.throws(() => coerce('b', {}, null), TypeError, /\[SharedState\] Invalid value "(.*)" for boolean parameter "b"/); - assert.throws(() => coerce('b', {}, /abc/), TypeError, /\[SharedState\] Invalid value "(.*)" for boolean parameter "b"/); - assert.throws(() => coerce('b', {}, undefined), TypeError, /\[SharedState\] Invalid value "(.*)" for boolean parameter "b"/); - assert.throws(() => coerce('b', {}, [0, 1, 2]), TypeError, /\[SharedState\] Invalid value "(.*)" for boolean parameter "b"/); + assert.throws(() => coerce('b', {}, 0.1), TypeError, /Invalid value \((.*)\) for boolean parameter 'b'/); + assert.throws(() => coerce('b', {}, -100000), TypeError, /Invalid value \((.*)\) for boolean parameter 'b'/); + assert.throws(() => coerce('b', {}, Math.PI), TypeError, /Invalid value \((.*)\) for boolean parameter 'b'/); + assert.throws(() => coerce('b', {}, -Infinity), TypeError, /Invalid value \((.*)\) for boolean parameter 'b'/); + assert.throws(() => coerce('b', {}, NaN), TypeError, /Invalid value \((.*)\) for boolean parameter 'b'/); + assert.throws(() => coerce('b', {}, 0), TypeError, /Invalid value \((.*)\) for boolean parameter 'b'/); + assert.throws(() => coerce('b', {}, '10'), TypeError, /Invalid value \((.*)\) for boolean parameter 'b'/); + assert.throws(() => coerce('b', {}, null), TypeError, /Invalid value \((.*)\) for boolean parameter 'b'/); + assert.throws(() => coerce('b', {}, /abc/), TypeError, /Invalid value \((.*)\) for boolean parameter 'b'/); + assert.throws(() => coerce('b', {}, undefined), TypeError, /Invalid value \((.*)\) for boolean parameter 'b'/); + assert.throws(() => coerce('b', {}, [0, 1, 2]), TypeError, /Invalid value \((.*)\) for boolean parameter 'b'/); }); }); @@ -348,18 +346,18 @@ describe('# [private] ParameterBag::types', () => { it('should coerce properly', () => { assert.doesNotThrow(() => coerce('s', {}, '10')); assert.doesNotThrow(() => coerce('s', {}, 'a text')); - assert.throws(() => coerce('s', {}, -100000), TypeError, /\[SharedState\] Invalid value "(.*)" for string parameter "s"/); - assert.throws(() => coerce('s', {}, -Infinity), TypeError, /\[SharedState\] Invalid value "(.*)" for string parameter "s"/); - assert.throws(() => coerce('s', {}, true), TypeError, /\[SharedState\] Invalid value "(.*)" for string parameter "s"/); - assert.throws(() => coerce('s', {}, false), TypeError, /\[SharedState\] Invalid value "(.*)" for string parameter "s"/); - assert.throws(() => coerce('s', {}, 0.1), TypeError, /\[SharedState\] Invalid value "(.*)" for string parameter "s"/); - assert.throws(() => coerce('s', {}, Math.PI), TypeError, /\[SharedState\] Invalid value "(.*)" for string parameter "s"/); - assert.throws(() => coerce('s', {}, NaN), TypeError, /\[SharedState\] Invalid value "(.*)" for string parameter "s"/); - assert.throws(() => coerce('s', {}, 0), TypeError, /\[SharedState\] Invalid value "(.*)" for string parameter "s"/); - assert.throws(() => coerce('s', {}, null), TypeError, /\[SharedState\] Invalid value "(.*)" for string parameter "s"/); - assert.throws(() => coerce('s', {}, /abc/), TypeError, /\[SharedState\] Invalid value "(.*)" for string parameter "s"/); - assert.throws(() => coerce('s', {}, undefined), TypeError, /\[SharedState\] Invalid value "(.*)" for string parameter "s"/); - assert.throws(() => coerce('s', {}, [0, 1, 2]), TypeError, /\[SharedState\] Invalid value "(.*)" for string parameter "s"/); + assert.throws(() => coerce('s', {}, -100000), TypeError, /Invalid value \((.*)\) for string parameter 's'/); + assert.throws(() => coerce('s', {}, -Infinity), TypeError, /Invalid value \((.*)\) for string parameter 's'/); + assert.throws(() => coerce('s', {}, true), TypeError, /Invalid value \((.*)\) for string parameter 's'/); + assert.throws(() => coerce('s', {}, false), TypeError, /Invalid value \((.*)\) for string parameter 's'/); + assert.throws(() => coerce('s', {}, 0.1), TypeError, /Invalid value \((.*)\) for string parameter 's'/); + assert.throws(() => coerce('s', {}, Math.PI), TypeError, /Invalid value \((.*)\) for string parameter 's'/); + assert.throws(() => coerce('s', {}, NaN), TypeError, /Invalid value \((.*)\) for string parameter 's'/); + assert.throws(() => coerce('s', {}, 0), TypeError, /Invalid value \((.*)\) for string parameter 's'/); + assert.throws(() => coerce('s', {}, null), TypeError, /Invalid value \((.*)\) for string parameter 's'/); + assert.throws(() => coerce('s', {}, /abc/), TypeError, /Invalid value \((.*)\) for string parameter 's'/); + assert.throws(() => coerce('s', {}, undefined), TypeError, /Invalid value \((.*)\) for string parameter 's'/); + assert.throws(() => coerce('s', {}, [0, 1, 2]), TypeError, /Invalid value \((.*)\) for string parameter 's'/); }); }); @@ -376,16 +374,16 @@ describe('# [private] ParameterBag::types', () => { assert.equal(coerce('i', def, -Infinity), -Infinity); assert.equal(coerce('i', def, 0), 0); - assert.throws(() => coerce('i', def, true), TypeError, /\[SharedState\] Invalid value "(.*)" for integer parameter "i"/); - assert.throws(() => coerce('i', def, false), TypeError, /\[SharedState\] Invalid value "(.*)" for integer parameter "i"/); - assert.throws(() => coerce('i', def, 0.1), TypeError, /\[SharedState\] Invalid value "(.*)" for integer parameter "i"/); - assert.throws(() => coerce('i', def, Math.PI), TypeError, /\[SharedState\] Invalid value "(.*)" for integer parameter "i"/); - assert.throws(() => coerce('i', def, NaN), TypeError, /\[SharedState\] Invalid value "(.*)" for integer parameter "i"/); - assert.throws(() => coerce('i', def, '10'), TypeError, /\[SharedState\] Invalid value "(.*)" for integer parameter "i"/); - assert.throws(() => coerce('i', def, null), TypeError, /\[SharedState\] Invalid value "(.*)" for integer parameter "i"/); - assert.throws(() => coerce('i', def, /abc/), TypeError, /\[SharedState\] Invalid value "(.*)" for integer parameter "i"/); - assert.throws(() => coerce('i', def, undefined), TypeError, /\[SharedState\] Invalid value "(.*)" for integer parameter "i"/); - assert.throws(() => coerce('i', def, [0, 1, 2]), TypeError, /\[SharedState\] Invalid value "(.*)" for integer parameter "i"/); + assert.throws(() => coerce('i', def, true), TypeError, /Invalid value \((.*)\) for integer parameter 'i'/); + assert.throws(() => coerce('i', def, false), TypeError, /Invalid value \((.*)\) for integer parameter 'i'/); + assert.throws(() => coerce('i', def, 0.1), TypeError, /Invalid value \((.*)\) for integer parameter 'i'/); + assert.throws(() => coerce('i', def, Math.PI), TypeError, /Invalid value \((.*)\) for integer parameter 'i'/); + assert.throws(() => coerce('i', def, NaN), TypeError, /Invalid value \((.*)\) for integer parameter 'i'/); + assert.throws(() => coerce('i', def, '10'), TypeError, /Invalid value \((.*)\) for integer parameter 'i'/); + assert.throws(() => coerce('i', def, null), TypeError, /Invalid value \((.*)\) for integer parameter 'i'/); + assert.throws(() => coerce('i', def, /abc/), TypeError, /Invalid value \((.*)\) for integer parameter 'i'/); + assert.throws(() => coerce('i', def, undefined), TypeError, /Invalid value \((.*)\) for integer parameter 'i'/); + assert.throws(() => coerce('i', def, [0, 1, 2]), TypeError, /Invalid value \((.*)\) for integer parameter 'i'/); }); it('should clip properly', () => { @@ -412,14 +410,14 @@ describe('# [private] ParameterBag::types', () => { assert.equal(coerce('f', def, 0.1), 0.1); assert.equal(coerce('f', def, Math.PI), Math.PI); - assert.throws(() => coerce('f', def, true), TypeError, /\[SharedState\] Invalid value "(.*)" for float parameter "f"/); - assert.throws(() => coerce('f', def, false), TypeError, /\[SharedState\] Invalid value "(.*)" for float parameter "f"/); - assert.throws(() => coerce('f', def, NaN), TypeError, /\[SharedState\] Invalid value "(.*)" for float parameter "f"/); - assert.throws(() => coerce('f', def, '10'), TypeError, /\[SharedState\] Invalid value "(.*)" for float parameter "f"/); - assert.throws(() => coerce('f', def, null), TypeError, /\[SharedState\] Invalid value "(.*)" for float parameter "f"/); - assert.throws(() => coerce('f', def, /abc/), TypeError, /\[SharedState\] Invalid value "(.*)" for float parameter "f"/); - assert.throws(() => coerce('f', def, undefined), TypeError, /\[SharedState\] Invalid value "(.*)" for float parameter "f"/); - assert.throws(() => coerce('f', def, [0, 1, 2]), TypeError, /\[SharedState\] Invalid value "(.*)" for float parameter "f"/); + assert.throws(() => coerce('f', def, true), TypeError, /Invalid value \((.*)\) for float parameter 'f'/); + assert.throws(() => coerce('f', def, false), TypeError, /Invalid value \((.*)\) for float parameter 'f'/); + assert.throws(() => coerce('f', def, NaN), TypeError, /Invalid value \((.*)\) for float parameter 'f'/); + assert.throws(() => coerce('f', def, '10'), TypeError, /Invalid value \((.*)\) for float parameter 'f'/); + assert.throws(() => coerce('f', def, null), TypeError, /Invalid value \((.*)\) for float parameter 'f'/); + assert.throws(() => coerce('f', def, /abc/), TypeError, /Invalid value \((.*)\) for float parameter 'f'/); + assert.throws(() => coerce('f', def, undefined), TypeError, /Invalid value \((.*)\) for float parameter 'f'/); + assert.throws(() => coerce('f', def, [0, 1, 2]), TypeError, /Invalid value \((.*)\) for float parameter 'f'/); }); it('should clip properly', () => { @@ -437,8 +435,8 @@ describe('# [private] ParameterBag::types', () => { assert.doesNotThrow(() => coerce('e', def, 'a')); assert.doesNotThrow(() => coerce('e', def, 1)); - assert.throws(() => coerce('e', def, 'e'), TypeError, /\[SharedState\] Invalid value "(.*)" for enum parameter "e"/); - assert.throws(() => coerce('e', def, '1'), TypeError, /\[SharedState\] Invalid value "(.*)" for enum parameter "e"/); + assert.throws(() => coerce('e', def, 'e'), TypeError, /Invalid value \((.*)\) for enum parameter 'e'/); + assert.throws(() => coerce('e', def, '1'), TypeError, /Invalid value \((.*)\) for enum parameter 'e'/); }); }); }); diff --git a/tests/states/StateManager.spec.js b/tests/states/StateManager.spec.js index dee1c399..5bf48695 100644 --- a/tests/states/StateManager.spec.js +++ b/tests/states/StateManager.spec.js @@ -44,7 +44,7 @@ describe(`# StateManager`, () => { it('should throw if reusing same schema name', () => { assert.throws(() => { server.stateManager.defineClass('a', a); - }, Error, '[stateManager.defineClass] Cannot define class with name: "a", class already exists'); + }); });