silphius/app/util/script.js
2024-06-25 07:14:17 -05:00

159 lines
3.2 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';
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 contextDefaults() {
return {
console,
Math,
wait: (ms) => new Promise((resolve) => { setTimeout(resolve, ms); }),
};
}
static createContext(locals = {}) {
if (locals[Populated]) {
return locals;
}
return {
[Populated]: true,
...this.contextDefaults(),
...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);
},
);
}
}