Socket
Socket
Sign inDemoInstall

@soundworks/core

Package Overview
Dependencies
Maintainers
1
Versions
64
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@soundworks/core - npm Package Compare versions

Comparing version 3.0.4 to 3.1.0-beta.0

common/ParameterBag.js

3

common/shared-state-utils.js

@@ -38,4 +38,3 @@ "use strict";

exports.DETACH_RESPONSE = DETACH_RESPONSE;
const DETACH_ERROR = 's:dt:err'; // usefull
const DETACH_ERROR = 's:dt:err';
exports.DETACH_ERROR = DETACH_ERROR;

@@ -42,0 +41,0 @@ const OBSERVE_REQUEST = 's:o:req';

@@ -8,6 +8,4 @@ "use strict";

var _parameters = _interopRequireDefault(require("@ircam/parameters"));
var _ParameterBag = _interopRequireDefault(require("./ParameterBag.js"));
var _lodash = _interopRequireDefault(require("lodash.clonedeep"));
var _sharedStateUtils = require("./shared-state-utils.js");

@@ -31,8 +29,7 @@

class SharedState {
constructor(id, remoteId, schemaName, schema, client, isCreator, manager, initValues = {}) {
constructor(id, remoteId, schemaName, schema, client, isOwner, manager, initValues = {}) {
this.id = id;
this.remoteId = remoteId;
this.schemaName = schemaName;
this._schema = (0, _lodash.default)(schema);
this._isCreator = isCreator; // may be the server or any client
this._isOwner = isOwner; // may be the server or any client

@@ -43,3 +40,3 @@ this._client = client;

try {
this._parameters = (0, _parameters.default)(schema, initValues);
this._parameters = new _ParameterBag.default(schema, initValues);
} catch (err) {

@@ -55,4 +52,4 @@ console.error(err.stack);

client.transport.addListener(`${_sharedStateUtils.UPDATE_RESPONSE}-${id}-${this.remoteId}`, (reqId, updates) => {
const updated = this._commit(updates);
client.transport.addListener(`${_sharedStateUtils.UPDATE_RESPONSE}-${id}-${this.remoteId}`, (reqId, updates, context) => {
const updated = this._commit(updates, context, true, true);

@@ -62,10 +59,10 @@ (0, _sharedStateUtils.resolveRequest)(reqId, updated);

client.transport.addListener(`${_sharedStateUtils.UPDATE_ABORT}-${id}-${this.remoteId}`, (reqId, updates) => {
const updated = this._commit(updates, false);
client.transport.addListener(`${_sharedStateUtils.UPDATE_ABORT}-${id}-${this.remoteId}`, (reqId, updates, context) => {
const updated = this._commit(updates, context, false, true);
(0, _sharedStateUtils.resolveRequest)(reqId, updated);
});
client.transport.addListener(`${_sharedStateUtils.UPDATE_NOTIFICATION}-${id}-${this.remoteId}`, updates => {
client.transport.addListener(`${_sharedStateUtils.UPDATE_NOTIFICATION}-${id}-${this.remoteId}`, (updates, context) => {
// cf. https://github.com/collective-soundworks/soundworks/issues/18
this._commit(updates);
this._commit(updates, context, true, false);
}); // ---------------------------------------------

@@ -87,3 +84,3 @@ // DELETE initiated by creator, or schema deleted

if (this._isCreator) {
if (this._isOwner) {
// ---------------------------------------------

@@ -133,8 +130,8 @@ // DELETE (can only delete if creator)

this._onDeleteCallbacks.clear(); // Monkey patch detach so it throws we called twice. Doing nothing blocks
// the process on a second `detach` call as the Promise was never resolved
this._onDeleteCallbacks.clear(); // Monkey patch detach so it throws if called twice. Doing nothing blocks
// the process on a second `detach` call as the Promise never resolves
this.detach = () => {
throw new Error(`State "${this.schemaName} (${this.id})" already detached, cannot detach twice`);
throw new Error(`[stateManager] State "${this.schemaName} (${this.id})" already detached, cannot detach twice`);
};

@@ -158,40 +155,104 @@ }

_commit(obj, propagate = true) {
const updated = {};
_commit(updates, context, propagate = true, initiator = false) {
const newValues = {};
const oldValues = {};
for (let name in obj) {
updated[name] = this._parameters.set(name, obj[name]);
for (let name in updates) {
const {
immediate,
filterChange,
event
} = this._parameters.getSchema(name);
const oldValue = this._parameters.get(name);
const [newValue, changed] = this._parameters.set(name, updates[name]); // handle immediate stuff
if (initiator && immediate) {
// @note - we don't need to check filterChange here because the value
// has been updated in parameters on the `set` side so can rely on `changed`
// to avoid retrigger listeners.
// If the value has overriden by the server, `changed` will true
// anyway so it should behave correctly.
if (!changed || event) {
continue;
}
}
newValues[name] = newValue;
oldValues[name] = oldValue;
} // if the `UPDATE_REQUEST` as been aborted by the server, do not propagate
if (propagate) {
this._subscriptions.forEach(listener => listener(updated));
if (propagate && Object.keys(newValues).length > 0) {
this._subscriptions.forEach(listener => listener(newValues, oldValues, context));
}
return updated;
return newValues;
}
/**
* Get the schema that describes the state.
*
* @return {Object}
*/
getSchema() {
return this._schema;
}
/**
* Updates values of the state.
*
* @async
* @param {Object} updates - key / value pairs of updates to apply to the state
* @return {Object}
* @param {Object} updates - key / value pairs of updates to apply to the state.
* @param {Mixed} [context=null] - optionnal context that will be propagated
* alongside the updates of the state. The context is valid only for the
* current call and will be passed as third argument to any subscribe listeners.
* @return {Promise<Object>} A promise to the (coerced) updates.
*
* @see {common.SharedState~subscribeCallback}
*/
async set(updates) {
async set(updates, context = null) {
// handle immediate option
const immediateNewValues = {};
const immediateOldValues = {};
let propagateNow = false;
for (let name in updates) {
// throw early (client-side and not only server-side) if parameter is undefined
if (!this._parameters.has(name)) {
throw new ReferenceError(`[stateManager] Cannot set value of undefined parameter "${name}"`);
} // @note: general idea...
// if immediate=true
// - call listeners if value changed
// - go through normal server path
// - retrigger only if response from server is different from current value
// if immediate=true && (filterChange=false || event=true)
// - call listeners with value regarless it changed
// - go through normal server path
// - if the node is initiator of the update (UPDATE_RESPONSE), (re-)check
// to prevent execute the listeners twice
const {
immediate,
filterChange,
event
} = this._parameters.getSchema(name);
if (immediate) {
const oldValue = this._parameters.get(name);
const [newValue, changed] = this._parameters.set(name, updates[name]);
if (changed || filterChange === false) {
immediateOldValues[name] = oldValue;
immediateNewValues[name] = newValue;
propagateNow = true;
}
}
}
if (propagateNow) {
this._subscriptions.forEach(listener => listener(immediateNewValues, immediateOldValues, context));
} // go through server-side normal behavior
return new Promise((resolve, reject) => {
const reqId = (0, _sharedStateUtils.storeRequestPromise)(resolve, reject);
this._client.transport.emit(`${_sharedStateUtils.UPDATE_REQUEST}-${this.id}-${this.remoteId}`, reqId, updates);
this._client.transport.emit(`${_sharedStateUtils.UPDATE_REQUEST}-${this.id}-${this.remoteId}`, reqId, updates, context);
});

@@ -202,3 +263,3 @@ }

*
* @param {String} name - Name of the param
* @param {String} name - Name of the param. Throws an error if the name is invalid.
* @return {Mixed}

@@ -222,5 +283,53 @@ */

/**
* Get the schema that describes the state.
*
* @param {String} [name=null] - if given, returns only the definition
* of the given param name. Throws an error if the name is invalid.
* @return {Object}
*/
getSchema(name = null) {
return this._parameters.getSchema(name);
}
/**
* Get the values with which the state has been initialized.
*
* @return {Object}
*/
getInitValues() {
return this._parameters.getInitValues();
}
/**
* Get the default values that has been declared in the schema.
*
* @return {Object}
*/
getDefaults() {
return this._parameters.getDefaults();
}
/**
* @callback common.SharedState~subscribeCallback
* @param {Object} updates - key / value pairs of the updates that have been
* applied to the state
* @param {Object} newValues - key / value pairs of the updates that have been
* applied to the state.
* @param {Object} oldValues - key / value pairs of the related params before
* the updates has been applied to the state.
* @param {Mixed} [context=null] - Optionnal context data that has been passed
* with the updates in the `set` call.
*
* @example
* state.subscribe(async (newValues, oldValues[, context=null]) => {
* for (let [key, value] of Object.entries(newValues)) {
* switch (key) {
* // do something
* }
* }
* }
*
* @see {common.SharedState#set}
* @see {common.SharedState#subscribe}
*/

@@ -232,7 +341,10 @@

* @param {common.SharedState~subscribeCallback} callback - callback to execute
* when an update is commited on the state.
* when an update is applied on the state.
*
* @example
* state.subscribe(async (updates) => {
* for (let [key, value] of Object.entries(updates)) {
* // dispatch
* state.subscribe(async (newValues, oldValues) => {
* for (let [key, value] of Object.entries(newValues)) {
* switch (key) {
* // do something
* }
* }

@@ -267,3 +379,3 @@ * }

if (this._isCreator) {
if (this._isOwner) {
return new Promise((resolve, reject) => {

@@ -283,2 +395,24 @@ const reqId = (0, _sharedStateUtils.storeRequestPromise)(resolve, reject);

/**
* Delete the state. Only the creator/owner of the state (i.e. a state created using
* `create`) can use this method. If a non-owner call this method (i.e. a
* state created using `attach`), an error will be thrown.
*
* @async
* @see {common.SharedState#onDetach}
* @see {common.SharedState#onDelete}
* @see {client.SharedStateManagerClient#create}
* @see {server.SharedStateManagerClient#create}
* @see {client.SharedStateManagerClient#attach}
* @see {server.SharedStateManagerClient#attach}
*/
async delete() {
if (this._isOwner) {
return this.detach();
} else {
throw new Error(`[stateManager] can delete state "${this.schemaName}", only owner of the state (i.e. the node that "create[d]" it) can delete it`);
}
}
/**
* Register a function to execute when detaching from the state

@@ -285,0 +419,0 @@ *

@@ -18,3 +18,3 @@ "use strict";

* An instance of `SharedStateManagerClient` is automatically created by the
* `soundworks.Client` (cf. {@link client.Client#stateManager}).
* `soundworks.Client` at initialization (cf. {@link client.Client#stateManager}).
*

@@ -21,0 +21,0 @@ * Tutorial: [https://collective-soundworks.github.io/tutorials/state-manager.html](https://collective-soundworks.github.io/tutorials/state-manager.html)

@@ -10,3 +10,3 @@ "use strict";

var _parameters = _interopRequireDefault(require("@ircam/parameters"));
var _ParameterBag = _interopRequireDefault(require("./ParameterBag.js"));

@@ -30,4 +30,4 @@ var _lodash = _interopRequireDefault(require("lodash.clonedeep"));

*
* An instance of `SharedStateManagerClient` is automatically created by the
* `soundworks.Server` (cf. {@link server.Server#stateManager}).
* An instance of `SharedStateManagerServer` is automatically created by the
* `soundworks.Server` at initialization (cf. {@link server.Server#stateManager}).
*

@@ -54,2 +54,4 @@ * Tutorial: [https://collective-soundworks.github.io/tutorials/state-manager.html](https://collective-soundworks.github.io/tutorials/state-manager.html)

this._observers = new Set();
this._hooksBySchemaName = new Map(); // protected
this.addClient(localClientId, localTransport);

@@ -228,7 +230,25 @@ }

/**
* Register a schema. The schema definition follows the convention described
* here [https://github.com/ircam-jstools/parameters#booleandefinition--object](https://github.com/ircam-jstools/parameters#booleandefinition--object) (@todo - document somewhere else).
* Register a schema from which shared states (cf. {@link common.SharedState})
* can be instanciated.
*
* @param {String} schemaName - Name of the schema.
* @param {Object} schema - Description of the state data structure.
* @param {server.SharedStateManagerServer~schema} schema - Data structure
* describing the states that will be created from this schema.
*
* @see {@link server.SharedStateManagerServer#create}
* @see {@link client.SharedStateManagerClient#create}
*
* @example
* server.stateManager.registerSchema('my-schema', {
* myBoolean: {
* type: 'boolean'
* default: false,
* },
* myFloat: {
* type: 'float'
* default: 0.1,
* min: -1,
* max: 1
* }
* })
*/

@@ -239,18 +259,17 @@

if (this._schemas.has(schemaName)) {
throw new Error(`schema "${schemaName}" already registered`);
} // throw is schema is invalid
throw new Error(`[stateManager.registerSchema] cannot register schema with name: "${schemaName}", schema name already exists`);
}
_ParameterBag.default.validateSchema(schema);
try {
(0, _parameters.default)(schema, {});
} catch (err) {
throw new Error(`Invalid schema "${schemaName}": ${err.message}`);
}
this._schemas.set(schemaName, (0, _lodash.default)(schema)); // create hooks list
this._schemas.set(schemaName, (0, _lodash.default)(schema));
this._hooksBySchemaName.set(schemaName, new Set());
}
/**
* Delete a schema and all associated states.
* When a schema is deleted, all attached clients are detached
* and the `onDetach` and `onDelete` callbacks are called.
* When a schema is deleted, all states created from this schema are deleted
* as well, therefore all attached clients are detached and the `onDetach`
* and `onDelete` callbacks are called on the related states.
*

@@ -274,5 +293,63 @@ * @param {String} schemaName - Name of the schema.

this._schemas.delete(schemaName);
this._schemas.delete(schemaName); // delete registered hooks
this._hooksBySchemaName.delete(schemaName);
}
/**
* @callback server.SharedStateManagerServer~updateHook
*
* @param {Object} updates - Update object as given on a set callback, or
* result of the previous hook
* @param {Object} currentValues - Current values of the state.
* @param {Object} [context=null] - Optionnal context passed by the creator
* of the update.
* @return {Object} The "real" updates to be applied on the state.
*/
/**
* 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
* 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 `subscribe`).
*
* @example
* server.stateManager.registerSchema('hooked', schema);
* server.stateManager.registerUpdateHook('hooked', (updates, currentValues) => {
* return {
* ...updates
* numUpdates: currentValues.numUpdates + 1,
* };
* });
*
* const state = await server.stateManager.create('hooked');
*
* await state.set({ name: 'test' });
* state.getValues();
* // > { name: 'test', numUpdates: 1 };
*
* @param {String} schemaName - Kind of states on which applying the hook.
* @param {server.SharedStateManagerServer~updateHook} updateHook - Function
* called between the `set` call and the actual update.
*/
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`);
}
const hooks = this._hooksBySchemaName.get(schemaName);
hooks.add(updateHook);
return () => hooks.delete(updateHook);
}
}

@@ -279,0 +356,0 @@

@@ -8,6 +8,4 @@ "use strict";

var _parameters = _interopRequireDefault(require("@ircam/parameters"));
var _ParameterBag = _interopRequireDefault(require("./ParameterBag.js"));
var _lodash = _interopRequireDefault(require("lodash.clonedeep"));
var _sharedStateUtils = require("./shared-state-utils");

@@ -26,5 +24,4 @@

this.schemaName = schemaName;
this._schema = (0, _lodash.default)(schema);
this._manager = manager;
this._parameters = (0, _parameters.default)(schema, initValues);
this._parameters = new _ParameterBag.default(schema, initValues);
this._attachedClients = new Map(); // other peers interested in watching / controlling the state

@@ -36,6 +33,6 @@

_attachClient(remoteId, client, isCreator = false) {
_attachClient(remoteId, client, isOwner = false) {
this._attachedClients.set(remoteId, client);
if (isCreator) {
if (isOwner) {
this._creatorRemoteId = remoteId;

@@ -46,25 +43,42 @@ this._creatorId = client.id;

client.transport.addListener(`${_sharedStateUtils.UPDATE_REQUEST}-${this.id}-${remoteId}`, (reqId, updates) => {
const updated = {};
let dirty = false;
client.transport.addListener(`${_sharedStateUtils.UPDATE_REQUEST}-${this.id}-${remoteId}`, async (reqId, updates, context) => {
// apply registered hooks
const hooks = this._manager._hooksBySchemaName.get(this.schemaName);
const values = this._parameters.getValues(); // @note: we may need a proper update queue to avoid race conditions
for (let hook of hooks.values()) {
updates = await hook(updates, values, context);
}
const filteredUpdates = {};
let hasUpdates = false;
for (let name in updates) {
const currentValue = this._parameters.get(name); // get new value this way to store events return values
// from v3.1.0 - the `filteredUpdates` check is made using 'fast-deep-equal'
// cf. https://github.com/epoberezkin/fast-deep-equal
// therefore unchanged objects are not considered changed
// nor propagated anymore.
// until v3.0.4 - we checked the `schema[name].type === 'any'`, to always consider
// objects as dirty, because if the state is attached locally, we
// compare the Object instances instead of their values.
// @note - this should be made more robust but how?
const [newValue, changed] = this._parameters.set(name, updates[name]); // if `filterChange` is set to `false` we don't check if the value
// has been changed or not, it is always propagated to client states
const newValue = this._parameters.set(name, updates[name]); // we check the `schema[name].type === 'any'`, to always consider
// objects as dirty, because if the state is attached locally, we
// compare the Object instances instead of their values.
// @todo - this should be made more robust but how?
const {
filterChange
} = this._parameters.getSchema(name);
if (newValue !== currentValue || this._schema[name].type === 'any') {
updated[name] = newValue;
dirty = true;
if (filterChange && changed || !filterChange) {
filteredUpdates[name] = newValue;
hasUpdates = true;
}
}
if (dirty) {
if (hasUpdates) {
// send response to requester
// client.transport.emit(`${UPDATE_RESPONSE}-${this.id}-${remoteId}`, reqId, updated);
// client.transport.emit(`${UPDATE_RESPONSE}-${this.id}-${remoteId}`, reqId, filteredUpdates);
// @note: we propagate server-side last, because as the server transport

@@ -89,3 +103,3 @@ // is synchronous it can break ordering if a subscription function makes

if (remoteId !== peerRemoteId && peer.id !== -1) {
peer.transport.emit(`${_sharedStateUtils.UPDATE_NOTIFICATION}-${this.id}-${peerRemoteId}`, updated);
peer.transport.emit(`${_sharedStateUtils.UPDATE_NOTIFICATION}-${this.id}-${peerRemoteId}`, filteredUpdates, context);
}

@@ -95,3 +109,3 @@ }

if (client.id !== -1) {
client.transport.emit(`${_sharedStateUtils.UPDATE_RESPONSE}-${this.id}-${remoteId}`, reqId, updated);
client.transport.emit(`${_sharedStateUtils.UPDATE_RESPONSE}-${this.id}-${remoteId}`, reqId, filteredUpdates, context);
}

@@ -102,3 +116,3 @@

if (remoteId !== peerRemoteId && peer.id === -1) {
peer.transport.emit(`${_sharedStateUtils.UPDATE_NOTIFICATION}-${this.id}-${peerRemoteId}`, updated);
peer.transport.emit(`${_sharedStateUtils.UPDATE_NOTIFICATION}-${this.id}-${peerRemoteId}`, filteredUpdates, context);
}

@@ -108,3 +122,3 @@ }

if (client.id === -1) {
client.transport.emit(`${_sharedStateUtils.UPDATE_RESPONSE}-${this.id}-${remoteId}`, reqId, updated);
client.transport.emit(`${_sharedStateUtils.UPDATE_RESPONSE}-${this.id}-${remoteId}`, reqId, filteredUpdates, context);
}

@@ -114,7 +128,7 @@ } else {

// ignore all other attached clients.
client.transport.emit(`${_sharedStateUtils.UPDATE_ABORT}-${this.id}-${remoteId}`, reqId, updates);
client.transport.emit(`${_sharedStateUtils.UPDATE_ABORT}-${this.id}-${remoteId}`, reqId, updates, context);
}
});
if (isCreator) {
if (isOwner) {
// delete only if creator

@@ -121,0 +135,0 @@ client.transport.addListener(`${_sharedStateUtils.DELETE_REQUEST}-${this.id}-${remoteId}`, reqId => {

{
"name": "@soundworks/core",
"version": "3.0.4",
"version": "3.1.0-beta.0",
"description": "full-stack javascript framework for distributed audio visual experiences on the web",

@@ -21,6 +21,6 @@ "authors": [

"clean": "rm -Rf client && rm -Rf server && rm -Rf common",
"docs": "rm -Rf docs && jsdoc -c .jsdoc.json --verbose && cp -R assets docs/",
"doc": "rm -Rf docs && jsdoc -c .jsdoc.json --verbose && cp -R assets docs/",
"prepublishOnly": "npm run build",
"toc": "markdown-toc -i README.md",
"version": "npm run toc && npm run docs && git add docs",
"version": "npm run toc && npm run doc && git add docs",
"build:client": "babel src/client --out-dir client",

@@ -36,4 +36,2 @@ "build:server": "babel src/server --out-dir server",

"dependencies": {
"@ircam/parameters": "^1.2.2",
"braintree-jsdoc-template": "^3.3.0",
"chalk": "^2.4.2",

@@ -43,2 +41,3 @@ "columnify": "^1.5.4",

"debug": "^4.1.1",
"fast-deep-equal": "^3.1.3",
"fast-text-encoding": "^1.0.0",

@@ -61,4 +60,4 @@ "isomorphic-ws": "^4.0.1",

"@babel/core": "^7.4.5",
"@babel/plugin-proposal-export-default-from": "^7.5.2",
"@babel/preset-env": "^7.4.5",
"braintree-jsdoc-template": "^3.3.0",
"chai": "^4.2.0",

@@ -65,0 +64,0 @@ "chokidar": "^3.0.1",

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc