164 lines
3.2 KiB
JavaScript
164 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,
|
|
Array,
|
|
Math,
|
|
Promise,
|
|
wait: (seconds) => (
|
|
new Promise((resolve) => {
|
|
setTimeout(resolve, seconds * 1000);
|
|
})
|
|
),
|
|
};
|
|
}
|
|
|
|
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);
|
|
// }
|
|
// }
|
|
|
|
evaluateSync() {
|
|
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);
|
|
},
|
|
);
|
|
}
|
|
|
|
}
|