import {parse as acornParse} from 'acorn'; import {LRUCache} from 'lru-cache'; import Sandbox from '@/astride/sandbox.js'; import TickingPromise from '@/util/ticking-promise.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; } get context() { return this.sandbox.context; } static createContext(locals = {}) { if (locals[Populated]) { return locals; } return { [Populated]: true, ...locals, }; } // async evaluate(callback) { // this.sandbox.reset(); // let {done, value} = this.sandbox.next(); // if (value instanceof Promise) { // await value; // } // while (!done) { // ({done, value} = this.sandbox.next()); // if (value instanceof Promise) { // // eslint-disable-next-line no-await-in-loop // await value; // } // } // if (value instanceof Promise) { // value.then(callback); // } // else { // callback(value); // } // } static async fromCode(code, context = {}) { let ast; if (cache.has(code)) { ast = cache.get(code); } else { cache.set(code, ast = await this.parse(code)); } return new this( new Sandbox(ast, 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 {done, value} = this.sandbox.step(); if (value) { const {value: result} = value; if (result instanceof Promise) { this.promise = result; result .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); }, ); } };