silphius/app/util/script.js
2024-07-11 15:43:08 -05:00

154 lines
3.0 KiB
JavaScript

import {parse as acornParse} from 'acorn';
import {LRUCache} from 'lru-cache';
import Sandbox from '@/astride/sandbox.js';
import TickingPromise from '@/util/ticking-promise.js';
import delta from '@/util/delta.js';
import lfo from '@/util/lfo.js';
import * as MathUtil from '@/util/math.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) {
this.sandbox = sandbox;
this.promise = null;
}
clone() {
return new this.constructor(this.sandbox.clone());
}
get context() {
return this.sandbox.context;
}
static contextDefaults() {
return {
Array,
console,
delta,
lfo,
Math: MathUtil,
Promise,
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)),
);
}
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 TickingPromise) {
this.promise.tick(elapsed);
}
return;
}
while (true) {
this.sandbox.context.elapsed = elapsed;
const {async, done, value} = this.sandbox.step();
if (async) {
this.promise = value;
value
.catch(reject)
.then(() => {
if (done) {
resolve();
}
})
.finally(() => {
this.promise = null;
});
break;
}
if (done) {
resolve();
break;
}
}
}
tickingPromise() {
return new TickingPromise(
() => {},
(elapsed, resolve, reject) => {
this.tick(elapsed, resolve, reject);
},
);
}
static tickingPromise(code, context = {}) {
let tickingPromise;
return new TickingPromise(
(resolve) => {
this.fromCode(code, context)
.then((script) => {
tickingPromise = script.tickingPromise();
resolve(tickingPromise);
})
},
(elapsed) => {
tickingPromise?.tick?.(elapsed);
},
);
}
}