173 lines
3.5 KiB
JavaScript
173 lines
3.5 KiB
JavaScript
import {parse as acornParse} from 'acorn';
|
|
import {LRUCache} from 'lru-cache';
|
|
|
|
import Sandbox from '@/astride/sandbox.js';
|
|
import * as color from '@/util/color.js';
|
|
import delta from '@/util/delta.js';
|
|
import lfo from '@/util/lfo.js';
|
|
import * as MathUtil from '@/util/math.js';
|
|
import * as PromiseUtil from '@/util/promise.js';
|
|
import transition from '@/util/transition.js';
|
|
|
|
function parse(code, options = {}) {
|
|
return acornParse(code, {
|
|
ecmaVersion: 'latest',
|
|
sourceType: 'module',
|
|
...options,
|
|
})
|
|
}
|
|
|
|
const Populated = Symbol.for('sandbox.populated');
|
|
|
|
export const cache = new LRUCache({
|
|
max: 128,
|
|
});
|
|
|
|
export default class Script {
|
|
|
|
constructor(sandbox, code) {
|
|
this.code = code;
|
|
this.sandbox = sandbox;
|
|
this.promise = null;
|
|
}
|
|
|
|
clone() {
|
|
return new this.constructor(this.sandbox.clone(), this.code);
|
|
}
|
|
|
|
get context() {
|
|
return this.sandbox.context;
|
|
}
|
|
|
|
static contextDefaults() {
|
|
return {
|
|
color,
|
|
console,
|
|
delta,
|
|
lfo,
|
|
Math: MathUtil,
|
|
Promise: PromiseUtil,
|
|
transition,
|
|
wait: (seconds) => (
|
|
new Promise((resolve) => {
|
|
setTimeout(resolve, seconds * 1000);
|
|
})
|
|
),
|
|
};
|
|
}
|
|
|
|
static createContext(locals = {}) {
|
|
if (locals[Populated]) {
|
|
return locals;
|
|
}
|
|
return {
|
|
[Populated]: true,
|
|
...this.contextDefaults(),
|
|
...locals,
|
|
};
|
|
}
|
|
|
|
evaluate() {
|
|
this.sandbox.reset();
|
|
const {value} = this.sandbox.run();
|
|
return value;
|
|
}
|
|
|
|
static async fromCode(code, context = {}) {
|
|
if (!cache.has(code)) {
|
|
cache.set(code, this.parse(code));
|
|
}
|
|
return new this(
|
|
new Sandbox(await cache.get(code), this.createContext(context)),
|
|
code,
|
|
);
|
|
}
|
|
|
|
static async parse(code) {
|
|
return parse(
|
|
code,
|
|
{
|
|
allowReturnOutsideFunction: true,
|
|
},
|
|
);
|
|
}
|
|
|
|
reset() {
|
|
this.promise = null;
|
|
this.sandbox.compile();
|
|
}
|
|
|
|
tick(elapsed, resolve, reject) {
|
|
if (this.promise) {
|
|
if (this.promise instanceof PromiseUtil.Ticker) {
|
|
this.promise.tick(elapsed);
|
|
}
|
|
return;
|
|
}
|
|
while (true) {
|
|
this.sandbox.context.elapsed = elapsed;
|
|
let async, done, value;
|
|
try {
|
|
({async, done, value} = this.sandbox.step());
|
|
}
|
|
catch (error) {
|
|
const node = this.sandbox.$$execution.stack.pop();
|
|
console.warn('Script ran into a problem at', this.code.slice(node.start, node.end));
|
|
console.warn(error);
|
|
if (resolve) {
|
|
resolve();
|
|
}
|
|
return;
|
|
}
|
|
if (async || value instanceof Promise) {
|
|
this.promise = value;
|
|
value
|
|
.catch(reject ? reject : () => {})
|
|
.then(() => {
|
|
if (done) {
|
|
if (resolve) {
|
|
resolve();
|
|
}
|
|
}
|
|
})
|
|
.finally(() => {
|
|
this.promise = null;
|
|
});
|
|
break;
|
|
}
|
|
if (done) {
|
|
if (resolve) {
|
|
resolve();
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
ticker() {
|
|
return new PromiseUtil.Ticker(
|
|
() => {},
|
|
(elapsed, resolve, reject) => {
|
|
this.tick(elapsed, resolve, reject);
|
|
},
|
|
);
|
|
}
|
|
|
|
static ticker(code, context = {}) {
|
|
let ticker;
|
|
return new PromiseUtil.Ticker(
|
|
(resolve) => {
|
|
this.fromCode(code, context)
|
|
.then((script) => {
|
|
ticker = script.ticker();
|
|
resolve(ticker);
|
|
})
|
|
},
|
|
(elapsed) => {
|
|
ticker?.tick?.(elapsed);
|
|
},
|
|
);
|
|
}
|
|
|
|
}
|