packages/oo7/src/reactiveBond.js
// (C) Copyright 2016-2017 Parity Technologies (UK) Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
const Bond = require('./bond');
/* Determines whether a `resolvable` value is actually resolved.
* If true, then `resolvable` is not an unready {@link Bond} or
* a {@link Promise}, nor is a possibly recursive structure that contains such
* a thing up to a depth `depthLeft` into it.
*/
function isReady (resolvable, depthLeft) {
if (typeof (resolvable) === 'object' && resolvable !== null) {
if (Bond.instanceOf(resolvable)) { return resolvable._ready; } else if (resolvable instanceof Promise) { return typeof (resolvable._value) !== 'undefined'; } else if (depthLeft > 0 && resolvable.constructor === Array) { return resolvable.every(index => isReady(index, depthLeft - 1)); } else if (depthLeft > 0 && resolvable.constructor === Object) {
return Object.keys(resolvable).every(key =>
isReady(resolvable[key], depthLeft - 1)
);
} else { return true; }
} else { return true; }
}
/* Determines whether a `value` is an array which has at least one item which is
* either a {@link Bond} or a {@link Promise}, or, if `depthLeft` is greater
* than 1, another array or object. Returns `false` if `depthLeft` is zero.
*/
function isArrayWithNonPlainItems (array, depthLeft) {
return depthLeft > 0 &&
array.constructor === Array &&
(
(depthLeft === 1 && array.findIndex(item =>
Bond.instanceOf(item) ||
item instanceof Promise
) !== -1) ||
(depthLeft > 1 && array.findIndex(item =>
Bond.instanceOf(item) ||
item instanceof Promise ||
item instanceof Array ||
item instanceof Object
) !== -1)
);
}
/* Determines whether a `value` is an object which has at least one item which is
* either a {@link Bond} or a {@link Promise}, or, if `depthLeft` is greater
* than 1, another array or object. Returns `false` if `depthLeft` is zero.
*/
function isObjectWithNonPlainItems (object, depthLeft) {
return depthLeft > 0 &&
object.constructor === Object &&
(
(depthLeft === 1 && Object.keys(object).findIndex(item =>
Bond.instanceOf(object[item]) ||
object[item] instanceof Promise
) !== -1) ||
(depthLeft > 1 && Object.keys(object).findIndex(item =>
Bond.instanceOf(object[item]) ||
object[item] instanceof Promise ||
object[item] instanceof Array ||
object[item] instanceof Object
) !== -1)
);
}
/* Returns the value represented by `resolvable`, resolving Bonds and
* Promises as necessary up to a depth of `depthLeft`.
*/
function resolved (resolvable, depthLeft) {
/* if (!isReady(resolvable, depthLeft)) {
throw `Internal error: Unready value being resolved`;
} */
// console.log(`resolvable info: ${resolvable} ${typeof(resolvable)} ${resolvable.constructor.name} ${JSON.stringify(resolvable)}; depthLeft: ${depthLeft}`);
if (typeof (resolvable) === 'object' && resolvable !== null) {
if (Bond.instanceOf(resolvable)) {
if (resolvable._ready !== true) {
throw new Error(`Internal error: Unready Bond being resolved`);
}
if (typeof (resolvable._value) === 'undefined') {
throw new Error(`Internal error: Ready Bond with undefined value in resolved`);
}
// console.log(`Bond: ${JSON.stringify(resolvable._value)}}`);
return resolvable._value;
} else if (resolvable instanceof Promise) {
if (typeof (resolvable._value) === 'undefined') {
throw new Error(`Internal error: Ready Promise has undefined value`);
}
// console.log(`Promise: ${JSON.stringify(resolvable._value)}}`);
return resolvable._value;
} else if (isArrayWithNonPlainItems(resolvable, depthLeft)) {
// console.log(`Deep array...`);
return resolvable.slice().map(item =>
resolved(item, depthLeft - 1)
);
} else if (isObjectWithNonPlainItems(resolvable, depthLeft)) {
var result = {};
// console.log(`Deep object...`);
Object.keys(resolvable).forEach(key => {
result[key] = resolved(resolvable[key], depthLeft - 1);
});
// console.log(`...Deep object: ${JSON.stringify(o)}`);
return result;
} else {
// console.log(`Shallow object.`);
return resolvable;
}
} else {
// console.log(`Basic value.`);
return resolvable;
}
}
/* Recurses up to `depthLeft` levels into the possibly deep structure
* `resolvable`, placing a notify callback `callback` onto any `Bond`s found
* and a then callback `callback` onto any `Promise`s found.
* All resultant identifiers for the `notify` callbacks are added to `notifyKeys`s in
* depth-first order of traveral of the possible deep structure `resolvable`.
*
* Returns `true` if there were any `Bond`s or `Promise`s encountered.
*/
function deepNotify (resolvable, callback, notifyKeys, depthLeft) {
// console.log(`Setitng up deep notification on object: ${JSON.stringify(resolvable)} - ${typeof(resolvable)}/${resolvable === null}/${resolvable.constructor.name} (depthLeft: ${depthLeft})`);
if (typeof (resolvable) === 'object' && resolvable !== null) {
if (Bond.instanceOf(resolvable)) {
notifyKeys.push(resolvable.notify(callback));
return true;
} else if (resolvable instanceof Promise) {
resolvable.then(resolved => {
resolvable._value = resolved;
callback();
});
return true;
} else if (isArrayWithNonPlainItems(resolvable, depthLeft)) {
let result = false;
resolvable.forEach(item => {
result = deepNotify(item, callback, notifyKeys, depthLeft - 1) || result;
});
return result;
} else if (isObjectWithNonPlainItems(resolvable, depthLeft)) {
let result = false;
Object.keys(resolvable).forEach(key => {
result = deepNotify(resolvable[key], callback, notifyKeys, depthLeft - 1) || result;
});
return result;
} else {
return false;
}
} else {
return false;
}
}
/* Recurses up to `depthLeft` levels into the possibly deep structure
* `resolvable`, placing an unnotify call onto any `Bond`s found, using
* `notifyKeys` as the depth-first sequence of notify key identifiers.
*/
function deepUnnotify (resolvable, notifyKeys, depthLeft) {
if (typeof (resolvable) === 'object' && resolvable !== null) {
if (Bond.instanceOf(resolvable)) {
resolvable.unnotify(notifyKeys.shift());
return true;
} else if (isArrayWithNonPlainItems(resolvable, depthLeft)) {
let result = false;
resolvable.forEach(item => {
result = deepUnnotify(item, notifyKeys, depthLeft - 1) || result;
});
return result;
} else if (isObjectWithNonPlainItems(resolvable, depthLeft)) {
let result = false;
Object.keys(resolvable).forEach(key => {
result = deepUnnotify(resolvable[key], notifyKeys, depthLeft - 1) || result;
});
return result;
} else {
return false;
}
} else {
return false;
}
}
/**
* @summary A {@link Bond} which retains dependencies on other {@link Bond}s.
* @description This inherits from the {@link Bond} class, providing its full API,
* but also allows for dependencies to other `Bond`s to be registered. When
* any dependency changes value (or _readiness_), a callback is executed and
* is passed the new set of underlying values corresponding to each dependency.
*
* The callback is made if and only if this object is in use (i.e. {@link Bond#use}
* or one of its dependents has been called).
*/
class ReactiveBond extends Bond {
/**
* Constructs a new object.
*
* @param {array} args - Each item that this object's representative value
* is dependent upon, and which needs to be used by the callback function
* (presumably to determine that value to be passed into {@link Bond#changed}).
* @param {array} dependencies - {@link Bond}s or {Promise}s that the representative
* value is dependent on, but which are not needed for passing into the
* callback.
* @param {function} execute - The callback function which is called when
* any item of `args` or `deps` changes its underlying value. A value corresponding
* to each item in `args` are passed to the callback:
* items that are {@link Bond}s are resolved to the value they represent before
* being passed into the callback `execute` function. {Promise} objects are
* likewise resolved for their underlying value. Structures such as arrays
* and objects are traversed recursively and likewise interpreted. Other
* types are passed straight through.
* The callback is only made when all items of `args` are considered _ready_.
* @param {boolean} mayBeNull - Noramlly, `null` is a valid value for dependent `Bond`s
* and `Promise`s to represent. Pass `false` here to disallow `null` to be
* considered valid (and thus any `null` dependencies in `args` will mean that
* dependency is considered not _ready_ and no callback will happen).
* @defaultValue true
* @param {number} resolveDepth - The maximum number of times to recurse into
* arrays or objects of `args` items in searching for {@link Bond}s or {Promise}s
* to resolve.
* @defaultValue 1
*/
constructor (
args,
dependencies,
execute,
mayBeNull = true,
resolveDepth = 3,
cache = { id: null, stringify: JSON.stringify, parse: JSON.parse }
) {
super(mayBeNull, cache);
this._execute = (execute ? typeof execute === 'object' ? execute.ready : execute : this.changed).bind(this);
this._executeReset = (execute && typeof execute === 'object' ? execute.reset : this.reset).bind(this);
this._active = false;
this._dependencies = dependencies.slice();
this._args = args.slice();
this._resolveDepth = resolveDepth;
}
_notified () {
// console.log(`Polling ReactiveBond with resolveDepth ${resolveDepth}`);
if (this._args.every(item => isReady(item, this._resolveDepth))) {
// console.log(`poll: All dependencies good...`, a, resolveDepth);
let resolvedArgs = this._args.map(argument =>
resolved(argument, this._resolveDepth)
);
// console.log(`poll: Mapped dependencies:`, am);
this._execute(resolvedArgs);
} else {
// console.log("poll: One or more dependencies undefined");
this._executeReset();
}
}
// TODO: implement isDone.
initialise () {
// console.log(`Initialising ReactiveBond for resolveDepth ${this.resolveDepth}`);
this._notifyKeys = [];
this._dependencies.forEach(dependency =>
this._notifyKeys.push(dependency.notify(this._notified.bind(this)))
);
// true if any of our args are/contain Bonds/Promises.
var active = false;
this._args.forEach(argument => {
if (deepNotify(
argument,
this._notified.bind(this),
this._notifyKeys,
this._resolveDepth
)) {
active = true;
}
});
// no active args, no dependencies - nothing will happen. make the
// _notified call now.
if (!active && this._dependencies.length === 0) {
this._notified();
}
}
finalise () {
// console.log(`Finalising ReactiveBond with resolveDepth ${this.resolveDepth}`);
this._dependencies.forEach(dependency =>
dependency.unnotify(this._notifyKeys.shift())
);
this._args.forEach(argument =>
deepUnnotify(argument, this._notifyKeys, this._resolveDepth)
);
}
}
module.exports = ReactiveBond;