import D from 'debug'; import {fastApply} from '@avocado/core'; import {Globals} from './globals'; import {TraversalCompiler} from './traversal-compiler'; import {TypeMap} from './types'; const compiled = new Map(); const debug = D('@avocado:behavior:context'); class Context extends Map { add(key, value) { this.set(key, value); } 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); } renderStepsUntilNow(steps, step) { const stepsUntilNow = steps.slice(0, steps.indexOf(step)); return TypeMap.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"`); } return rest.reduce((walk, step, index) => { return this.takeStep(walk, (walk) => { return fn(walk, step, index); }); }, this.get(first.key)); } traverseOneStep(steps, node, step) { if ('undefined' === typeof node) { const rendered = this.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)); return fastApply(null, node, args); } } traverseRaw(traversal) { const {steps, value} = traversal; return this.traverseAndDo(steps, (node, 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, node, step); } // Try to set the value. switch (step.type) { case 'key': return node[step.key] = value.get(this); case 'invoke': const rendered = this.renderStepsUntilNow(steps, step); debug(`invalid assignment to function "${rendered}"`); return; } }); } } export function createContext() { const context = new Context(); context.add('context', context); context.add('global', new Globals()); return context; }