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 { 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); }, ); } }