Skip to content

Commit

Permalink
refactor: remove schema semantics
Browse files Browse the repository at this point in the history
  • Loading branch information
b-ma committed Oct 1, 2024
1 parent b30b6e0 commit 9c55d51
Show file tree
Hide file tree
Showing 7 changed files with 152 additions and 58 deletions.
4 changes: 2 additions & 2 deletions src/common/BaseStateManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
*/
Expand Down
75 changes: 36 additions & 39 deletions src/common/ParameterBag.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand Down Expand Up @@ -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 });
Expand Down Expand Up @@ -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;
Expand All @@ -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];
Expand Down Expand Up @@ -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;
}
}
Expand All @@ -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);
}

/**
Expand Down Expand Up @@ -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]);
Expand Down Expand Up @@ -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.
Expand All @@ -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`);
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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;
}

Expand All @@ -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;
}

Expand Down
4 changes: 2 additions & 2 deletions src/common/shared-state-types.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions src/server/ServerStateManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down Expand Up @@ -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
Expand Down
42 changes: 42 additions & 0 deletions src/server/audit-state-class-description.js
Original file line number Diff line number Diff line change
@@ -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,
},
};
26 changes: 13 additions & 13 deletions tests/states/ParameterBag.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
}));
});
Expand All @@ -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', () => {
Expand Down Expand Up @@ -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"`
);
});
});
Expand Down
Loading

0 comments on commit 9c55d51

Please sign in to comment.