Skip to content

Commit

Permalink
feat: implement filter for state collections
Browse files Browse the repository at this point in the history
  • Loading branch information
b-ma committed Apr 30, 2024
1 parent 982816b commit f326230
Show file tree
Hide file tree
Showing 5 changed files with 344 additions and 23 deletions.
9 changes: 5 additions & 4 deletions src/common/BaseSharedState.js
Original file line number Diff line number Diff line change
Expand Up @@ -492,8 +492,9 @@ ${JSON.stringify(initValues, null, 2)}`);
}

/**
* Get all the key / value pairs of the state. If a parameter is of `any`
* type, a deep copy is made.
* Get all the key / value pairs of the state.
*
* If a parameter is of `any` type, a deep copy is made.
*
* @return {object}
* @example
Expand All @@ -514,8 +515,8 @@ ${JSON.stringify(initValues, null, 2)}`);
}

/**
* Get all the key / value pairs of the state. If a parameter is of `any`
* type, a deep copy is made.
* Get all the key / value pairs of the state.
*
* Similar to `getValues` but returns a reference to the underlying value in
* case of `any` type. May be usefull if the underlying value is big (e.g.
* sensors recordings, etc.) and deep cloning expensive. Be aware that if
Expand Down
53 changes: 49 additions & 4 deletions src/common/BaseSharedStateCollection.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
/** @private */
class BaseSharedStateCollection {
constructor(stateManager, schemaName, options = {}) {
constructor(stateManager, schemaName, filter = null, options = {}) {
this._stateManager = stateManager;
this._schemaName = schemaName;
this._filter = filter;
this._options = Object.assign({ excludeLocal: false }, options);

this._schema = null;
Expand All @@ -18,8 +19,19 @@ class BaseSharedStateCollection {
async _init() {
this._schema = await this._stateManager.getSchema(this._schemaName);

// if filter is set, check that it contains only valid param names
if (this._filter !== null) {
const keys = Object.keys(this._schema);

for (let filter of this._filter) {
if (!keys.includes(filter)) {
throw new ReferenceError(`[SharedStateCollection] Invalid filter key (${filter}) for schema "${this._schemaName}"`)
}
}
}

this._unobserve = await this._stateManager.observe(this._schemaName, async (schemaName, stateId) => {
const state = await this._stateManager.attach(schemaName, stateId);
const state = await this._stateManager.attach(schemaName, stateId, this._filter);
this._states.push(state);

state.onDetach(() => {
Expand Down Expand Up @@ -108,13 +120,46 @@ class BaseSharedStateCollection {
return this._states.map(state => state.getValues());
}

/**
* Return the current values of all the states in the collection.
*
* Similar to `getValues` but returns a reference to the underlying value in
* case of `any` type. May be usefull if the underlying value is big (e.g.
* sensors recordings, etc.) and deep cloning expensive. Be aware that if
* changes are made on the returned object, the state of your application will
* become inconsistent.
*
* @return {Object[]}
*/
getValuesUnsafe() {
return this._states.map(state => state.getValues());
}

/**
* Return the current param value of all the states in the collection.
*
* @param {String} name - Name of the parameter
* @return {any[]}
*/
get(name) {
// we can delegate to the state.get(name) method for throwing in case of filtered
// keys, as the Promise.all will reject on first reject Promise
return this._states.map(state => state.get(name));
}

/**
* Similar to `get` but returns a reference to the underlying value in case of
* `any` type. May be usefull if the underlying value is big (e.g. sensors
* recordings, etc.) and deep cloning expensive. Be aware that if changes are
* made on the returned object, the state of your application will become
* inconsistent.
*
* @param {String} name - Name of the parameter
* @return {any[]}
*/
getUnsafe(name) {
// we can delegate to the state.get(name) method for throwing in case of filtered
// keys, as the Promise.all will reject on first reject Promise
return this._states.map(state => state.get(name));
}

Expand All @@ -126,8 +171,8 @@ class BaseSharedStateCollection {
* current call and will be passed as third argument to all update listeners.
*/
async set(updates, context = null) {
// hot fix for https://github.com/collective-soundworks/soundworks/issues/85
// to be cleaned soon
// we can delegate to the state.set(update) method for throwing in case of
// filtered keys, as the Promise.all will reject on first reject Promise
const promises = this._states.map(state => state.set(updates, context));
return Promise.all(promises);
}
Expand Down
62 changes: 47 additions & 15 deletions src/common/BaseStateManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -251,26 +251,29 @@ class BaseStateManager {
}

if (arguments.length === 2) {
if (Number.isFinite(stateIdOrFilter)) {
if (stateIdOrFilter === null) {
stateId = null;
filter = null;
} else if (Number.isFinite(stateIdOrFilter)) {
stateId = stateIdOrFilter;
filter = null;
} else if (Array.isArray(stateIdOrFilter)) {
stateId = null;
filter = stateIdOrFilter;
} else {
throw new TypeError(`Cannot execute 'attach' on 'StateManager': argument 2 should be either a number or an array`);
throw new TypeError(`Cannot execute 'attach' on 'StateManager': argument 2 should be either null, a number or an array`);
}
}

if (arguments.length === 3) {
stateId = stateIdOrFilter;

if (!Number.isFinite(stateId)) {
throw new TypeError(`Cannot execute 'attach' on 'StateManager': argument 2 should be a number`);
if (stateId !== null && !Number.isFinite(stateId)) {
throw new TypeError(`Cannot execute 'attach' on 'StateManager': argument 2 should be either null or a number`);
}

if (!Array.isArray(filter)) {
throw new TypeError(`Cannot execute 'attach' on 'StateManager': argument 2 should be a number`);
if (filter !== null && !Array.isArray(filter)) {
throw new TypeError(`Cannot execute 'attach' on 'StateManager': argument 3 should be either null or an array`);
}
}

Expand Down Expand Up @@ -366,7 +369,7 @@ class BaseStateManager {
options = Object.assign(defaultOptions, args[1]);

} else {
throw new Error(`[stateManager] Invalid signature, refer to the StateManager.observe documentation"`);
throw new TypeError(`[stateManager] Invalid signature, refer to the StateManager.observe documentation"`);
}

break;
Expand Down Expand Up @@ -431,21 +434,50 @@ class BaseStateManager {
* Returns a collection of all the states created from the schema name.
*
* @param {string} schemaName - Name of the schema.
* @param {array|null} [filter=null] - Array of parameter names that are of interest
* for every state of the collection. No filter is apllied if set to `null` (default).
* @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.
* @returns {server.SharedStateCollection|client.SharedStateCollection}
*/
async getCollection(schemaName, options) {
const collection = new SharedStateCollection(this, schemaName, options);

try {
await collection._init();
} catch (err) {
console.log(err.message);
throw new Error(`[stateManager] Cannot create collection, schema "${schemaName}" does not exists`);
async getCollection(schemaName, filterOrOptions = null, options = {}) {
if (!isString(schemaName)) {
throw new TypeError(`[stateManager] Cannot execute 'getCollection' on 'StateManager': 'schemaName' should be a string"`);
}

let filter;

if (arguments.length === 2) {
if (filterOrOptions === null) {
filter = null;
options = null;
} else if (Array.isArray(filterOrOptions)) {
filter = filterOrOptions;
options = {};
} else if (typeof filterOrOptions === 'object') {
filter = null;
options = filterOrOptions;
} else {
throw new TypeError(`[stateManager] Cannot execute 'getCollection' on 'StateManager': argument 2 should be either null, an array or an object"`);
}
}

if (arguments.length === 3) {
filter = filterOrOptions;

if (filter !== null && !Array.isArray(filter)) {
throw new TypeError(`[stateManager] Cannot execute 'getCollection' on 'StateManager': 'filter' should be either an array or null"`);
}

if (options === null || typeof options !== 'object') {
throw new TypeError(`[stateManager] Cannot execute 'getCollection' on 'StateManager': 'options' should be an object"`);
}
}

const collection = new SharedStateCollection(this, schemaName, filter, options);
await collection._init();

return collection;
}
}
Expand Down
25 changes: 25 additions & 0 deletions tests/states/SharedState.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -566,11 +566,36 @@ describe('# SharedState - filtered attached state', () => {
it(`should support attach(schemaName, filter)`, async () => {
const owned = await server.stateManager.create('filtered');
const attached = await client.stateManager.attach('filtered', ['bool', 'string']);

assert.equal(attached.id, owned.id);
});

it(`should support attach(schemaName, stateId, filter)`, async () => {
const owned = await server.stateManager.create('filtered');
const attached = await client.stateManager.attach('filtered', owned.id, ['bool', 'string']);

assert.equal(attached.id, owned.id);
});

it(`should support explicit default values, attach(schemaName, null)`, async () => {
const owned = await server.stateManager.create('filtered');
const attached = await client.stateManager.attach('filtered', null);

assert.equal(attached.id, owned.id);
});

it(`should support attach(schemaName, stateId, null)`, async () => {
const owned = await server.stateManager.create('filtered');
const attached = await client.stateManager.attach('filtered', owned.id, null);

assert.equal(attached.id, owned.id);
});

it(`should support explicit default values, attach(schemaName, null, null)`, async () => {
const owned = await server.stateManager.create('filtered');
const attached = await client.stateManager.attach('filtered', null, null);

assert.equal(attached.id, owned.id);
});

it(`should throw if filter contains invalid keys`, async () => {
Expand Down
Loading

0 comments on commit f326230

Please sign in to comment.