|
| 1 | +import { QuickJSHandle } from "./sandbox/quick.ts"; |
| 2 | +import { type JsScript } from "@commontools/js-runtime"; |
| 3 | +import { GuestMessage, SandboxValue } from "./types.ts"; |
| 4 | +import { Sandbox, SandboxConfig, SandboxStats } from "./sandbox/mod.ts"; |
| 5 | + |
| 6 | +type SandboxState = "unloaded" | "loaded" | "disposed"; |
| 7 | + |
| 8 | +// A VM to execute recipe code. |
| 9 | +// |
| 10 | +// Before usage, `await RecipeSandbox.initialize()` |
| 11 | +// must be called at least once. |
| 12 | +// |
| 13 | +// Each sandbox can load a single recipe compiled with |
| 14 | +// `js-runtime`, and then loaded via `sandbox.load(..)`. |
| 15 | +// Functions exported by the typescript modules can be |
| 16 | +// then be invoked. |
| 17 | +export class RecipeSandbox { |
| 18 | + #state: SandboxState; |
| 19 | + #sandbox: Sandbox; |
| 20 | + // Bundle function return value containing |
| 21 | + // "main" property, and "exportMap" property. |
| 22 | + #exports?: QuickJSHandle; |
| 23 | + |
| 24 | + constructor(config: SandboxConfig = {}) { |
| 25 | + this.#sandbox = new Sandbox(config); |
| 26 | + this.#state = "unloaded"; |
| 27 | + } |
| 28 | + |
| 29 | + messages(): GuestMessage[] { |
| 30 | + return this.#sandbox.messages(); |
| 31 | + } |
| 32 | + |
| 33 | + // Invoke function exported by load script. |
| 34 | + invoke( |
| 35 | + exportFile: string, |
| 36 | + exportName: string, |
| 37 | + args: SandboxValue[], |
| 38 | + ): unknown { |
| 39 | + if (this.#state !== "loaded" || !this.#exports) { |
| 40 | + throw new Error(`Cannot invoke a non-loaded sandbox.`); |
| 41 | + } |
| 42 | + const vm = this.#sandbox.vm(); |
| 43 | + using exportMap = vm.getProp(this.#exports, "exportMap"); |
| 44 | + using fileExports = vm.getProp(exportMap, exportFile); |
| 45 | + using fn = vm.getProp(fileExports, exportName); |
| 46 | + |
| 47 | + return this.#sandbox.invoke(fn, vm.undefined, args); |
| 48 | + } |
| 49 | + |
| 50 | + // Load and evaluate a compiled recipe script within the sandbox. |
| 51 | + load(module: JsScript): unknown { |
| 52 | + if (this.#state !== "unloaded") { |
| 53 | + throw new Error(`Cannot load script in ${this.#state} sandbox.`); |
| 54 | + } |
| 55 | + const vm = this.#sandbox.vm(); |
| 56 | + using result = this.#sandbox.loadRaw(module); |
| 57 | + this.#exports = this.#sandbox.invokeRaw(result, vm.undefined, [{}]); |
| 58 | + using main = vm.getProp(this.#exports, "main"); |
| 59 | + this.#state = "loaded"; |
| 60 | + return this.#sandbox.fromVm(main); |
| 61 | + } |
| 62 | + |
| 63 | + stats(): SandboxStats { |
| 64 | + return this.#sandbox.stats(); |
| 65 | + } |
| 66 | + |
| 67 | + [Symbol.dispose]() { |
| 68 | + this.dispose(); |
| 69 | + } |
| 70 | + |
| 71 | + dispose() { |
| 72 | + if (this.#state === "disposed") { |
| 73 | + return; |
| 74 | + } |
| 75 | + this.#state = "disposed"; |
| 76 | + if (this.#exports) this.#exports.dispose(); |
| 77 | + this.#sandbox.dispose(); |
| 78 | + } |
| 79 | + |
| 80 | + static async initialize() { |
| 81 | + await Sandbox.initialize(); |
| 82 | + } |
| 83 | +} |
0 commit comments