silphius/app/util/script.js
2024-09-14 17:30:42 -05:00

183 lines
3.7 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 = 0) => (
new PromiseUtil.Ticker(
(resolve) => {
if (0 === seconds) {
resolve();
}
},
(elapsed, resolve) => {
seconds -= elapsed;
if (seconds <= 0) {
resolve();
}
}
)
),
};
}
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);
},
);
}
}