234 lines
6.1 KiB
JavaScript
234 lines
6.1 KiB
JavaScript
import {arrayUnique, flatten, mapObject} from '@avocado/core';
|
|
import D from 'debug';
|
|
import {invokeHookFlat} from 'scwp';
|
|
|
|
import {fastApply, objectFromEntries} from '@avocado/core';
|
|
|
|
const debug = D('@avocado:behavior:context');
|
|
|
|
export class Context {
|
|
|
|
constructor(defaults = {}) {
|
|
this.typeMap = new Map();
|
|
this.variableMap = new Map();
|
|
this.initialize();
|
|
this.addObjectMap(defaults);
|
|
}
|
|
|
|
static allTypesDigraph() {
|
|
return Object.keys(this.types())
|
|
.reduce((r, type) => ({...r, [type]: this.typeDigraph(type)}), {});
|
|
}
|
|
|
|
static allTypesInvertedDigraph() {
|
|
const digraph = this.allTypesDigraph();
|
|
let inverted = {};
|
|
const fromTypes = Object.keys(digraph);
|
|
for (let i = 0; i < fromTypes.length; i++) {
|
|
const fromType = fromTypes[i];
|
|
const toTypes = digraph[fromType];
|
|
for (let j = 0; j < toTypes.length; j++) {
|
|
const toType = toTypes[j];
|
|
inverted[toType] = (inverted[toType] || {});
|
|
inverted[toType][fromType] = true;
|
|
}
|
|
}
|
|
inverted = mapObject(inverted, (types) => Object.keys(types));
|
|
inverted = mapObject(
|
|
inverted,
|
|
(types) => arrayUnique(flatten(types.map((type) => (inverted[type] || []).concat(type)))),
|
|
);
|
|
return inverted;
|
|
}
|
|
|
|
static describe(description, fn) {
|
|
const {type} = description;
|
|
fn.type = type;
|
|
return fn;
|
|
}
|
|
|
|
static globals() {
|
|
return invokeHookFlat('behaviorContextGlobals')
|
|
.reduce((r, results) => ({...r, ...results}), {});
|
|
}
|
|
|
|
static typeDigraph(type, marked = {}) {
|
|
const types = this.types();
|
|
if (marked[type] || !types[type]) {
|
|
marked[type] = true;
|
|
return [];
|
|
}
|
|
const description = this.typeDescription(type, undefined);
|
|
const subtypes = Object.keys(
|
|
Object.values(description)
|
|
.reduce((r, spec) => ({...r, [spec.type]: true}), {})
|
|
);
|
|
subtypes.forEach((type) => marked[type] = true);
|
|
return arrayUnique(
|
|
flatten(
|
|
subtypes.map(
|
|
(type) => this.typeDigraph(type, marked),
|
|
),
|
|
).concat(subtypes),
|
|
);
|
|
}
|
|
|
|
static typeDescription(type, variable) {
|
|
const types = this.types();
|
|
if (!types[type]) {
|
|
return {};
|
|
}
|
|
if ('function' === typeof types[type]) {
|
|
return types[type](variable);
|
|
}
|
|
return types[type];
|
|
}
|
|
|
|
static types() {
|
|
return invokeHookFlat('behaviorContextTypes')
|
|
.reduce((r, results) => ({...r, ...results}), {});
|
|
}
|
|
|
|
add(key, value, type) {
|
|
this.typeMap.set(key, type);
|
|
this.variableMap.set(key, value);
|
|
}
|
|
|
|
addObjectMap(map) {
|
|
Object.entries(map).forEach(([key, [variable, type]]) => this.add(key, variable, type));
|
|
}
|
|
|
|
all() {
|
|
return Array.from(this.variableMap.keys())
|
|
.reduce((r, key) => ({...r, [key]: this.get(key)}), {});
|
|
}
|
|
|
|
initialize() {
|
|
this.typeMap.clear()
|
|
this.variableMap.clear()
|
|
this.add('context', this, 'context');
|
|
this.addObjectMap(this.constructor.globals());
|
|
}
|
|
|
|
clear() {
|
|
this.initialize();
|
|
}
|
|
|
|
destroy() {
|
|
this.typeMap.clear()
|
|
this.variableMap.clear();
|
|
}
|
|
|
|
get(key) {
|
|
const type = this.typeMap.has(key) ? this.typeMap.get(key) : 'undefined';
|
|
return [
|
|
this.variableMap.get(key),
|
|
type,
|
|
];
|
|
}
|
|
|
|
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) {
|
|
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':
|
|
if ('object' === typeof node) {
|
|
return node[step.key] = value.get(this);
|
|
}
|
|
case 'invoke':
|
|
const rendered = this.constructor.renderStepsUntilNow(steps, step);
|
|
debug(`invalid assignment to function "${rendered}"`);
|
|
return;
|
|
}
|
|
});
|
|
}
|
|
|
|
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.variableMap.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);
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|