Skip to content

Commit

Permalink
refactor: move Server to es2022
Browse files Browse the repository at this point in the history
  • Loading branch information
b-ma committed May 25, 2024
1 parent 1d1db84 commit ea6e5e6
Show file tree
Hide file tree
Showing 24 changed files with 702 additions and 754 deletions.
2 changes: 1 addition & 1 deletion .jsdoc
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@
"typedefs": true,
"sectionOrder": [
"Namespaces",
"Global",
"Classes",
"Global",
"Modules",
"Externals",
"Events",
Expand Down
10 changes: 6 additions & 4 deletions src/client/Client.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import VERSION from '../common/version.js';

// for testing purposes
export const kClientVersionTest = Symbol('soundworks:client-version-test');
export const kClientOnStatusChangeCallbacks = Symbol('soundworks:client-on-status-change-callbacks');

/**
* Configuration object for a client running in a browser runtime.
Expand Down Expand Up @@ -74,7 +75,6 @@ class Client {
#status = 'idle';
// Token of the client if connected through HTTP authentication.
#token = null;
#onStatusChangeCallbacks = new Set();
#auditState = null;

/**
Expand Down Expand Up @@ -135,6 +135,8 @@ class Client {
this.#stateManager = new ClientStateManager();
this.#status = 'idle';

this[kClientOnStatusChangeCallbacks] = new Set();

logger.configure(!!config.env.verbose);
}

Expand Down Expand Up @@ -264,7 +266,7 @@ class Client {
// execute all callbacks in parallel
const promises = [];

for (let callback of this.#onStatusChangeCallbacks) {
for (let callback of this[kClientOnStatusChangeCallbacks]) {
promises.push(callback(status));
}

Expand Down Expand Up @@ -455,8 +457,8 @@ class Client {
* @returns {Function} Function that delete the listener when executed.
*/
onStatusChange(callback) {
this.#onStatusChangeCallbacks.add(callback);
return () => this.#onStatusChangeCallbacks.delete(callback);
this[kClientOnStatusChangeCallbacks].add(callback);
return () => this[kClientOnStatusChangeCallbacks].delete(callback);
}
}

Expand Down
16 changes: 0 additions & 16 deletions src/client/ClientPluginManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,6 @@ import BasePluginManager from '../common/BasePluginManager.js';
import ClientPlugin from './ClientPlugin.js';
import Client from './Client.js';

/**
* Callback executed when a plugin internal state is updated.
*
* @callback ClientPluginManager~onStateChangeCallback
* @param {Object.<string, ClientPlugin>} fullState - List of all plugins.
* @param {ClientPlugin|null} initiator - Plugin that initiated the update or `null`
* if the change was initiated by the state manager (i.e. when the initialization
* of the plugins starts).
*/

/**
* Delete the registered {@link ClientPluginManager~onStateChangeCallback}.
*
* @callback ClientPluginManager~deleteOnStateChangeCallback
*/

/**
* The `PluginManager` allows to register and retrieve `soundworks` plugins.
*
Expand Down
210 changes: 112 additions & 98 deletions src/common/BasePluginManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,22 @@ import { isPlainObject, isString } from '@ircam/sc-utils';

import logger from './logger.js';

/**
* Callback executed when a plugin internal state is updated.
*
* @callback pluginManagerOnStateChangeCallback
* @param {object<server.Plugin#id, server.Plugin>} fullState - List of all plugins.
* @param {server.Plugin|null} initiator - Plugin that initiated the update or `null`
* if the change was initiated by the state manager (i.e. when the initialization
* of the plugins starts).
*/

/**
* Delete the registered {@link pluginManagerOnStateChangeCallback}.
*
* @callback pluginManagerDeleteOnStateChangeCallback
*/

/**
* Shared functionnality between server-side and client-size plugin manager
*
Expand All @@ -24,83 +40,23 @@ class BasePluginManager {
this.status = 'idle';
}

/**
* Register a plugin into soundworks.
*
* _A plugin must always be registered both on client-side and on server-side_
*
* Refer to the plugin documentation to check its options and proper way of
* registering it.
*
* @param {string} id - Unique id of the plugin. Enables the registration of the
* same plugin factory under different ids.
* @param {Function} factory - Factory function that returns the Plugin class.
* @param {object} [options={}] - Options to configure the plugin.
* @param {array} [deps=[]] - List of plugins' names the plugin depends on, i.e.
* the plugin initialization will start only after the plugins it depends on are
* fully started themselves.
* @see {@link client.PluginManager#register}
* @see {@link server.PluginManager#register}
* @example
* // client-side
* client.pluginManager.register('user-defined-id', pluginFactory);
* // server-side
* server.pluginManager.register('user-defined-id', pluginFactory);
*/
register(id, ctor, options = {}, deps = []) {
// For now we don't allow to register a plugin after `client|server.init()`.
// This is subject to change in the future as we may want to dynamically
// register new plugins during application lifetime.
if (this._node.status === 'inited') {
throw new Error(`[soundworks.PluginManager] Cannot register plugin (${id}) after "client.init()"`);
}

if (!isString(id)) {
throw new Error(`[soundworks.PluginManager] Invalid argument, "pluginManager.register" first argument should be a string`);
}

if (!isPlainObject(options)) {
throw new Error(`[soundworks.PluginManager] Invalid argument, "pluginManager.register" third optionnal argument should be an object`);
}

if (!Array.isArray(deps)) {
throw new Error(`[soundworks.PluginManager] Invalid argument, "pluginManager.register" fourth optionnal argument should be an array`);
}
/** @private */
#propagateStateChange(instance = null, status = null) {
if (instance !== null) {
// status is null if wew forward some inner state change from the instance
if (status !== null) {
instance.status = status;
}

if (this._instances.has(id)) {
throw new Error(`[soundworks:PluginManager] Plugin "${id}" already registered`);
const fullState = Object.fromEntries(this._instances);
this._onStateChangeCallbacks.forEach(callback => callback(fullState, instance));
} else {
const fullState = Object.fromEntries(this._instances);
this._onStateChangeCallbacks.forEach(callback => callback(fullState, null));
}

// we instanciate the plugin here, so that a plugin can register another one
// in its own constructor.
//
// the dependencies must be created first, so that the instance can call
// addDependency in its constructor
this._dependencies.set(id, deps);

const instance = new ctor(this._node, id, options);
this._instances.set(id, instance);
}

/**
* Manually add a dependency to a given plugin. Usefull to require a plugin
* within a plugin
*
*/
addDependency(pluginId, dependencyId) {
const deps = this._dependencies.get(pluginId);
deps.push(dependencyId);
}

/**
* Returns the list of the registered plugins ids
* @returns {string[]}
*/
getRegisteredPlugins() {
return Array.from(this._instances.keys());
}

/**
/**
* Initialize all the registered plugin. Executed during the `Client.init()` or
* `Server.init()` initialization step.
* @private
Expand All @@ -115,11 +71,11 @@ class BasePluginManager {
this.status = 'inited';
// instanciate all plugins
for (let [_id, instance] of this._instances.entries()) {
instance.onStateChange(_values => this._propagateStateChange(instance));
instance.onStateChange(_values => this.#propagateStateChange(instance));
}

// propagate all 'idle' statuses before start
this._propagateStateChange();
this.#propagateStateChange();

const promises = Array.from(this._instances.keys()).map(id => this.unsafeGet(id));

Expand Down Expand Up @@ -177,7 +133,7 @@ class BasePluginManager {
if (this._instanceStartPromises.has(id)) {
await this._instanceStartPromises.get(id);
} else {
this._propagateStateChange(instance, 'inited');
this.#propagateStateChange(instance, 'inited');
let errored = false;

try {
Expand All @@ -187,31 +143,105 @@ class BasePluginManager {
await startPromise;
} catch (err) {
errored = true;
this._propagateStateChange(instance, 'errored');
this.#propagateStateChange(instance, 'errored');
throw err;
}

// this looks silly but it prevents the try / catch to catch errors that could
// be triggered by the propagate status callback, putting the plugin in errored state
if (!errored) {
this._propagateStateChange(instance, 'started');
this.#propagateStateChange(instance, 'started');
}
}

return instance;
}

/**
* Register a plugin into the manager.
*
* _A plugin must always be registered both on client-side and on server-side_
*
* Refer to the plugin documentation to check its options and proper way of
* registering it.
*
* @param {string} id - Unique id of the plugin. Enables the registration of the
* same plugin factory under different ids.
* @param {Function} factory - Factory function that returns the Plugin class.
* @param {object} [options={}] - Options to configure the plugin.
* @param {array} [deps=[]] - List of plugins' names the plugin depends on, i.e.
* the plugin initialization will start only after the plugins it depends on are
* fully started themselves.
* @see {@link ClientPluginManager#register}
* @see {@link ServerPluginManager#register}
* @example
* // client-side
* client.pluginManager.register('user-defined-id', pluginFactory);
* // server-side
* server.pluginManager.register('user-defined-id', pluginFactory);
*/
register(id, ctor, options = {}, deps = []) {
// For now we don't allow to register a plugin after `client|server.init()`.
// This is subject to change in the future as we may want to dynamically
// register new plugins during application lifetime.
if (this._node.status === 'inited') {
throw new Error(`[soundworks.PluginManager] Cannot register plugin (${id}) after "client.init()"`);
}

if (!isString(id)) {
throw new Error(`[soundworks.PluginManager] Invalid argument, "pluginManager.register" first argument should be a string`);
}

if (!isPlainObject(options)) {
throw new Error(`[soundworks.PluginManager] Invalid argument, "pluginManager.register" third optionnal argument should be an object`);
}

if (!Array.isArray(deps)) {
throw new Error(`[soundworks.PluginManager] Invalid argument, "pluginManager.register" fourth optionnal argument should be an array`);
}

if (this._instances.has(id)) {
throw new Error(`[soundworks:PluginManager] Plugin "${id}" already registered`);
}

// we instanciate the plugin here, so that a plugin can register another one
// in its own constructor.
//
// the dependencies must be created first, so that the instance can call
// addDependency in its constructor
this._dependencies.set(id, deps);

const instance = new ctor(this._node, id, options);
this._instances.set(id, instance);
}

/**
* Manually add a dependency to a given plugin.
*
* Usefull to require a plugin within a plugin
*/
addDependency(pluginId, dependencyId) {
const deps = this._dependencies.get(pluginId);
deps.push(dependencyId);
}

/**
* Returns the list of the registered plugins ids
* @returns {string[]}
*/
getRegisteredPlugins() {
return Array.from(this._instances.keys());
}

/**
* Propagate a notification each time a plugin is updated (status or inner state).
* The callback will receive the list of all plugins as first parameter, and the
* plugin instance that initiated the state change event as second parameter.
*
* _In most cases, you should not have to rely on this method._
*
* @param {client.PluginManager~onStateChangeCallback|server.PluginManager~onStateChangeCallback} callback
* Callback to be executed on state change
* @param {client.PluginManager~deleteOnStateChangeCallback|client.PluginManager~deleteOnStateChangeCallback}
* Function to execute to listening for changes.
* @param {pluginManagerOnStateChangeCallback} callback - Callback to execute on state change
* @returns {pluginManagerDeleteOnStateChangeCallback} - Clear the subscription when executed
* @example
* const unsubscribe = client.pluginManager.onStateChange(pluginList, initiator => {
* // log the current status of all plugins
Expand All @@ -230,22 +260,6 @@ class BasePluginManager {
this._onStateChangeCallbacks.add(callback);
return () => this._onStateChangeCallbacks.delete(callback);
}

/** @private */
_propagateStateChange(instance = null, status = null) {
if (instance !== null) {
// status is null if wew forward some inner state change from the instance
if (status !== null) {
instance.status = status;
}

const fullState = Object.fromEntries(this._instances);
this._onStateChangeCallbacks.forEach(callback => callback(fullState, instance));
} else {
const fullState = Object.fromEntries(this._instances);
this._onStateChangeCallbacks.forEach(callback => callback(fullState, null));
}
}
}

export default BasePluginManager;
13 changes: 11 additions & 2 deletions src/common/BaseStateManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@ export const kStateManagerDeleteState = Symbol('soundworks:state-manager-delete-
// for testing purposes
export const kStateManagerClient = Symbol('soundworks:state-manager-client');


/**
* @callback stateManagerObserveCallback
* @async
* @param {string} schemaName - name of the schema
* @param {number} stateId - id of the state
* @param {number} nodeId - id of the node that created the state
*/

/** @private */
class BaseStateManager {
#client = null;
Expand Down Expand Up @@ -321,11 +330,11 @@ class BaseStateManager {
*
* @param {string} [schemaName] - optionnal schema name to filter the observed
* states.
* @param {server.StateManager~ObserveCallback|client.StateManager~ObserveCallback}
* @param {stateManagerObserveCallback}
* callback - Function to be called when a new state is created on the network.
* @param {object} options - Options.
* @param {boolean} [options.excludeLocal = false] - If set to true, exclude states
* created locallly, i.e. by the same node, from the collection.
* created locally, i.e. by the same node, from the collection.
* @returns {Promise<Function>} - Returns a Promise that resolves when the given
* callback as been executed on each existing states. The promise value is a
* function which allows to stop observing the states on the network.
Expand Down
Loading

0 comments on commit ea6e5e6

Please sign in to comment.