openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
3,187 lines • 118 kB
JavaScript
import { c as isRecord } from "../../../record-coerce-TIsquoVR.mjs";
import { t as _usingCtx } from "../../../usingCtx-BhiZp_rS.mjs";
import { n as serveWorkerTasks } from "../../../worker-task-server-sbLPhqBg.mjs";
import { a as buildUserSource, i as USER_SOURCE_FILE, n as prepareSource, o as normalizeSourceStack, r as SOURCE_LOCATION_KEY, s as CODE_MODE_CONTROLLER_SOURCE } from "../../../code-mode-source--Ra1hKRv.mjs";
import { a as captureCodeModeValue, i as captureCodeModeOutput, n as EMPTY_CODE_MODE_OUTPUT, r as boundCodeModeError } from "../../../code-mode-json-CZv_Sy6J.mjs";
import { n as ToolInputError } from "../../../tool-input-error-mjW74R8m.mjs";
import "../../../worker-task-server-B3ABe5qj.mjs";
import "../../../string-coerce-runtime-BbIJhy7G.mjs";
import "../../../code-mode-executor-runtime-DD2c0ig2.mjs";
//#region node_modules/.pnpm/quickjs-wasi@3.6.2/node_modules/quickjs-wasi/dist/wasi-shim.js
/**
* Minimal WASI shim for running QuickJS in any WebAssembly environment.
*
* Implements the subset of WASI snapshot_preview1 that QuickJS needs:
* - clock_time_get (for Date.now() and Math.random() PRNG seeding)
* - fd_write (for QuickJS runtime internal logging)
* - fd_close (stub)
* - fd_fdstat_get (stub)
* - fd_seek (stub)
* - random_get (for crypto extension and WASI libc init)
*
* Users can override any of these by providing their own implementations
* in the `wasi` option when creating a VM. Overrides are applied to both
* the main module and all loaded extensions.
*/
/** Returns a wasi_snapshot_preview1 import object with built-in defaults. */
function createWasiShim(memoryAccessor) {
const ERRNO_SUCCESS = 0;
const ERRNO_BADF = 8;
const ERRNO_NOSYS = 52;
const CLOCK_REALTIME = 0;
const CLOCK_MONOTONIC = 1;
return {
clock_time_get(clockId, _precision, resultPtr) {
const mem = memoryAccessor();
const view = new DataView(mem.buffer);
if (clockId === CLOCK_REALTIME || clockId === CLOCK_MONOTONIC) {
const timeNs = BigInt(Date.now()) * 1000000n;
view.setBigUint64(resultPtr, timeNs, true);
return ERRNO_SUCCESS;
}
return ERRNO_NOSYS;
},
fd_write(fd, iovsPtr, iovsLen, nwrittenPtr) {
const mem = memoryAccessor();
const view = new DataView(mem.buffer);
const bytes = new Uint8Array(mem.buffer);
let totalWritten = 0;
if (fd !== 1 && fd !== 2) return ERRNO_BADF;
for (let i = 0; i < iovsLen; i++) {
const bufPtr = view.getUint32(iovsPtr + i * 8, true);
const bufLen = view.getUint32(iovsPtr + i * 8 + 4, true);
const chunk = bytes.slice(bufPtr, bufPtr + bufLen);
const text = new TextDecoder().decode(chunk);
if (fd === 1) {
if (typeof process !== "undefined" && process.stdout) process.stdout.write(text);
else console.log(text);
} else if (typeof process !== "undefined" && process.stderr) process.stderr.write(text);
else console.error(text);
totalWritten += bufLen;
}
view.setUint32(nwrittenPtr, totalWritten, true);
return ERRNO_SUCCESS;
},
fd_close(_fd) {
return ERRNO_NOSYS;
},
fd_fdstat_get(fd, statPtr) {
const mem = memoryAccessor();
const view = new DataView(mem.buffer);
if (fd === 1 || fd === 2) {
view.setUint8(statPtr, 2);
view.setUint16(statPtr + 2, 0, true);
view.setBigUint64(statPtr + 8, 0n, true);
view.setBigUint64(statPtr + 16, 0n, true);
return ERRNO_SUCCESS;
}
return ERRNO_BADF;
},
fd_seek(_fd, _offset, _whence, _resultPtr) {
return ERRNO_NOSYS;
},
random_get(bufPtr, bufLen) {
const mem = memoryAccessor();
const bytes = new Uint8Array(mem.buffer, bufPtr, bufLen);
if (typeof crypto !== "undefined" && crypto.getRandomValues) crypto.getRandomValues(bytes);
else for (let i = 0; i < bufLen; i++) bytes[i] = Math.floor(Math.random() * 256);
return ERRNO_SUCCESS;
}
};
}
//#endregion
//#region node_modules/.pnpm/quickjs-wasi@3.6.2/node_modules/quickjs-wasi/dist/extensions.js
/**
* WASM Dynamic Linking - Extension Loader
*
* Loads WASM shared libraries (.so) compiled with wasi-sdk as extensions
* for the QuickJS WASM runtime. Extensions can call QuickJS C API functions
* directly through dynamic linking (shared memory + symbol resolution).
*
* Key concepts:
* - Extensions are WASM shared libraries with a `dylink.0` custom section
* - They share linear memory and the indirect function table with the main module
* - Symbol imports (env.*) are resolved against the main module's exports
* - Each extension gets a unique __memory_base and __table_base
* - Extensions export an init function (e.g., `qjs_ext_url_init`)
*/
const decoder = new TextDecoder();
/**
* Parse a null-terminated "key=value\nkey=value" string from WASM memory
* into a Record<string, string>. Used to read version info from extensions.
*/
function parseVersionString(memory, ptr) {
const mem = new Uint8Array(memory.buffer);
let end = ptr;
while (mem[end] !== 0) end++;
if (end === ptr) return {};
const str = decoder.decode(mem.slice(ptr, end));
const result = {};
for (const line of str.split("\n")) {
const eq = line.indexOf("=");
if (eq > 0) result[line.slice(0, eq)] = line.slice(eq + 1);
}
return result;
}
/**
* Check if an extension exports a versions function and parse the result.
*/
function extractExtensionVersions(ext, memory) {
const versionsFnName = `qjs_ext_${ext.name.replace(/-/g, "_")}_versions`;
const versionsFn = ext.instance.exports[versionsFnName];
if (typeof versionsFn !== "function") return void 0;
const ptr = versionsFn();
if (ptr === 0) return void 0;
return parseVersionString(memory, ptr);
}
/** Read a ULEB128-encoded integer from a byte array */
function readULEB128(bytes, offset) {
let result = 0;
let shift = 0;
while (offset.value < bytes.length) {
const byte = bytes[offset.value++];
result |= (byte & 127) << shift;
if ((byte & 128) === 0) break;
shift += 7;
}
return result;
}
/** Parse the dylink.0 custom section from a WASM module */
function parseDylink(module) {
const sections = WebAssembly.Module.customSections(module, "dylink.0");
if (sections.length === 0) return null;
const bytes = new Uint8Array(sections[0]);
const offset = { value: 0 };
const info = {
memorySize: 0,
memoryAlignment: 0,
tableSize: 0,
tableAlignment: 0,
needed: []
};
while (offset.value < bytes.length) {
const subsectionType = readULEB128(bytes, offset);
const subsectionSize = readULEB128(bytes, offset);
const subsectionEnd = offset.value + subsectionSize;
if (subsectionType === 1) {
info.memorySize = readULEB128(bytes, offset);
info.memoryAlignment = readULEB128(bytes, offset);
info.tableSize = readULEB128(bytes, offset);
info.tableAlignment = readULEB128(bytes, offset);
} else if (subsectionType === 2) {
const count = readULEB128(bytes, offset);
for (let i = 0; i < count; i++) {
const len = readULEB128(bytes, offset);
const name = new TextDecoder().decode(bytes.slice(offset.value, offset.value + len));
offset.value += len;
info.needed.push(name);
}
}
offset.value = subsectionEnd;
}
return info;
}
/**
* Load a WASM shared library extension and link it against the main module.
*
* @param descriptor - Extension description (name, wasm bytes, init function)
* @param mainExports - The main QuickJS WASM module's exports
* @param wasiBuiltins - Built-in WASI implementations (lowest priority)
* @param wasiUserOverrides - User-provided WASI overrides (highest priority)
* @param memoryProxy - A memory proxy for extension WASI factories
* @param allocBase - Optional fixed memory/table bases (for snapshot restore)
*/
async function loadExtension(descriptor, mainExports, wasiBuiltins, wasiUserOverrides, memoryProxy, allocBase) {
let module;
if (descriptor.wasm instanceof WebAssembly.Module) module = descriptor.wasm;
else module = await WebAssembly.compile(descriptor.wasm);
const dylink = parseDylink(module);
if (!dylink) throw new Error(`Extension "${descriptor.name}" is not a WASM shared library (missing dylink.0 section)`);
const memory = mainExports.memory;
const table = mainExports.__indirect_function_table;
const stackPointer = mainExports.__stack_pointer;
const mallocFn = mainExports.malloc;
if (!memory) throw new Error("Main module does not export memory");
if (!table) throw new Error("Main module does not export __indirect_function_table");
if (!mallocFn) throw new Error("Main module does not export malloc");
let memoryBase;
let tableBase;
if (allocBase) {
memoryBase = allocBase.memoryBase;
tableBase = allocBase.tableBase;
} else if (dylink.memorySize > 0) {
memoryBase = mallocFn(dylink.memorySize);
if (memoryBase === 0) throw new Error(`Failed to allocate ${dylink.memorySize} bytes for extension "${descriptor.name}"`);
new Uint8Array(memory.buffer, memoryBase, dylink.memorySize).fill(0);
} else memoryBase = 0;
if (allocBase) {
tableBase = allocBase.tableBase;
if (tableBase + dylink.tableSize > table.length) table.grow(tableBase + dylink.tableSize - table.length);
} else {
tableBase = table.length;
if (dylink.tableSize > 0) table.grow(dylink.tableSize);
}
const extImports = WebAssembly.Module.imports(module);
const importObj = {
env: {},
"GOT.mem": {},
"GOT.func": {}
};
if (extImports.some((imp) => imp.module === "wasi_snapshot_preview1")) {
const extWasi = descriptor.wasi && memoryProxy ? descriptor.wasi(memoryProxy) : void 0;
importObj["wasi_snapshot_preview1"] = {
...wasiBuiltins,
...extWasi,
...wasiUserOverrides
};
}
const extExportNames = new Set(WebAssembly.Module.exports(module).filter((e) => e.kind === "function").map((e) => e.name));
const unresolvedFuncs = /* @__PURE__ */ new Set();
for (const imp of extImports) if (imp.module === "env") {
if (imp.name === "memory" && imp.kind === "memory") importObj.env.memory = memory;
else if (imp.name === "__indirect_function_table" && imp.kind === "table") importObj.env.__indirect_function_table = table;
else if (imp.name === "__memory_base" && imp.kind === "global") importObj.env.__memory_base = new WebAssembly.Global({
value: "i32",
mutable: false
}, memoryBase);
else if (imp.name === "__table_base" && imp.kind === "global") importObj.env.__table_base = new WebAssembly.Global({
value: "i32",
mutable: false
}, tableBase);
else if (imp.name === "__stack_pointer" && imp.kind === "global") importObj.env.__stack_pointer = stackPointer;
else if (imp.kind === "function") {
const resolved = mainExports[imp.name];
if (resolved && typeof resolved === "function") importObj.env[imp.name] = resolved;
else {
unresolvedFuncs.add(imp.name);
importObj.env[imp.name] = () => {
throw new Error(`Extension "${descriptor.name}" called unresolved symbol: env.${imp.name}`);
};
}
} else if (imp.kind === "global") {
const resolved = mainExports[imp.name];
if (resolved instanceof WebAssembly.Global) importObj.env[imp.name] = resolved;
else importObj.env[imp.name] = new WebAssembly.Global({
value: "i32",
mutable: true
}, 0);
}
} else if (imp.module === "GOT.mem" && imp.kind === "global") importObj["GOT.mem"][imp.name] = new WebAssembly.Global({
value: "i32",
mutable: true
}, 0);
else if (imp.module === "GOT.func" && imp.kind === "global") {
const resolved = mainExports[imp.name];
if (resolved && typeof resolved === "function") {
const idx = table.length;
table.grow(1);
table.set(idx, resolved);
importObj["GOT.func"][imp.name] = new WebAssembly.Global({
value: "i32",
mutable: true
}, idx);
} else importObj["GOT.func"][imp.name] = new WebAssembly.Global({
value: "i32",
mutable: true
}, 0);
}
const selfResolvable = [...unresolvedFuncs].filter((name) => extExportNames.has(name));
const wrappers = {};
for (const name of selfResolvable) {
const wrapper = { target: null };
wrappers[name] = wrapper;
importObj.env[name] = (...args) => {
if (!wrapper.target) throw new Error(`Extension "${descriptor.name}" called unresolved symbol during init: env.${name}`);
return wrapper.target(...args);
};
}
const instance = await WebAssembly.instantiate(module, importObj);
const extExports = instance.exports;
for (const name of selfResolvable) {
const selfExport = extExports[name];
if (typeof selfExport === "function") wrappers[name].target = selfExport;
}
if (typeof extExports.__wasm_apply_data_relocs === "function") extExports.__wasm_apply_data_relocs();
if (typeof extExports.__wasm_call_ctors === "function") extExports.__wasm_call_ctors();
const initFn = descriptor.initFn ?? `qjs_ext_${descriptor.name.replace(/-/g, "_")}_init`;
const ext = {
name: descriptor.name,
module,
instance,
dylink,
memoryBase,
tableBase,
initFn
};
ext.versions = extractExtensionVersions(ext, memory);
return ext;
}
/**
* Call the extension's init function, passing the JSContext and JSRuntime
* pointers from the main module.
*/
function initExtension(ext, mainExports) {
const initFunc = ext.instance.exports[ext.initFn];
if (typeof initFunc !== "function") throw new Error(`Extension "${ext.name}" does not export init function "${ext.initFn}"`);
const result = initFunc(mainExports.qjs_get_context_ptr(), mainExports.qjs_get_runtime_ptr());
if (result !== 0) throw new Error(`Extension "${ext.name}" init function returned error code ${result}`);
}
/**
* Re-instantiate extensions for snapshot restore.
*
* During restore, we need to:
* 1. Instantiate extension modules with the same memory/table bases
* 2. Let them populate the function table (via elem segments and __wasm_apply_data_relocs)
* 3. Do NOT call the init function (the state is already in the snapshot memory)
*
* This reconstructs the function table entries that extensions need, without
* modifying linear memory (which will be overwritten by the snapshot).
*/
async function restoreExtensions(descriptors, extensionMeta, mainExports, wasiBuiltins, wasiUserOverrides, memoryProxy) {
const loaded = [];
for (const meta of extensionMeta) {
const descriptor = descriptors.find((d) => d.name === meta.name);
if (!descriptor) throw new Error(`Extension "${meta.name}" required by snapshot but not provided`);
const ext = await loadExtension(descriptor, mainExports, wasiBuiltins, wasiUserOverrides, memoryProxy, {
memoryBase: meta.memoryBase,
tableBase: meta.tableBase
});
ext.initFn = meta.initFn;
loaded.push(ext);
}
return loaded;
}
//#endregion
//#region node_modules/.pnpm/quickjs-wasi@3.6.2/node_modules/quickjs-wasi/dist/version.js
const VERSION = "3.6.2";
//#endregion
//#region node_modules/.pnpm/quickjs-wasi@3.6.2/node_modules/quickjs-wasi/dist/index.js
/**
* QuickJS WASM - A snapshotable JavaScript runtime via WebAssembly.
*
* Provides a clean JavaScript API for running sandboxed JS code in a QuickJS
* VM compiled to WASM. The key differentiator is the ability to snapshot the
* entire VM state (including pending promises) and restore it in a fresh
* WASM instance.
*/
var __addDisposableResource = function(env, value, async) {
if (value !== null && value !== void 0) {
if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
var dispose, inner;
if (async) {
if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
dispose = value[Symbol.asyncDispose];
}
if (dispose === void 0) {
if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
dispose = value[Symbol.dispose];
if (async) inner = dispose;
}
if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
if (inner) dispose = function() {
try {
inner.call(this);
} catch (e) {
return Promise.reject(e);
}
};
env.stack.push({
value,
dispose,
async
});
} else if (async) env.stack.push({ async: true });
return value;
};
var __disposeResources = (function(SuppressedError) {
return function(env) {
function fail(e) {
env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
env.hasError = true;
}
var r, s = 0;
function next() {
while (r = env.stack.pop()) try {
if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
if (r.dispose) {
var result = r.dispose.call(r.value);
if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) {
fail(e);
return next();
});
} else s |= 1;
} catch (e) {
fail(e);
}
if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
if (env.hasError) throw env.error;
}
return next();
};
})(typeof SuppressedError === "function" ? SuppressedError : function(error, suppressed, message) {
var e = new Error(message);
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
});
/**
* Largest supported QuickJS native stack limit for the shipped WASM binary.
*
* The binary has a 1 MiB linker-defined stack; reserving half of it leaves
* headroom for native frames and stack-overflow exception handling.
*/
const MAX_STACK_SIZE = 524288;
/**
* Flags for `evalCode()`, matching the QuickJS `JS_EVAL_*` constants.
*/
const EvalFlags = {
/** Global script mode (default). */
TYPE_GLOBAL: 0,
/**
* Module mode. `evalCode()` returns a handle to a Promise that resolves
* to the module's namespace object (its exports), or rejects if module
* evaluation throws. Use together with `executePendingJobs()` and
* `resolvePromise()`.
*/
TYPE_MODULE: 1,
/** Force strict mode. */
STRICT: 8,
/** Compile only; do not execute. */
COMPILE_ONLY: 32,
/** Omit stack frames before this eval from Error backtraces. */
BACKTRACE_BARRIER: 64,
/**
* Allow top-level `await` in global scripts. When used, `evalCode()`
* returns a handle to a Promise that resolves to the completion value.
* Use together with `executePendingJobs()` and `resolvePromise()`.
*/
ASYNC: 128
};
/** Magic bytes: "QJSS" (QuickJS Snapshot) */
const SNAPSHOT_MAGIC = 1363825491;
/** Current serialization format version (2 = added extension metadata) */
const SNAPSHOT_VERSION = 2;
/**
* Header layout (version 2):
* 0-3: Magic "QJSS" (u32 big-endian)
* 4: Version (u8)
* 5-7: Reserved (zero)
* 8-11: Memory size in bytes (u32 little-endian)
* 12-15: Stack pointer (u32 little-endian)
* 16-19: Runtime pointer (u32 little-endian)
* 20-23: Context pointer (u32 little-endian)
* 24-27: Extension count (u32 little-endian)
* 28+: Extension entries (variable length):
* nameLen(u32) + name(utf8) + memoryBase(u32) + tableBase(u32) + initFnLen(u32) + initFn(utf8)
* N+: Memory data (N = memory size from offset 8)
*
* Version 1 (legacy): no extension metadata, memory starts at offset 24.
*/
const SNAPSHOT_HEADER_SIZE = 24;
var QuickJS = class QuickJS {
exports;
module;
instance;
encoder = new TextEncoder();
decoder = new TextDecoder();
disposed = false;
/** Registry of host callbacks, keyed by function name */
hostCallbacks = /* @__PURE__ */ new Map();
/** Counter for internal-only callbacks (e.g. promise settle handlers) */
nextInternalId = 1;
interruptHandler = null;
unhandledRejectionHandler = null;
moduleNormalizeHandler = null;
moduleLoadHandler = null;
timezoneOffsetHandler = null;
_global = null;
_versions = null;
_undefined = null;
_null = null;
_true = null;
_false = null;
_ownedHandles = /* @__PURE__ */ new Set();
/**
* The innermost active `withScope()` batch, if any. New non-singleton
* handles register themselves here so they can be freed together.
* @internal
*/
_activeScope = null;
/** Loaded extensions in deterministic order */
loadedExtensions = [];
constructor(module) {
this.module = module;
this.instance = null;
this.exports = null;
}
setInstance(instance) {
this.instance = instance;
this.exports = instance.exports;
}
/**
* Version information for the runtime and loaded native libraries.
* Always includes `"quickjs-wasi"` (the npm package version) and
* `"quickjs"` (the QuickJS engine version). Extensions may contribute
* additional entries for their native dependencies (e.g. `"ada"`, `"mbedtls"`).
*/
get versions() {
this.assertNotDisposed();
if (!this._versions) {
const result = {
"quickjs-wasi": VERSION,
quickjs: this.readCString(this.exports.qjs_get_quickjs_version())
};
for (const ext of this.loadedExtensions) if (ext.versions) Object.assign(result, ext.versions);
this._versions = result;
}
return this._versions;
}
/** The global object. Cached; do not dispose. */
get global() {
if (!this._global) this._global = new JSValueHandle(this, this.exports.qjs_get_global(), true);
return this._global;
}
/** The undefined value. Cached; do not dispose. */
get undefined() {
if (!this._undefined) this._undefined = new JSValueHandle(this, this.exports.qjs_get_undefined(), true);
return this._undefined;
}
/** The null value. Cached; do not dispose. */
get null() {
if (!this._null) this._null = new JSValueHandle(this, this.exports.qjs_get_null(), true);
return this._null;
}
/** The true value. Cached; do not dispose. */
get true() {
if (!this._true) this._true = new JSValueHandle(this, this.exports.qjs_get_true(), true);
return this._true;
}
/** The false value. Cached; do not dispose. */
get false() {
if (!this._false) this._false = new JSValueHandle(this, this.exports.qjs_get_false(), true);
return this._false;
}
/**
* Create a fresh QuickJS VM instance.
*
* @param options - Optional configuration. Can also pass raw WASM bytes
* directly for backwards compatibility.
*/
static async create(options) {
const opts = QuickJS.normalizeOptions(options);
const module = await QuickJS.resolveModule(opts.wasm);
const vm = new QuickJS(module);
const { instance, wasiBuiltins, wasiUserOverrides, memoryProxy } = await QuickJS.instantiate(module, vm, opts.wasi);
vm.setInstance(instance);
vm.exports._initialize();
if ((opts.intrinsics !== void 0 ? vm.exports.qjs_init2(opts.intrinsics) : vm.exports.qjs_init()) !== 0) throw new Error("Failed to initialize QuickJS runtime");
if (opts.extensions) {
const mainExports = instance.exports;
for (const desc of opts.extensions) {
const ext = await loadExtension(desc, mainExports, wasiBuiltins, wasiUserOverrides, memoryProxy);
vm.loadedExtensions.push(ext);
initExtension(ext, mainExports);
}
}
QuickJS.applyLimits(vm, opts);
return vm;
}
/**
* Restore a QuickJS VM from a snapshot.
*
* @param snapshot - The snapshot to restore from.
* @param options - Optional configuration. Can also pass raw WASM bytes
* directly for backwards compatibility.
*/
static async restore(snapshot, options) {
const opts = QuickJS.normalizeOptions(options);
const module = await QuickJS.resolveModule(opts.wasm);
const vm = new QuickJS(module);
const { instance, wasiBuiltins, wasiUserOverrides, memoryProxy } = await QuickJS.instantiate(module, vm, opts.wasi);
vm.setInstance(instance);
const mainExports = instance.exports;
const exportedMemory = vm.exports.memory;
const currentPages = exportedMemory.buffer.byteLength / 65536;
const neededPages = Math.ceil(snapshot.memory.byteLength / 65536);
if (neededPages > currentPages) exportedMemory.grow(neededPages - currentPages);
if (snapshot.extensions.length > 0) vm.loadedExtensions = await restoreExtensions(opts.extensions ?? [], snapshot.extensions, mainExports, wasiBuiltins, wasiUserOverrides, memoryProxy);
new Uint8Array(exportedMemory.buffer).set(snapshot.memory);
vm.exports.qjs_set_runtime_and_context(snapshot.runtimePtr, snapshot.contextPtr);
vm.exports.__stack_pointer.value = snapshot.stackPointer;
QuickJS.applyLimits(vm, opts);
return vm;
}
/**
* Serialize a snapshot to a binary buffer for persistent storage.
*
* The format includes a versioned header followed by the raw memory.
* Apply your own compression (gzip, zstd, etc.) on top for smaller
* storage. The memory compresses well due to its large zero regions.
*
* Format (version 1):
* ```
* Offset Size Field
* 0 4 Magic: "QJSS" (0x514A5353, big-endian)
* 4 1 Version: 1
* 5 3 Reserved (zero)
* 8 4 Memory size in bytes (u32 little-endian)
* 12 4 Stack pointer (u32 little-endian)
* 16 4 Runtime pointer (u32 little-endian)
* 20 4 Context pointer (u32 little-endian)
* 24 N Memory data (N = memory size from offset 8)
* ```
*/
static serializeSnapshot(snapshot) {
const textEncoder = new TextEncoder();
let extMetaSize = 4;
const extEncodedNames = [];
const extEncodedInitFns = [];
for (const ext of snapshot.extensions) {
const nameBytes = textEncoder.encode(ext.name);
const initFnBytes = textEncoder.encode(ext.initFn);
extEncodedNames.push(nameBytes);
extEncodedInitFns.push(initFnBytes);
extMetaSize += 4 + nameBytes.length + 4 + 4 + 4 + initFnBytes.length;
}
const totalSize = SNAPSHOT_HEADER_SIZE + extMetaSize + snapshot.memory.byteLength;
const buffer = new ArrayBuffer(totalSize);
const view = new DataView(buffer);
const bytes = new Uint8Array(buffer);
view.setUint32(0, SNAPSHOT_MAGIC, false);
view.setUint8(4, SNAPSHOT_VERSION);
view.setUint32(8, snapshot.memory.byteLength, true);
view.setUint32(12, snapshot.stackPointer, true);
view.setUint32(16, snapshot.runtimePtr, true);
view.setUint32(20, snapshot.contextPtr, true);
let offset = SNAPSHOT_HEADER_SIZE;
view.setUint32(offset, snapshot.extensions.length, true);
offset += 4;
for (let i = 0; i < snapshot.extensions.length; i++) {
const ext = snapshot.extensions[i];
const nameBytes = extEncodedNames[i];
const initFnBytes = extEncodedInitFns[i];
view.setUint32(offset, nameBytes.length, true);
offset += 4;
bytes.set(nameBytes, offset);
offset += nameBytes.length;
view.setUint32(offset, ext.memoryBase, true);
offset += 4;
view.setUint32(offset, ext.tableBase, true);
offset += 4;
view.setUint32(offset, initFnBytes.length, true);
offset += 4;
bytes.set(initFnBytes, offset);
offset += initFnBytes.length;
}
bytes.set(snapshot.memory, offset);
return bytes;
}
/**
* Deserialize a snapshot from a binary buffer produced by `serializeSnapshot()`.
*/
static deserializeSnapshot(data) {
if (data.length < SNAPSHOT_HEADER_SIZE) throw new Error("Invalid snapshot: too small");
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
const magic = view.getUint32(0, false);
if (magic !== SNAPSHOT_MAGIC) throw new Error(`Invalid snapshot: bad magic (expected 0x${SNAPSHOT_MAGIC.toString(16)}, got 0x${magic.toString(16)})`);
const version = view.getUint8(4);
if (version !== SNAPSHOT_VERSION && version !== 1) throw new Error(`Unsupported snapshot version: ${version} (expected ${SNAPSHOT_VERSION})`);
const memorySize = view.getUint32(8, true);
const stackPointer = view.getUint32(12, true);
const runtimePtr = view.getUint32(16, true);
const contextPtr = view.getUint32(20, true);
let extensions = [];
let memoryOffset = SNAPSHOT_HEADER_SIZE;
if (version >= 2) {
const extCount = view.getUint32(24, true);
let offset = 28;
const textDecoder = new TextDecoder();
for (let i = 0; i < extCount; i++) {
const nameLen = view.getUint32(offset, true);
offset += 4;
const name = textDecoder.decode(data.slice(offset, offset + nameLen));
offset += nameLen;
const memBase = view.getUint32(offset, true);
offset += 4;
const tblBase = view.getUint32(offset, true);
offset += 4;
const initFnLen = view.getUint32(offset, true);
offset += 4;
const initFn = textDecoder.decode(data.slice(offset, offset + initFnLen));
offset += initFnLen;
extensions.push({
name,
memoryBase: memBase,
tableBase: tblBase,
initFn
});
}
memoryOffset = offset;
}
const expectedSize = memoryOffset + memorySize;
if (data.length < expectedSize) throw new Error(`Invalid snapshot: expected ${expectedSize} bytes, got ${data.length}`);
return {
memory: data.slice(memoryOffset, memoryOffset + memorySize),
stackPointer,
runtimePtr,
contextPtr,
extensions
};
}
static normalizeOptions(options) {
if (!options) return { wasm: void 0 };
if (options instanceof WebAssembly.Module) return { wasm: options };
if (typeof options === "object" && ("wasm" in options || "wasi" in options || "memoryLimit" in options || "maxStackSize" in options || "interruptHandler" in options || "onUnhandledRejection" in options || "moduleLoader" in options || "intrinsics" in options || "extensions" in options || "timezoneOffset" in options)) {
const opts = options;
if (opts.maxStackSize !== void 0 && (!Number.isInteger(opts.maxStackSize) || opts.maxStackSize < 0 || opts.maxStackSize > 524288)) throw new RangeError(`maxStackSize must be an integer between 0 and ${MAX_STACK_SIZE}`);
return opts;
}
return { wasm: options };
}
static applyLimits(vm, opts) {
if (opts.memoryLimit !== void 0) vm.exports.qjs_set_memory_limit(opts.memoryLimit);
if (opts.maxStackSize !== void 0) vm.exports.qjs_set_max_stack_size(opts.maxStackSize);
if (opts.interruptHandler) {
vm.interruptHandler = opts.interruptHandler;
vm.exports.qjs_set_interrupt_handler(1);
}
if (opts.onUnhandledRejection) {
vm.unhandledRejectionHandler = opts.onUnhandledRejection;
vm.exports.qjs_set_promise_rejection_handler(1);
}
if (opts.moduleLoader) {
vm.moduleLoadHandler = opts.moduleLoader.load;
vm.moduleNormalizeHandler = opts.moduleLoader.normalize ?? null;
vm.exports.qjs_set_module_loader(1);
}
const tz = opts.timezoneOffset;
if (typeof tz === "function") vm.timezoneOffsetHandler = (timeSecs) => -tz(timeSecs) * 60;
else if (typeof tz === "number") {
const offsetSecs = -tz * 60;
vm.timezoneOffsetHandler = () => offsetSecs;
} else vm.timezoneOffsetHandler = (timeSecs) => {
return -(/* @__PURE__ */ new Date(timeSecs * 1e3)).getTimezoneOffset() * 60;
};
}
static async resolveModule(wasmInput) {
if (wasmInput instanceof WebAssembly.Module) return wasmInput;
if (wasmInput) return WebAssembly.compile(wasmInput);
throw new TypeError("QuickJS: `wasm` option is required. Provide WASM bytes or a compiled `WebAssembly.Module`. The binary is shipped at `quickjs-wasi/quickjs.wasm` and can be loaded via your environment's preferred mechanism (e.g. `fetch()`, `node:fs/promises`, or a bundler import like Vite's `?url`).");
}
static async instantiate(module, vm, wasiOptions) {
let memory = null;
const memoryProxy = new Proxy({}, { get(_target, prop) {
return memory[prop];
} });
const wasiBuiltins = createWasiShim(() => memory);
const wasiUserOverrides = wasiOptions ? wasiOptions(memoryProxy) : void 0;
const wasiShim = {
...wasiBuiltins,
...wasiUserOverrides
};
const hostCall = (namePtr, nameLen, thisPtr, argc, argvPtr) => {
return vm.handleHostCall(namePtr, nameLen, thisPtr, argc, argvPtr);
};
const hostInterrupt = () => {
return vm.interruptHandler ? vm.interruptHandler() ? 1 : 0 : 0;
};
const hostPromiseRejection = (promisePtr, reasonPtr, isHandled) => {
if (!vm.unhandledRejectionHandler) {
vm.exports.qjs_free_value(promisePtr);
vm.exports.qjs_free_value(reasonPtr);
return;
}
const promise = new JSValueHandle(vm, promisePtr);
const reason = new JSValueHandle(vm, reasonPtr);
try {
vm.unhandledRejectionHandler(promise, reason, isHandled !== 0);
} finally {
promise.dispose();
reason.dispose();
}
};
const throwIntoContext = (err) => {
const errHandle = vm.newError(err instanceof Error ? err : String(err));
vm.exports.qjs_throw(errHandle.ptr);
errHandle.dispose();
};
const assertSyncString = (value, callbackName) => {
if (typeof value === "string") return value;
const got = value !== null && typeof value === "object" && typeof value.then === "function" ? "a Promise" : `type ${typeof value}`;
throw new TypeError(`moduleLoader.${callbackName} must synchronously return a string (got ${got}). Async module loading is not supported. Pre-fetch module sources instead (see the "ES Modules" section of the quickjs-wasi README).`);
};
const hostModuleNormalize = (baseNamePtr, namePtr) => {
if (!vm.moduleNormalizeHandler) {
const name = vm.readCString(namePtr);
return vm.writeString(name).ptr;
}
const baseName = vm.readCString(baseNamePtr);
const specifier = vm.readCString(namePtr);
try {
const normalized = assertSyncString(vm.moduleNormalizeHandler(baseName, specifier), "normalize");
return vm.writeString(normalized).ptr;
} catch (err) {
throwIntoContext(err);
return 0;
}
};
const hostModuleLoad = (namePtr, outLenPtr) => {
if (!vm.moduleLoadHandler) return 0;
const name = vm.readCString(namePtr);
try {
const source = assertSyncString(vm.moduleLoadHandler(name), "load");
const { ptr, len } = vm.writeString(source);
new Uint32Array(vm.exports.memory.buffer, outLenPtr, 1)[0] = len;
return ptr;
} catch (err) {
throwIntoContext(err);
return 0;
}
};
const hostGetTimezoneOffset = (hi, lo) => {
const timeSecs = Number(BigInt(hi) << 32n | BigInt(lo >>> 0));
return vm.timezoneOffsetHandler ? vm.timezoneOffsetHandler(timeSecs) : 0;
};
const instance = await WebAssembly.instantiate(module, {
env: {
host_call: hostCall,
host_interrupt: hostInterrupt,
host_promise_rejection: hostPromiseRejection,
host_module_normalize: hostModuleNormalize,
host_module_load: hostModuleLoad,
host_get_timezone_offset: hostGetTimezoneOffset
},
wasi_snapshot_preview1: wasiShim
});
memory = instance.exports.memory;
return {
instance,
wasiBuiltins,
wasiUserOverrides,
memoryProxy
};
}
/**
* Called from WASM when a host function is invoked from QuickJS code.
*/
handleHostCall(namePtr, nameLen, thisPtr, argc, argvPtr) {
const name = this.decoder.decode(new Uint8Array(this.exports.memory.buffer, namePtr, nameLen));
const callback = this.hostCallbacks.get(name);
if (!callback) {
const errHandle = this.newError(`Host callback "${name}" is not registered: it was unregistered, its ephemeral function handle was disposed, or it was never re-registered after a snapshot restore.`);
this.exports.qjs_throw(errHandle.ptr);
errHandle.dispose();
return 0;
}
const thisHandle = new JSValueHandle(this, thisPtr, false, true);
const args = [];
if (argc > 0 && argvPtr !== 0) {
const view = new DataView(this.exports.memory.buffer);
for (let i = 0; i < argc; i++) {
const argPtr = view.getUint32(argvPtr + i * 4, true);
args.push(new JSValueHandle(this, argPtr, false, true));
}
}
try {
const result = callback.call(thisHandle, ...args);
return this.exports.qjs_dup_value(result.ptr);
} catch (err) {
const errHandle = this.newError(err instanceof Error ? err : String(err));
this.exports.qjs_throw(errHandle.ptr);
errHandle.dispose();
return 0;
}
}
/** Write a JS string into WASM memory, returning the pointer. Caller must free. */
writeString(str) {
const encoded = encodeWtf8(str);
const ptr = this.exports.wasm_malloc(encoded.length + 1);
if (ptr === 0) throw new Error("wasm_malloc failed");
const mem = new Uint8Array(this.exports.memory.buffer);
mem.set(encoded, ptr);
mem[ptr + encoded.length] = 0;
return {
ptr,
len: encoded.length
};
}
/** Read a null-terminated C string from WASM memory */
readCString(ptr) {
const mem = new Uint8Array(this.exports.memory.buffer);
let end = ptr;
while (mem[end] !== 0) end++;
return this.decoder.decode(mem.slice(ptr, end));
}
/**
* Check if a result handle is an exception and throw a JSException if so.
* Used internally by evalCode and callFunction.
*/
throwIfException(result) {
if (this.exports.qjs_is_exception(result.ptr) !== 0) {
const exc = this.getException();
result.dispose();
this._ownedHandles.add(exc);
throw new JSException(exc);
}
return result;
}
/**
* Evaluate JavaScript code and return the result as a handle.
* If the code throws, a `JSException` (which extends `Error`) is thrown
* on the host side, matching standard JavaScript semantics.
*
* @param code - The JavaScript source code to evaluate.
* @param filename - Optional filename for error stack traces (default `'<eval>'`).
* @param flags - Optional bitwise OR of `EvalFlags.*` constants.
* For example, pass `EvalFlags.ASYNC` to allow top-level `await`; the
* returned handle will be a Promise that resolves to the completion value.
* With `EvalFlags.TYPE_MODULE` the returned handle is a Promise that
* resolves to the module's namespace object (its exports).
*/
evalCode(code, filename = "<eval>", flags = 0) {
this.assertNotDisposed();
const codeStr = this.writeString(code);
const fnStr = this.writeString(filename);
const resultPtr = this.exports.qjs_eval(codeStr.ptr, codeStr.len, fnStr.ptr, flags);
this.exports.wasm_free(codeStr.ptr);
this.exports.wasm_free(fnStr.ptr);
return this.throwIfException(new JSValueHandle(this, resultPtr));
}
/**
* Compile JavaScript source code to bytecode without executing it.
* The returned `Uint8Array` can be stored, transferred, or later executed
* with `evalBytecode()`.
*
* @param code - The JavaScript source code to compile.
* @param filename - Optional filename for error stack traces (default `'<compile>'`).
* @param evalFlags - Optional bitwise OR of `EvalFlags.*` constants.
* Use `EvalFlags.TYPE_MODULE` to compile as a module.
* @param compileFlags - Optional bitwise OR of `CompileFlags.*` constants.
* Use `CompileFlags.STRIP_SOURCE` and/or `CompileFlags.STRIP_DEBUG` to
* reduce bytecode size.
*/
compile(code, filename = "<compile>", evalFlags = 0, compileFlags = 0) {
this.assertNotDisposed();
const codeStr = this.writeString(code);
const fnStr = this.writeString(filename);
const outLenPtr = this.exports.wasm_malloc(4);
const bufPtr = this.exports.qjs_compile(codeStr.ptr, codeStr.len, fnStr.ptr, evalFlags, compileFlags, outLenPtr);
this.exports.wasm_free(codeStr.ptr);
this.exports.wasm_free(fnStr.ptr);
if (bufPtr === 0) {
this.exports.wasm_free(outLenPtr);
const exc = this.getException();
throw new Error(`Compilation error: ${exc.toString()}`);
}
const outLen = new Uint32Array(this.exports.memory.buffer, outLenPtr, 1)[0];
this.exports.wasm_free(outLenPtr);
const bytecode = new Uint8Array(this.exports.memory.buffer, bufPtr, outLen).slice();
this.exports.wasm_free(bufPtr);
return bytecode;
}
/**
* Execute previously compiled bytecode (from `compile()`).
* Returns the evaluation result as a handle.
*
* For module bytecode (compiled with `EvalFlags.TYPE_MODULE`), the
* returned handle is a Promise that resolves to the module's namespace
* object (its exports).
*
* @param bytecode - The bytecode `Uint8Array` from `compile()`.
*/
evalBytecode(bytecode) {
this.assertNotDisposed();
const bufPtr = this.exports.wasm_malloc(bytecode.byteLength);
new Uint8Array(this.exports.memory.buffer, bufPtr, bytecode.byteLength).set(bytecode);
const resultPtr = this.exports.qjs_eval_bytecode(bufPtr, bytecode.byteLength);
this.exports.wasm_free(bufPtr);
return this.throwIfException(new JSValueHandle(this, resultPtr));
}
/**
* Execute all pending microtask jobs (promise reactions, etc.)
* Returns the number of jobs executed.
*/
executePendingJobs() {
this.assertNotDisposed();
let count = 0;
while (this.exports.qjs_is_job_pending()) {
if (this.exports.qjs_execute_pending_job() < 0) {
const exc = this.getException();
throw new Error(`Job execution error: ${exc.toString()}`);
}
count++;
}
return count;
}
/**
* Explicitly trigger garbage collection. QuickJS runs GC automatically,
* but this can be useful to reclaim memory at a known point or before
* taking a snapshot.
*/
runGC() {
this.assertNotDisposed();
this.exports.qjs_run_gc();
}
/**
* The GC threshold in bytes. When allocated memory exceeds this value,
* garbage collection is triggered automatically. Set to 0 to disable
* automatic GC.
*/
get gcThreshold() {
this.assertNotDisposed();
return this.exports.qjs_get_gc_threshold();
}
set gcThreshold(threshold) {
this.assertNotDisposed();
this.exports.qjs_set_gc_threshold(threshold);
}
/**
* Get detailed memory usage statistics from the QuickJS runtime.
* Returns counts and sizes for atoms, strings, objects, functions, etc.
*/
getMemoryUsage() {
this.assertNotDisposed();
const bufPtr = this.exports.wasm_malloc(208);
this.exports.qjs_compute_memory_usage(bufPtr);
const view = new BigInt64Array(this.exports.memory.buffer, bufPtr, 26);
const result = {
mallocSize: Number(view[0]),
mallocLimit: Number(view[1]),
memoryUsedSize: Number(view[2]),
mallocCount: Number(view[3]),
memoryUsedCount: Number(view[4]),
atomCount: Number(view[5]),
atomSize: Number(view[6]),
strCount: Number(view[7]),
strSize: Number(view[8]),
objCount: Number(view[9]),
objSize: Number(view[10]),
propCount: Number(view[11]),
propSize: Number(view[12]),
shapeCount: Number(view[13]),
shapeSize: Number(view[14]),
jsFuncCount: Number(view[15]),
jsFuncSize: Number(view[16]),
jsFuncCodeSize: Number(view[17]),
jsFuncPc2lineCount: Number(view[18]),
jsFuncPc2lineSize: Number(view[19]),
cFuncCount: Number(view[20]),
arrayCount: Number(view[21]),
fastArrayCount: Number(view[22]),
fastArrayElements: Number(view[23]),
binaryObjectCount: Number(view[24]),
binaryObjectSize: Number(view[25])
};
this.exports.wasm_free(bufPtr);
return result;
}
/**
* Get the global object. Prefer the cached `vm.global` property.
*/
getGlobal() {
this.assertNotDisposed();
return new JSValueHandle(this, this.exports.qjs_get_global());
}
/**
* Create a new QuickJS string value.
*/
newString(str) {
this.assertNotDisposed();
const { ptr, len } = this.writeString(str);
const resultPtr = this.exports.qjs_new_string(ptr, len);
this.exports.wasm_free(ptr);
return new JSValueHandle(this, resultPtr);
}
/**
* Create a new QuickJS number value.
*/
newNumber(num) {
this.assertNotDisposed();
return new JSValueHandle(this, this.exports.qjs_new_number(num));
}
/**
* Create a new QuickJS BigInt value.
*/
newBigInt(val) {
this.assertNotDisposed();
const lo = Number(val & 4294967295n);
const hi = Number(val >> 32n & 4294967295n);
return new JSValueHandle(this, this.exports.qjs_new_big_int64(lo, hi));
}
/**
* Create a new QuickJS object value.
*/
newObject() {
this.assertNotDisposed();
return new JSValueHandle(this, this.exports.qjs_new_object());
}
/**
* Create a new QuickJS array value.
*/
newArray() {
this.assertNotDisposed();
return new JSValueHandle(this, this.exports.qjs_new_array());
}
/**
* Create a global symbol (`Symbol.for(description)`).
* Global symbols with the same description are always the same symbol,
* even across snapshot/restore.
*/
newSymbolFor(description) {
this.assertNotDisposed();
const { ptr, len } = this.writeString(description);
const result = new JSValueHandle(this, this.exports.qjs_new_symbol(ptr, len, 1));
this.exports.wasm_free(ptr);
return result;
}
/**
* Create a new QuickJS ArrayBuffer by copying data from a host buffer.
*/
newArrayBuffer(data) {
this.assertNotDisposed();
const bytes = data instanceof ArrayBuffer ? new Uint8Array(data) : data;
const ptr = this.exports.wasm_malloc(bytes.length);
if (ptr === 0) throw new Error("wasm_malloc failed");
new Uint8Array(this.exports.memory.buffer).set(bytes, ptr);
const result = new JSValueHandle(this, this.exports.qjs_new_array_buffer(ptr, bytes.length));
this.exports.wasm_free(ptr);
return result;
}
/**
* Create a new QuickJS Uint8Array by copying data from a host buffer.
*/
newUint8Array(data) {
this.assertNotDisposed();
const ptr = this.exports.wasm_malloc(data.length);
if (ptr === 0) throw new Error("wasm_malloc failed");
new Uint8Array(this.exports.memory.buffer).set(data, ptr);
const result = new JSValueHandle(this, this.exports.qjs_new_uint8_array(ptr, data.length));
this.exports.wasm_free(ptr);
return result;
}
/**
* Get undefined. Prefer the cached `vm.undefined` property.
*/
getUndefined() {
this.assertNotDisposed();
return new JSValueHandle(this, this.exports.qjs_get_undefined());
}
/**
* Get null. Prefer the cached `vm.null` property.
*/
getNull() {
this.assertNotDisposed();
return new JSValueHandle(this, this.exports.qjs_get_null());
}
/**
* Get true. Prefer the cached `vm.true` property.
*/
getTrue() {
this.assertNotDisposed();
return new JSValueHandle(this, this.exports.qjs_get_true());
}
/**
* Get false. Prefer the cached `vm.false` property.
*/
getFalse() {
this.assertNotDisposed();
return new JSValueHandle(this, this.exports.qjs_get_false());
}
/**
* Create a new QuickJS function backed by a host callback.
*
* When the function is called inside QuickJS, the host callback is invoked
* with the `this` value and arguments as JSValueHandles.
*/
newFunction(name, fn) {
this.assertNotDisposed();
if (this.hostCallbacks.has(name)) throw new Error(`Host callback with name "${name}" is already registered`);
this.hostCallbacks.set(name, fn);
const { ptr: namePtr, len: nameLen } = this.writeString(name);
const resultPtr = this.exports.qjs_new_host_function(namePtr, nameLen, 0);
this.exports.wasm_free(namePtr);
return new JSValueHandle(this, resultPtr);
}
/**
* Run `fn` with a handle scope: every handle created during the call is
* disposed when it returns, except those passed to `scope.escape()`.
*
* This is the bulk alternative to disposing handles individually, for code
* that creates many intermediates, such as walking a large value:
*
* ```ts
* const name = vm.withScope((scope) => {
* const user = root.getProp('user'); // freed automatically
* const profile = user.getProp('profile'); // freed automatically
* return scope.escape(profile.getProp('name'));
* });
* ```
*
* Scopes nest: `escape()` transfers the handle to the enclosing scope when
* there is one, so it is still cleaned up at the outer boundary.
*
* `fn` must be synchronous. Handles created after an `await` are outside
* the scope, because it closes as soon as `fn` returns.
*
* Host callbacks are safe to trigger inside a scope: the `this`/argument
* handles the trampoline passes to a callback wrap C-owned pointers and
* are exempt from scope tracking (see `handleHostCall`), so the scope
* frees only handles the host actually owns. Handles a callback CREATES
* (including `dup()`s of its arguments) are tracked normally.
*/
withScope(fn) {
this.assertNotDisposed();
const enclosing = this._activeScope;
const tracked = /* @__PURE__ */ new Set();
this._activeScope = tracked;
const scope = { escape: (handle) => {
tracked.delete(handle);
enclosing?.add(handle);
return handle;
} };
try {
return fn(scope);
} finally {
this._activeScope = enclosing;
for (const handle of tracked) handle.dispose();
}
}
/**
* Export a handle as a snapshot-portable token.
*
* A handle's heap box lives in the VM's linear memory, so a
* `snapshot()` taken while the handle is alive carries it, and a VM
* restored from that snapshot has the identical box at the identical
* offset. `importHandle(token)` on the restored VM (or on this VM)
* re-materializes an owned handle for the same guest value without
* evaluating any guest code.
*
* Contract:
* - the handle must stay undisposed until after `snapshot()`; its
* box (and the reference it holds) must be part of the memory image;
* - the token is only meaningful to THIS VM and VMs restored from a
* snapshot of it taken while the handle was alive;
* - `importHandle` duplicates the underlying value (fresh reference,
* fresh box), so it can be called any number of times and each
* returned handle is independently owned and disposable. The
* exported box's own reference is intentionally never released on
* restored VMs (one retained reference per VM image, reclaimed
* with the VM).
*
* The intended use is boot-time capture: snapshot a VM after capturing
* references to pristine intrinsics but BEFORE evaluating untrusted or
* user code, then restore per task and import the captured handles,
* guaranteeing the references predate anything user code patched,
* without re-running capture code in the restored VM (where user-
* patched globals could observe it). See vercel/workflow's host-side
* serde for a worked example.
*/
exportHandle(handle) {
this.assertNotDisposed();
if (handle.vm !== this) throw new Error("exportHandle: handle belongs to a different VM");
if (handle.disposed) throw new Error("exportHandle: handle is disposed");
if (handle._isBorrowed) throw new Error("exportHandle: cannot export a borrowed handle (host-callback this/argument); its box is freed when the callback returns. dup() it and export the duplicate.");
return handle.ptr;
}
/**
* Re-materialize a handle from a token produced by `exportHandle`,
* on this VM, or on a VM restored from a snapshot taken while the
* exported handle was alive. Returns a NEW owned handle (the
* underlying value's refcount is incremented); dispose it like any
* other handle. See `exportHandle` for the full contract.
*/
importHandle(token) {
this.assertNotDisposed();
if (!Number.isInteger(token) || token <= 0 || token >= this.exports.memory.buffer.byteLength) throw new Error(`importHandle: invalid token ${token}`);
return new JSValueHandle(this, this.exports.qjs_dup_value(token));
}
/**
* Create a QuickJS function backed by a host callback whose registration is
* tied to the returned handle: disposing the handle unregisters the
* callback.
*
* Use this for short-lived callbacks (e.g. a visitor passed to
* `Map.prototype.forEach`) where the name is an implementation detail.
* `newFunction()` keeps its callback registered for the lifetime of the VM
* (by design, so that names can be re-registered after a snapshot is
* restored), which makes it unsuitable for callbacks created in a loop.
*
* The guest must not retain the function past disposal: calling it after
* the handle is disposed throws, because the callback is gone. Ephemeral
* functions do not survive snapshot/restore.
*/
newEphemeralFunction(fn) {
this.assertNotDisposed();
const name = `__ephemeral:${this.nextInternalId++}`;
this.hostCallbacks.set(name, fn);
const { ptr: namePtr, len: nameLen } = this.writeString(name);
const resultPtr = this.exports.qjs_new_host_function(namePtr, nameLen, 0);
this.exports.wasm_free(namePtr);
const handle = new JSValueHandle(this, resultPtr);
handle._onDispose = () => {
this.hostCallbacks.delete(name);
};
return handle;
}
/**
* Remove a host callback registered with `newFunction()` or
* `registerHostCallback()`. Returns true if a callback was removed.
*
* Any QuickJS function still referencing the name will throw when called,
* so only unregister once the guest can no longer reach it.
*/
unregisterHostCallback(name) {
return this.hostCallbacks.delete(name);
}
/**
* Create an internal host function that bypasses the duplicate-name check.
* Used for ephemeral callbacks (promise settle handlers, resolvePromise, etc.)
* that are not intended to survive snapshot/restore.
*/
newInternalFunction(name, fn) {
this.hostCallbacks.set(name, fn);
const { ptr: namePtr, len: nameLen } = this.writeString(name);
const resultPtr = this.exports.qjs_new_host_function(namePtr, nameLen, 0);
this.exports.wasm_free(namePtr);
return new JSValueHandle(this, resultPtr);
}
/**
* Create a new promise.
*
* Returns a Deferred with:
* - `handle` - the QuickJS promise object
* - `settled` - a host Promise that resolves when the QuickJS promise settles
* - `resolve(value)` - resolve the promise with a QuickJS value
* - `reject(value)` - reject the promise with a QuickJS value
*/
newPromise() {
this.assertNotDisposed();
const resolveOutPtr = this.exports.wasm_malloc(4);
const rejectOutPtr = this.exports.wasm_malloc(4);
const promisePtr = this.exports.qjs_new_promise(resolveOutPtr, rejectOutPtr);
const view = new DataView(this.exports.memory.buffer);
const resolvePtr = view.getUint32(resolveOutPtr, true);
const rejectPtr = view.getUint32(rejectOutPtr, true);
this.exports.wasm_free(resolveOutPtr);
this.exports.wasm_free(rejectOutPtr);
const promiseHandle = new JSValueHandle(this, promisePtr);
const resolveHandle = new JSValueHandle(this, resolvePtr);
const rejectHandle = new JSValueHandle(this, rejectPtr);
const vm = this;
vm._ownedHandles.add(resolveHandle);
vm._ownedHandles.add(rejectHandle);
let _settled = null;
return {
handle: promiseHandle,
get settled() {
if (!_settled) {
let settledResolve;
_settled = new Promise((res) => {
settledResolve = res;
});
const settleName = `__settle:${vm.nextInternalId++}`;
const onSettleFn = vm.newInternalFunction(settleName, () => {
settledResolve();
vm.hostCallbacks.delete(settleName);
return vm.undefined;
});
vm.promiseThenRaw(promiseHandle, onSettleFn, onSettleFn).dispose();
onSettleFn.dispose();
}
return _settled;
},
resolve(value) {
vm.callFunctionRaw(resolveHandle, vm.undefined, value).dispose();
vm._ownedHandles.delete(resolveHandle);
resolveHandle.dispose();
},
reject(value) {
vm.callFunctionRaw(rejectHandle, vm.undefined, value).dispose();
vm._ownedHandles.delete(rejectHandle);
rejectHandle.dispose();
}
};
}
/**
* Resolve a promise handle. Returns a host-side Promise that resolves
* with the settled value/error of the QuickJS promise.
*
* If the handle is not a promise, it is treated as an already-fulfilled value.
*
* The returned host Promise resolves to `{ value: JSValueHandle }` on
* fulfillment or `{ error: JSValueHandle }` on rejection.
*/
resolvePromise(promiseHandle) {
this.assertNotDisposed();
if (!this.exports.qjs_is_promise(promiseHandle.ptr)) return Promise.resolve({ value: promiseHandle.dup() });
const state = this.exports.qjs_promise_state(promiseHandle.ptr);
if (state === 1) return Promise.resolve({ value: new JSValueHandle(this, this.exports.qjs_promise_result(promiseHandle.ptr)) });
else if (state === 2) return Promise.resolve({ error: new JSValueHandle(this, this.exports.qjs_promise_result(promiseHandle.ptr)) });
return new Promise((hostResolve) => {
const id = this.nextInternalId++;
const fulfilledName = `__onFulfilled:${id}`;
const rejectedName = `__onRejected:${id}`;
const onFulfilled = this.newInternalFunction(fulfilledName, (...args) => {
const val = args[0]?.dup() ?? this.undefined;
this.hostCallbacks.delete(fulfilledName);
this.hostCallbacks.delete(rejectedName);
hostResolve({ value: val });
return this.undefined;
});
const onRejected = this.newInternalFunction(rejectedName, (...args) => {
const val = args[0]?.dup() ?? this.undefined;
this.hostCallbacks.delete(fulfilledName);
this.hostCallbacks.delete(rejectedName);
hostResolve({ error: val });
return this.undefined;
});
this.promiseThenRaw(promiseHandle, onFulfilled, onRejected).dispose();
onFulfilled.dispose();
onRejected.dispose();
});
}
/**
* Subscribe to a promise without executing guest code, via quickjs-ng's
* JS_PromiseThen: no Promise.prototype.then lookup, no Symbol.species.
* Returns the chained promise. Handler handles are borrowed (caller
* still owns and disposes them).
* @internal
*/
promiseThenRaw(promise, onFulfilled, onRejected) {
return new JSValueHandle(this, this.exports.qjs_promise_then(promise.ptr, onFulfilled.ptr, onRejected.ptr));
}
/**
* Mark a promise as handled: an eventual (or already-recorded) rejection
* will not be reported to `onUnhandledRejection`. Useful when the host
* observes a rejection through other means (e.g. `resolvePromise()`) and
* wants to suppress the unhandled-rejection callback for it.
*
* No-op if the handle is not a promise.
*/
markPromiseHandled(promise) {
this.assertNotDisposed();
this.exports.qjs_promise_mark_as_handled(promise.ptr);
}
/**
* Call a QuickJS function. If the function throws, a `JSException`
* is thrown on the host side.
*/
callFunction(func, thisVal, ...args) {
return this.throwIfException(this.callFunctionRaw(func, thisVal, ...args));
}
/**
* Invoke a QuickJS constructor with `new`, i.e. `new ctor(...args)`.
* If the constructor throws (including when `ctor` is not a constructor),
* a `JSException` is thrown on the host side.
*
* This is the counterpart to `callFunction` for building values inside
* the VM from the host, e.g. `new Date(iso)` on a constructor captured
* before any user code ran.
*/
construct(ctor, ...args) {
this.assertNotDisposed();
const argc = args.length;
let argvPtr = 0;
if (argc > 0) {
argvPtr = this.exports.wasm_malloc(argc * 4);
const view = new DataView(this.exports.memory.buffer);
for (let i = 0; i < argc; i++) view.setUint32(argvPtr + i * 4, args[i].ptr, true);
}
const resultPtr = this.exports.qjs_call_constructor(ctor.ptr, argc, argvPtr);
if (argvPtr) this.exports.wasm_free(argvPtr);
return this.throwIfException(new JSValueHandle(this, resultPtr));
}
/**
* Internal: call a QuickJS function without throwing on exception.
* Used by promise plumbing where exceptions are handled differently.
*/
callFunctionRaw(func, thisVal, ...args) {
this.assertNotDisposed();
const argc = args.length;
let argvPtr = 0;
if (argc > 0) {
argvPtr = this.exports.wasm_malloc(argc * 4);
const view = new DataView(this.exports.memory.buffer);
for (let i = 0; i < argc; i++) view.setUint32(argvPtr + i * 4, args[i].ptr, true);
}
const resultPtr = this.exports.qjs_call(func.ptr, thisVal.ptr, argc, argvPtr);
if (argvPtr) this.exports.wasm_free(argvPtr);
return new JSValueHandle(this, resultPtr);
}
/**
* Set a property on an object. Accepts string or JSValueHandle as key.
* JSValueHandle keys support symbols (including `Symbol.for()`).
*/
setProp(obj, key, value) {
this.assertNotDisposed();
if (typeof key === "string") {
const { ptr: namePtr } = this.writeString(key);
this.exports.qjs_set_prop_string(obj.ptr, namePtr, value.ptr);
this.exports.wasm_free(namePtr);
} else this.exports.qjs_set_prop_value(obj.ptr, key.ptr, value.ptr);
}
/**
* Define a property on an object with explicit property descriptor flags.
* Unlike `setProp`, this allows controlling `writable`, `enumerable`, and
* `configurable` attributes, matching `Object.defineProperty()` semantics.
* Accepts string or JSValueHandle as key (JSValueHandle keys support symbols).
*
* All flags default to `false` when not specified.
*/
defineProp(obj, key, value, descriptor) {
this.assertNotDisposed();
let flags = 0;
if (descriptor?.configurable) flags |= 1;
if (descriptor?.writable) flags |= 2;
if (descriptor?.enumerable) flags |= 4;
if (typeof key === "string") {
const { ptr: namePtr } = this.writeString(key);
this.exports.qjs_define_prop_string(obj.ptr, namePtr, value.ptr, flags);
this.exports.wasm_free(namePtr);
} else this.exports.qjs_define_prop_value(obj.ptr, key.ptr, value.ptr, flags);
}
/**
* Get a property from an object using a JSValueHandle key.
* Supports symbol keys (including `Symbol.for()`).
*/
getProp(obj, key) {
this.assertNotDisposed();
return new JSValueHandle(this, this.exports.qjs_get_prop_value(obj.ptr, key.ptr));
}
/**
* Get the current exception, if any.
*/
getException() {
this.assertNotDisposed();
return new JSValueHandle(this, this.exports.qjs_get_exception());
}
/**
* Create a new QuickJS Error object.
* Accepts a string message or a native Error object.
*/
newError(messageOrError) {
this.assertNotDisposed();
const errPtr = this.exports.qjs_new_error();
const errHandle = new JSValueHandle(this, errPtr);
if (typeof messageOrError === "string") {
const msgHandle = this.newString(messageOrError);
errHandle.setProp("message", msgHandle);
msgHandle.dispose();
} else {
const msgHandle = this.newString(messageOrError.message);
errHandle.setProp("message", msgHandle);
msgHandle.dispose();
if (messageOrError.name) {
const nameHandle = this.newString(messageOrError.name);
errHandle.setProp("name", nameHandle);
nameHandle.dispose();
}
if (messageOrError.stack) {
const stackHandle = this.newString(messageOrError.stack);
errHandle.setProp("stack", stackHandle);
stackHandle.dispose();
}
}
return errHandle;
}
/**
* Get the typeof a handle as a string.
*/
typeof(handle) {
this.assertNotDisposed();
const e = this.exports;
if (e.qjs_is_undefined(handle.ptr)) return "undefined";
if (e.qjs_is_null(handle.ptr)) return "object";
if (e.qjs_is_bool(handle.ptr)) return "boolean";
if (e.qjs_is_number(handle.ptr)) return "number";
if (e.qjs_is_big_int(handle.ptr)) return "bigint";
if (e.qjs_is_string(handle.ptr)) return "string";
if (e.qjs_is_symbol(handle.ptr)) return "symbol";
if (e.qjs_is_function(handle.ptr)) return "function";
if (e.qjs_is_object(handle.ptr)) return "object";
return "unknown";
}
/**
* Convert a QuickJS handle to a host JavaScript value.
* Handles strings, numbers, booleans, null, undefined, bigint, arrays,
* errors, functions, and plain objects. Circular references in objects
* are returned as `undefined`.
*/
dump(handle) {
this.assertNotDisposed();
return this._dump(handle, /* @__PURE__ */ new Map());
}
_dump(handle, visited) {
const e = this.exports;
if (e.qjs_is_undefined(handle.ptr)) return void 0;
if (e.qjs_is_null(handle.ptr)) return null;
if (e.qjs_is_bool(handle.ptr)) return e.qjs_get_bool(handle.ptr) !== 0;
if (e.qjs_is_number(handle.ptr)) return e.qjs_get_float64(handle.ptr);
if (e.qjs_is_string(handle.ptr)) return handle.toString();
if (e.qjs_is_big_int(handle.ptr)) return handle.toBigInt();
if (e.qjs_is_symbol(handle.ptr)) {
const descOutPtr = e.wasm_malloc(4);
const kind = e.qjs_get_symbol_description(handle.ptr, descOutPtr);
const descPtr = new DataView(e.memory.buffer).getUint32(descOutPtr, true);
e.wasm_free(descOutPtr);
if (kind === 1) {
const descHandle = new JSValueHandle(this, descPtr);
const description = descHandle.toString();
descHandle.dispose();
return Symbol.for(description);
} else if (kind === 2) {
new JSValueHandle(this, descPtr).dispose();
return;
}
return;
}
if (e.qjs_is_array_buffer(handle.ptr)) return handle.toArrayBuffer();
if (e.qjs_is_exception(handle.ptr)) {
const exc = this.getException();
const msg = exc.toString();
exc.dispose();
return new Error(msg);
}
if (e.qjs_is_function(handle.ptr)) return void 0;
if (e.qjs_is_object(handle.ptr)) {
const objPtr = e.qjs_get_value_ptr(handle.ptr);
if (objPtr) {
const existing = visited.get(objPtr);
if (existing !== void 0) return existing;
}
}
if (e.qjs_is_object(handle.ptr)) {
const byteOffsetPtr = e.wasm_malloc(4);
const byteLengthPtr = e.wasm_malloc(4);
const bytesPerElemPtr = e.wasm_malloc(4);
const abPtr = e.qjs_get_typed_array_buffer(handle.ptr, byteOffsetPtr, byteLengthPtr, bytesPerElemPtr);
const abHandle = new JSValueHandle(this, abPtr);
if (e.qjs_is_exception(abHandle.ptr) === 0) {
const view = new DataView(e.memory.buffer);
const byteOffset = view.getUint32(byteOffsetPtr, true);
const byteLength = view.getUint32(byteLengthPtr, true);
const bytesPerElement = view.getUint32(bytesPerElemPtr, true);
e.wasm_free(byteOffsetPtr);
e.wasm_free(byteLengthPtr);
e.wasm_free(bytesPerElemPtr);
const abLenPtr = e.wasm_malloc(4);
const abDataPtr = e.qjs_get_array_buffer(abHandle.ptr, abLenPtr);
e.wasm_free(abLenPtr);
abHandle.dispose();
if (abDataPtr !== 0) {
const rawBytes = new Uint8Array(e.memory.buffer, abDataPtr + byteOffset, byteLength).slice();
switch (bytesPerElement) {
case 1: return rawBytes;
case 2: return new Uint16Array(rawBytes.buffer);
case 4: return new Uint32Array(rawBytes.buffer);
case 8: return new Float64Array(rawBytes.buffer);
default: return rawBytes;
}
}
} else {
abHandle.dispose();
e.wasm_free(byteOffsetPtr);
e.wasm_free(byteLengthPtr);
e.wasm_free(bytesPerElemPtr);
}
}
if (e.qjs_is_array(handle.ptr)) {
const lenHandle = handle.getProp("length");
const len = e.qjs_get_float64(lenHandle.ptr);
lenHandle.dispose();
const arr = [];
const objPtr = e.qjs_get_value_ptr(handle.ptr);
if (objPtr) visited.set(objPtr, arr);
for (let i = 0; i < len; i++) {
const elemPtr = e.qjs_get_prop_uint32(handle.ptr, i);
const elemHandle = new JSValueHandle(this, elemPtr);
arr.push(this._dump(elemHandle, visited));
elemHandle.dispose();
}
return arr;
}
if (e.qjs_is_error(handle.ptr)) {
const nameHandle = handle.getProp("name");
const msgHandle = handle.getProp("message");
const stackHandle = handle.getProp("stack");
const name = nameHandle.isUndefined ? "Error" : nameHandle.toString();
const message = msgHandle.isUndefined ? "" : msgHandle.toString();
const stack = stackHandle.isUndefined ? void 0 : stackHandle.toString();
nameHandle.dispose();
msgHandle.dispose();
stackHandle.dispose();
const err = new Error(message);
err.name = name;
if (stack !== void 0) err.stack = stack;
return err;
}
if (e.qjs_is_object(handle.ptr)) {
const keysPtr = e.qjs_get_own_property_names(handle.ptr);
const keysHandle = new JSValueHandle(this, keysPtr);
if (e.qjs_is_exception(keysHandle.ptr) !== 0) {
keysHandle.dispose();
return {};
}
const lenHandle = keysHandle.getProp("length");
const len = e.qjs_get_float64(lenHandle.ptr);
lenHandle.dispose();
const obj = {};
const objPtr = e.qjs_get_value_ptr(handle.ptr);
if (objPtr) visited.set(objPtr, obj);
for (let i = 0; i < len; i++) {
const keyPtr = e.qjs_get_prop_uint32(keysHandle.ptr, i);
const keyHandle = new JSValueHandle(this, keyPtr);
const key = keyHandle.toString();
keyHandle.dispose();
const valHandle = handle.getProp(key);
obj[key] = this._dump(valHandle, visited);
valHandle.dispose();
}
keysHandle.dispose();
return obj;
}
}
/**
* Convert a host JavaScript value to a QuickJS handle.
*/
hostToHandle(value) {
this.assertNotDisposed();
if (value === void 0) return this.undefined;
if (value === null) return this.null;
if (value === true) return this.true;
if (value === false) return this.false;
if (typeof value === "number") return this.newNumber(value);
if (typeof value === "string") return this.newString(value);
if (typeof value === "bigint") return this.newBigInt(value);
if (typeof value === "symbol") {
const key = Symbol.keyFor(value);
if (key !== void 0) return this.newSymbolFor(key);
throw new Error(`Cannot convert local symbol to QuickJS handle. Use Symbol.for() for cross-boundary symbols.`);
}
if (value instanceof Promise) {
const deferred = this.newPromise();
value.then((r) => {
deferred.resolve(this.hostToHandle(r));
this.executePendingJobs();
}, (err) => {
deferred.reject(this.hostToHandle(err));
this.executePendingJobs();
});
return deferred.handle;
}
if (value instanceof Error) return this.newError(value);
if (value instanceof ArrayBuffer) return this.newArrayBuffer(value);
if (value instanceof Uint8Array) return this.newUint8Array(value);
if (ArrayBuffer.isView(value)) return this.newArrayBuffer(new Uint8Array(value.buffer, value.byteOffset, value.byteLength));
if (Array.isArray(value)) {
const arr = this.newArray();
for (let i = 0; i < value.length; i++) {
const elemHandle = this.hostToHandle(value[i]);
this.exports.qjs_set_prop_uint32(arr.ptr, i, elemHandle.ptr);
elemHandle.dispose();
}
return arr;
}
if (typeof value === "object" && value !== null) {
const obj = this.newObject();
for (const [key, val] of Object.entries(value)) {
const valHandle = this.hostToHandle(val);
obj.setProp(key, valHandle);
valHandle.dispose();
}
return obj;
}
return this.undefined;
}
/**
* Snapshot the entire VM state.
*
* Returns a snapshot containing the full WASM linear memory. Use
* `QuickJS.serializeSnapshot()` to convert to a versioned binary
* buffer for persistent storage.
*/
snapshot() {
this.assertNotDisposed();
return {
memory: new Uint8Array(this.exports.memory.buffer).slice(),
stackPointer: this.exports.__stack_pointer.value,
runtimePtr: this.exports.qjs_get_runtime_ptr(),
contextPtr: this.exports.qjs_get_context_ptr(),
extensions: this.loadedExtensions.map((ext) => ({
name: ext.name,
memoryBase: ext.memoryBase,
tableBase: ext.tableBase,
initFn: ext.initFn
}))
};
}
/**
* Re-register a host callback after restoring from a snapshot.
* The name must match the name passed to `newFunction()` before the snapshot.
*/
registerHostCallback(name, fn) {
this.hostCallbacks.set(name, fn);
}
/**
* Dispose the VM, releasing all references to the WASM instance
* so it can be garbage collected by the host JS engine.
*/
dispose() {
if (!this.disposed) {
this.disposed = true;
this._global = null;
this._undefined = null;
this._null = null;
this._true = null;
this._false = null;
this._ownedHandles.clear();
this.hostCallbacks.clear();
this._activeScope = null;
this.exports = null;
this.instance = null;
this.module = null;
}
}
/**
* Support for `using` declarations (Explicit Resource Management).
* Automatically disposes the VM when it goes out of scope.
*
* ```typescript
* using vm = await QuickJS.create(wasmBytes);
* vm.evalCode('1 + 2');
* // vm is automatically disposed here
* ```
*/
[Symbol.dispose]() {
this.dispose();
}
assertNotDisposed() {
if (this.disposed) throw new Error("QuickJS instance has been disposed");
}
/** @internal */
_getExports() {
return this.exports;
}
/** @internal */
_getMemory() {
return this.exports.memory;
}
/** @internal */
_writeString(str) {
return this.writeString(str);
}
/** @internal */
_readCString(ptr) {
return this.readCString(ptr);
}
};
/**
* Whether a string-typed property key survives the NUL-terminated
* C-string key APIs: embedded U+0000 truncates the key, and an UNPAIRED
* surrogate cannot be UTF-8 encoded (paired surrogates, such as those
* that encode emoji, are fine). Keys that don't survive are routed through length-aware
* guest string values instead.
*/
function stringKeyNeedsValuePath(key) {
return key.includes("\0") || LONE_SURROGATE_RE.test(key);
}
const wtf8Decoder = new TextDecoder();
const wtf8Encoder = new TextEncoder();
const LONE_SURROGATE_RE = /(?:[\uD800-\uDBFF](?![\uDC00-\uDFFF]))|(?:(?<![\uD800-\uDBFF])[\uDC00-\uDFFF])/;
/**
* Encode a JS string to WTF-8 bytes. Well-formed strings (including
* paired surrogates, such as emoji) take the native TextEncoder; strings with
* LONE surrogates take a manual encode that writes each unpaired
* surrogate as the 3-byte sequence quickjs's tolerant UTF-8 decoder
* accepts; TextEncoder would replace them with U+FFFD, silently
* corrupting every host→guest string (sources, property keys,
* newString values).
*/
function encodeWtf8(str) {
if (!LONE_SURROGATE_RE.test(str)) return wtf8Encoder.encode(str);
const bytes = [];
for (let i = 0; i < str.length; i++) {
const code = str.charCodeAt(i);
if (code < 128) bytes.push(code);
else if (code < 2048) bytes.push(192 | code >> 6, 128 | code & 63);
else if (code >= 55296 && code <= 56319 && i + 1 < str.length) {
const next = str.charCodeAt(i + 1);
if (next >= 56320 && next <= 57343) {
const cp = 65536 + (code - 55296 << 10) + (next - 56320);
bytes.push(240 | cp >> 18, 128 | cp >> 12 & 63, 128 | cp >> 6 & 63, 128 | cp & 63);
i++;
continue;
}
bytes.push(224 | code >> 12, 128 | code >> 6 & 63, 128 | code & 63);
} else bytes.push(224 | code >> 12, 128 | code >> 6 & 63, 128 | code & 63);
}
return new Uint8Array(bytes);
}
/**
* Decode WTF-8 bytes to a JS string. WTF-8 is UTF-8 extended with 3-byte
* sequences for surrogate code points (0xED 0xA0-0xBF 0x80-0xBF), which
* is how quickjs's JS_ToCStringLen2 encodes lone surrogates ("keep
* unmatched surrogate code points"). TextDecoder replaces those
* sequences with U+FFFD, so they are detected first and the rare strings
* containing them take a manual decode; everything else (the
* overwhelming majority) uses the native decoder.
*/
function decodeWtf8(bytes) {
let hasSurrogateSequence = false;
for (let i = 0; i < bytes.length - 1; i++) if (bytes[i] === 237 && bytes[i + 1] >= 160 && bytes[i + 1] <= 191) {
hasSurrogateSequence = true;
break;
}
if (!hasSurrogateSequence) return wtf8Decoder.decode(bytes);
let out = "";
let i = 0;
while (i < bytes.length) {
const b0 = bytes[i];
if (b0 < 128) {
out += String.fromCharCode(b0);
i += 1;
} else if (b0 < 224) {
out += String.fromCharCode((b0 & 31) << 6 | bytes[i + 1] & 63);
i += 2;
} else if (b0 < 240) {
out += String.fromCharCode((b0 & 15) << 12 | (bytes[i + 1] & 63) << 6 | bytes[i + 2] & 63);
i += 3;
} else {
const cp = (b0 & 7) << 18 | (bytes[i + 1] & 63) << 12 | (bytes[i + 2] & 63) << 6 | bytes[i + 3] & 63;
out += String.fromCodePoint(cp);
i += 4;
}
}
return out;
}
/**
* An exception thrown from QuickJS code. Extends `Error` so it works with
* standard error handling (`instanceof Error`, `.message`, `.stack`), and
* also exposes a `handle` property, a live `JSValueHandle` to the QuickJS
* exception value, allowing direct inspection of custom properties.
*
* The `handle` must be disposed when you're done with it (or use `using`).
* If the error propagates uncaught, the handle will be cleaned up when the
* VM is disposed.
*/
var JSException = class extends Error {
/**
* A live handle to the QuickJS exception value. You can read custom
* properties, call methods, etc. Must be disposed when done.
*/
handle;
#name;
#message;
#stack;
/** @internal */
constructor(handle) {
const env_1 = {
stack: [],
error: void 0,
hasError: false
};
try {
super();
this.handle = handle;
delete this.stack;
const msgHandle = __addDisposableResource(env_1, handle.getProp("message"), false);
this.#name = handle.getProp("name").consume((h) => h.isUndefined ? "Error" : h.toString());
this.#message = msgHandle.isUndefined ? handle.toString() : msgHandle.toString();
this.#stack = handle.getProp("stack").consume((h) => h.isUndefined ? void 0 : h.toString());
} catch (e_1) {
env_1.error = e_1;
env_1.hasError = true;
} finally {
__disposeResources(env_1);
}
}
get name() {
return this.#name;
}
set name(v) {
this.#name = v;
}
get message() {
return this.#message;
}
set message(v) {
this.#message = v;
}
get stack() {
return this.#stack;
}
set stack(v) {
this.#stack = v;
}
dispose() {
this.handle.dispose();
}
[Symbol.dispose]() {
this.handle.dispose();
}
};
/**
* A handle to a JSValue inside the QuickJS WASM instance.
*/
var JSValueHandle = class JSValueHandle {
/** The QuickJS VM instance this handle belongs to. */
vm;
/** @internal */
ptr;
disposed_ = false;
/**
* When true, this handle is a cached singleton (e.g. `undefined`, `null`,
* `true`, `false`, the global object) and `dispose()` is a no-op. This
* prevents code that routinely disposes handles (such as the object/array
* branches of `hostToHandle`) from freeing the shared heap `JSValue*` that
* the cached singleton still references, which would corrupt later reads.
* @internal
*/
singleton;
/**
* When true, this handle wraps a `JSValue*` OWNED BY THE C CALLER: the
* `this`/argument handles the host-call trampoline passes to a host
* callback (`handleHostCall`). The C side frees those values after the
* call returns, so `dispose()` is a no-op and the handle is never
* registered with an active `withScope()` (either would double-free
* the guest value and corrupt the heap). A callback that needs to
* retain an argument past its own invocation must `dup()` it; the
* duplicate takes a fresh reference and behaves like any owned handle.
* @internal
*/
borrowed;
/**
* Extra cleanup to run when this handle is disposed. Used by
* `newEphemeralFunction()` to unregister its host callback.
* @internal
*/
_onDispose;
constructor(vm, ptr, singleton = false, borrowed = false) {
this.vm = vm;
this.ptr = ptr;
this.singleton = singleton;
this.borrowed = borrowed;
if (!singleton && !borrowed) vm._activeScope?.add(this);
}
/**
* Whether this handle wraps a C-owned pointer (host-callback
* `this`/arguments). Borrowed handles must never be exported as
* snapshot tokens: the trampoline frees their boxes after the
* callback returns. @internal
*/
get _isBorrowed() {
return this.borrowed;
}
/**
* Whether `dispose()` has been called on this handle.
*
* Note that handle methods do not currently guard against use after
* disposal: reading from a disposed handle reads freed memory. Check this
* when a handle's lifetime is managed elsewhere (e.g. by `withScope()`).
*/
get disposed() {
return this.disposed_;
}
get isUndefined() {
return this.vm._getExports().qjs_is_undefined(this.ptr) !== 0;
}
get isNull() {
return this.vm._getExports().qjs_is_null(this.ptr) !== 0;
}
/**
* Get the promise state: 0 = pending, 1 = fulfilled, 2 = rejected
*/
get isBool() {
return this.vm._getExports().qjs_is_bool(this.ptr) !== 0;
}
get isNumber() {
return this.vm._getExports().qjs_is_number(this.ptr) !== 0;
}
get isString() {
return this.vm._getExports().qjs_is_string(this.ptr) !== 0;
}
get isSymbol() {
return this.vm._getExports().qjs_is_symbol(this.ptr) !== 0;
}
get isBigInt() {
return this.vm._getExports().qjs_is_big_int(this.ptr) !== 0;
}
get isObject() {
return this.vm._getExports().qjs_is_object(this.ptr) !== 0;
}
get isArray() {
return this.vm._getExports().qjs_is_array(this.ptr) !== 0;
}
get isFunction() {
return this.vm._getExports().qjs_is_function(this.ptr) !== 0;
}
get isError() {
return this.vm._getExports().qjs_is_error(this.ptr) !== 0;
}
get isPromise() {
return this.vm._getExports().qjs_is_promise(this.ptr) !== 0;
}
get isArrayBuffer() {
return this.vm._getExports().qjs_is_array_buffer(this.ptr) !== 0;
}
/**
* Whether this value is a Proxy exotic object.
*
* This is an engine-level check: it never fires proxy traps and cannot
* be determined (or spoofed) from within guest JavaScript. Use
* {@link getProxyTarget} / {@link getProxyHandler} to introspect a
* detected proxy without executing guest code.
*/
get isProxy() {
return this.vm._getExports().qjs_is_proxy(this.ptr) !== 0;
}
/**
* Whether this value is a Map (engine brand check: trap-free,
* spoof-proof, and unaffected by prototype/constructor mutation).
* A Proxy wrapping a Map returns false.
*/
get isMap() {
return this.vm._getExports().qjs_is_map(this.ptr) !== 0;
}
/**
* Whether this value is a Set (engine brand check: trap-free,
* spoof-proof, and unaffected by prototype/constructor mutation).
* A Proxy wrapping a Set returns false.
*/
get isSet() {
return this.vm._getExports().qjs_is_set(this.ptr) !== 0;
}
/**
* Whether this value is a Date (engine brand check: trap-free,
* spoof-proof, and unaffected by prototype/constructor mutation).
* A Proxy wrapping a Date returns false.
*/
get isDate() {
return this.vm._getExports().qjs_is_date(this.ptr) !== 0;
}
/**
* Whether this value is a RegExp (engine brand check: trap-free,
* spoof-proof, and unaffected by prototype/constructor mutation).
* A Proxy wrapping a RegExp returns false.
*/
get isRegExp() {
return this.vm._getExports().qjs_is_regexp(this.ptr) !== 0;
}
/** Whether this value is a WeakRef (engine brand check). */
get isWeakRef() {
return this.vm._getExports().qjs_is_weak_ref(this.ptr) !== 0;
}
/** Whether this value is a WeakMap (engine brand check). */
get isWeakMap() {
return this.vm._getExports().qjs_is_weak_map(this.ptr) !== 0;
}
/** Whether this value is a WeakSet (engine brand check). */
get isWeakSet() {
return this.vm._getExports().qjs_is_weak_set(this.ptr) !== 0;
}
/** Whether this value is a DataView (engine brand check). */
get isDataView() {
return this.vm._getExports().qjs_is_data_view(this.ptr) !== 0;
}
/**
* A numeric identity for the underlying heap value, or 0 for values that
* are not heap-allocated (numbers, booleans, `null`, `undefined`).
*
* Two handles to the same underlying object always report the same
* identity, and two live handles to different objects always report
* different identities, so this is the value to key a `Map` on when
* deduplicating or detecting cycles across handles (`dump()` uses it for
* exactly that).
*
* The identity is only meaningful while the value is alive; it is an
* address, so it may be reused after every handle to the value has been
* disposed. Do not persist it, and do not treat it as unforgeable: a
* number read out of the guest can trivially collide with one.
*/
get identity() {
return this.vm._getExports().qjs_get_value_ptr(this.ptr);
}
/**
* Extract the value as a boolean, applying JavaScript truthiness
* (equivalent to `!!value` inside the VM).
*/
toBoolean() {
return this.vm._getExports().qjs_get_bool(this.ptr) !== 0;
}
/**
* The internal QuickJS class ID of this value, or 0 for non-objects.
* Useful as a generic engine-level brand when no dedicated `is*`
* getter exists. Class IDs are stable within a VM instance but are an
* engine implementation detail, so prefer the dedicated getters.
*/
get classId() {
return this.vm._getExports().qjs_get_class_id(this.ptr);
}
/**
* The engine-level class name of this value, e.g. `"Object"`, `"Map"`,
* `"Date"`, `"RegExp"`, or the registered name of an extension-defined
* class like `"URL"`, or `undefined` for non-objects and unnamed
* internal classes.
*
* Unlike `constructorName` (which reads the `constructor` and `name`
* properties and can therefore fire getters/proxy traps and be spoofed),
* this is trap-free: it reads the engine's class table directly, never
* executes guest code, and cannot be forged by reassigning prototypes or
* constructors. Note that the engine registers the Proxy class under the
* name `"Object"` (mirroring `Object.prototype.toString`), so use `isProxy`
* to detect proxies and `getProxyTarget()` to read the target's brand.
*/
get className() {
const h = new JSValueHandle(this.vm, this.vm._getExports().qjs_get_class_name(this.ptr));
if (this.vm._getExports().qjs_is_exception(h.ptr) !== 0) {
h.dispose();
throw new JSException(this.vm.getException());
}
try {
return h.isUndefined ? void 0 : h.toString();
} finally {
h.dispose();
}
}
get promiseState() {
return this.vm._getExports().qjs_promise_state(this.ptr);
}
/**
* Get the typeof this value as a string.
* Returns the same values as the native `typeof` operator.
*/
get typeof() {
return this.vm.typeof(this);
}
/**
* Get the length property of this value (for arrays, strings, etc.).
*/
get length() {
const h = this.getProp("length");
const n = h.toNumber();
h.dispose();
return n;
}
/**
* Get the constructor name of this object, or undefined if unavailable.
*/
get constructorName() {
const ctor = this.getProp("constructor");
if (ctor.isUndefined || ctor.isNull) {
ctor.dispose();
return;
}
const name = ctor.getProp("name");
ctor.dispose();
if (name.isUndefined || name.isNull) {
name.dispose();
return;
}
const result = name.toString();
name.dispose();
return result;
}
/**
* Get the own enumerable string property names (equivalent to Object.keys()).
*/
keys() {
const e = this.vm._getExports();
const keysPtr = e.qjs_get_own_property_names(this.ptr);
const keysHandle = new JSValueHandle(this.vm, keysPtr);
if (e.qjs_is_exception(keysHandle.ptr) !== 0) {
keysHandle.dispose();
return [];
}
const lenHandle = keysHandle.getProp("length");
const len = e.qjs_get_float64(lenHandle.ptr);
lenHandle.dispose();
const result = [];
for (let i = 0; i < len; i++) {
const keyPtr = e.qjs_get_prop_uint32(keysHandle.ptr, i);
const keyHandle = new JSValueHandle(this.vm, keyPtr);
result.push(keyHandle.toString());
keyHandle.dispose();
}
keysHandle.dispose();
return result;
}
/**
* Get all own property names including non-enumerable ones
* (equivalent to Object.getOwnPropertyNames()).
*/
getOwnPropertyNames() {
const e = this.vm._getExports();
const keysPtr = e.qjs_get_own_property_names_all(this.ptr);
const keysHandle = new JSValueHandle(this.vm, keysPtr);
if (e.qjs_is_exception(keysHandle.ptr) !== 0) {
keysHandle.dispose();
return [];
}
const lenHandle = keysHandle.getProp("length");
const len = e.qjs_get_float64(lenHandle.ptr);
lenHandle.dispose();
const result = [];
for (let i = 0; i < len; i++) {
const keyPtr = e.qjs_get_prop_uint32(keysHandle.ptr, i);
const keyHandle = new JSValueHandle(this.vm, keyPtr);
result.push(keyHandle.toString());
keyHandle.dispose();
}
keysHandle.dispose();
return result;
}
/**
* Get ALL own property keys (strings and symbols), including
* non-enumerable (equivalent to Reflect.ownKeys()).
*
* String keys are returned as strings; symbol keys are returned as
* JSValueHandles which the caller must dispose.
*
* Trap-free for ordinary objects; fires the `ownKeys` trap for a
* Proxy (check {@link isProxy} first if that matters).
*/
getOwnPropertyKeys() {
const e = this.vm._getExports();
const keysPtr = e.qjs_get_own_property_keys(this.ptr);
const keysHandle = new JSValueHandle(this.vm, keysPtr);
if (e.qjs_is_exception(keysHandle.ptr) !== 0) {
keysHandle.dispose();
return [];
}
const lenHandle = keysHandle.getProp("length");
const len = e.qjs_get_float64(lenHandle.ptr);
lenHandle.dispose();
const result = [];
for (let i = 0; i < len; i++) {
const keyPtr = e.qjs_get_prop_uint32(keysHandle.ptr, i);
const keyHandle = new JSValueHandle(this.vm, keyPtr);
if (keyHandle.isSymbol) result.push(keyHandle);
else {
result.push(keyHandle.toString());
keyHandle.dispose();
}
}
keysHandle.dispose();
return result;
}
/**
* Get the own property descriptor for a key WITHOUT invoking getters
* (equivalent to Object.getOwnPropertyDescriptor()).
*
* This is the safe way to inspect a property that may be an accessor:
* a data property yields `{ value, writable, enumerable, configurable }`,
* an accessor property yields `{ get, set, enumerable, configurable }`
* where `get`/`set` are handles to the accessor functions themselves
* (never invoked). Returns undefined if there is no such own property.
*
* The `value`/`get`/`set` handles are owned by the caller and must be
* disposed.
*
* Trap-free for ordinary objects; fires the `getOwnPropertyDescriptor`
* trap for a Proxy (check {@link isProxy} first if that matters).
*/
getOwnPropertyDescriptor(key) {
const env_2 = {
stack: [],
error: void 0,
hasError: false
};
try {
const e = this.vm._getExports();
let keyHandle;
let keyPtr;
if (typeof key === "string") {
keyHandle = this.vm.newString(key);
keyPtr = keyHandle.ptr;
} else keyPtr = key.ptr;
const descPtr = e.qjs_get_own_property_descriptor(this.ptr, keyPtr);
keyHandle?.dispose();
if (descPtr === 0) return void 0;
const descHandle = __addDisposableResource(env_2, new JSValueHandle(this.vm, descPtr), false);
if (e.qjs_is_exception(descHandle.ptr) !== 0) throw new JSException(this.vm.getException());
const enumerable = descHandle.getProp("enumerable").consume((h) => e.qjs_get_bool(h.ptr) !== 0);
const configurable = descHandle.getProp("configurable").consume((h) => e.qjs_get_bool(h.ptr) !== 0);
if (descHandle.hasOwnProperty("value")) return {
value: descHandle.getProp("value"),
writable: descHandle.getProp("writable").consume((h) => e.qjs_get_bool(h.ptr) !== 0),
enumerable,
configurable
};
return {
get: descHandle.getProp("get"),
set: descHandle.getProp("set"),
enumerable,
configurable
};
} catch (e_2) {
env_2.error = e_2;
env_2.hasError = true;
} finally {
__disposeResources(env_2);
}
}
/**
* Check if a property is an own property (equivalent to Object.prototype.hasOwnProperty).
*/
hasOwnProperty(name) {
if (stringKeyNeedsValuePath(name)) {
const env_3 = {
stack: [],
error: void 0,
hasError: false
};
try {
const keyHandle = __addDisposableResource(env_3, this.vm.newString(name), false);
return this.vm._getExports().qjs_has_own_property_value(this.ptr, keyHandle.ptr) === 1;
} catch (e_3) {
env_3.error = e_3;
env_3.hasError = true;
} finally {
__disposeResources(env_3);
}
}
const { ptr: namePtr } = this.vm._writeString(name);
const result = this.vm._getExports().qjs_has_own_property(this.ptr, namePtr);
this.vm._getExports().wasm_free(namePtr);
return result === 1;
}
/**
* Check if a property is enumerable (equivalent to Object.prototype.propertyIsEnumerable).
*/
propertyIsEnumerable(name) {
if (stringKeyNeedsValuePath(name)) {
const env_4 = {
stack: [],
error: void 0,
hasError: false
};
try {
const keyHandle = __addDisposableResource(env_4, this.vm.newString(name), false);
return this.vm._getExports().qjs_property_is_enumerable_value(this.ptr, keyHandle.ptr) === 1;
} catch (e_4) {
env_4.error = e_4;
env_4.hasError = true;
} finally {
__disposeResources(env_4);
}
}
const { ptr: namePtr } = this.vm._writeString(name);
const result = this.vm._getExports().qjs_property_is_enumerable(this.ptr, namePtr);
this.vm._getExports().wasm_free(namePtr);
return result === 1;
}
/**
* Get the prototype of this object (equivalent to Object.getPrototypeOf()).
*/
getPrototypeOf() {
const protoPtr = this.vm._getExports().qjs_get_prototype_of(this.ptr);
return new JSValueHandle(this.vm, protoPtr);
}
/**
* Get the `[[ProxyTarget]]` of this Proxy without firing any traps.
* Throws {@link JSException} if this value is not a Proxy; check
* {@link isProxy} first. Note the target may itself be a Proxy.
*/
getProxyTarget() {
const ptr = this.vm._getExports().qjs_get_proxy_target(this.ptr);
const handle = new JSValueHandle(this.vm, ptr);
if (this.vm._getExports().qjs_is_exception(handle.ptr) !== 0) {
handle.dispose();
throw new JSException(this.vm.getException());
}
return handle;
}
/**
* Get the `[[ProxyHandler]]` of this Proxy without firing any traps.
* Throws {@link JSException} if this value is not a Proxy; check
* {@link isProxy} first.
*/
getProxyHandler() {
const ptr = this.vm._getExports().qjs_get_proxy_handler(this.ptr);
const handle = new JSValueHandle(this.vm, ptr);
if (this.vm._getExports().qjs_is_exception(handle.ptr) !== 0) {
handle.dispose();
throw new JSException(this.vm.getException());
}
return handle;
}
/**
* Get a property by name.
*/
getProp(name) {
if (stringKeyNeedsValuePath(name)) {
const env_5 = {
stack: [],
error: void 0,
hasError: false
};
try {
const keyHandle = __addDisposableResource(env_5, this.vm.newString(name), false);
return this.vm.getProp(this, keyHandle);
} catch (e_5) {
env_5.error = e_5;
env_5.hasError = true;
} finally {
__disposeResources(env_5);
}
}
const { ptr: namePtr } = this.vm._writeString(name);
const resultPtr = this.vm._getExports().qjs_get_prop_string(this.ptr, namePtr);
this.vm._getExports().wasm_free(namePtr);
return new JSValueHandle(this.vm, resultPtr);
}
/**
* Set a property by name.
*/
setProp(name, value) {
if (stringKeyNeedsValuePath(name)) {
const env_6 = {
stack: [],
error: void 0,
hasError: false
};
try {
const keyHandle = __addDisposableResource(env_6, this.vm.newString(name), false);
this.vm.setProp(this, keyHandle, value);
return;
} catch (e_6) {
env_6.error = e_6;
env_6.hasError = true;
} finally {
__disposeResources(env_6);
}
}
const { ptr: namePtr } = this.vm._writeString(name);
this.vm._getExports().qjs_set_prop_string(this.ptr, namePtr, value.ptr);
this.vm._getExports().wasm_free(namePtr);
}
/**
* Define a property with explicit property descriptor flags.
* Unlike `setProp`, this allows controlling `writable`, `enumerable`, and
* `configurable` attributes, matching `Object.defineProperty()` semantics.
* Accepts string or JSValueHandle as key (JSValueHandle keys support symbols).
*
* All flags default to `false` when not specified.
*/
defineProp(key, value, descriptor) {
let flags = 0;
if (descriptor?.configurable) flags |= 1;
if (descriptor?.writable) flags |= 2;
if (descriptor?.enumerable) flags |= 4;
if (typeof key === "string") {
if (stringKeyNeedsValuePath(key)) {
const env_7 = {
stack: [],
error: void 0,
hasError: false
};
try {
const keyHandle = __addDisposableResource(env_7, this.vm.newString(key), false);
this.vm._getExports().qjs_define_prop_value(this.ptr, keyHandle.ptr, value.ptr, flags);
return;
} catch (e_7) {
env_7.error = e_7;
env_7.hasError = true;
} finally {
__disposeResources(env_7);
}
}
const { ptr: namePtr } = this.vm._writeString(key);
this.vm._getExports().qjs_define_prop_string(this.ptr, namePtr, value.ptr, flags);
this.vm._getExports().wasm_free(namePtr);
} else this.vm._getExports().qjs_define_prop_value(this.ptr, key.ptr, value.ptr, flags);
}
/**
* Extract the value as a number.
*/
toNumber() {
return this.vm._getExports().qjs_get_float64(this.ptr);
}
/**
* Extract the value as a BigInt.
*/
toBigInt() {
const e = this.vm._getExports();
const loPtr = e.wasm_malloc(4);
const hiPtr = e.wasm_malloc(4);
if (e.qjs_get_big_int64(this.ptr, loPtr, hiPtr) !== 0) {
e.wasm_free(loPtr);
e.wasm_free(hiPtr);
throw new Error("Failed to convert value to BigInt");
}
const view = new DataView(e.memory.buffer);
const lo = view.getUint32(loPtr, true);
const hi = view.getInt32(hiPtr, true);
e.wasm_free(loPtr);
e.wasm_free(hiPtr);
return BigInt(hi) << 32n | BigInt(lo);
}
/**
* Extract the value as an ArrayBuffer (copies from WASM memory).
* Works on ArrayBuffer values. For typed arrays, gets the underlying buffer.
*/
toArrayBuffer() {
const e = this.vm._getExports();
const lenOutPtr = e.wasm_malloc(4);
if (e.qjs_is_array_buffer(this.ptr)) {
const dataPtr = e.qjs_get_array_buffer(this.ptr, lenOutPtr);
if (dataPtr === 0) {
e.wasm_free(lenOutPtr);
throw new Error("Failed to get ArrayBuffer data");
}
const len = new DataView(e.memory.buffer).getUint32(lenOutPtr, true);
e.wasm_free(lenOutPtr);
return new Uint8Array(e.memory.buffer, dataPtr, len).slice().buffer;
}
e.wasm_free(lenOutPtr);
const byteOffsetPtr = e.wasm_malloc(4);
const byteLengthPtr = e.wasm_malloc(4);
const bytesPerElemPtr = e.wasm_malloc(4);
const abPtr = e.qjs_get_typed_array_buffer(this.ptr, byteOffsetPtr, byteLengthPtr, bytesPerElemPtr);
const abHandle = new JSValueHandle(this.vm, abPtr);
if (this.vm._getExports().qjs_is_exception(abHandle.ptr) !== 0) {
abHandle.dispose();
e.wasm_free(byteOffsetPtr);
e.wasm_free(byteLengthPtr);
e.wasm_free(bytesPerElemPtr);
throw new Error("Value is not an ArrayBuffer or typed array");
}
const view = new DataView(e.memory.buffer);
const byteOffset = view.getUint32(byteOffsetPtr, true);
const byteLength = view.getUint32(byteLengthPtr, true);
e.wasm_free(byteOffsetPtr);
e.wasm_free(byteLengthPtr);
e.wasm_free(bytesPerElemPtr);
const abLenPtr = e.wasm_malloc(4);
const abDataPtr = e.qjs_get_array_buffer(abHandle.ptr, abLenPtr);
e.wasm_free(abLenPtr);
abHandle.dispose();
if (abDataPtr === 0) throw new Error("Failed to get ArrayBuffer data from typed array");
return new Uint8Array(e.memory.buffer, abDataPtr + byteOffset, byteLength).slice().buffer;
}
/**
* Extract the value as a Uint8Array (copies from WASM memory).
* Works on Uint8Array, ArrayBuffer, and other typed array values.
*/
toUint8Array() {
return new Uint8Array(this.toArrayBuffer());
}
/**
* Extract the value as a string. Works on any value.
*
* For values that are not already strings this performs a JavaScript
* string conversion, which **executes guest code**: `toString()` /
* `valueOf()` / `Symbol.toPrimitive` on the value or its prototype chain,
* and proxy traps. Guard with `isString` when the caller must not run guest
* code, and call a captured intrinsic (e.g. `URL.prototype.toString` via
* `vm.callFunction`) when a specific conversion is wanted.
*/
toString() {
const e = this.vm._getExports();
const lenPtr = e.wasm_malloc(4);
if (lenPtr === 0) throw new Error("wasm_malloc failed");
try {
const cstrPtr = e.qjs_get_string_len(this.ptr, lenPtr);
if (cstrPtr === 0) return "<null>";
const len = new DataView(e.memory.buffer).getUint32(lenPtr, true);
const str = decodeWtf8(new Uint8Array(e.memory.buffer, cstrPtr, len));
e.qjs_free_cstring(cstrPtr);
return str;
} finally {
e.wasm_free(lenPtr);
}
}
/**
* Use this handle, then dispose it. Returns the callback's return value.
*/
consume(fn) {
try {
return fn(this);
} finally {
this.dispose();
}
}
/**
* Duplicate this handle (increment refcount).
*/
dup() {
return new JSValueHandle(this.vm, this.vm._getExports().qjs_dup_value(this.ptr));
}
/**
* Dispose this handle, freeing the heap-allocated JSValue.
* Safe to call after the VM has been disposed (becomes a no-op).
*/
dispose() {
if (this.singleton || this.borrowed) return;
if (!this.disposed_) {
this.disposed_ = true;
this._onDispose?.();
this._onDispose = void 0;
const exports = this.vm._getExports();
if (exports) exports.qjs_free_value(this.ptr);
}
}
/**
* Support for `using` declarations (Explicit Resource Management).
* Automatically disposes the handle when it goes out of scope.
*
* ```typescript
* using result = vm.evalCode('1 + 2');
* console.log(result.toNumber()); // 3
* // result is automatically disposed here
* ```
*/
[Symbol.dispose]() {
this.dispose();
}
};
//#endregion
//#region extensions/code-mode-quickjs/src/source-location.ts
function readSourceLocation(vm) {
const descriptor = vm.global.getOwnPropertyDescriptor(SOURCE_LOCATION_KEY);
if (!descriptor) return;
try {
if (descriptor.writable || descriptor.configurable || descriptor.enumerable || !descriptor.value?.isString) return;
const value = JSON.parse(descriptor.value.toString());
if (!isRecord(value)) return;
const { file, lineOffset, lineCount, columnOffset, endColumn } = value;
const isOffset = (offset) => typeof offset === "number" && Number.isSafeInteger(offset) && offset >= 0;
if (file !== "openclaw-code-mode:user.js" || !isOffset(lineOffset) || !isOffset(lineCount) || lineCount === 0 || !isOffset(columnOffset) || !isOffset(endColumn) || endColumn === 0 || !Number.isSafeInteger(lineOffset + lineCount) || lineCount === 1 && endColumn <= columnOffset) return;
return {
file,
lineOffset,
lineCount,
columnOffset,
endColumn
};
} catch {
return;
} finally {
descriptor.value?.dispose();
descriptor.get?.dispose();
descriptor.set?.dispose();
}
}
//#endregion
//#region extensions/code-mode-quickjs/src/code-mode.worker.ts
/** QuickJS worker for Code Mode guest execution and suspended VM snapshots. */
var CodeModeWorkerFailure = class extends Error {
constructor(code, message, failurePhase) {
super(message);
this.failurePhase = failurePhase;
this.name = "CodeModeWorkerFailure";
this.code = code;
}
};
function isQuickJsInterruptedError(error) {
return error instanceof JSException && error.message === "interrupted";
}
function formatQuickJsError(name, message, stack, location) {
const header = message ? `${name}: ${message}` : name;
const sourceStack = normalizeSourceStack(stack, location);
if (!sourceStack || sourceStack.split(/\r?\n/, 1)[0] === header) return header;
return `${header}\n${sourceStack}`;
}
function errorMessage(error, location) {
if (error instanceof JSException) return formatQuickJsError(error.name, error.message, error.stack, location);
if (error instanceof Error) return error.message || String(error);
return String(error);
}
function trackPromiseRejection(promise, reason, handled) {
const vm = promise.vm;
vm.global.getProp("__openclawTrackRejection").consume((track) => vm.callFunction(track, vm.undefined, promise, reason, handled ? vm.true : vm.false).dispose());
}
function createHostRequestHandler(params) {
return (methodHandle, argsHandle, bridgeIdHandle, callStackHandle) => {
if (params.bridge.pendingRequests.length >= params.config.maxPendingToolCalls) {
params.bridge.admissionFailure ??= new CodeModeWorkerFailure("invalid_input", "too many pending code mode tool calls");
throw params.bridge.admissionFailure;
}
const method = methodHandle.toString();
if (method !== "search" && method !== "describe" && method !== "callValue" && method !== "resultSave" && method !== "resultLoad" && method !== "resultDelete" && method !== "nodes" && method !== "yield" && method !== "namespace" && method !== "agentSpawn" && method !== "agentWait" && method !== "skillsList" && method !== "skillsSearch" && method !== "skillsRead" && method !== "sleep" && method !== "swarmNote") throw new Error("unsupported code mode bridge method");
const args = JSON.parse(argsHandle.toString());
if (!Array.isArray(args)) throw new Error("invalid code mode bridge arguments: expected an array");
const id = bridgeIdHandle?.toString();
if (!id?.startsWith(`bridge:${method}:`) || !/^bridge:[A-Za-z]+:[1-9]\d*$/u.test(id)) throw new Error("invalid code mode bridge id");
if (params.bridge.pendingRequests.some((request) => request.id === id)) throw new Error("duplicate code mode bridge id");
params.bridge.pendingRequests.push({
id,
method,
args
});
const stack = callStackHandle?.isString ? callStackHandle.toString().slice(0, 8192) : "";
return params.vm.newString(normalizeSourceStack(stack, readSourceLocation(params.vm)) ?? "");
};
}
function createHostCancelRequestHandler(params) {
return (idHandle) => {
const id = idHandle.toString();
const index = params.bridge.pendingRequests.findIndex((request) => request.id === id);
if (index >= 0) {
params.bridge.pendingRequests.splice(index, 1);
params.bridge.canceledRequestIds.push(id);
}
return params.vm.undefined;
};
}
async function createVm(input, bridge) {
let deadlineMs = performance.now() + input.config.timeoutMs;
let timedOut = false;
let paused = false;
const deadlineReached = () => !paused && performance.now() >= deadlineMs;
const options = {
wasm: input.wasmModule,
extensions: input.wasmExtensions,
memoryLimit: input.config.memoryLimitBytes,
maxStackSize: MAX_STACK_SIZE,
timezoneOffset: 0,
onUnhandledRejection: trackPromiseRejection,
interruptHandler: () => {
timedOut = deadlineReached();
return timedOut;
}
};
const vm = input.kind === "resume" ? await QuickJS.restore(input.continuation, options) : await QuickJS.create(options);
try {
if (input.kind === "resume") input.continuation.memory = /* @__PURE__ */ new Uint8Array();
const callbacks = [
["__openclawHostRequest", createHostRequestHandler({
vm,
bridge,
config: input.config
})],
["__openclawHostCancelRequest", createHostCancelRequestHandler({
vm,
bridge
})],
["__openclawHostTakeBridgeReply", () => {
const request = bridge.replies?.[bridge.replyIndex];
if (!request) return vm.undefined;
bridge.replyIndex++;
const reply = vm.newObject();
try {
try {
var _usingCtx$1 = _usingCtx();
const id = _usingCtx$1.u(vm.newString(request.id));
const json = _usingCtx$1.u(vm.newString(request.json));
vm.defineProp(reply, "id", id);
vm.defineProp(reply, "ok", request.ok ? vm.true : vm.false);
vm.defineProp(reply, "json", json);
} catch (_) {
_usingCtx$1.e = _;
} finally {
_usingCtx$1.d();
}
} catch (error) {
reply.dispose();
throw error;
} finally {
request.json = "";
}
return reply;
}],
["__openclawHostObserveNetworkContent", () => {
bridge.networkContentObserved = true;
return vm.undefined;
}]
];
for (const [name, callback] of callbacks) if (input.kind === "resume") vm.registerHostCallback(name, callback);
else vm.newFunction(name, callback).consume((handle) => vm.global.setProp(name, handle));
if (input.kind === "exec") {
for (const [name, value] of [
["__openclawCatalog", input.catalog],
["__openclawNamespaces", input.namespaces],
["__openclawApiFiles", input.apiFiles ?? []],
["__openclawSwarmEnabled", input.swarmEnabled === true],
["__openclawMaxPendingToolCalls", input.config.maxPendingToolCalls]
]) vm.hostToHandle(value).consume((handle) => vm.global.setProp(name, handle));
vm.evalCode(CODE_MODE_CONTROLLER_SOURCE, "openclaw-code-mode:controller.js").dispose();
}
return {
vm,
didTimeout: () => timedOut || deadlineReached(),
pauseBudget: () => {
timedOut ||= deadlineReached();
paused = true;
},
setBudget: (timeoutMs) => {
paused = false;
timedOut = false;
deadlineMs = performance.now() + timeoutMs;
}
};
} catch (error) {
vm.dispose();
throw error;
}
}
function takeOutput(vm) {
return vm.global.getProp("__openclawTakeOutputJson").consume((take) => vm.callFunction(take, vm.undefined).consume((output) => {
const parsed = JSON.parse(output.toString());
return Array.isArray(parsed) ? parsed : [];
}));
}
function takeOutputSafely(vm) {
try {
return takeOutput(vm);
} catch {
return [];
}
}
function captureWorkerResult(result, config, retainFinalValue = false) {
const output = captureCodeModeOutput(result.output, config.maxOutputBytes);
if (result.status === "completed") return {
...result,
output,
value: captureCodeModeValue(result.value, config.maxOutputBytes, retainFinalValue ? Math.min(config.memoryLimitBytes, config.maxSnapshotBytes) : config.maxOutputBytes)
};
return result.status === "failed" ? {
...result,
output,
error: boundCodeModeError(result.error, config.maxOutputBytes)
} : {
...result,
output
};
}
function failedWorkerResult(code, error, output = [], failurePhase) {
return {
status: "failed",
code,
error,
failurePhase: failurePhase ?? (code === "invalid_input" ? "input" : "guest"),
bridgeDispatchStarted: false,
output
};
}
function workerFailureResult(params) {
const timedOut = params.didTimeout() || isQuickJsInterruptedError(params.error);
const output = params.output.length > 0 ? params.output : takeOutputSafely(params.vm);
if (timedOut) return failedWorkerResult("timeout", "code mode timeout exceeded", output);
if (params.error instanceof CodeModeWorkerFailure) return failedWorkerResult(params.error.code, params.error.message, output, params.error.failurePhase);
return failedWorkerResult(params.error instanceof ToolInputError ? "invalid_input" : "internal_error", errorMessage(params.error, readSourceLocation(params.vm)), output);
}
async function readCompletedResult(vm, resultHandle) {
const settled = await vm.resolvePromise(resultHandle);
if ("error" in settled) return settled.error.consume((error) => {
const dumped = vm.dump(error);
const bridgeCode = vm.global.getProp("__openclawBridgeFailureCode").consume((read) => vm.callFunction(read, vm.undefined, error).consume((value) => {
const code = vm.dump(value);
return code === "invalid_input" || code === "internal_error" ? code : void 0;
}));
if (bridgeCode === void 0 && dumped instanceof Error && dumped.name === "ReferenceError" && /^(?:require|module|process) is not defined$/u.test(dumped.message)) throw new CodeModeWorkerFailure("invalid_input", "code mode module access is disabled.");
const text = dumped instanceof Error ? formatQuickJsError(dumped.name, dumped.message, dumped.stack, readSourceLocation(vm)) : errorMessage(dumped);
throw new CodeModeWorkerFailure(bridgeCode ?? "internal_error", text, bridgeCode === void 0 ? void 0 : "bridge");
});
return settled.value.consume((value) => JSON.parse(value.toString()));
}
function waitingResult(params) {
const snapshot = params.vm.snapshot();
const metadata = QuickJS.serializeSnapshot({
...snapshot,
memory: /* @__PURE__ */ new Uint8Array()
});
if (snapshot.memory.byteLength + metadata.byteLength > params.config.maxSnapshotBytes) throw new CodeModeWorkerFailure("snapshot_limit_exceeded", "code mode snapshot limit exceeded");
return {
status: "waiting",
continuation: snapshot,
pendingRequests: params.bridge.pendingRequests,
canceledRequestIds: params.bridge.canceledRequestIds,
settlementMode: params.settlementMode,
output: params.output
};
}
async function runVmExecution(params) {
let output = [];
let prepare = params.prepare;
let consumed = params.channel?.consumeInput;
try {
for (;;) {
prepare();
consumed?.();
consumed = void 0;
params.vm.executePendingJobs();
if (params.bridge.admissionFailure) throw params.bridge.admissionFailure;
const admissionError = params.vm.global.getProp("__openclawAdmissionError").consume((read) => params.vm.callFunction(read, params.vm.undefined).consume((error) => error.isString ? error.toString() : void 0));
if (admissionError) throw new CodeModeWorkerFailure("invalid_input", admissionError);
params.vm.global.getProp("__openclawDrainQueuedRequests").consume((drain) => params.vm.callFunction(drain, params.vm.undefined).dispose());
output = takeOutput(params.vm);
const resultHandle = params.vm.global.getProp("__openclawResult");
try {
try {
var _usingCtx3 = _usingCtx();
const promisePending = resultHandle.isPromise && resultHandle.promiseState === 0;
if (promisePending && params.bridge.pendingRequests.length === 0) throw new Error("code mode promise is pending without host work");
const requiredPendingRequestIds = params.bridge.pendingRequests.map((request) => request.id);
if (promisePending || requiredPendingRequestIds.length > 0) {
const settlementMode = promisePending ? { kind: "awaiting" } : {
kind: "draining",
requiredRequestIds: requiredPendingRequestIds
};
if (params.channel) {
params.pauseBudget();
const response = await params.channel.request({
status: "boundary",
...params.bridge.networkContentObserved ? { networkContentObserved: true } : {},
pendingRequests: params.bridge.pendingRequests,
canceledRequestIds: params.bridge.canceledRequestIds,
settlementMode,
output: captureCodeModeOutput(output, params.config.maxOutputBytes),
memoryUsedBytes: params.vm.getMemoryUsage().memoryUsedSize
});
output = [];
consumed = response.consumed;
const command = response.input;
if (command.kind === "continue") {
if (!Number.isFinite(command.timeoutMs) || command.timeoutMs <= 0 || command.timeoutMs > params.maxTimeoutMs) throw new CodeModeWorkerFailure("timeout", "invalid code mode continuation budget");
params.setBudget(command.timeoutMs);
params.bridge.pendingRequests = command.pendingRequests;
params.bridge.canceledRequestIds = [];
prepare = () => settleRequests(params.vm, params.bridge, command.settledRequests);
continue;
}
if (command.kind !== "checkpoint") throw new Error("invalid code mode continuation");
consumed();
consumed = void 0;
}
return waitingResult({
vm: params.vm,
bridge: params.bridge,
settlementMode,
output,
config: params.config
});
}
const value = await readCompletedResult(params.vm, resultHandle);
const rejection = _usingCtx3.u(params.vm.global.getProp("__openclawUnhandledRejection").consume((read) => params.vm.callFunction(read, params.vm.undefined)));
if (rejection.isPromise) await readCompletedResult(params.vm, rejection);
return {
status: "completed",
value,
output
};
} catch (_) {
_usingCtx3.e = _;
} finally {
_usingCtx3.d();
}
} finally {
resultHandle.dispose();
}
}
} catch (error) {
return workerFailureResult({
error,
didTimeout: params.didTimeout,
output,
vm: params.vm
});
} finally {
params.vm.dispose();
}
}
function settleRequests(vm, bridge, requests) {
bridge.replies = requests;
bridge.replyIndex = 0;
try {
vm.global.getProp("__openclawSettleBridge").consume((settle) => vm.callFunction(settle, vm.undefined).dispose());
} finally {
bridge.replies = void 0;
bridge.replyIndex = 0;
for (const request of requests) request.json = "";
requests.length = 0;
}
}
async function run(input, channel) {
const startedAt = performance.now();
const source = input.kind === "exec" ? prepareSource(input.source) : "";
const config = {
...input.config,
timeoutMs: Math.min(input.config.timeoutMs - (performance.now() - startedAt), input.kind === "exec" ? input.executionTimeoutMs ?? Infinity : Infinity)
};
if (config.timeoutMs <= 0) throw new CodeModeWorkerFailure("timeout", "code mode timeout exceeded");
const bridge = {
pendingRequests: input.kind === "resume" ? [...input.pendingRequests ?? []] : [],
canceledRequestIds: [],
replyIndex: 0
};
const { vm, didTimeout, setBudget, pauseBudget } = await createVm({
...input,
config
}, bridge);
const result = await runVmExecution({
vm,
didTimeout,
setBudget,
pauseBudget,
channel,
bridge,
config,
maxTimeoutMs: input.config.timeoutMs,
prepare: () => {
if (input.kind === "exec") {
const program = buildUserSource(source, input.prelude);
vm.newString(JSON.stringify(program.location)).consume((location) => vm.global.defineProp(SOURCE_LOCATION_KEY, location));
vm.evalCode(program.source, USER_SOURCE_FILE, EvalFlags.ASYNC).dispose();
return;
}
settleRequests(vm, bridge, input.settledRequests);
}
});
return bridge.networkContentObserved ? {
...result,
networkContentObserved: true
} : result;
}
function isQuickJsWasmModule(value) {
return Object.prototype.toString.call(value) === "[object WebAssembly.Module]";
}
function isQuickJsWasmExtensions(value) {
return Array.isArray(value) && value.every((extension) => isRecord(extension) && typeof extension.name === "string" && isQuickJsWasmModule(extension.wasm));
}
async function main(input, channel) {
if (!isRecord(input) || !isRecord(input.config) || !isQuickJsWasmModule(input.wasmModule) || !isQuickJsWasmExtensions(input.wasmExtensions)) return {
...failedWorkerResult("invalid_input", "invalid code mode worker input"),
output: EMPTY_CODE_MODE_OUTPUT
};
const config = input.config;
try {
if (config.timeoutMs <= 0) throw new CodeModeWorkerFailure("timeout", "code mode timeout exceeded");
let payload;
if (input.kind === "exec" && typeof input.source === "string") payload = {
kind: "exec",
wasmModule: input.wasmModule,
wasmExtensions: input.wasmExtensions,
source: input.source,
prelude: typeof input.prelude === "string" ? input.prelude : void 0,
executionTimeoutMs: typeof input.executionTimeoutMs === "number" ? input.executionTimeoutMs : void 0,
config,
catalog: Array.isArray(input.catalog) ? input.catalog : [],
apiFiles: Array.isArray(input.apiFiles) ? input.apiFiles : [],
namespaces: Array.isArray(input.namespaces) ? input.namespaces : [],
swarmEnabled: input.swarmEnabled === true
};
else {
const snapshot = input.continuation;
if (input.kind !== "resume" || !(snapshot?.memory instanceof Uint8Array)) return {
...failedWorkerResult("invalid_input", "invalid code mode worker input"),
output: EMPTY_CODE_MODE_OUTPUT
};
payload = {
kind: "resume",
wasmModule: input.wasmModule,
wasmExtensions: input.wasmExtensions,
continuation: snapshot,
config,
settledRequests: Array.isArray(input.settledRequests) ? input.settledRequests : [],
pendingRequests: Array.isArray(input.pendingRequests) ? input.pendingRequests : []
};
}
return captureWorkerResult(await run(payload, channel), config, input.retainFinalValue === true);
} catch (error) {
const timedOut = isQuickJsInterruptedError(error);
return captureWorkerResult(failedWorkerResult(timedOut ? "timeout" : error instanceof CodeModeWorkerFailure ? error.code : error instanceof ToolInputError ? "invalid_input" : "internal_error", timedOut ? "code mode timeout exceeded" : errorMessage(error)), config);
}
}
serveWorkerTasks(main, { transferList: (result) => result.status === "waiting" ? [result.continuation.memory.buffer] : [] });
//#endregion
export {};