silphius/app/util/script.js

183 lines
3.7 KiB
JavaScript
Raw Normal View History

2024-06-22 10:47:17 -05:00
import {parse as acornParse} from 'acorn';
import {LRUCache} from 'lru-cache';
import Sandbox from '@/astride/sandbox.js';
2024-08-01 00:39:54 -05:00
import * as color from '@/util/color.js';
2024-07-04 09:08:47 -05:00
import delta from '@/util/delta.js';
import lfo from '@/util/lfo.js';
import * as MathUtil from '@/util/math.js';
2024-07-22 01:31:52 -05:00
import * as PromiseUtil from '@/util/promise.js';
2024-07-04 09:08:47 -05:00
import transition from '@/util/transition.js';
2024-06-22 10:47:17 -05:00
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, code) {
this.code = code;
this.sandbox = sandbox;
this.promise = null;
}
2024-07-04 09:24:49 -05:00
clone() {
return new this.constructor(this.sandbox.clone(), this.code);
2024-07-04 09:24:49 -05:00
}
get context() {
return this.sandbox.context;
}
2024-06-25 07:14:17 -05:00
static contextDefaults() {
return {
2024-08-01 00:39:54 -05:00
color,
2024-07-04 09:08:47 -05:00
console,
delta,
lfo,
Math: MathUtil,
2024-07-22 01:31:52 -05:00
Promise: PromiseUtil,
2024-07-04 09:08:47 -05:00
transition,
2024-09-14 17:30:42 -05:00
wait: (seconds = 0) => (
new PromiseUtil.Ticker(
(resolve) => {
if (0 === seconds) {
resolve();
}
},
(elapsed, resolve) => {
seconds -= elapsed;
if (seconds <= 0) {
resolve();
}
}
)
2024-06-28 13:10:05 -05:00
),
2024-06-25 07:14:17 -05:00
};
}
static createContext(locals = {}) {
if (locals[Populated]) {
return locals;
}
return {
[Populated]: true,
2024-06-25 07:14:17 -05:00
...this.contextDefaults(),
...locals,
};
}
2024-07-11 15:43:08 -05:00
evaluate() {
2024-06-27 13:56:43 -05:00
this.sandbox.reset();
2024-06-30 13:05:53 -05:00
const {value} = this.sandbox.run();
2024-06-27 13:56:43 -05:00
return value;
}
static async fromCode(code, context = {}) {
2024-06-27 13:56:43 -05:00
if (!cache.has(code)) {
cache.set(code, this.parse(code));
}
return new this(
2024-06-27 13:56:43 -05:00
new Sandbox(await cache.get(code), this.createContext(context)),
code,
);
}
static async parse(code) {
return parse(
code,
{
allowReturnOutsideFunction: true,
},
);
}
reset() {
this.promise = null;
this.sandbox.compile();
}
tick(elapsed, resolve, reject) {
if (this.promise) {
2024-07-22 01:31:52 -05:00
if (this.promise instanceof PromiseUtil.Ticker) {
this.promise.tick(elapsed);
}
return;
}
while (true) {
this.sandbox.context.elapsed = elapsed;
let async, done, value;
try {
({async, done, value} = this.sandbox.step());
}
catch (error) {
const node = this.sandbox.$$execution.stack.pop();
console.warn('Script ran into a problem at', this.code.slice(node.start, node.end));
console.warn(error);
if (resolve) {
resolve();
}
return;
}
2024-08-01 14:32:09 -05:00
if (async || value instanceof Promise) {
2024-06-30 13:05:53 -05:00
this.promise = value;
value
2024-07-21 11:14:51 -05:00
.catch(reject ? reject : () => {})
2024-06-30 13:05:53 -05:00
.then(() => {
if (done) {
2024-07-21 11:14:51 -05:00
if (resolve) {
resolve();
}
2024-06-30 13:05:53 -05:00
}
})
.finally(() => {
this.promise = null;
});
break;
}
if (done) {
2024-07-21 11:14:51 -05:00
if (resolve) {
resolve();
}
break;
}
}
}
2024-07-22 01:31:52 -05:00
ticker() {
return new PromiseUtil.Ticker(
() => {},
(elapsed, resolve, reject) => {
this.tick(elapsed, resolve, reject);
},
);
}
2024-07-22 01:31:52 -05:00
static ticker(code, context = {}) {
let ticker;
return new PromiseUtil.Ticker(
(resolve) => {
this.fromCode(code, context)
.then((script) => {
2024-07-22 01:31:52 -05:00
ticker = script.ticker();
resolve(ticker);
})
},
(elapsed) => {
2024-07-22 01:31:52 -05:00
ticker?.tick?.(elapsed);
},
);
}
2024-06-23 07:35:56 -05:00
}