packages/oo7/src/bond.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 BondCache = require('./bondCache');
-
- var subscripted = {};
- // Any names which should never be subscripted.
- const reservedNames = { toJSON: true, toString: true };
-
- function symbolValues (o) {
- return Object.getOwnPropertySymbols(o).map(k => o[k]);
- }
-
- function equivalent (a, b) {
- return JSON.stringify(a) === JSON.stringify(b);
- }
-
- /**
- * An object which tracks a single, potentially variable, value.
- * {@link Bond}s may be updated to new values with {@link Bond#changed} and reset to an indeterminate
- * ("not ready") value with {@link Bond#reset}.
- *
- * {@link Bond}s track their dependents - aspects of the program, including other {@link Bond}s,
- * which reference their current value. Dependents may be added with {@link Bond#use} and
- * removed with {@link Bond#drop}.
- *
- * A {@link Bond} may be tied to a particular function to ensure it is called whenever
- * the value changes. This implies a dependency, and can be registered with {@link Bond#tie} and
- * dropped with {@link Bond#untie}. A function may also be called should the {@link Bond} be reverted
- * to an undefined value; in this case {@link Bond#notify} and {@link Bond#unnotify} should
- * be used.
- *
- * {@link Bond}s can be made to execute a function once their value becomes ready
- * using {@link Bond#then}, which in some sense replicates the same function in the
- * context of a `Promise`. The similar function {@link Bond#done} is also supplied which
- * executes a given function when the {@link Bond} reaches a value which is considered
- * "final", determined by {@link Bond#isDone} being implemented and `true`. Precisely
- * what any given {@link Bond} considers final depends entirely on the subclass of
- * {@link Bond}; for the {@link Bond} class itself, `isDone` always returns `false` and thus
- * {@link Bond#done} is unusable. The value of the {@link Bond}, once _ready_, may
- * be logged to the console with the {@link Bond#log} function.
- *
- * A {@link Bond} can provide a derivative {@link Bond} whose value reflects the "readiness"
- * of the original, using {@link Bond#ready} and conversely {@link Bond#notReady}. This
- * can also be queried normally with {@link Bond#isReady}.
- *
- * One or a number of {@link Bond}s can be converted into a single {Promise} with the
- * {@link Bond#promise} function.
- *
- * `Bonds` can be composed. {@link Bond#map} creates a new {@link Bond} whose value is a
- * transformation. {@link Bond.all} creates a new {@link Bond} which evaluates to the array
- * of values of each of a number of dependent {@link Bond}s. {@link Bond.mapAll} combines
- * both. {@link Bond#reduce} allows a {@link Bond} that evaluates to array to be
- * transformed into some other value recursively.
- *
- * {@link Bond#sub} forms a derivative {@link Bond} as the subscript (square-bracket
- * indexing). {@link Bond#subscriptable} may be used to return a `Proxy` object that
- * allows the {@link Bond} to be subscripted (square-bracket indexed) directly without
- * need of the {@link Bond#sub} function.
- *
- * {@link Bond} is built to be subclassed. When subclassing, three functions are
- * useful to implement. {@link Bond#isDone} may be implemented
- * in order to make {@link Bond#done} be useful. {@link Bond#initialise} is called exactly once
- * when there becomes at least one dependent; {@link Bond#finalise} is called when there
- * are no longer any dependents.
- *
- * _WARNING_: You should not attempt to use the `toString` function with this
- * class. It cannot be meaningfully converted into a string, and to attempt it
- * will give an undefined result.
- */
- class Bond {
- /**
- * Constructs a new {@link Bond} object whose value is _not ready_.
- *
- * @param {boolean} mayBeNull - `true` if this instance's value may ever
- * validly be `null`. If `false`, then setting this object's value to `null`
- * is equivalent to reseting back to being _not ready_.
- */
- constructor (mayBeNull = true, cache = null) {
- // Functions that should execute whenever we resolve to a new, "ready"
- // value. They are passed the new value as a single parameter.
- // Each function is mapped to from a `Symbol`, which can be used to
- // remove it.
- this._subscribers = {};
- // Equivalent to `_subscribers`, except that after executing, the
- // function is removed from this array. No mapping is provided so they
- // cannot be removed except by triggering.
- this._thens = [];
- // Functions that should execute whenever either the resolved value
- // changes, or our readiness changes. No parameters are passed.
- // Each function is mapped to from a `Symbol`, which can be used to
- // remove it.
- this._notifies = {};
-
- // Are we resolved to a value at all. If `false`, we are not yet
- // resolved to a value and `_value` is meaningless.
- this._ready = false;
- // Our currently resolved value, if any.
- this._value = null;
- // Is the value in the middle of having an update triggered?
- this._triggering = null;
-
- // Is it valid to resolve to `null`? By default it is value.
- this._mayBeNull = mayBeNull;
-
- // The reference count of the number of dependents. If zero, then there
- // is no need to go to any effort to track changes. This is used for
- // specialisations where tracking changes requires holding or managing
- // resources.
- // This is never smaller but can be larger than the total number of
- // callbacks registered between `_subscribers`, `_thens` and
- // `_notifies`.
- this._users = 0;
-
- // The Universally Unique ID, a string used to manage caching and
- // inter-tab result sharing.
- this._uuid = cache ? cache.id : null;
- // A method for stringifying this Bond's result when using with the cache.
- this._stringify = cache ? cache.stringify : null;
- // A method for unstringifying this Bond's result when using with the cache.
- this._parse = cache ? cache.parse : null;
- }
-
- toString () {
- // Bonds make little sense as strings, and our subscripting trick (where
- // we are able to use Bonds as keys) only works if we can coerce into a
- // string. We store the reverse lookup (symbol -> Bond) in a global
- // table `subscripted` so that it can be retrieved while interpreting
- // the subscript in the code Proxy code found in `subscriptable`.
- let s = Symbol('Bond');
- subscripted[s] = this;
- return s;
- }
-
- /**
- * Provides a transparently subscriptable version of this object.
- *
- * The object that is returned from this function is a convenience `Proxy`
- * which acts exactly equivalent
- * to the original {@link Bond}, except that any subscripting of fields that are
- * not members of the {@link Bond} object will create a new {@link Bond} that
- * itself evaluates to this {@link Bond}'s value when subscripted with the same
- * field.
- *
- * @example
- * let x = (new Bond).subscriptable();
- * let y = x.foo;
- * y.log(); // nothing yet
- * x.changed({foo: 42, bar: 69}); // logs 42
- *
- * @param {number} depth - The maximum number of levels of subscripting that
- * the returned `Proxy` will support.
- * @returns {Proxy} - `Proxy` object that acts as a subscriptable variation
- * for convenience.
- */
- subscriptable (depth = 1) {
- // No subscripting at all if depth is 0.
- // We will recurse if > 1.
- if (depth === 0) { return this; }
-
- let r = new Proxy(this, {
- // We proxy the get object field:
- get (receiver, name) {
- // Skip the magic proxy and just interpret directly if the field
- // name is a string/number and it's either an extent key in the
- // underlying `Bond` or it's a reserved field name (e.g. toString).
- if (
- (typeof (name) === 'string' || typeof (name) === 'number') &&
- (reservedNames[name] || typeof (receiver[name]) !== 'undefined')
- ) {
- return receiver[name];
- }
-
- // If it's a symbolic key, then it's probably a `Bond` symbolified
- // in our toString function. Look it up in the global Bond symbol
- // table and recurse into one less depth.
- if (typeof (name) === 'symbol') {
- if (Bond._knowSymbol(name)) {
- return receiver
- .sub(Bond._fromSymbol(name))
- .subscriptable(depth - 1);
- } else {
- // console.warn(`Unknown symbol given`);
- return null;
- }
- }
- // console.log(`Subscripting: ${JSON.stringify(name)}`)
- // Otherwise fall back with a simple subscript and recurse
- // back with one less depth.
- return receiver.sub(name).subscriptable(depth - 1);
- }
- });
- return r;
- }
-
- // Check to see if there's a symbolic reference for a Bond.
- static _knowSymbol (name) {
- return !!subscripted[name];
- }
- // Lookup a symbolic Bond reference and remove it from the global table.
- static _fromSymbol (name) {
- let sub = subscripted[name];
- delete subscripted[name];
- return sub;
- }
-
- /**
- * Alters this object so that it is always _ready_.
- *
- * If this object is ever {@link Bond#reset}, then it will be changed to the
- * value given.
- *
- * @example
- * let x = (new Bond).defaultTo(42);
- * x.log(); // 42
- * x.changed(69);
- * x.log(); // 69
- * x.reset();
- * x.log() // 42
- *
- * @param {*} x - The value that this object represents if it would otherwise
- * be _not ready_.
- * @returns {@link Bond} - This (mutated) object.
- */
- defaultTo (_defaultValue) {
- this._defaultTo = _defaultValue;
- if (!this._ready) {
- this.trigger(_defaultValue);
- }
- return this;
- }
-
- /**
- * Resets the state of this Bond into being _not ready_.
- *
- * Any functions that are registered for _notification_ (see {@link Bond#notify})
- * will be called if this {@link Bond} is currently _ready_.
- */
- reset () {
- if (this._defaultTo !== undefined) {
- this.trigger(this._defaultTo);
- return;
- }
- if (this._ready) {
- this._ready = false;
- this._value = null;
- symbolValues(this._notifies).forEach(callback => callback());
- }
- }
- /**
- * Makes the object _ready_ and sets its current value.
- *
- * Any functions that are registered for _notification_ (see {@link Bond#notify})
- * or are _tied_ (see {@link Bond#tie}) will be called if this {@link Bond} is not
- * currently _ready_ or is _ready_ but has a different value.
- *
- * This function is a no-op if the JSON representations of `v` and of the
- * current value, if any, are equal.
- *
- * @param {*} v - The new value that this object should represent. If `undefined`
- * then the function does nothing.
- */
- changed (newValue) {
- if (typeof (newValue) === 'undefined') {
- return;
- }
- // console.log(`maybe changed (${this._value} -> ${v})`);
- if (!this._mayBeNull && newValue === null) {
- this.reset();
- } else if (!this._ready || !equivalent(newValue, this._value)) {
- this.trigger(newValue);
- }
- }
-
- /**
- * Makes the object _ready_ and sets its current value.
- *
- * Any functions that are registered for _notification_ (see {@link Bond#notify})
- * or are _tied_ (see {@link Bond#tie}) will be called if this {@link Bond} is not
- * currently _ready_ or is _ready_ but has a different value.
- *
- * Unlike {@link Bond#changed}, this function doesn't check equivalence
- * between the new value and the current value.
- *
- * @param {*} v - The new value that this object should represent. By default,
- * it will reissue the current value. It is an error to call it without
- * an argument if it is not _ready_.
- */
- trigger (newValue = this._value) {
- // Cannot trigger to an undefined value (just reset it or call with `null`).
- if (typeof (newValue) === 'undefined') {
- console.error(`Trigger called with undefined value`);
- return;
- }
- // Cannot trigger as a recourse to an existing trigger.
- if (this._triggering !== null) {
- console.error(`Trigger cannot be called while already triggering.`, this._triggering.becoming, newValue);
- return;
- }
- this._triggering = { becoming: newValue };
-
- if (!this._mayBeNull && newValue === null) {
- this.reset();
- } else {
- // console.log(`firing (${JSON.stringify(v)})`);
- this._ready = true;
- this._value = newValue;
- symbolValues(this._notifies).forEach(callback => callback());
- symbolValues(this._subscribers).forEach(callback => callback(this._value));
- this._thens.forEach(callback => {
- callback(this._value);
- this.drop();
- });
- this._thens = [];
- }
-
- this._triggering = null;
-
- if (this._uuid && !this._noCache && Bond.cache) {
- Bond.cache.changed(this._uuid, newValue);
- }
- }
-
- /**
- * Register a single dependency for this object.
- *
- * Notes that the object's value is in use, and that it should be computed.
- * {@link Bond} sub-classes are allowed to not work properly unless there is
- * at least one dependency registered.
- *
- * @see {@link Bond#initialise}, {@link Bond#finalise}.
- */
- use () {
- if (this._users === 0) {
- if (!this._uuid || !!this._noCache || !Bond.cache) {
- this.initialise();
- } else {
- Bond.cache.initialise(this._uuid, this, this._stringify, this._parse);
- }
- }
- this._users++;
- return this;
- }
-
- /**
- * Unregister a single dependency for this object.
- *
- * Notes that a previously registered dependency has since expired. Must be
- * called exactly once for each time {@link Bond#use} was called.
- */
- drop () {
- if (this._users === 0) {
- throw new Error(`mismatched use()/drop(): drop() called once more than expected!`);
- }
- this._users--;
- if (this._users === 0) {
- if (!this._uuid || !!this._noCache || !Bond.cache) {
- this.finalise();
- } else {
- Bond.cache.finalise(this._uuid, this);
- }
- }
- }
-
- /**
- * Initialise the object.
- *
- * Will be called at most once before an accompanying {@link Bond#finalise}
- * and should initialise/open/create any resources that are required for the
- * sub-class to maintain its value.
- *
- * @access protected
- */
- initialise () {}
-
- /**
- * Uninitialise the object.
- *
- * Will be called at most once after an accompanying {@link Bond#initialise}
- * and should close/finalise/drop any resources that are required for the
- * sub-class to maintain its value.
- *
- * @access protected
- */
- finalise () {}
-
- /**
- * Returns whether the object is currently in a terminal state.
- *
- * _WARNING_: The output of this function should not change outside of a
- * value change. If it ever changes without the value changing, `trigger`
- * should be called to force an update.
- *
- * @returns {boolean} - `true` when the value should be interpreted as being
- * in a final state.
- *
- * @access protected
- * @see {@link Bond#done}
- */
- isDone () { return false; }
-
- /**
- * Notification callback.
- * @callback Bond~notifyCallback
- */
-
- /**
- * Register a function to be called when the value or the _readiness_
- * changes.
- *
- * Calling this function already implies calling {@link Bond#use} - there
- * is no need to call both.
- *
- * Use this only when you need to be notified should the object be reset to
- * a not _ready_ state. In general you will want to use {@link Bond#tie}
- * instead.
- *
- * @param {Bond~notifyCallback} f - The function to be called. Takes no parameters.
- * @returns {Symbol} An identifier for this registration. Must be provided
- * to {@link Bond#unnotify} when the function no longer needs to be called.
- */
- notify (callback) {
- this.use();
- let id = Symbol('notify::id');
- this._notifies[id] = callback;
- if (this._ready) {
- callback();
- }
- return id;
- }
-
- /**
- * Unregister a function previously registered with {@link Bond#notify}.
- *
- * Calling this function already implies calling {@link Bond#drop} - there
- * is no need to call both.
- *
- * @param {Symbol} id - The identifier returned from the corresponding
- * {@link Bond#notify} call.
- */
- unnotify (id) {
- delete this._notifies[id];
- this.drop();
- }
-
- /**
- * Tie callback.
- * @callback Bond~tieCallback
- * @param {&} value - The current value to which the object just changed.
- * @param {Symbol} id - The identifier of the registration for this callback.
- */
-
- /**
- * Register a function to be called when the value changes.
- *
- * Calling this function already implies calling {@link Bond#use} - there
- * is no need to call both.
- *
- * Unlike {@link Bond#notify}, this does not get
- * called should the object become reset into being not _ready_.
- *
- * @param {Bond~tieCallback} f - The function to be called.
- * @returns {Symbol} - An identifier for this registration. Must be provided
- * to {@link Bond#untie} when the function no longer needs to be called.
- */
- tie (callback) {
- this.use();
- let id = Symbol('tie::id');
- this._subscribers[id] = callback;
- if (this._ready) {
- callback(this._value, id);
- }
- return id;
- }
-
- /**
- * Unregister a function previously registered with {@link Bond#tie}.
- *
- * Calling this function already implies calling {@link Bond#drop} - there
- * is no need to call both.
- *
- * @param {Symbol} id - The identifier returned from the corresponding
- * {@link Bond#tie} call.
- */
- untie (id) {
- delete this._subscribers[id];
- this.drop();
- }
-
- /**
- * Determine if there is a definite value that this object represents at
- * present.
- *
- * @returns {boolean} - `true` if there is presently a value that this object represents.
- */
- isReady () { return this._ready; }
-
- /**
- * Provide a derivative {@link Bond} which represents the same as this object
- * except that before it is ready it evaluates to a given default value and
- * after it becomes ready for the first time it stays fixed to that value
- * indefinitely.
- *
- * @param {Symbol} defaultValue - The value that the new bond should take when
- * this bond is not ready.
- * @returns {@link Bond} - Object representing the value returned by
- * this {@link Bond} except that it evaluates to the given default value when
- * this bond is not ready and sticks to the first value that made it ready.
- */
- latched (defaultValue = undefined, mayBeNull = undefined, cache = null) {
- const LatchBond = require('./latchBond');
-
- return new LatchBond(
- this,
- typeof defaultValue === 'undefined' ? undefined : defaultValue,
- typeof mayBeNull === 'undefined' ? undefined : mayBeNull,
- cache
- );
- }
-
- /**
- * Provide a {@link Bond} which represents the same as this object except that
- * it takes a particular value when this would be unready.
- *
- * @param {Symbol} defaultValue - The value that the new bond should take when
- * this bond is not ready.
- * @returns {@link Bond} - Object representing the value returned by
- * this {@link Bond} except that it evaluates to the given default value when
- * this bond is not ready. The returned object itself is always _ready_.
- */
- default (defaultValue = null) {
- const DefaultBond = require('./defaultBond');
-
- return new DefaultBond(defaultValue, this);
- }
-
- /**
- * Provide a {@link Bond} which represents whether this object itself represents
- * a particular value.
- *
- * @returns {@link Bond} - Object representing the value returned by
- * this {@link Bond}'s {@link Bond#isReady} result. The returned object is
- * itself always _ready_.
- */
- ready () {
- const ReadyBond = require('./readyBond');
-
- if (!this._readyBond) {
- this._readyBond = new ReadyBond(this);
- }
- return this._readyBond;
- }
-
- /**
- * Convenience function for the logical negation of {@link Bond#ready}.
- *
- * @example
- * // These two expressions are exactly equivalent:
- * bond.notReady();
- * bond.ready().map(_ => !_);
- *
- * @returns {@link Bond} Object representing the logical opposite
- * of the value returned by
- * this {@link Bond}'s {@link Bond#isReady} result. The returned object is
- * itself always _ready_.
- */
- notReady () {
- const NotReadyBond = require('./notReadyBond');
-
- if (!this._notReadyBond) {
- this._notReadyBond = new NotReadyBond(this);
- }
- return this._notReadyBond;
- }
-
- /**
- * Then callback.
- * @callback Bond~thenCallback
- * @param {*} value - The current value to which the object just changed.
- */
-
- /**
- * Register a function to be called when this object becomes _ready_.
- *
- * For an object to be considered _ready_, it must represent a definite
- * value. In this case, {@link Bond#isReady} will return `true`.
- *
- * If the object is already _ready_, then `f` will be called immediately. If
- * not, `f` will be deferred until the object assumes a value. `f` will be
- * called at most once.
- *
- * @param {Bond~thenCallback} f The callback to be made once the object is ready.
- *
- * @example
- * let x = new Bond;
- * x.then(console.log);
- * x.changed(42); // 42 is written to the console.
- */
- then (callback) {
- this.use();
- if (this._ready) {
- callback(this._value);
- this.drop();
- } else {
- this._thens.push(callback);
- }
- return this;
- }
-
- /**
- * Register a function to be called when this object becomes _done_.
- *
- * For an object to be considered `done`, it must be _ready_ and the
- * function {@link Bond#isDone} should exist and return `true`.
- *
- * If the object is already _done_, then `f` will be called immediately. If
- * not, `f` will be deferred until the object assumes a value. `f` will be
- * called at most once.
- *
- * @param {Bond~thenCallback} f The callback to be made once the object is ready.
- *
- * @example
- * let x = new Bond;
- * x.then(console.log);
- * x.changed(42); // 42 is written to the console.
- */
- done (callback) {
- if (this.isDone === undefined) {
- throw new Error('Cannot call done() on Bond that has no implementation of isDone.');
- }
- var id;
- let cleanupCallback = newValue => {
- if (this.isDone(newValue)) {
- callback(newValue);
- this.untie(id);
- }
- };
- id = this.tie(cleanupCallback);
- return this;
- }
-
- /**
- * Logs the current value to the console.
- *
- * @returns {@link Bond} The current object.
- */
- log () { this.then(console.log); return this; }
-
- /**
- * Maps the represented value to a string.
- *
- * @returns {@link Bond} A new {link Bond} which represents the `toString`
- * function on whatever value this {@link Bond} represents.
- */
- mapToString () {
- return this.map(_ => _.toString());
- }
-
- /**
- * Make a new {@link Bond} which is the functional transformation of this object.
- *
- * @example
- * let b = new Bond;
- * let t = b.map(_ => _ * 2);
- * t.tie(console.log);
- * b.changed(21); // logs 42
- * b.changed(34.5); // logs 69
- *
- * @example
- * let b = new Bond;
- * let t = b.map(_ => { let r = new Bond; r.changed(_ * 2); return r; });
- * t.tie(console.log);
- * b.changed(21); // logs 42
- * b.changed(34.5); // logs 69
- *
- * @example
- * let b = new Bond;
- * let t = b.map(_ => { let r = new Bond; r.changed(_ * 2); return [r]; }, 1);
- * t.tie(console.log);
- * b.changed(21); // logs [42]
- * b.changed(34.5); // logs [69]
- *
- * @param {function} transform - The transformation to apply to the value represented
- * by this {@link Bond}.
- * @param {number} outResolveDepth - The number of levels deep in any array
- * object values of the result of the transformation that {@link Bond} values
- * will be resolved.
- * @default 3
- * @param {*} cache - Cache information. See constructor.
- * @default null
- * @param {*} latched - Should the value be latched so that once ready it stays ready?
- * @default false
- * @param {*} mayBeNull - Should the value be allowed to be `null` such that if it ever becomes
- * null, it is treated as being unready?
- * @default true
- * @returns {@link Bond} - An object representing this object's value with
- * the function `transform` applied to it.
- */
- map (transform, outResolveDepth = 3, cache = undefined, latched = false, mayBeNull = true) {
- const TransformBond = require('./transformBond');
- return new TransformBond(transform, [this], [], outResolveDepth, 3, cache, latched, mayBeNull);
- }
-
- /**
- * Just like `map`, except that it defaults to no latching and mayBeNull.
- * @param {function} transform - The transformation to apply to the value represented
- * by this {@link Bond}.
- * @param {number} outResolveDepth - The number of levels deep in any array
- * object values of the result of the transformation that {@link Bond} values
- * will be resolved.
- * @default 3
- * @param {*} cache - Cache information. See constructor.
- * @default null
- * @param {*} latched - Should the value be latched so that once ready it stays ready?
- * @default true
- * @param {*} mayBeNull - Should the value be allowed to be `null` such that if it ever becomes
- * null, it is treated as being unready?
- * @default false
- * @returns {@link Bond} - An object representing this object's value with
- * the function `transform` applied to it.
- */
- xform (transform, outResolveDepth = 3, cache = undefined, latched = true, mayBeNull = false) {
- const TransformBond = require('./transformBond');
- return new TransformBond(transform, [this], [], outResolveDepth, 3, cache, latched, mayBeNull);
- }
-
- /**
- * Create a new {@link Bond} which represents this object's array value with
- * its elements transformed by a function.
- *
- * @example
- * let b = new Bond;
- * let t = b.mapEach(_ => _ * 2);
- * t.tie(console.log);
- * b.changed([1, 2, 3]); // logs [2, 4, 6]
- * b.changed([21]); // logs [42]
- *
- * @param {function} transform - The transformation to apply to each element.
- * @returns The new {@link Bond} object representing the element-wise
- * Transformation.
- */
- mapEach (transform, cache = undefined, latched = false, mayBeNull = true) {
- return this.map(item => item.map(transform), 3, cache, latched, mayBeNull);
- }
-
- /**
- * Create a new {@link Bond} which represents this object's value when
- * subscripted.
- *
- * @example
- * let b = new Bond;
- * let t = b.sub('foo');
- * t.tie(console.log);
- * b.changed({foo: 42}); // logs 42
- * b.changed({foo: 69}); // logs 69
- *
- * @example
- * let b = new Bond;
- * let c = new Bond;
- * let t = b.sub(c);
- * t.tie(console.log);
- * b.changed([42, 4, 2]);
- * c.changed(0); // logs 42
- * c.changed(1); // logs 4
- * b.changed([68, 69, 70]); // logs 69
- *
- * @param {string|number} name - The field or index by which to subscript this object's
- * represented value. May itself be a {@link Bond}, in which case, the
- * resolved value is used.
- * @param {number} outResolveDepth - The depth in any returned structure
- * that a {@link Bond} may be for it to be resolved.
- * @returns {@link Bond} - The object representing the value which is the
- * value represented by this object subscripted by the value represented by
- * `name`.
- */
- sub (name, outResolveDepth = 3, cache = undefined, latched = false, mayBeNull = true) {
- const TransformBond = require('./transformBond');
- return new TransformBond(
- (object, field) => object[field],
- [this, name],
- [],
- outResolveDepth,
- 3,
- cache
- );
- }
-
- /**
- * Create a new {@link Bond} which represents the array of many objects'
- * representative values.
- *
- * This object will be _ready_ if and only if all objects in `list` are
- * themselves _ready_.
- *
- * @example
- * let b = new Bond;
- * let c = new Bond;
- * let t = Bond.all([b, c]);
- * t.tie(console.log);
- * b.changed(42);
- * c.changed(69); // logs [42, 69]
- * b.changed(3); // logs [3, 69]
- *
- * @example
- * let b = new Bond;
- * let c = new Bond;
- * let t = Bond.all(['a', {b, c}, 'd'], 2);
- * t.tie(console.log);
- * b.changed(42);
- * c.changed(69); // logs ['a', {b: 42, c: 69}, 'd']
- * b.changed(null); // logs ['a', {b: null, c: 69}, 'd']
- *
- * @param {array} list - An array of {@link Bond} objects, plain values or
- * structures (arrays/objects) which contain either of these.
- * @param {number} resolveDepth - The depth in a structure (array or object)
- * that a {@link Bond} may be in any of `list`'s items for it to be resolved.
- * @returns {@link Bond} - The object representing the value of the array of
- * each object's representative value in `list`.
- */
- static all (list, resolveDepth = 3, cache = undefined, latched = false, mayBeNull = true) {
- const TransformBond = require('./transformBond');
- return new TransformBond((...args) => args, list, [], 3, resolveDepth, cache, latched, mayBeNull);
- }
-
- /**
- * Create a new {@link Bond} which represents a functional transformation of
- * many objects' representative values.
- *
- * @example
- * let b = new Bond;
- * b.changed(23);
- * let c = new Bond;
- * c.changed(3);
- * let multiply = (x, y) => x * y;
- * // These two are exactly equivalent:
- * let bc = Bond.all([b, c]).map(([b, c]) => multiply(b, c));
- * let bc2 = Bond.mapAll([b, c], multiply);
- *
- * @param {array} list - An array of {@link Bond} objects or plain values.
- * @param {function} f - A function which accepts as many parameters are there
- * values in `list` and transforms it into a {@link Bond}, {@link Promise}
- * or other value.
- * @param {number} resolveDepth - The depth in a structure (array or object)
- * that a {@link Bond} may be in any of `list`'s items for it to be resolved.
- * @param {number} outResolveDepth - The depth in any returned structure
- * that a {@link Bond} may be for it to be resolved.
- */
- static mapAll (list, transform, outResolveDepth = 3, resolveDepth = 3, cache = undefined, latched = false, mayBeNull = true) {
- const TransformBond = require('./transformBond');
- return new TransformBond(transform, list, [], outResolveDepth, resolveDepth, cache, latched, mayBeNull);
- }
-
- // Takes a Bond which evaluates to a = [a[0], a[1], ...]
- // Returns Bond which evaluates to:
- // null iff a.length === 0
- // f(i, a[0])[0] iff f(i, a[0])[1] === true
- // fold(f(0, a[0]), a.mid(1)) otherwise
- /**
- * Lazily transforms the contents of this object's value when it is an array.
- *
- * This operates on a {@link Bond} which should represent an array. It
- * transforms this into a value based on a number of elements at the
- * beginning of that array using a recursive _reduce_ algorithm.
- *
- * The reduce algorithm works around an accumulator model. It begins with
- * the `init` value, and incremenetally accumulates
- * elements from the array by changing its value to one returned from the
- * `accum` function, when passed the current accumulator and the next value
- * from the array. The `accum` function may return a {@link Bond}, in which case it
- * will be resolved (using {@link Bond#then}) and that value used.
- *
- * The `accum` function returns a value (or a {@link Bond} which resolves to a value)
- * of an array with exactly two elements; the first is the new value for the
- * accumulator. The second is a boolean _early exit_ flag.
- *
- * Accumulation will continue until either there are no more elements in the
- * array to be processed, or until the _early exit_ flag is true, which ever
- * happens first.
- *
- * @param {function} accum - The reduce's accumulator function.
- * @param {*} init - The initialisation value for the reduce algorithm.
- * @returns {Bond} - A {@link Bond} representing `init` when the input array is empty,
- * otherwise the reduction of that array.
- */
- reduce (accum, init, cache = undefined, latched = false, mayBeNull = true) {
- var nextItem = function (acc, rest) {
- let next = rest.pop();
- return accum(acc, next).map(([result, finished]) =>
- finished
- ? result
- : rest.length > 0
- ? nextItem(result, rest)
- : null
- );
- };
- return this.map(array => array.length > 0 ? nextItem(init, array) : init, 3, cache, latched, mayBeNull);
- }
-
- /**
- * Create a Promise which represents one or more {@link Bond}s.
- *
- * @example
- * let b = new Bond;
- * let p = Bond.promise([b, 42])
- * p.then(console.log);
- * b.changed(69); // logs [69, 42]
- * b.changed(42); // nothing.
- *
- * @param {array} list - A list of values, {Promise}s or {@link Bond}s.
- * @returns {Promise} - A object which resolves to an array of values
- * corresponding to those passed in `list`.
- */
- static promise (list) {
- return new Promise((resolve, reject) => {
- var finished = 0;
- var resolved = [];
- resolved.length = list.length;
-
- let done = (index, value) => {
- // console.log(`done ${i} ${v}`);
- resolved[index] = value;
- finished++;
- // console.log(`finished ${finished}; l.length ${l.length}`);
- if (finished === resolved.length) {
- // console.log(`resolving with ${l}`);
- resolve(resolved);
- }
- };
-
- list.forEach((unresolvedObject, index) => {
- if (Bond.instanceOf(unresolvedObject)) {
- // unresolvedObject is a Bond.
- unresolvedObject.then(value => done(index, value));
- } else if (unresolvedObject instanceof Promise) {
- // unresolvedObject is a Promise.
- unresolvedObject.then(value => done(index, value), reject);
- } else {
- // unresolvedObject is actually just a normal value.
- done(index, unresolvedObject);
- }
- });
- });
- }
-
- /**
- * Duck-typed alternative to `instanceof Bond`, when multiple instantiations
- * of `Bond` may be available.
- */
- static instanceOf (b) {
- return (
- typeof (b) === 'object' &&
- b !== null &&
- typeof (b.reset) === 'function' &&
- typeof (b.changed) === 'function'
- );
- }
- }
-
- Bond.backupStorage = {};
- Bond.cache = new BondCache(Bond.backupStorage);
-
- module.exports = Bond;