import D from 'debug'; import {fastApply} from '@avocado/core'; import * as MathExt from '@avocado/math'; import * as Flow from './flow'; import * as Timing from './timing'; import {TraversalCompiler} from './traversal-compiler'; import * as Utility from './utility'; const compiled = new Map(); const debug = D('@avocado:behavior:context'); class Context { constructor() { this.map = new Map(); } add(key, value) { this.map.set(key, value); } clear() { this.map.clear(); this.add('context', this); this.add('Flow', Flow); this.add('Math', MathExt); this.add('Timing', Timing); this.add('Utility', Utility); } compile(traversal) { // Compile traversal. if (!compiled.has(traversal.hash)) { const compilation = new TraversalCompiler(traversal); const fn = new Function('context', compilation.emit()); compiled.set(traversal.hash, fn); } return compiled.get(traversal.hash); } destroy() { this.map.clear(); } static renderSteps(steps) { return steps.reduce((rendered, step) => { switch (step.type) { case 'key': if (rendered) { rendered += '.'; } rendered += step.key; break; case 'invoke': rendered += `(${step.args.length > 0 ? '...' : ''})`; break; } return rendered; }, ''); } static renderStepsUntilNow(steps, step) { const stepsUntilNow = steps.slice(0, steps.indexOf(step)); return this.renderSteps(stepsUntilNow); } takeStep(value, fn) { if (value instanceof Promise) { return value.then((value) => { return fn(value); }); } else { return fn(value); } } traverse(traversal) { // Compile? if (traversal.hash) { return this.compile(traversal)(this); } else { return this.traverseRaw(traversal); } } traverseAndDo(steps, fn) { const [first, ...rest] = steps; if ('key' !== first.type) { throw new TypeError(`First step in a traversal must be type "key"`); } let previousNode = null; return rest.reduce((node, step, index) => { return this.takeStep(node, (node) => { const result = fn(node, previousNode, step, index); previousNode = node; return result; }); }, this.map.get(first.key)); } traverseOneStep(steps, previousNode, node, step) { if ('undefined' === typeof node) { const rendered = this.constructor.renderStepsUntilNow(steps, step); debug(`"${rendered}" is traversed through but undefined`); return; } switch (step.type) { case 'key': return node[step.key]; case 'invoke': const args = step.args.map((arg) => arg.get(this)); // Pass the context itself as the last arg. args.push(this); // Any arg promises will be resolved; the arg values will be passed // transparently to the invocation. let hasPromise = false; for (let i = 0; i < args.length; i++) { if (args[i] instanceof Promise) { hasPromise = true; break; } } if (hasPromise) { return Promise.all(args).then((args) => { return fastApply(previousNode, node, args); }); } else { return fastApply(previousNode, node, args); } } } traverseRaw(traversal) { const {steps, value} = traversal; return this.traverseAndDo(steps, (node, previousNode, step, index) => { const isLastStep = index === steps.length - 2; // Traverse if we're not at the final step with a value. if (!isLastStep || !value) { return this.traverseOneStep(steps, previousNode, node, step); } // Try to set the value. switch (step.type) { case 'key': return node[step.key] = value.get(this); case 'invoke': const rendered = this.constructor.renderStepsUntilNow(steps, step); debug(`invalid assignment to function "${rendered}"`); return; } }); } } export function createContext() { const context = new Context(); context.clear(); return context; }