Home Reference Source

packages/oo7/src/transformBond.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');
const ReactiveBond = require('./reactiveBond');

let defaultContext = typeof (global.parity) === 'undefined' ? null : global.parity.api;

/* Determines whether a `value` is not a {@link Bond} or
 * a {@link Promise}, nor a possibly recursive structure that contains such
 * a thing up to a depth `depthLeft` into it.
 */
function isPlain (value, depthLeft) {
	if (typeof (value) !== 'object' || value === null) {
		return true;
	}

	if (Bond.instanceOf(value)) {
		return false;
	}

	if (value instanceof Promise) {
		return false;
	}

	if (depthLeft > 0 && value.constructor === Array) {
		return value.every(index => isPlain(index, depthLeft - 1));
	}

	if (depthLeft > 0 && value.constructor === Object) {
		return Object.keys(value).every(key =>
			isPlain(value[key], depthLeft - 1)
		);
	}

	return true;
}

/**
 * @summary Configurable {@link Bond}-derivation representing a functional transformation
 * of a number of other items.
 * @description This is the underlying class which powers the {@link Bond#map} and {@link Bond#mapAll}
 * functions; you'll generally want to use those unless there is some particular
 * aspect of this class's configurability that you need.
 *
 * It is constructed with a transform function and a number of args; this
 * {@link Bond} represents the result of the function when applied to those arguemnts'
 * representative values. `Bond`s and `Promises`, are resolved automatically at
 * a configurable depth within complex structures, both as input items and
 * the value resulting from the transform function.
 */
class TransformBond extends ReactiveBond {
	/**
	 * Constructs a new object.
	 *
	 * @param {function} transform - The transformation function. It is called with
	 * values corresponding (in order) to the items of `args`. It may return a
	 * {@link Bond}, {Promise} or plain value resolving to representative values.
	 * @param {array} args - A list of items whose representative values should be
	 * passed to `transform`.
	 * @defaultValue [].
	 * @param {array} dependencies - A list of {@link Bond}s on which `transform` indirectly
	 * depends.
	 * @defaultValue [].
	 * @param {number} outResolveDepth - The depth in any returned structure
	 * that a {@link Bond} may be for it to be resolved.
	 * @defaultValue 0.
	 * @param {number} resolveDepth - The depth in a structure (array or object)
	 * that a {@link Bond} may be in any of `args`'s items for it to be resolved
	 * (in place) to its representative value. Beyond this depth, {@link Bond}s amd
	 * {Promise}s will be left alone.
	 * @defaultValue 1.
	 * @param {number} latched - If `false`, this object becomes _not ready_ as
	 * long as there is an output value waiting for resolution.
	 * @defaultValue `true`
	 * @param {boolean} mayBeNull - If `false`, a resultant value of `null` from
	 * `transform` causes this {@link Bond} to become _not ready_. Optional.
	 * @defaultValue `true`
	 * @param {object} context - The context (i.e. `this` object) that `transform`
	 * is bound to. Optional; defaults to the value set by {@link setDefaultTransformBondContext}.
	 * @defaultValue `null`
	 */
	constructor (
		transform,
		args = [],
		dependencies = [],
		outResolveDepth = 3,
		resolveDepth = 3,
		cache = { id: null, stringify: JSON.stringify, parse: JSON.parse },
		latched = false,
		mayBeNull = true,
		context = defaultContext
	) {
		super(args, dependencies, function (resolvedArguments) {
			//			console.log(`Applying: ${JSON.stringify(args)}`);
			// Cancel any previous result-resolving.
			this.dropOut();

			// Apply transform to the resolved argument values.
			let result = transform.apply(context, resolvedArguments);

			// Assue an undefined result means "reset".
			if (typeof (result) === 'undefined') {
				console.warn(`Transformation returned undefined: Applied ${transform} to ${JSON.stringify(resolvedArguments)}.`);
				this.reset();
			} else if (result instanceof Promise) {
				// If we're not latching, we reset while we resolve the
				// resultant promise.
				if (!latched) {
					this.reset();
				}
				// Then resolve the Promise; by calling `changed`, we recurse
				// as necessary.
				result.then(this.changed.bind(this));
			} else if (!isPlain(result, outResolveDepth)) {
				//				console.log(`Using ReactiveBond to resolve and trigger non-plain result (at depth ${outResolveDepth})`);
				// If we're not latching, we reset while we resolve the
				// resultant Bond(s)/Promise(s).
				if (!latched) {
					this.reset();
				}
				// Then create a new `Bond` which we own to maintain the
				// resultant complex resolvable structure.
				this.useOut(new ReactiveBond([result], [], ([resolvedResult]) => {
					//					console.log(`Resolved results: ${JSON.stringify(v)}. Triggering...`);
					// Call `changed` to recurse as neccessary.
					this.changed.bind(this)(resolvedResult);
				}, false, outResolveDepth));
			} else {
				// Nothing special here - just call changed with the result.
				this.changed(result);
			}
		}, mayBeNull, resolveDepth, cache);

		// the current Bond used to resolve the result (output) value if the
		// result of our transform is itself a Bond.
		this._outBond = null;
	}

	// Register `newOutBond` as our result-resolving bond. Ensures it knows
	// we depend on it via `use`.
	useOut (newOutBond) {
		this._outBond = newOutBond.use();
	}

	// Unregister our current result-resolving bond. Ensures it knows
	// we no longer depend on it via `drop`.
	dropOut () {
		if (this._outBond !== null) {
			this._outBond.drop();
		}
		this._outBond = null;
	}

	// If nobody depends on us (anymore), then drop our result-resolving Bond.
	finalise () {
		this.dropOut();
		ReactiveBond.prototype.finalise.call(this);
	}

	/**
	 * Set the default context under which {@link Bond} transformations run.
	 *
	 * @see {@link Bond#map} {@link Bond#mapAll} {@link TransformBond}
	 */
	static setDefaultContext (c) {
		defaultContext = c;
	}
}

module.exports = TransformBond;