From 9c55d51538bab84abb0d071bb66f16ebb7a0ead0 Mon Sep 17 00:00:00 2001 From: b-ma Date: Tue, 1 Oct 2024 17:57:37 +0200 Subject: [PATCH] refactor: remove schema semantics --- src/common/BaseStateManager.js | 4 +- src/common/ParameterBag.js | 75 ++++++++++----------- src/common/shared-state-types.js | 4 +- src/server/ServerStateManager.js | 4 +- src/server/audit-state-class-description.js | 42 ++++++++++++ tests/states/ParameterBag.spec.js | 26 +++---- tests/utils/class-description.js | 55 +++++++++++++++ 7 files changed, 152 insertions(+), 58 deletions(-) create mode 100644 src/server/audit-state-class-description.js create mode 100644 tests/utils/class-description.js diff --git a/src/common/BaseStateManager.js b/src/common/BaseStateManager.js index f40588eb..3fa5bca9 100644 --- a/src/common/BaseStateManager.js +++ b/src/common/BaseStateManager.js @@ -221,9 +221,9 @@ class BaseStateManager { /** * Return the schema from a given registered schema name * - * @param {String} schemaName - Name of the schema as given on registration + * @param {SharedStateClassName} schemaName - Name of the schema as given on registration * (cf. ServerStateManager) - * @return {SharedStateSchema} + * @return {SharedStateClassDescription} * @example * const schema = await client.stateManager.getSchema('my-class'); */ diff --git a/src/common/ParameterBag.js b/src/common/ParameterBag.js index 2e9a0308..72c0489d 100644 --- a/src/common/ParameterBag.js +++ b/src/common/ParameterBag.js @@ -45,9 +45,9 @@ export const types = { max: +Infinity, }); }, - sanitizeSchema: (def) => { - // sanitize `null` values in received schema, this prevent a bug when - // `min` and `max` are explicitely set to `±Infinity`, the schema is stringified + 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 // when sent over the network and therefore Infinity is transformed to `null` // // JSON.parse({ a: Infinity }); @@ -78,9 +78,9 @@ export const types = { max: +Infinity, }); }, - sanitizeSchema: (def) => { - // sanitize `null` values in received schema, this prevent a bug when - // `min` and `max` are explicitely set to `±Infinity`, the schema is stringified + 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 // when sent over the network and therefore Infinity is transformed to `null` // // JSON.parse({ a: Infinity }); @@ -132,16 +132,16 @@ export const types = { /** @private */ class ParameterBag { - static validateSchema(schema) { - for (let name in schema) { - const def = schema[name]; + static validateDescription(description) { + for (let name in description) { + const def = description[name]; if (!Object.prototype.hasOwnProperty.call(def, 'type')) { - throw new TypeError(`[StateManager.registerSchema] Invalid schema definition - param "${name}": "type" key is required`); + throw new TypeError(`[StateManager.registerSchema] Invalid class description - param "${name}": "type" key is required`); } if (!Object.prototype.hasOwnProperty.call(types, def.type)) { - throw new TypeError(`[StateManager.registerSchema] Invalid schema definition - param "${name}": "{ type: '${def.type}' }" does not exists`); + throw new TypeError(`[StateManager.registerSchema] Invalid class description - param "${name}": "{ type: '${def.type}' }" does not exists`); } const required = types[def.type].required; @@ -152,38 +152,38 @@ 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.registerSchema] Invalid schema definition for param ${name} - "default" propaerty is set and not null while the parameter definition is declared as "event" or "required"`); + throw new TypeError(`[StateManager.registerSchema] Invalid class description for param ${name} - "default" propaerty 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.registerSchema] Invalid schema definition - param "${name}" (type "${def.type}"): "${key}" key is required`); + throw new TypeError(`[StateManager.registerSchema] Invalid class description - param "${name}" (type "${def.type}"): "${key}" key is required`); } }); } } - #schema = {}; + #description = {}; #values = {}; - constructor(schema, initValues = {}) { - if (!schema) { - throw new Error(`[ParameterBag] schema is mandatory`); + constructor(description, initValues = {}) { + if (!description) { + throw new Error(`[ParameterBag] description is mandatory`); } - schema = cloneDeep(schema); + description = cloneDeep(description); initValues = cloneDeep(initValues); - ParameterBag.validateSchema(schema); + ParameterBag.validateDescription(description); - // make shure initValues make sens according to the given schema + // make shure initValues make sens according to the given description for (let name in initValues) { - if (!Object.prototype.hasOwnProperty.call(schema, name)) { + if (!Object.prototype.hasOwnProperty.call(description, name)) { throw new ReferenceError(`[StateManager.create] init value defined for undefined param "${name}"`); } } - for (let [name, def] of Object.entries(schema)) { - if (types[def.type].sanitizeSchema) { - def = types[def.type].sanitizeSchema(def); + for (let [name, def] of Object.entries(description)) { + if (types[def.type].sanitizeDescription) { + def = types[def.type].sanitizeDescription(def); } const { defaultOptions } = types[def.type]; @@ -213,11 +213,11 @@ class ParameterBag { } - this.#schema[name] = def; + this.#description[name] = def; // coerce init value and store in definition initValue = this.set(name, initValue)[0]; - this.#schema[name].initValue = initValue; + this.#description[name].initValue = initValue; this.#values[name] = initValue; } } @@ -229,7 +229,7 @@ class ParameterBag { * @return {Boolean} */ has(name) { - return Object.prototype.hasOwnProperty.call(this.#schema, name); + return Object.prototype.hasOwnProperty.call(this.#description, name); } /** @@ -279,7 +279,7 @@ class ParameterBag { throw new ReferenceError(`[SharedState] Cannot get value of undefined parameter "${name}"`); } - if (this.#schema[name].type === 'any') { + if (this.#description[name].type === 'any') { // we return a deep copy of the object as we don't want the client code to // be able to modify our underlying data. return cloneDeep(this.#values[name]); @@ -307,8 +307,7 @@ class ParameterBag { } /** - * Check that the value is valid according to the schema and return it coerced - * to the schema definition + * Check that the value is valid according to the class definition and return it coerced. * * @param {String} name - Name of the parameter. * @param {Mixed} value - Value of the parameter. @@ -318,7 +317,7 @@ class ParameterBag { throw new ReferenceError(`[SharedState] Cannot set value of undefined parameter "${name}"`); } - const def = this.#schema[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`); @@ -351,7 +350,7 @@ class ParameterBag { // deep equal check to returns true, and therefore the update is not triggered. // @see tests/common.state-manager.spec.js // 'should copy stored value for "any" type to have a predictable behavior' - if (this.#schema[name].type === 'any') { + if (this.#description[name].type === 'any') { value = cloneDeep(value); } @@ -379,27 +378,25 @@ class ParameterBag { // } /** - * Return the given schema along with the initialization values. - * * @return {object} */ getDescription(name = null) { if (name === null) { - return this.#schema; + return this.#description; } if (!this.has(name)) { - throw new ReferenceError(`[SharedState] Cannot get schema description of undefined parameter "${name}"`); + throw new ReferenceError(`[SharedState] Cannot get description of undefined parameter "${name}"`); } - return this.#schema[name]; + return this.#description[name]; } // return the default value, if initValue has been given, return init values getInitValues() { const initValues = {}; - for (let [name, def] of Object.entries(this.#schema)) { + for (let [name, def] of Object.entries(this.#description)) { initValues[name] = def.initValue; } @@ -410,7 +407,7 @@ class ParameterBag { getDefaults() { const defaults = {}; - for (let [name, def] of Object.entries(this.#schema)) { + for (let [name, def] of Object.entries(this.#description)) { defaults[name] = def.default; } diff --git a/src/common/shared-state-types.js b/src/common/shared-state-types.js index b47fc1e9..42922844 100644 --- a/src/common/shared-state-types.js +++ b/src/common/shared-state-types.js @@ -52,13 +52,13 @@ * @property {boolean} [event=false] - Define if the parameter is a volatile, i.e. * its value only exists on an update and is set back to `null` after propagation. * When `true`, `nullable` is automatically set to `true` and `default` to `null`. - * @property {boolean} [filterChange=true] - When set to `false`, an update will + * @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 * (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 + * @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 diff --git a/src/server/ServerStateManager.js b/src/server/ServerStateManager.js index 707e1e9f..10f55813 100644 --- a/src/server/ServerStateManager.js +++ b/src/server/ServerStateManager.js @@ -388,7 +388,7 @@ class ServerStateManager extends BaseStateManager { * _In a future revision, this method and its arguments will be renamed_ * * @param {SharedStateClassName} schemaName - Name of the schema. - * @param {SharedStateSchema} schema - Data structure + * @param {SharedStateClassDescription} schema - Data structure * describing the states that will be created from this schema. * * @see {@link ServerStateManager#create} @@ -421,7 +421,7 @@ class ServerStateManager extends BaseStateManager { throw new Error(`[stateManager.registerSchema] Invalid schema, should be an object`); } - ParameterBag.validateSchema(schema); + ParameterBag.validateDescription(schema); this.#schemas.set(schemaName, clonedeep(schema)); // create hooks list diff --git a/src/server/audit-state-class-description.js b/src/server/audit-state-class-description.js new file mode 100644 index 00000000..92f465b4 --- /dev/null +++ b/src/server/audit-state-class-description.js @@ -0,0 +1,42 @@ +/** + * Internal schema used to audit the application. + */ +export default { + /** + * Number of connected clients by role. + * + * @example + * { + * player: 12, + * controller: 1, + * } + */ + numClients: { + type: 'any', + default: {}, + }, + + /** + * Average latency in seconds computed from ping/pong informations. + */ + averageNetworkLatency: { + type: 'float', + default: 0, + }, + + /** + * Time window in second used to compute the average latency. Defaults to 5 + */ + averageNetworkLatencyWindow: { + type: 'float', + default: 5, + }, + + /** + * Period in second at which the average latency is computed. Defaults to 2 + */ + averageNetworkLatencyPeriod: { + type: 'float', + default: 2, + }, +}; diff --git a/tests/states/ParameterBag.spec.js b/tests/states/ParameterBag.spec.js index d34190ca..a9c6df0a 100644 --- a/tests/states/ParameterBag.spec.js +++ b/tests/states/ParameterBag.spec.js @@ -10,39 +10,39 @@ describe('# [private] ParameterBag', () => { // --------------------------------------------------------------- // MAIN API // --------------------------------------------------------------- - describe('## static validateSchema(schema)', () => { + describe('## static validateDescription(schema)', () => { it('should check if schema is invalid', () => { - assert.throw(() => ParameterBag.validateSchema({ + assert.throw(() => ParameterBag.validateDescription({ noType: {} - }), TypeError, `[StateManager.registerSchema] Invalid schema definition - param "noType": "type" key is required`); + }), TypeError, `[StateManager.registerSchema] Invalid class description - param "noType": "type" key is required`); - assert.throw(() => ParameterBag.validateSchema({ + assert.throw(() => ParameterBag.validateDescription({ invalidType: { type: 'invalid' } - }), TypeError, `[StateManager.registerSchema] Invalid schema definition - param "invalidType": "{ type: 'invalid' }" does not exists`); + }), TypeError, `[StateManager.registerSchema] Invalid class description - param "invalidType": "{ type: 'invalid' }" does not exists`); - assert.throw(() => ParameterBag.validateSchema({ + assert.throw(() => ParameterBag.validateDescription({ myBoolean: { type: 'boolean' } - }), TypeError, `[StateManager.registerSchema] Invalid schema definition - param "myBoolean" (type "boolean"): "default" key is required`); + }), TypeError, `[StateManager.registerSchema] Invalid class description - param "myBoolean" (type "boolean"): "default" key is required`); }); it(`should throw if "default" is declared when "event" is true`, () => { // event: true does not require `default` value - assert.doesNotThrow(() => ParameterBag.validateSchema({ + assert.doesNotThrow(() => ParameterBag.validateDescription({ myBoolean: { type: 'boolean', event: true } })); - assert.throws(() => ParameterBag.validateSchema({ + assert.throws(() => ParameterBag.validateDescription({ myBoolean: { type: 'boolean', event: true, default: false } })); }); it(`should throw if "default" is declared when "required" is true`, () => { // required: true does not require `default` value - assert.doesNotThrow(() => ParameterBag.validateSchema({ + assert.doesNotThrow(() => ParameterBag.validateDescription({ myBoolean: { type: 'boolean', required: true } })); - assert.throws(() => ParameterBag.validateSchema({ + assert.throws(() => ParameterBag.validateDescription({ myBoolean: { type: 'boolean', required: true, default: true } })); }); @@ -52,7 +52,7 @@ describe('# [private] ParameterBag', () => { it('should validate the given schema', () => { assert.throws(() => new ParameterBag({ invalidType: { type: 'invalid' } - }), TypeError, `[StateManager.registerSchema] Invalid schema definition - param "invalidType": "{ type: 'invalid' }" does not exists`); + }), TypeError, `[StateManager.registerSchema] Invalid class description - param "invalidType": "{ type: 'invalid' }" does not exists`); }); it('should check initValues consistency', () => { @@ -238,7 +238,7 @@ describe('# [private] ParameterBag', () => { it(`should throw if name does not exists`, () => { assert.throw(() => params.getDescription('42'), ReferenceError, - `[SharedState] Cannot get schema description of undefined parameter "42"` + `[SharedState] Cannot get description of undefined parameter "42"` ); }); }); diff --git a/tests/utils/class-description.js b/tests/utils/class-description.js new file mode 100644 index 00000000..cdc8e6c3 --- /dev/null +++ b/tests/utils/class-description.js @@ -0,0 +1,55 @@ +export const a = { + bool: { + type: 'boolean', + default: false, + }, + int: { + type: 'integer', + min: 0, + max: 100, + default: 0, + step: 1, + }, +}; + +export const aExpectedDescription = { + "bool": { + "default": false, + "event": false, + "filterChange": true, + "immediate": false, + "initValue": false, + "metas": {}, + "nullable": false, + "required": false, + "type": "boolean", + }, + "int": { + "default": 0, + "event": false, + "filterChange": true, + "immediate": false, + "initValue": 0, + "max": 100, + "metas": {}, + "min": 0, + "nullable": false, + "required": false, + "step": 1, + "type": "integer", + } +} + +export const b = { + bool: { + type: 'boolean', + default: true, + }, + int: { + type: 'integer', + min: 0, + max: 100, + default: 20, + step: 1, + }, +};