diff --git a/src/common/SharedState.js b/src/common/SharedState.js index 43867397..06378ae4 100644 --- a/src/common/SharedState.js +++ b/src/common/SharedState.js @@ -50,7 +50,7 @@ export const kSharedStatePromiseStore = Symbol('soundworks:shared-state-promise- * to the shared state. * * A `SharedState` instance is created according to a shared state class definition - * which is composed of a {@link SharedStateClassName} and of a {@link SharedStateClassSchema} + * which is composed of a {@link SharedStateClassName} and of a {@link SharedStateClassDescription} * registered in the {@link ServerStateManager}. Any number of `SharedState`s * can be created from a single class definition. * @@ -114,7 +114,7 @@ class SharedState { #onDetachCallbacks = new Set(); #onDeleteCallbacks = new Set(); - constructor(id, remoteId, className, schema, client, isOwner, manager, initValues, filter) { + constructor(id, remoteId, className, classDescription, client, isOwner, manager, initValues, filter) { this.#id = id; this.#remoteId = remoteId; this.#className = className; @@ -124,7 +124,7 @@ class SharedState { this.#filter = filter; try { - this.#parameters = new ParameterBag(schema, initValues); + this.#parameters = new ParameterBag(classDescription, initValues); } catch (err) { console.error(err.stack); @@ -174,7 +174,7 @@ ${JSON.stringify(initValues, null, 2)}`); }); // --------------------------------------------- - // state has been deleted by its creator or the schema has been deleted + // state has been deleted by its creator or the class has been deleted // --------------------------------------------- this.#client.transport.addListener(`${DELETE_NOTIFICATION}-${this.#id}-${this.#remoteId}`, async () => { this.#manager[kStateManagerDeleteState](this.#id); @@ -635,7 +635,7 @@ ${JSON.stringify(initValues, null, 2)}`); /** * Get the values with which the state has been created. May defer from the - * default values declared in the schema. + * default values declared in the class description. * * @return {object} * @example @@ -646,7 +646,7 @@ ${JSON.stringify(initValues, null, 2)}`); } /** - * Get the default values as declared in the schema. + * Get the default values as declared in the class description. * * @return {object} * @example @@ -697,7 +697,7 @@ ${JSON.stringify(initValues, null, 2)}`); * @throws Throws if the method is called by a node which is not the owner of * the state. * @example - * const state = await client.state.create('my-schema-name'); + * const state = await client.stateManaager.create('my-class-name'); * // later * await state.delete(); */ diff --git a/src/common/SharedStateCollection.js b/src/common/SharedStateCollection.js index e743bbcc..5810b665 100644 --- a/src/common/SharedStateCollection.js +++ b/src/common/SharedStateCollection.js @@ -13,7 +13,7 @@ import logger from './logger.js'; /** * The `SharedStateCollection` interface represent a collection of all states - * created from a given schema name on the network. + * created from a given class name on the network. * * It can optionnaly exclude the states created by the current node. * @@ -21,7 +21,7 @@ import logger from './logger.js'; * {@link ServerStateManager#getCollection} for factory methods API * * ``` - * const collection = await client.stateManager.getCollection('my-schema'); + * const collection = await client.stateManager.getCollection('my-class'); * const allValues = collection.getValues(); * collection.onUpdate((state, newValues, oldValues, context) => { * // do something @@ -34,7 +34,7 @@ class SharedStateCollection { #className = null; #filter = null; #options = null; - #schema = null; + #classDescription = null; #states = []; #onUpdateCallbacks = new Set(); #onAttachCallbacks = new Set(); @@ -50,15 +50,15 @@ class SharedStateCollection { /** @private */ async _init() { - this.#schema = await this.#stateManager.getSchema(this.#className); + this.#classDescription = await this.#stateManager.getSchema(this.#className); // if filter is set, check that it contains only valid param names if (this.#filter !== null) { - const keys = Object.keys(this.#schema); + const keys = Object.keys(this.#classDescription); for (let filter of this.#filter) { if (!keys.includes(filter)) { - throw new ReferenceError(`[SharedStateCollection] Invalid filter key (${filter}) for schema "${this.#className}"`) + throw new ReferenceError(`[SharedStateCollection] Invalid filter key (${filter}) for class "${this.#className}"`) } } } @@ -139,18 +139,18 @@ class SharedStateCollection { */ getDescription(paramName = null) { if (paramName) { - if (!(paramName in this.#schema)) { + if (!(paramName in this.#classDescription)) { throw new ReferenceError(`Cannot execute "getDescription" on "SharedStateCollection": Parameter "${paramName}" does not exists`); } - return this.#schema[paramName]; + return this.#classDescription[paramName]; } - return this.#schema; + return this.#classDescription; } /** - * Get the default values as declared in the schema. + * Get the default values as declared in the class description. * * @return {object} * @example @@ -158,8 +158,8 @@ class SharedStateCollection { */ getDefaults() { const defaults = {}; - for (let name in this.#schema) { - defaults[name] = this.#schema[name].default; + for (let name in this.#classDescription) { + defaults[name] = this.#classDescription[name].default; } return defaults; } @@ -245,13 +245,13 @@ class SharedStateCollection { if (executeListener === true) { // filter `event: true` parameters from currentValues, this is missleading // as we are in the context of a callback, not from an active read - const schema = this.getDescription(); + const description = this.getDescription(); this.#states.forEach(state => { const currentValues = state.getValues(); - for (let name in schema) { - if (schema[name].event === true) { + for (let name in description) { + if (description[name].event === true) { delete currentValues[name]; } } diff --git a/src/server/Server.js b/src/server/Server.js index feeb2828..ac7b1c70 100644 --- a/src/server/Server.js +++ b/src/server/Server.js @@ -24,7 +24,7 @@ import merge from 'lodash/merge.js'; import pem from 'pem'; import compile from 'template-literal'; -import auditSchema from './audit-state-class-description.js'; +import auditClassDescription from './audit-state-class-description.js'; import { encryptData, decryptData, @@ -238,7 +238,7 @@ class Server { }; // register audit state schema - this.#stateManager.registerSchema(AUDIT_STATE_NAME, auditSchema); + this.#stateManager.registerSchema(AUDIT_STATE_NAME, auditClassDescription); logger.configure(this.#config.env.verbose); } diff --git a/src/server/ServerStateManager.js b/src/server/ServerStateManager.js index 10f55813..73aacd8c 100644 --- a/src/server/ServerStateManager.js +++ b/src/server/ServerStateManager.js @@ -65,8 +65,8 @@ export const kStateManagerClientsByNodeId = Symbol('soundworks:server-state-clie * at initialization (cf. {@link Server#stateManager}). * * Compared to the {@link ClientStateManager}, the `ServerStateManager` can also - * register and delete schemas, as well as register update hook that are executed when - * a state is updated. + * define and delete shared state classes, as well as register hooks executed at + * lifecycle phases of a shared state * * See {@link Server#stateManager} * @@ -77,7 +77,7 @@ export const kStateManagerClientsByNodeId = Symbol('soundworks:server-state-clie * import { Server } from '@soundworks/server/index.js'; * * const server = new Server(config); - * // declare and register the schema of a shared state. + * // declare and register the class of a shared state. * server.stateManager.registerSchema('some-global-state', { * myRandom: { * type: 'float', @@ -115,9 +115,9 @@ export const kStateManagerClientsByNodeId = Symbol('soundworks:server-state-clie */ class ServerStateManager extends BaseStateManager { #sharedStatePrivateById = new Map(); - #schemas = new Map(); + #classes = new Map(); #observers = new Set(); - #hooksBySchemaName = new Map(); // protected + #hooksByClassName = new Map(); // protected constructor() { super(); @@ -125,10 +125,11 @@ class ServerStateManager extends BaseStateManager { this[kStateManagerClientsByNodeId] = new Map(); } - #isObservableState(state) { - const { schemaName, isCollectionController } = state; - // is observable only if not in private state and not a controller state - return !PRIVATE_STATES.includes(schemaName) && !isCollectionController; + /** @private */ + [kStateManagerInit](id, transport) { + super[kStateManagerInit](id, transport); + // add itself as client of the state manager server + this[kServerStateManagerAddClient](id, transport); } /** @private */ @@ -137,15 +138,8 @@ class ServerStateManager extends BaseStateManager { } /** @private */ - [kServerStateManagerGetHooks](schemaName) { - return this.#hooksBySchemaName.get(schemaName); - } - - /** @private */ - [kStateManagerInit](id, transport) { - super[kStateManagerInit](id, transport); - // add itself as client of the state manager server - this[kServerStateManagerAddClient](id, transport); + [kServerStateManagerGetHooks](className) { + return this.#hooksByClassName.get(className); } /** @@ -174,13 +168,13 @@ class ServerStateManager extends BaseStateManager { // --------------------------------------------- client.transport.addListener( CREATE_REQUEST, - (reqId, schemaName, requireSchema, initValues = {}) => { - if (this.#schemas.has(schemaName)) { + (reqId, className, requireDescription, initValues = {}) => { + if (this.#classes.has(className)) { try { - const schema = this.#schemas.get(schemaName); + const classDescription = this.#classes.get(className); const stateId = generateStateId.next().value; const remoteId = generateRemoteId.next().value; - const state = new SharedStatePrivate(stateId, schemaName, schema, this, initValues); + const state = new SharedStatePrivate(stateId, className, classDescription, this, initValues); // attach client to the state as owner const isOwner = true; @@ -190,28 +184,28 @@ class ServerStateManager extends BaseStateManager { this.#sharedStatePrivateById.set(stateId, state); const currentValues = state.parameters.getValues(); - const schemaOption = requireSchema ? schema : null; + const classDescriptionOption = requireDescription ? classDescription : null; client.transport.emit( CREATE_RESPONSE, - reqId, stateId, remoteId, schemaName, schemaOption, currentValues, + reqId, stateId, remoteId, className, classDescriptionOption, currentValues, ); const isObservable = this.#isObservableState(state); if (isObservable) { this.#observers.forEach(observer => { - observer.transport.emit(OBSERVE_NOTIFICATION, schemaName, stateId, nodeId); + observer.transport.emit(OBSERVE_NOTIFICATION, className, stateId, nodeId); }); } } catch (err) { - const msg = `[stateManager] Cannot create state "${schemaName}", ${err.message}`; + const msg = `[stateManager] Cannot create state "${className}", ${err.message}`; console.error(msg); client.transport.emit(CREATE_ERROR, reqId, msg); } } else { - const msg = `[stateManager] Cannot create state "${schemaName}", schema does not exists`; + const msg = `[stateManager] Cannot create state "${className}", class is not defined`; console.error(msg); client.transport.emit(CREATE_ERROR, reqId, msg); @@ -224,19 +218,19 @@ class ServerStateManager extends BaseStateManager { // --------------------------------------------- client.transport.addListener( ATTACH_REQUEST, - (reqId, schemaName, stateId = null, requireSchema = true, filter = null) => { - if (this.#schemas.has(schemaName)) { + (reqId, className, stateId = null, requireDescription = true, filter = null) => { + if (this.#classes.has(className)) { let state = null; if (stateId !== null && this.#sharedStatePrivateById.has(stateId)) { state = this.#sharedStatePrivateById.get(stateId); } else if (stateId === null) { // if no `stateId` given, we try to find the first state with the given - // `schemaName` in the list, this allow a client to attach to a global + // `className` in the list, this allow a client to attach to a global // state created by the server (or some persistant client) without // having to know the `stateId` (e.g. some global state...) for (let existingState of this.#sharedStatePrivateById.values()) { - if (existingState.schemaName === schemaName) { + if (existingState.className === className) { state = existingState; break; } @@ -250,17 +244,17 @@ class ServerStateManager extends BaseStateManager { const remoteId = generateRemoteId.next().value; const isOwner = false; const currentValues = state.parameters.getValues(); - const schema = this.#schemas.get(schemaName); - const schemaOption = requireSchema ? schema : null; + const classDescription = this.#classes.get(className); + const classDescriptionOption = requireDescription ? classDescription : null; - // if filter given, check that all filter entries are valid schema keys - // @todo - improve error reportin: report invalid filters + // if filter given, check that all filter entries are valid class keys + // @todo - improve error reporting: report invalid filters if (filter !== null) { - const keys = Object.keys(schema); + const keys = Object.keys(classDescription); const isValid = filter.reduce((acc, key) => acc && keys.includes(key), true); if (!isValid) { - const msg = `[stateManager] Cannot attach, invalid filter (${filter.join(', ')}) for schema "${schemaName}"`; + const msg = `[stateManager] Cannot attach, invalid filter (${filter.join(', ')}) for class "${className}"`; console.error(msg); return client.transport.emit(ATTACH_ERROR, reqId, msg); @@ -271,17 +265,17 @@ class ServerStateManager extends BaseStateManager { client.transport.emit( ATTACH_RESPONSE, - reqId, state.id, remoteId, schemaName, schemaOption, currentValues, filter, + reqId, state.id, remoteId, className, classDescriptionOption, currentValues, filter, ); } else { - const msg = `[stateManager] Cannot attach, no existing state for schema "${schemaName}" with stateId: "${stateId}"`; + const msg = `[stateManager] Cannot attach, no existing state for class "${className}" with stateId: "${stateId}"`; console.error(msg); client.transport.emit(ATTACH_ERROR, reqId, msg); } } else { - const msg = `[stateManager] Cannot attach, schema "${schemaName}" does not exists`; + const msg = `[stateManager] Cannot attach, class "${className}" does not exists`; console.error(msg); client.transport.emit(ATTACH_ERROR, reqId, msg); @@ -292,16 +286,16 @@ class ServerStateManager extends BaseStateManager { // --------------------------------------------- // OBSERVE PEERS (be notified when a state is created, lazy) // --------------------------------------------- - client.transport.addListener(OBSERVE_REQUEST, (reqId, observedSchemaName) => { - if (observedSchemaName === null || this.#schemas.has(observedSchemaName)) { + client.transport.addListener(OBSERVE_REQUEST, (reqId, observedClassName) => { + if (observedClassName === null || this.#classes.has(observedClassName)) { const statesInfos = []; this.#sharedStatePrivateById.forEach(state => { const isObservable = this.#isObservableState(state); if (isObservable) { - const { schemaName, id, creatorId } = state; - statesInfos.push([schemaName, id, creatorId]); + const { className, id, creatorId } = state; + statesInfos.push([className, id, creatorId]); } }); @@ -311,7 +305,7 @@ class ServerStateManager extends BaseStateManager { client.transport.emit(OBSERVE_RESPONSE, reqId, ...statesInfos); } else { - const msg = `[stateManager] Cannot observe, schema "${observedSchemaName}" does not exists`; + const msg = `[stateManager] Cannot observe class "${observedClassName}", class does not exists`; client.transport.emit(OBSERVE_ERROR, reqId, msg); } }); @@ -323,12 +317,12 @@ class ServerStateManager extends BaseStateManager { // --------------------------------------------- // GET SCHEMA // --------------------------------------------- - client.transport.addListener(GET_SCHEMA_REQUEST, (reqId, schemaName) => { - if (this.#schemas.has(schemaName)) { - const schema = this.#schemas.get(schemaName); - client.transport.emit(GET_SCHEMA_RESPONSE, reqId, schemaName, schema); + client.transport.addListener(GET_SCHEMA_REQUEST, (reqId, className) => { + if (this.#classes.has(className)) { + const classDescription = this.#classes.get(className); + client.transport.emit(GET_SCHEMA_RESPONSE, reqId, className, classDescription); } else { - const msg = `[stateManager] Cannot get schema, schema "${schemaName}" does not exists`; + const msg = `[stateManager] Cannot get class "${className}", class does not exists`; client.transport.emit(GET_SCHEMA_ERROR, reqId, msg); } }); @@ -382,20 +376,24 @@ class ServerStateManager extends BaseStateManager { this[kStateManagerClientsByNodeId].delete(nodeId); } + #isObservableState(state) { + // is observable if not in private states list + return !PRIVATE_STATES.includes(state.className); + } + /** - * Define a class of data structure from which {@link SharedState} can be instanciated. + * Define a generic class from which {@link SharedState} can be created. * * _In a future revision, this method and its arguments will be renamed_ * - * @param {SharedStateClassName} schemaName - Name of the schema. - * @param {SharedStateClassDescription} schema - Data structure - * describing the states that will be created from this schema. + * @param {SharedStateClassName} className - Name of the class. + * @param {SharedStateClassDescription} classDescription - Description of the class. * * @see {@link ServerStateManager#create} * @see {@link ClientStateManager#create} * * @example - * server.stateManager.registerSchema('my-schema', { + * server.stateManager.registerSchema('my-class', { * myBoolean: { * type: 'boolean' * default: false, @@ -408,41 +406,41 @@ class ServerStateManager extends BaseStateManager { * } * }) */ - registerSchema(schemaName, schema) { - if (!isString(schemaName)) { - throw new Error(`[stateManager.registerSchema] Invalid schema name "${schemaName}", should be a string`); + registerSchema(className, classDescription) { + if (!isString(className)) { + throw new Error(`[stateManager.registerSchema] Invalid class name "${className}", should be a string`); } - if (this.#schemas.has(schemaName)) { - throw new Error(`[stateManager.registerSchema] cannot register schema with name: "${schemaName}", schema name already exists`); + if (this.#classes.has(className)) { + throw new Error(`[stateManager.registerSchema] Cannot define class with name: "${className}", class already exists`); } - if (!isPlainObject(schema)) { - throw new Error(`[stateManager.registerSchema] Invalid schema, should be an object`); + if (!isPlainObject(classDescription)) { + throw new Error(`[stateManager.registerSchema] Invalid class description, should be an object`); } - ParameterBag.validateDescription(schema); + ParameterBag.validateDescription(classDescription); - this.#schemas.set(schemaName, clonedeep(schema)); + this.#classes.set(className, clonedeep(classDescription)); // create hooks list - this.#hooksBySchemaName.set(schemaName, new Set()); + this.#hooksByClassName.set(className, new Set()); } /** * Delete a whole class of {@link ShareState}. * * All {@link SharedState} instance that belong to this class are deleted - * as well, triggering the `onDetach` and `onDelete` callbacks are called on - * the actual {@link SharedState} instances. + * as well. The `onDetach` and `onDelete` callbacks of existing {@link SharedState} + * instances will be called. * * _In a future revision, this method and its arguments will be renamed_ * - * @param {SharedStateClassName} schemaName - Name of the schema. + * @param {SharedStateClassName} className - Name of the shared state class to delete. */ - deleteSchema(schemaName) { + deleteSchema(className) { // @note: deleting schema for (let [_, state] of this.#sharedStatePrivateById) { - if (state.schemaName === schemaName) { + if (state.className === className) { for (let [remoteId, clientInfos] of state.attachedClients) { const attached = clientInfos.client; state[kSharedStatePrivateDetachClient](remoteId, attached); @@ -453,31 +451,32 @@ class ServerStateManager extends BaseStateManager { } } - // clear schema cache of all connected clients + // clear class cache of all connected clients for (let client of this[kStateManagerClientsByNodeId].values()) { - client.transport.emit(`${DELETE_SCHEMA}`, schemaName); + client.transport.emit(`${DELETE_SCHEMA}`, className); } - this.#schemas.delete(schemaName); + this.#classes.delete(className); // delete registered hooks - this.#hooksBySchemaName.delete(schemaName); + this.#hooksByClassName.delete(className); } /** - * Register a function for a given schema (e.g. will be applied on all states - * created from this schema) that will be executed before the update values - * are propagated. For example, this could be used to implement a preset system + * Register a function for a given shared state class the be executed between + * `set` instructions and `onUpdate` callback(s). + * + * For example, this could be used to implement a preset system * where all the values of the state are updated from e.g. some data stored in * filesystem while the consumer of the state only want to update the preset name. * - * The hook is associated to every state of its kind (i.e. schemaName) and - * executed on every update (call of `set`). Note that the hooks are executed - * server-side regarless the node on which `set` has been called and before - * the "actual" update of the state (e.g. before the call of `onUpdate`). + * The hook is associated to each states created from the given class name + * 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 + * before the call of the `onUpdate` callback of the shared state. * - * @param {string} schemaName - Kind of states on which applying the hook. - * @param {serverStateManagerUpdateHook} updateHook - Function - * called between the `set` call and the actual update. + * @param {string} className - Kind of states on which applying the hook. + * @param {serverStateManagerUpdateHook} updateHook - Function called between + * the `set` call and the actual update. * * @returns {Fuction} deleteHook - Handler that deletes the hook when executed. * @@ -499,13 +498,13 @@ class ServerStateManager extends BaseStateManager { * const values = state.getValues(); * assert.deepEqual(result, { value: 'test', numUpdates: 1 }); */ - registerUpdateHook(schemaName, updateHook) { - // throw error if schemaName has not been registered - if (!this.#schemas.has(schemaName)) { - throw new Error(`[stateManager.registerUpdateHook] cannot register update hook for schema name "${schemaName}", schema name does not exists`); + registerUpdateHook(className, updateHook) { + // throw error if className has not been registered + if (!this.#classes.has(className)) { + throw new Error(`[stateManager.registerUpdateHook] Cannot register update hook for class "${className}", class does not exists`); } - const hooks = this.#hooksBySchemaName.get(schemaName); + const hooks = this.#hooksByClassName.get(className); hooks.add(updateHook); return () => hooks.delete(updateHook); diff --git a/src/server/SharedStatePrivate.js b/src/server/SharedStatePrivate.js index f2c5dc16..63a0d0d7 100644 --- a/src/server/SharedStatePrivate.js +++ b/src/server/SharedStatePrivate.js @@ -46,26 +46,26 @@ export const kSharedStatePrivateDetachClient = Symbol('soundworks:shared-state-p */ class SharedStatePrivate { #id = null; - #schemaName = null; + #className = null; #manager = null; #parameters = null; #creatorId = null; #creatorRemoteId = null; #attachedClients = new Map(); - constructor(id, schemaName, schema, manager, initValues = {}) { + constructor(id, className, classDefinition, manager, initValues = {}) { this.#id = id; - this.#schemaName = schemaName; + this.#className = className; this.#manager = manager; - this.#parameters = new ParameterBag(schema, initValues); + this.#parameters = new ParameterBag(classDefinition, initValues); } get id() { return this.#id; } - get schemaName() { - return this.#schemaName; + get className() { + return this.#className; } get creatorId() { @@ -96,7 +96,7 @@ class SharedStatePrivate { // attach client listeners client.transport.addListener(`${UPDATE_REQUEST}-${this.id}-${remoteId}`, async (reqId, updates, context) => { // apply registered hooks - const hooks = this.#manager[kServerStateManagerGetHooks](this.schemaName); + const hooks = this.#manager[kServerStateManagerGetHooks](this.className); const values = this.#parameters.getValues(); let hookAborted = false; diff --git a/src/server/audit-state-class-description.js b/src/server/audit-state-class-description.js index 92f465b4..8b4a2770 100644 --- a/src/server/audit-state-class-description.js +++ b/src/server/audit-state-class-description.js @@ -1,5 +1,5 @@ /** - * Internal schema used to audit the application. + * Internal shared state class used to audit the application. */ export default { /** diff --git a/tests/states/StateCollection.spec.js b/tests/states/StateCollection.spec.js deleted file mode 100644 index bb00972f..00000000 --- a/tests/states/StateCollection.spec.js +++ /dev/null @@ -1,688 +0,0 @@ -import { assert } from 'chai'; -import { delay } from '@ircam/sc-utils'; - -import { Server } from '../../src/server/index.js'; -import { Client } from '../../src/client/index.js'; -import { BATCHED_TRANSPORT_CHANNEL } from '../../src/common/constants.js'; - -import config from '../utils/config.js'; -import { a, aExpectedDescription, b } from '../utils/class-description.js'; - -describe(`# SharedStateCollection`, () => { - let server; - let clients = []; - - beforeEach(async () => { - // --------------------------------------------------- - // server - // --------------------------------------------------- - server = new Server(config); - server.stateManager.registerSchema('a', a); - server.stateManager.registerSchema('b', b); - await server.start(); - - // --------------------------------------------------- - // clients - // --------------------------------------------------- - clients[0] = new Client({ role: 'test', ...config }); - clients[1] = new Client({ role: 'test', ...config }); - clients[2] = new Client({ role: 'test', ...config }); - await clients[0].start(); - await clients[1].start(); - await clients[2].start(); - }); - - afterEach(async function() { - server.stop(); - }); - - describe(`## StateManager::getCollection(className)`, () => { - it(`should return a working state collection`, async () => { - const client0 = clients[0]; - const client1 = clients[1]; - const client2 = clients[2]; - - const stateb = await client0.stateManager.create('b'); - const stateA0 = await client0.stateManager.create('a'); - await stateA0.set({ int: 42 }); - const stateA1 = await client1.stateManager.create('a'); - await stateA1.set({ int: 21 }); - - const collection = await client2.stateManager.getCollection('a'); - - collection.sort((a, b) => a.get('int') < b.get('int') ? -1 : 1); - - const values = collection.getValues(); - assert.deepEqual(values, [ { bool: false, int: 21 }, { bool: false, int: 42 } ]); - const ints = collection.get('int'); - assert.deepEqual(ints, [21, 42]); - - await stateA0.detach(); - - await delay(50); - - const ints2 = collection.get('int'); - assert.deepEqual(ints2, [21]); - - await collection.detach(); - - assert.equal(collection.length, 0); - - await stateb.delete(); - await stateA1.delete(); - await delay(50); - }); - - it(`should behave properly if getting same collection twice`, async () => { - const state = await clients[0].stateManager.create('a'); - const collection1 = await clients[1].stateManager.getCollection('a'); - const collection2 = await clients[1].stateManager.getCollection('a'); - - assert.equal(collection1.size, 1); - assert.equal(collection2.size, 1); - - await state.delete(); - await delay(50); // delay is required here, see #73 - - await collection1.detach(); - await collection2.detach(); - }); - - it.skip(`[FIXME #74] getting same collection twice should return same instance`, async () => { - const state = await clients[0].stateManager.create('a'); - const collection1 = await clients[1].stateManager.getCollection('a'); - const collection2 = await clients[1].stateManager.getCollection('a'); - - assert.isTrue(collection1 === collection2); - }); - - it(`should not exclude locally created states by default`, async () => { - const state = await clients[0].stateManager.create('a'); - const collection = await clients[0].stateManager.getCollection('a'); - - assert.equal(collection.length, 1); - - await state.delete(); - await delay(50); - }); - - it(`should exclude locally created states is excludeLocal is set to true`, async () => { - const state = await clients[0].stateManager.create('a'); - const collection = await clients[0].stateManager.getCollection('a', { excludeLocal: true }); - - assert.equal(collection.length, 0); - - await state.delete(); - await delay(50); - }); - - it('should thow if collection, i.e. className, does not exists', async () => { - let errored = false; - - try { - const collection = await server.stateManager.getCollection('do-not-exists'); - } catch (err) { - console.log(err.message); - errored = true; - } - - if (!errored) { - assert.fail('should have failed'); - } - }); - }); - - describe(`## size (alias length)`, async () => { - it(`should have proper length`, async () => { - const state1 = await clients[0].stateManager.create('a'); - const collection = await clients[1].stateManager.getCollection('a'); - // make sure the first collection doesn't "leak" into the other one, cf. 2058d6e - const collection2 = await clients[1].stateManager.getCollection('a'); - - assert.equal(collection.size, 1); - assert.equal(collection.length, 1); - - const state2 = await clients[2].stateManager.create('a'); - await delay(50); - - assert.equal(collection.size, 2); - assert.equal(collection.length, 2); - - await state1.delete(); - await state2.delete(); - await delay(50); - - assert.equal(collection.size, 0); - assert.equal(collection.length, 0); - }); - }); - - describe(`## className`, () => { - it(`should return the schema name`, async () => { - const collection = await clients[0].stateManager.getCollection('a'); - const className = collection.className; - assert.equal(className, 'a') - - await collection.detach(); - }); - }); - - describe(`## getDescription()`, () => { - it(`should return the class description`, async () => { - const collection = await clients[0].stateManager.getCollection('a'); - const description = collection.getDescription(); - assert.deepEqual(description, aExpectedDescription); - - await collection.detach(); - }); - }); - - describe(`## getDefaults()`, () => { - it(`should return the default values`, async () => { - const collection = await clients[0].stateManager.getCollection('a'); - const defaults = collection.getDefaults(); - const expected = { - bool: false, - int: 0, - }; - - assert.deepEqual(defaults, expected); - - await collection.detach(); - }); - }); - - describe(`## set(updates, context = null)`, () => { - it(`should properly progate updates`, async () => { - const state0 = await clients[0].stateManager.create('a'); - const state1 = await clients[1].stateManager.create('a'); - // cross attached states - const attached0 = await clients[1].stateManager.attach('a', state0.id); - const attached1 = await clients[0].stateManager.attach('a', state1.id); - - const collection = await clients[2].stateManager.getCollection('a'); - - assert.equal(collection.size, 2); - - const result = await collection.set({ bool: true }); - const expected = [{ bool: true }, { bool: true }]; - assert.deepEqual(result, expected); - - await delay(50); - // should be propagated to everyone - assert.equal(state0.get('bool'), true); - assert.equal(state1.get('bool'), true); - assert.equal(attached0.get('bool'), true); - assert.equal(attached1.get('bool'), true); - - await collection.detach(); - await state0.delete(); - await state1.delete(); - }); - - it(`test several collections from same schema`, async () => { - const state0 = await clients[0].stateManager.create('a'); - const state1 = await clients[1].stateManager.create('a'); - // cross attached states - const attached0 = await clients[1].stateManager.attach('a', state0.id); - const attached1 = await clients[0].stateManager.attach('a', state1.id); - - const collection0 = await clients[2].stateManager.getCollection('a'); - const collection1 = await clients[0].stateManager.getCollection('a'); - - assert.equal(collection0.size, 2); - assert.equal(collection1.size, 2); - - // update from collection0 - await collection0.set({ bool: true }); - await delay(50); - - assert.equal(state0.get('bool'), true); - assert.equal(state1.get('bool'), true); - assert.equal(attached0.get('bool'), true); - assert.equal(attached1.get('bool'), true); - assert.deepEqual(collection0.get('bool'), [true, true]); - assert.deepEqual(collection1.get('bool'), [true, true]); - - await collection0.set({ int: 42 }); - await delay(50); - - assert.equal(state0.get('int'), 42); - assert.equal(state1.get('int'), 42); - assert.equal(attached0.get('int'), 42); - assert.equal(attached1.get('int'), 42); - assert.deepEqual(collection0.get('int'), [42, 42]); - assert.deepEqual(collection1.get('int'), [42, 42]); - - await collection0.detach(); - await collection1.detach(); - - await state0.delete(); - await state1.delete(); - }); - - it(`"normal" state communication should work as expected`, async () => { - const state0 = await clients[0].stateManager.create('a'); - const state1 = await clients[1].stateManager.create('a'); - // cross attached states - const attached0 = await clients[1].stateManager.attach('a', state0.id); - const attached1 = await clients[0].stateManager.attach('a', state1.id); - - const collection = await clients[2].stateManager.getCollection('a'); - - let onUpdateCalled = false; - collection.onUpdate((state, updates) => { - onUpdateCalled = true; - assert.equal(state.id, state0.id); - assert.deepEqual(updates, { bool: true }); - }); - - await state0.set({ bool: true }); - await delay(50); - // should be propagated to everyone - assert.equal(state0.get('bool'), true); - assert.equal(state1.get('bool'), false); - assert.equal(attached0.get('bool'), true); - assert.equal(attached1.get('bool'), false); - assert.isTrue(onUpdateCalled); - - await collection.detach(); - - await state0.delete(); - await state1.delete(); - }); - }); - - describe(`## onUpdate(callback)`, () => { - it(`should properly call onUpdate with state and updates as arguments`, async () => { - const state = await clients[0].stateManager.create('a'); - const collection = await clients[1].stateManager.getCollection('a'); - - let onUpdateCalled = false; - - collection.onUpdate((s, updates) => { - onUpdateCalled = true; - - assert.equal(s.id, state.id); - assert.equal(s.get('int'), 42); - assert.equal(updates.int, 42); - }); - - await state.set({ int: 42 }); - await delay(50); - - if (onUpdateCalled === false) { - assert.fail('onUpdate should have been called'); - } - - await state.delete(); - await delay(50); - }); - - it('should not propagate event parameters on first call if `executeListener=true`', async () => { - server.stateManager.registerSchema('with-event', { - bool: { type: 'boolean', event: true, }, - int: { type: 'integer', default: 20, }, - }); - const state = await server.stateManager.create('with-event'); - const collection = await server.stateManager.getCollection('with-event'); - - let onUpdateCalled = false; - collection.onUpdate((state, newValues, oldValues, context) => { - onUpdateCalled = true; - assert.deepEqual(newValues, { int: 20 }); - assert.deepEqual(oldValues, {}); - assert.deepEqual(context, null); - }, true); - - await delay(10); - - assert.equal(onUpdateCalled, true); - server.stateManager.deleteSchema('with-event'); - }); - - }); - - describe(`## onAttach(callback)`, () => { - it(`should properly call onAttach callback with state as argument`, async () => { - const collection = await clients[1].stateManager.getCollection('a'); - - let onAttachCalled = false; - let stateId = null; - - collection.onAttach((s) => { - stateId = s.id; - onAttachCalled = true; - }); - - const state = await clients[0].stateManager.create('a'); - await delay(50); - - assert.equal(stateId, state.id); - - await state.delete(); - await delay(50); - - if (onAttachCalled === false) { - assert.fail('onAttach should have been called'); - } - }); - }); - - describe(`## onDetach()`, () => { - it(`should properly call detach callback with state as attribute`, async () => { - const state = await clients[0].stateManager.create('a'); - const collection = await clients[1].stateManager.getCollection('a'); - - let onDetachCalled = false; - - collection.onDetach((s) => { - assert.equal(s.id, state.id); - onDetachCalled = true; - }); - - await state.delete(); - await delay(50); - - if (onDetachCalled === false) { - assert.fail('onDetach should have been called'); - } - }); - }); - - describe(`## [Symbol.iterator]`, () => { - // this tends to show a bug - it(`should implement iterator API`, async () => { - const state = await clients[0].stateManager.create('a'); - const collection = await clients[1].stateManager.getCollection('a'); - - let size = 0; - - for (let s of collection) { - assert.equal(state.id, s.id) - size += 1; - } - - await assert.equal(size, 1); - }); - }); - - describe(`## Batched transport`, () => { - it(`should send only one message on update requests and response`, async () => { - // launch new server so we can grab the server side representation of the client - const localConfig = structuredClone(config); - localConfig.env.port = 8083; - - const server = new Server(localConfig); - server.stateManager.registerSchema('a', a); - await server.start(); - - let states = []; - const numStates = 1000; - - for (let i = 0; i < numStates; i++) { - const state = await server.stateManager.create('a'); - states.push(state); - } - - let batchedRequests = 0; - let batchedResponses = 0; - - server.onClientConnect(client => { - client.socket.addListener(BATCHED_TRANSPORT_CHANNEL, (args) => { - // console.log('server BATCHED_TRANSPORT_CHANNEL'); - batchedRequests += 1; - }); - }); - - const client = new Client({ role: 'test', ...localConfig }); - await client.start(); - - // update response - client.socket.addListener(BATCHED_TRANSPORT_CHANNEL, (args) => { - // console.log('client BATCHED_TRANSPORT_CHANNEL', args); - batchedResponses += 1; - }); - - const collection = await client.stateManager.getCollection('a'); - await collection.set({ int: 42 }); - - // // await delay(20); - const expected = new Array(numStates).fill(42); - assert.deepEqual(collection.get('int'), expected); - - // console.log(collection.get('int')); - // 1 message for getSchema request / response - // 1 message for observe request / response - // 1 message for attach requests / responses - // 1 message for update requests / responses - assert.equal(batchedRequests, 4); - assert.equal(batchedResponses, 4); - - await collection.detach(); - for (let i = 0; i < states.length; i++) { - await states[i].delete(); - } - await client.stop(); - await server.stop(); - }); - }); -}); - -describe('# SharedStateCollection - filtered collection', () => { - let server; - let clients = []; - - beforeEach(async () => { - // --------------------------------------------------- - // server - // --------------------------------------------------- - server = new Server(config); - server.stateManager.registerSchema('filtered', { - bool: { - type: 'boolean', - default: false, - }, - int: { - type: 'integer', - default: 0, - }, - string: { - type: 'string', - default: 'a', - }, - }); - await server.start(); - - // --------------------------------------------------- - // clients - // --------------------------------------------------- - clients[0] = new Client({ role: 'test', ...config }); - clients[1] = new Client({ role: 'test', ...config }); - clients[2] = new Client({ role: 'test', ...config }); - await clients[0].start(); - await clients[1].start(); - await clients[2].start(); - }); - - afterEach(async function() { - server.stop(); - }); - - describe(`## getCollection(className, filter)`, () => { - it(`should throw if filter contains invalid keys`, async () => { - const owned1 = await clients[0].stateManager.create('filtered'); - const owned2 = await clients[1].stateManager.create('filtered'); - let errored = false; - - try { - const attached = await clients[2].stateManager.getCollection('filtered', ['invalid', 'toto']); - } catch (err) { - console.log(err.message); - errored = true; - } - - assert.isTrue(errored); - }); - - it(`should return valid collection`, async () => { - const owned1 = await clients[0].stateManager.create('filtered'); - const owned2 = await clients[1].stateManager.create('filtered'); - const attached = await clients[2].stateManager.getCollection('filtered', ['bool', 'string']); - - assert.equal(attached.size, 2); - }); - }); - - describe(`## onUpdate(callback)`, () => { - it(`should propagate only filtered keys`, async () => { - const filter = ['bool', 'string']; - const owned1 = await clients[0].stateManager.create('filtered'); - const owned2 = await clients[1].stateManager.create('filtered'); - const attached = await clients[2].stateManager.getCollection('filtered', filter); - const expected = { bool: true, int: 1, string: 'b' }; - - owned1.onUpdate(updates => { - assert.deepEqual(updates, expected); - }); - - attached.onUpdate((state, updates) => { - assert.deepEqual(Object.keys(updates), filter); - }); - - await owned1.set(expected); - await delay(20); - }); - - it(`should not propagate if filtered updates is empty object`, async () => { - const filter = ['bool', 'string']; - const owned1 = await clients[0].stateManager.create('filtered'); - const owned2 = await clients[1].stateManager.create('filtered'); - const attached = await clients[2].stateManager.getCollection('filtered', filter); - const expected = { int: 1 }; - let batchedResponses = 0; - let callbackExecuted = false; - - clients[2].socket.addListener(BATCHED_TRANSPORT_CHANNEL, (args) => { - batchedResponses += 1; - }); - - owned1.onUpdate(updates => { - assert.deepEqual(updates, expected); - }); - - attached.onUpdate((state, updates) => { - callbackExecuted = true; - }); - - await owned1.set(expected); - await delay(20); - - assert.isFalse(callbackExecuted); - assert.equal(batchedResponses, 0); - }); - }); - - describe(`## set(updates)`, () => { - it(`should throw early if trying to set modify a param which is not filtered`, async () => { - const filter = ['bool', 'string']; - const owned1 = await clients[0].stateManager.create('filtered'); - const owned2 = await clients[1].stateManager.create('filtered'); - const attached = await clients[2].stateManager.getCollection('filtered', filter); - let onUpdateCalled = false; - let errored = false; - - owned1.onUpdate(() => onUpdateCalled = true); - - try { - await attached.set({ int: 42 }); - } catch (err) { - console.log(err.message); - errored = true; - } - - await delay(20); - - assert.isTrue(errored); - assert.isFalse(onUpdateCalled); - }); - }); - - describe(`## get(name)`, () => { - it(`should throw if trying to access a param which is not filtered`, async () => { - const filter = ['bool', 'string']; - const owned1 = await clients[0].stateManager.create('filtered'); - const owned2 = await clients[1].stateManager.create('filtered'); - const attached = await clients[2].stateManager.getCollection('filtered', filter); - let errored = false; - - try { - await attached.get('int'); - } catch (err) { - console.log(err.message); - errored = true; - } - - await delay(20); - - assert.isTrue(errored); - }); - }); - - describe(`## getUnsafe(name)`, () => { - it(`should throw if trying to access a param which is not filtered`, async () => { - const filter = ['bool', 'string']; - const owned1 = await clients[0].stateManager.create('filtered'); - const owned2 = await clients[1].stateManager.create('filtered'); - const attached = await clients[2].stateManager.getCollection('filtered', filter); - let errored = false; - - try { - await attached.getUnsafe('int'); - } catch (err) { - console.log(err.message); - errored = true; - } - - await delay(20); - - assert.isTrue(errored); - }); - }); - - describe(`## getValues()`, () => { - it(`should return a filtered object`, async () => { - const filter = ['bool', 'string']; - const owned1 = await clients[0].stateManager.create('filtered'); - const owned2 = await clients[1].stateManager.create('filtered'); - const attached = await clients[2].stateManager.getCollection('filtered', filter); - - await owned1.set({ bool: true }); - await delay(20); - - const values = attached.getValues(); - assert.deepEqual(values, [ - { bool: true, string: 'a' }, - { bool: false, string: 'a' }, - ]); - }); - }); - - describe(`## getValuesUnsafe()`, () => { - it(`should return a filtered object`, async () => { - const filter = ['bool', 'string']; - const owned1 = await clients[0].stateManager.create('filtered'); - const owned2 = await clients[1].stateManager.create('filtered'); - const attached = await clients[2].stateManager.getCollection('filtered', filter); - - await owned1.set({ bool: true }); - await delay(20); - - const values = attached.getValuesUnsafe(); - assert.deepEqual(values, [ - { bool: true, string: 'a' }, - { bool: false, string: 'a' }, - ]); - }); - }); -}); diff --git a/tests/states/StateManager.spec.js b/tests/states/StateManager.spec.js index 67d7b6cf..774f70d7 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.registerSchema('a', a); - }, Error, '[stateManager.registerSchema] cannot register schema with name: "a", schema name already exists'); + }, Error, '[stateManager.registerSchema] Cannot define class with name: "a", class already exists'); }); @@ -311,7 +311,7 @@ describe(`# StateManager`, () => { }); server.stateManager.deleteSchema('a-delete-observe'); - await delay(100); + await delay(10); assert.equal(deleteCalled, true); @@ -320,7 +320,7 @@ describe(`# StateManager`, () => { observeCalled = true; }); - await delay(100); // is not needed as observe should await, but just to make sure + await delay(10); // is not needed as observe should await, but just to make sure assert.equal(observeCalled, false); unobserve();