@ohos-ports/quickjs-wasi 3.6.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,2787 @@
1
+ /**
2
+ * QuickJS WASM - A snapshotable JavaScript runtime via WebAssembly.
3
+ *
4
+ * Provides a clean JavaScript API for running sandboxed JS code in a QuickJS
5
+ * VM compiled to WASM. The key differentiator is the ability to snapshot the
6
+ * entire VM state (including pending promises) and restore it in a fresh
7
+ * WASM instance.
8
+ */
9
+ var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
10
+ if (value !== null && value !== void 0) {
11
+ if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
12
+ var dispose, inner;
13
+ if (async) {
14
+ if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
15
+ dispose = value[Symbol.asyncDispose];
16
+ }
17
+ if (dispose === void 0) {
18
+ if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
19
+ dispose = value[Symbol.dispose];
20
+ if (async) inner = dispose;
21
+ }
22
+ if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
23
+ if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
24
+ env.stack.push({ value: value, dispose: dispose, async: async });
25
+ }
26
+ else if (async) {
27
+ env.stack.push({ async: true });
28
+ }
29
+ return value;
30
+ };
31
+ var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
32
+ return function (env) {
33
+ function fail(e) {
34
+ env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
35
+ env.hasError = true;
36
+ }
37
+ var r, s = 0;
38
+ function next() {
39
+ while (r = env.stack.pop()) {
40
+ try {
41
+ if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
42
+ if (r.dispose) {
43
+ var result = r.dispose.call(r.value);
44
+ if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
45
+ }
46
+ else s |= 1;
47
+ }
48
+ catch (e) {
49
+ fail(e);
50
+ }
51
+ }
52
+ if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
53
+ if (env.hasError) throw env.error;
54
+ }
55
+ return next();
56
+ };
57
+ })(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
58
+ var e = new Error(message);
59
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
60
+ });
61
+ import { createWasiShim } from './wasi-shim.js';
62
+ import { loadExtension, initExtension, restoreExtensions, } from './extensions.js';
63
+ import { VERSION } from './version.js';
64
+ // ---- Public types ----
65
+ /**
66
+ * Largest supported QuickJS native stack limit for the shipped WASM binary.
67
+ *
68
+ * The binary has a 1 MiB linker-defined stack; reserving half of it leaves
69
+ * headroom for native frames and stack-overflow exception handling.
70
+ */
71
+ export const MAX_STACK_SIZE = 512 * 1024;
72
+ /**
73
+ * Flags for `evalCode()`, matching the QuickJS `JS_EVAL_*` constants.
74
+ */
75
+ export const EvalFlags = {
76
+ /** Global script mode (default). */
77
+ TYPE_GLOBAL: 0,
78
+ /**
79
+ * Module mode. `evalCode()` returns a handle to a Promise that resolves
80
+ * to the module's namespace object (its exports), or rejects if module
81
+ * evaluation throws. Use together with `executePendingJobs()` and
82
+ * `resolvePromise()`.
83
+ */
84
+ TYPE_MODULE: (1 << 0),
85
+ /** Force strict mode. */
86
+ STRICT: (1 << 3),
87
+ /** Compile only; do not execute. */
88
+ COMPILE_ONLY: (1 << 5),
89
+ /** Omit stack frames before this eval from Error backtraces. */
90
+ BACKTRACE_BARRIER: (1 << 6),
91
+ /**
92
+ * Allow top-level `await` in global scripts. When used, `evalCode()`
93
+ * returns a handle to a Promise that resolves to the completion value.
94
+ * Use together with `executePendingJobs()` and `resolvePromise()`.
95
+ */
96
+ ASYNC: (1 << 7),
97
+ };
98
+ /**
99
+ * Flags for `vm.compile()` controlling what is included in the bytecode output.
100
+ * These can be combined with bitwise OR.
101
+ */
102
+ export const CompileFlags = {
103
+ /** Strip source code from the bytecode (smaller output, no source in errors). */
104
+ STRIP_SOURCE: (1 << 4),
105
+ /** Strip debug information (line numbers, etc.) from the bytecode. */
106
+ STRIP_DEBUG: (1 << 5),
107
+ };
108
+ /**
109
+ * Intrinsic flags for `QuickJSOptions.intrinsics` controlling which
110
+ * built-in JavaScript features are available in the VM.
111
+ *
112
+ * By default all intrinsics are enabled. Pass a bitmask of these flags
113
+ * to create a minimal context. For example, omit `Intrinsics.EVAL` to
114
+ * prevent `eval()` usage, or omit `Intrinsics.PROXY` to disallow `Proxy`.
115
+ *
116
+ * `BaseObjects` (Object, Array, Number, String, Boolean, Error, etc.)
117
+ * is always included and cannot be disabled.
118
+ */
119
+ export const Intrinsics = {
120
+ /** `Date` constructor and prototype methods. */
121
+ DATE: (1 << 0),
122
+ /** `eval()` and `Function()` constructor. */
123
+ EVAL: (1 << 1),
124
+ /** `RegExp` constructor, prototype methods, and regex literals. */
125
+ REGEXP: (1 << 2),
126
+ /** `JSON.parse()` and `JSON.stringify()`. */
127
+ JSON: (1 << 3),
128
+ /** `Proxy` and `Reflect`. */
129
+ PROXY: (1 << 4),
130
+ /** `Map`, `Set`, `WeakMap`, `WeakSet`. */
131
+ MAP_SET: (1 << 5),
132
+ /** `ArrayBuffer`, `TypedArray` variants, `DataView`. */
133
+ TYPED_ARRAYS: (1 << 6),
134
+ /** `Promise`, `async`/`await`. */
135
+ PROMISE: (1 << 7),
136
+ /** `BigInt`. Note: BigInt is part of BaseObjects in quickjs-ng and cannot be fully removed. */
137
+ BIG_INT: (1 << 8),
138
+ /** `WeakRef` and `FinalizationRegistry`. */
139
+ WEAK_REF: (1 << 9),
140
+ /** `performance.now()`. */
141
+ PERFORMANCE: (1 << 10),
142
+ /** `DOMException` class. */
143
+ DOM_EXCEPTION: (1 << 11),
144
+ /**
145
+ * `atob()` and `btoa()` global functions. Also pulls in `DOMException` as
146
+ * a dependency (errors thrown by these functions are `DOMException`s).
147
+ */
148
+ ATOB_BTOA: (1 << 12),
149
+ /** All intrinsics enabled (default). */
150
+ ALL: 0xFFFFFFFF,
151
+ };
152
+ // ---- Snapshot serialization ----
153
+ /** Magic bytes: "QJSS" (QuickJS Snapshot) */
154
+ const SNAPSHOT_MAGIC = 0x514A5353;
155
+ /** Current serialization format version (2 = added extension metadata) */
156
+ const SNAPSHOT_VERSION = 2;
157
+ /**
158
+ * Header layout (version 2):
159
+ * 0-3: Magic "QJSS" (u32 big-endian)
160
+ * 4: Version (u8)
161
+ * 5-7: Reserved (zero)
162
+ * 8-11: Memory size in bytes (u32 little-endian)
163
+ * 12-15: Stack pointer (u32 little-endian)
164
+ * 16-19: Runtime pointer (u32 little-endian)
165
+ * 20-23: Context pointer (u32 little-endian)
166
+ * 24-27: Extension count (u32 little-endian)
167
+ * 28+: Extension entries (variable length):
168
+ * nameLen(u32) + name(utf8) + memoryBase(u32) + tableBase(u32) + initFnLen(u32) + initFn(utf8)
169
+ * N+: Memory data (N = memory size from offset 8)
170
+ *
171
+ * Version 1 (legacy): no extension metadata, memory starts at offset 24.
172
+ */
173
+ const SNAPSHOT_HEADER_SIZE = 24;
174
+ // ---- QuickJS VM ----
175
+ export class QuickJS {
176
+ exports;
177
+ module;
178
+ instance;
179
+ encoder = new TextEncoder();
180
+ decoder = new TextDecoder();
181
+ disposed = false;
182
+ /** Registry of host callbacks, keyed by function name */
183
+ hostCallbacks = new Map();
184
+ /** Counter for internal-only callbacks (e.g. promise settle handlers) */
185
+ nextInternalId = 1;
186
+ interruptHandler = null;
187
+ unhandledRejectionHandler = null;
188
+ moduleNormalizeHandler = null;
189
+ moduleLoadHandler = null;
190
+ timezoneOffsetHandler = null;
191
+ // Cached singleton handles
192
+ _global = null;
193
+ _versions = null;
194
+ _undefined = null;
195
+ _null = null;
196
+ _true = null;
197
+ _false = null;
198
+ // Handles that must be freed on dispose (e.g. unresolved promise resolve/reject functions)
199
+ _ownedHandles = new Set();
200
+ /**
201
+ * The innermost active `withScope()` batch, if any. New non-singleton
202
+ * handles register themselves here so they can be freed together.
203
+ * @internal
204
+ */
205
+ _activeScope = null;
206
+ /** Loaded extensions in deterministic order */
207
+ loadedExtensions = [];
208
+ constructor(module) {
209
+ this.module = module;
210
+ this.instance = null;
211
+ this.exports = null;
212
+ }
213
+ setInstance(instance) {
214
+ this.instance = instance;
215
+ this.exports = instance.exports;
216
+ }
217
+ // ---- Cached property accessors ----
218
+ /**
219
+ * Version information for the runtime and loaded native libraries.
220
+ * Always includes `"quickjs-wasi"` (the npm package version) and
221
+ * `"quickjs"` (the QuickJS engine version). Extensions may contribute
222
+ * additional entries for their native dependencies (e.g. `"ada"`, `"mbedtls"`).
223
+ */
224
+ get versions() {
225
+ this.assertNotDisposed();
226
+ if (!this._versions) {
227
+ const result = {
228
+ 'quickjs-wasi': VERSION,
229
+ quickjs: this.readCString(this.exports.qjs_get_quickjs_version()),
230
+ };
231
+ for (const ext of this.loadedExtensions) {
232
+ if (ext.versions) {
233
+ Object.assign(result, ext.versions);
234
+ }
235
+ }
236
+ this._versions = result;
237
+ }
238
+ return this._versions;
239
+ }
240
+ /** The global object. Cached; do not dispose. */
241
+ get global() {
242
+ if (!this._global) {
243
+ this._global = new JSValueHandle(this, this.exports.qjs_get_global(), true);
244
+ }
245
+ return this._global;
246
+ }
247
+ /** The undefined value. Cached; do not dispose. */
248
+ get undefined() {
249
+ if (!this._undefined) {
250
+ this._undefined = new JSValueHandle(this, this.exports.qjs_get_undefined(), true);
251
+ }
252
+ return this._undefined;
253
+ }
254
+ /** The null value. Cached; do not dispose. */
255
+ get null() {
256
+ if (!this._null) {
257
+ this._null = new JSValueHandle(this, this.exports.qjs_get_null(), true);
258
+ }
259
+ return this._null;
260
+ }
261
+ /** The true value. Cached; do not dispose. */
262
+ get true() {
263
+ if (!this._true) {
264
+ this._true = new JSValueHandle(this, this.exports.qjs_get_true(), true);
265
+ }
266
+ return this._true;
267
+ }
268
+ /** The false value. Cached; do not dispose. */
269
+ get false() {
270
+ if (!this._false) {
271
+ this._false = new JSValueHandle(this, this.exports.qjs_get_false(), true);
272
+ }
273
+ return this._false;
274
+ }
275
+ /**
276
+ * Create a fresh QuickJS VM instance.
277
+ *
278
+ * @param options - Optional configuration. Can also pass raw WASM bytes
279
+ * directly for backwards compatibility.
280
+ */
281
+ static async create(options) {
282
+ const opts = QuickJS.normalizeOptions(options);
283
+ const module = await QuickJS.resolveModule(opts.wasm);
284
+ const vm = new QuickJS(module);
285
+ const { instance, wasiBuiltins, wasiUserOverrides, memoryProxy } = await QuickJS.instantiate(module, vm, opts.wasi);
286
+ vm.setInstance(instance);
287
+ // Initialize the WASI reactor
288
+ vm.exports._initialize();
289
+ // Initialize QuickJS runtime and context
290
+ const result = opts.intrinsics !== undefined
291
+ ? vm.exports.qjs_init2(opts.intrinsics)
292
+ : vm.exports.qjs_init();
293
+ if (result !== 0) {
294
+ throw new Error('Failed to initialize QuickJS runtime');
295
+ }
296
+ // Load and initialize extensions
297
+ if (opts.extensions) {
298
+ const mainExports = instance.exports;
299
+ for (const desc of opts.extensions) {
300
+ const ext = await loadExtension(desc, mainExports, wasiBuiltins, wasiUserOverrides, memoryProxy);
301
+ vm.loadedExtensions.push(ext);
302
+ initExtension(ext, mainExports);
303
+ }
304
+ }
305
+ // Apply runtime limits
306
+ QuickJS.applyLimits(vm, opts);
307
+ return vm;
308
+ }
309
+ /**
310
+ * Restore a QuickJS VM from a snapshot.
311
+ *
312
+ * @param snapshot - The snapshot to restore from.
313
+ * @param options - Optional configuration. Can also pass raw WASM bytes
314
+ * directly for backwards compatibility.
315
+ */
316
+ static async restore(snapshot, options) {
317
+ const opts = QuickJS.normalizeOptions(options);
318
+ const module = await QuickJS.resolveModule(opts.wasm);
319
+ const vm = new QuickJS(module);
320
+ const { instance, wasiBuiltins, wasiUserOverrides, memoryProxy } = await QuickJS.instantiate(module, vm, opts.wasi);
321
+ vm.setInstance(instance);
322
+ const mainExports = instance.exports;
323
+ const exportedMemory = vm.exports.memory;
324
+ // Grow memory FIRST: extensions need the memory to be large enough
325
+ // for their __memory_base offsets (which were allocated in the original
326
+ // larger memory during create()).
327
+ const currentPages = exportedMemory.buffer.byteLength / 65536;
328
+ const neededPages = Math.ceil(snapshot.memory.byteLength / 65536);
329
+ if (neededPages > currentPages) {
330
+ exportedMemory.grow(neededPages - currentPages);
331
+ }
332
+ // Re-instantiate extensions BEFORE overwriting memory.
333
+ // This populates the indirect function table with the extension's
334
+ // function pointers (via elem segments and __wasm_apply_data_relocs).
335
+ // We use the exact same memory/table bases from the snapshot so that
336
+ // function table indices match what the snapshotted QuickJS state expects.
337
+ if (snapshot.extensions.length > 0) {
338
+ const descriptors = opts.extensions ?? [];
339
+ vm.loadedExtensions = await restoreExtensions(descriptors, snapshot.extensions, mainExports, wasiBuiltins, wasiUserOverrides, memoryProxy);
340
+ }
341
+ // Copy snapshot data into the module's own memory.
342
+ // This overwrites EVERYTHING, including the regions that extensions
343
+ // just initialized. That's correct because the snapshot already contains
344
+ // the complete state including extension data.
345
+ const dst = new Uint8Array(exportedMemory.buffer);
346
+ dst.set(snapshot.memory);
347
+ // Set runtime/context pointers (they already exist in the restored memory)
348
+ vm.exports.qjs_set_runtime_and_context(snapshot.runtimePtr, snapshot.contextPtr);
349
+ // Restore the stack pointer
350
+ vm.exports.__stack_pointer.value = snapshot.stackPointer;
351
+ // Apply runtime limits
352
+ QuickJS.applyLimits(vm, opts);
353
+ return vm;
354
+ }
355
+ // ---- Snapshot serialization ----
356
+ /**
357
+ * Serialize a snapshot to a binary buffer for persistent storage.
358
+ *
359
+ * The format includes a versioned header followed by the raw memory.
360
+ * Apply your own compression (gzip, zstd, etc.) on top for smaller
361
+ * storage. The memory compresses well due to its large zero regions.
362
+ *
363
+ * Format (version 1):
364
+ * ```
365
+ * Offset Size Field
366
+ * 0 4 Magic: "QJSS" (0x514A5353, big-endian)
367
+ * 4 1 Version: 1
368
+ * 5 3 Reserved (zero)
369
+ * 8 4 Memory size in bytes (u32 little-endian)
370
+ * 12 4 Stack pointer (u32 little-endian)
371
+ * 16 4 Runtime pointer (u32 little-endian)
372
+ * 20 4 Context pointer (u32 little-endian)
373
+ * 24 N Memory data (N = memory size from offset 8)
374
+ * ```
375
+ */
376
+ static serializeSnapshot(snapshot) {
377
+ const textEncoder = new TextEncoder();
378
+ // Calculate extension metadata size
379
+ let extMetaSize = 4; // extCount (u32)
380
+ const extEncodedNames = [];
381
+ const extEncodedInitFns = [];
382
+ for (const ext of snapshot.extensions) {
383
+ const nameBytes = textEncoder.encode(ext.name);
384
+ const initFnBytes = textEncoder.encode(ext.initFn);
385
+ extEncodedNames.push(nameBytes);
386
+ extEncodedInitFns.push(initFnBytes);
387
+ extMetaSize += 4 + nameBytes.length + 4 + 4 + 4 + initFnBytes.length;
388
+ }
389
+ const totalSize = SNAPSHOT_HEADER_SIZE + extMetaSize + snapshot.memory.byteLength;
390
+ const buffer = new ArrayBuffer(totalSize);
391
+ const view = new DataView(buffer);
392
+ const bytes = new Uint8Array(buffer);
393
+ // Header
394
+ view.setUint32(0, SNAPSHOT_MAGIC, false); // big-endian for readability in hex
395
+ view.setUint8(4, SNAPSHOT_VERSION);
396
+ // bytes 5-7 are reserved (already zero)
397
+ view.setUint32(8, snapshot.memory.byteLength, true);
398
+ view.setUint32(12, snapshot.stackPointer, true);
399
+ view.setUint32(16, snapshot.runtimePtr, true);
400
+ view.setUint32(20, snapshot.contextPtr, true);
401
+ // Extension metadata (version 2)
402
+ let offset = SNAPSHOT_HEADER_SIZE;
403
+ view.setUint32(offset, snapshot.extensions.length, true);
404
+ offset += 4;
405
+ for (let i = 0; i < snapshot.extensions.length; i++) {
406
+ const ext = snapshot.extensions[i];
407
+ const nameBytes = extEncodedNames[i];
408
+ const initFnBytes = extEncodedInitFns[i];
409
+ view.setUint32(offset, nameBytes.length, true);
410
+ offset += 4;
411
+ bytes.set(nameBytes, offset);
412
+ offset += nameBytes.length;
413
+ view.setUint32(offset, ext.memoryBase, true);
414
+ offset += 4;
415
+ view.setUint32(offset, ext.tableBase, true);
416
+ offset += 4;
417
+ view.setUint32(offset, initFnBytes.length, true);
418
+ offset += 4;
419
+ bytes.set(initFnBytes, offset);
420
+ offset += initFnBytes.length;
421
+ }
422
+ // Memory data
423
+ bytes.set(snapshot.memory, offset);
424
+ return bytes;
425
+ }
426
+ /**
427
+ * Deserialize a snapshot from a binary buffer produced by `serializeSnapshot()`.
428
+ */
429
+ static deserializeSnapshot(data) {
430
+ if (data.length < SNAPSHOT_HEADER_SIZE) {
431
+ throw new Error('Invalid snapshot: too small');
432
+ }
433
+ const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
434
+ // Validate magic
435
+ const magic = view.getUint32(0, false);
436
+ if (magic !== SNAPSHOT_MAGIC) {
437
+ throw new Error(`Invalid snapshot: bad magic (expected 0x${SNAPSHOT_MAGIC.toString(16)}, got 0x${magic.toString(16)})`);
438
+ }
439
+ // Validate version
440
+ const version = view.getUint8(4);
441
+ if (version !== SNAPSHOT_VERSION && version !== 1) {
442
+ throw new Error(`Unsupported snapshot version: ${version} (expected ${SNAPSHOT_VERSION})`);
443
+ }
444
+ const memorySize = view.getUint32(8, true);
445
+ const stackPointer = view.getUint32(12, true);
446
+ const runtimePtr = view.getUint32(16, true);
447
+ const contextPtr = view.getUint32(20, true);
448
+ let extensions = [];
449
+ let memoryOffset = SNAPSHOT_HEADER_SIZE;
450
+ if (version >= 2) {
451
+ // Version 2 adds extension metadata between the header and the memory data
452
+ const extCount = view.getUint32(24, true);
453
+ let offset = 28;
454
+ const textDecoder = new TextDecoder();
455
+ for (let i = 0; i < extCount; i++) {
456
+ // name length (u32) + name (utf8) + memoryBase (u32) + tableBase (u32) + initFn length (u32) + initFn (utf8)
457
+ const nameLen = view.getUint32(offset, true);
458
+ offset += 4;
459
+ const name = textDecoder.decode(data.slice(offset, offset + nameLen));
460
+ offset += nameLen;
461
+ const memBase = view.getUint32(offset, true);
462
+ offset += 4;
463
+ const tblBase = view.getUint32(offset, true);
464
+ offset += 4;
465
+ const initFnLen = view.getUint32(offset, true);
466
+ offset += 4;
467
+ const initFn = textDecoder.decode(data.slice(offset, offset + initFnLen));
468
+ offset += initFnLen;
469
+ extensions.push({ name, memoryBase: memBase, tableBase: tblBase, initFn });
470
+ }
471
+ memoryOffset = offset;
472
+ }
473
+ const expectedSize = memoryOffset + memorySize;
474
+ if (data.length < expectedSize) {
475
+ throw new Error(`Invalid snapshot: expected ${expectedSize} bytes, got ${data.length}`);
476
+ }
477
+ const memory = data.slice(memoryOffset, memoryOffset + memorySize);
478
+ return { memory, stackPointer, runtimePtr, contextPtr, extensions };
479
+ }
480
+ // ---- Internal instantiation helpers ----
481
+ static normalizeOptions(options) {
482
+ if (!options) {
483
+ // resolveModule() will throw a helpful error.
484
+ return { wasm: undefined };
485
+ }
486
+ if (options instanceof WebAssembly.Module)
487
+ return { wasm: options };
488
+ 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)) {
489
+ const opts = options;
490
+ if (opts.maxStackSize !== undefined &&
491
+ (!Number.isInteger(opts.maxStackSize) ||
492
+ opts.maxStackSize < 0 ||
493
+ opts.maxStackSize > MAX_STACK_SIZE)) {
494
+ throw new RangeError(`maxStackSize must be an integer between 0 and ${MAX_STACK_SIZE}`);
495
+ }
496
+ return opts;
497
+ }
498
+ // BufferSource (ArrayBuffer or ArrayBufferView)
499
+ return { wasm: options };
500
+ }
501
+ static applyLimits(vm, opts) {
502
+ if (opts.memoryLimit !== undefined) {
503
+ vm.exports.qjs_set_memory_limit(opts.memoryLimit);
504
+ }
505
+ if (opts.maxStackSize !== undefined) {
506
+ vm.exports.qjs_set_max_stack_size(opts.maxStackSize);
507
+ }
508
+ if (opts.interruptHandler) {
509
+ vm.interruptHandler = opts.interruptHandler;
510
+ vm.exports.qjs_set_interrupt_handler(1);
511
+ }
512
+ if (opts.onUnhandledRejection) {
513
+ vm.unhandledRejectionHandler = opts.onUnhandledRejection;
514
+ vm.exports.qjs_set_promise_rejection_handler(1);
515
+ }
516
+ if (opts.moduleLoader) {
517
+ vm.moduleLoadHandler = opts.moduleLoader.load;
518
+ vm.moduleNormalizeHandler = opts.moduleLoader.normalize ?? null;
519
+ vm.exports.qjs_set_module_loader(1);
520
+ }
521
+ // Configure timezone handler.
522
+ // The internal handler always returns the UTC offset in *seconds*
523
+ // (positive east of UTC), which is what libc's __secs_to_zone expects.
524
+ const tz = opts.timezoneOffset;
525
+ if (typeof tz === 'function') {
526
+ // User callback returns minutes (getTimezoneOffset convention:
527
+ // positive west of UTC). Convert to seconds with sign flip.
528
+ vm.timezoneOffsetHandler = (timeSecs) => -tz(timeSecs) * 60;
529
+ }
530
+ else if (typeof tz === 'number') {
531
+ // Fixed offset in minutes, convert to seconds with sign flip.
532
+ const offsetSecs = -tz * 60;
533
+ vm.timezoneOffsetHandler = () => offsetSecs;
534
+ }
535
+ else {
536
+ // 'host' (default): use the host's timezone
537
+ vm.timezoneOffsetHandler = (timeSecs) => {
538
+ return -new Date(timeSecs * 1000).getTimezoneOffset() * 60;
539
+ };
540
+ }
541
+ }
542
+ static async resolveModule(wasmInput) {
543
+ if (wasmInput instanceof WebAssembly.Module) {
544
+ return wasmInput;
545
+ }
546
+ if (wasmInput) {
547
+ return WebAssembly.compile(wasmInput);
548
+ }
549
+ throw new TypeError('QuickJS: `wasm` option is required. Provide WASM bytes or a compiled ' +
550
+ '`WebAssembly.Module`. The binary is shipped at `quickjs-wasi/quickjs.wasm` ' +
551
+ 'and can be loaded via your environment\'s preferred mechanism (e.g. ' +
552
+ "`fetch()`, `node:fs/promises`, or a bundler import like Vite's `?url`).");
553
+ }
554
+ static async instantiate(module, vm, wasiOptions) {
555
+ let memory = null;
556
+ // Create a memory proxy that defers to the actual memory once set.
557
+ // This allows WASI override factories to close over the memory reference
558
+ // before the WASM instance is created.
559
+ const memoryProxy = new Proxy({}, {
560
+ get(_target, prop) {
561
+ return memory[prop];
562
+ },
563
+ });
564
+ // Build the builtins (no user overrides)
565
+ const wasiBuiltins = createWasiShim(() => memory);
566
+ // Resolve user overrides via factory
567
+ const wasiUserOverrides = wasiOptions ? wasiOptions(memoryProxy) : undefined;
568
+ // Final shim for the main module: builtins + user overrides
569
+ const wasiShim = { ...wasiBuiltins, ...wasiUserOverrides };
570
+ const hostCall = (namePtr, nameLen, thisPtr, argc, argvPtr) => {
571
+ return vm.handleHostCall(namePtr, nameLen, thisPtr, argc, argvPtr);
572
+ };
573
+ const hostInterrupt = () => {
574
+ return vm.interruptHandler ? (vm.interruptHandler() ? 1 : 0) : 0;
575
+ };
576
+ const hostPromiseRejection = (promisePtr, reasonPtr, isHandled) => {
577
+ if (!vm.unhandledRejectionHandler) {
578
+ // No handler registered; free the heap-allocated values
579
+ vm.exports.qjs_free_value(promisePtr);
580
+ vm.exports.qjs_free_value(reasonPtr);
581
+ return;
582
+ }
583
+ const promise = new JSValueHandle(vm, promisePtr);
584
+ const reason = new JSValueHandle(vm, reasonPtr);
585
+ try {
586
+ vm.unhandledRejectionHandler(promise, reason, isHandled !== 0);
587
+ }
588
+ finally {
589
+ promise.dispose();
590
+ reason.dispose();
591
+ }
592
+ };
593
+ // Throw a host-side error into the QuickJS context so module loader
594
+ // failures surface with their real message instead of the generic
595
+ // "could not load module" error.
596
+ const throwIntoContext = (err) => {
597
+ const errHandle = vm.newError(err instanceof Error ? err : String(err));
598
+ vm.exports.qjs_throw(errHandle.ptr);
599
+ errHandle.dispose();
600
+ };
601
+ // Guard against async (or otherwise non-string-returning) module loader
602
+ // callbacks. The WASM boundary is synchronous (a Promise cannot be
603
+ // awaited here), so fail with a clear error instead of coercing the
604
+ // Promise to source text.
605
+ const assertSyncString = (value, callbackName) => {
606
+ if (typeof value === 'string')
607
+ return value;
608
+ const got = value !== null && typeof value === 'object' && typeof value.then === 'function'
609
+ ? 'a Promise'
610
+ : `type ${typeof value}`;
611
+ throw new TypeError(`moduleLoader.${callbackName} must synchronously return a string (got ${got}). ` +
612
+ `Async module loading is not supported. Pre-fetch module sources instead ` +
613
+ `(see the "ES Modules" section of the quickjs-wasi README).`);
614
+ };
615
+ // Module normalizer: resolve a specifier relative to a base name.
616
+ // Returns a malloc'd null-terminated string in WASM memory, or 0 (NULL) on error.
617
+ const hostModuleNormalize = (baseNamePtr, namePtr) => {
618
+ if (!vm.moduleNormalizeHandler) {
619
+ // No normalize handler; return a copy of the specifier as-is
620
+ const name = vm.readCString(namePtr);
621
+ return vm.writeString(name).ptr;
622
+ }
623
+ const baseName = vm.readCString(baseNamePtr);
624
+ const specifier = vm.readCString(namePtr);
625
+ try {
626
+ const normalized = assertSyncString(vm.moduleNormalizeHandler(baseName, specifier), 'normalize');
627
+ return vm.writeString(normalized).ptr;
628
+ }
629
+ catch (err) {
630
+ throwIntoContext(err);
631
+ return 0;
632
+ }
633
+ };
634
+ // Module loader: return source code for a module.
635
+ // Returns a malloc'd string pointer, writes length to *outLenPtr.
636
+ const hostModuleLoad = (namePtr, outLenPtr) => {
637
+ if (!vm.moduleLoadHandler)
638
+ return 0;
639
+ const name = vm.readCString(namePtr);
640
+ try {
641
+ const source = assertSyncString(vm.moduleLoadHandler(name), 'load');
642
+ const { ptr, len } = vm.writeString(source);
643
+ new Uint32Array(vm.exports.memory.buffer, outLenPtr, 1)[0] = len;
644
+ return ptr;
645
+ }
646
+ catch (err) {
647
+ throwIntoContext(err);
648
+ return 0;
649
+ }
650
+ };
651
+ // host_get_timezone_offset receives time as split i32 (hi, lo) and
652
+ // returns the UTC offset in seconds.
653
+ const hostGetTimezoneOffset = (hi, lo) => {
654
+ const timeSecs = Number((BigInt(hi) << 32n) | BigInt(lo >>> 0));
655
+ return vm.timezoneOffsetHandler ? vm.timezoneOffsetHandler(timeSecs) : 0;
656
+ };
657
+ const instance = await WebAssembly.instantiate(module, {
658
+ env: {
659
+ host_call: hostCall,
660
+ host_interrupt: hostInterrupt,
661
+ host_promise_rejection: hostPromiseRejection,
662
+ host_module_normalize: hostModuleNormalize,
663
+ host_module_load: hostModuleLoad,
664
+ host_get_timezone_offset: hostGetTimezoneOffset,
665
+ },
666
+ wasi_snapshot_preview1: wasiShim,
667
+ });
668
+ memory = instance.exports.memory;
669
+ return { instance, wasiBuiltins, wasiUserOverrides, memoryProxy };
670
+ }
671
+ /**
672
+ * Called from WASM when a host function is invoked from QuickJS code.
673
+ */
674
+ handleHostCall(namePtr, nameLen, thisPtr, argc, argvPtr) {
675
+ const name = this.decoder.decode(new Uint8Array(this.exports.memory.buffer, namePtr, nameLen));
676
+ const callback = this.hostCallbacks.get(name);
677
+ if (!callback) {
678
+ // Throw inside the guest, as the docs promise: `newEphemeralFunction`
679
+ // ("calling it after the handle is disposed throws, because the
680
+ // callback is gone") and `unregisterHostCallback` ("any QuickJS
681
+ // function still referencing the name will throw when called").
682
+ // Silently returning `undefined` here masked real bugs, e.g. a
683
+ // snapshot-restored VM calling a host function that was never
684
+ // re-registered would corrupt results instead of failing loud.
685
+ // Guest code can catch this like any other error.
686
+ // A string (not a host Error object): newError copies an Error's
687
+ // host .stack into the guest, which would leak host file paths into
688
+ // guest-observable space and shadow any guest backtrace. This error
689
+ // is library-generated; there is no host stack worth preserving.
690
+ const errHandle = this.newError(`Host callback "${name}" is not registered: it was unregistered, ` +
691
+ 'its ephemeral function handle was disposed, or it was never ' +
692
+ 're-registered after a snapshot restore.');
693
+ this.exports.qjs_throw(errHandle.ptr);
694
+ errHandle.dispose();
695
+ return 0;
696
+ }
697
+ // `thisPtr` and the argv entries are OWNED BY THE C TRAMPOLINE, which
698
+ // frees them after this call returns. Wrap them as borrowed handles:
699
+ // dispose() is a no-op and they are exempt from `withScope()`
700
+ // tracking: a scope active around guest execution (e.g. a host
701
+ // serializer driving a `forEach` visitor) must not free them at its
702
+ // boundary, which would double-free the guest values and corrupt the
703
+ // heap. Callbacks retain arguments past their invocation via `dup()`.
704
+ const thisHandle = new JSValueHandle(this, thisPtr, false, true);
705
+ const args = [];
706
+ if (argc > 0 && argvPtr !== 0) {
707
+ const view = new DataView(this.exports.memory.buffer);
708
+ for (let i = 0; i < argc; i++) {
709
+ const argPtr = view.getUint32(argvPtr + i * 4, true);
710
+ args.push(new JSValueHandle(this, argPtr, false, true));
711
+ }
712
+ }
713
+ try {
714
+ const result = callback.call(thisHandle, ...args);
715
+ return this.exports.qjs_dup_value(result.ptr);
716
+ }
717
+ catch (err) {
718
+ // Throw an exception inside QuickJS and return NULL to signal
719
+ // to the C trampoline that an exception was thrown.
720
+ const errHandle = this.newError(err instanceof Error ? err : String(err));
721
+ this.exports.qjs_throw(errHandle.ptr);
722
+ errHandle.dispose();
723
+ return 0;
724
+ }
725
+ }
726
+ // ---- String helpers ----
727
+ /** Write a JS string into WASM memory, returning the pointer. Caller must free. */
728
+ writeString(str) {
729
+ // WTF-8 (not plain TextEncoder): lone surrogates in the input must
730
+ // reach the guest intact: quickjs's decoder accepts the 3-byte
731
+ // surrogate sequences, so a JS string round-trips exactly.
732
+ const encoded = encodeWtf8(str);
733
+ const ptr = this.exports.wasm_malloc(encoded.length + 1);
734
+ if (ptr === 0)
735
+ throw new Error('wasm_malloc failed');
736
+ const mem = new Uint8Array(this.exports.memory.buffer);
737
+ mem.set(encoded, ptr);
738
+ mem[ptr + encoded.length] = 0;
739
+ return { ptr, len: encoded.length };
740
+ }
741
+ /** Read a null-terminated C string from WASM memory */
742
+ readCString(ptr) {
743
+ const mem = new Uint8Array(this.exports.memory.buffer);
744
+ let end = ptr;
745
+ while (mem[end] !== 0)
746
+ end++;
747
+ return this.decoder.decode(mem.slice(ptr, end));
748
+ }
749
+ // ---- Public API ----
750
+ /**
751
+ * Check if a result handle is an exception and throw a JSException if so.
752
+ * Used internally by evalCode and callFunction.
753
+ */
754
+ throwIfException(result) {
755
+ if (this.exports.qjs_is_exception(result.ptr) !== 0) {
756
+ const exc = this.getException();
757
+ result.dispose();
758
+ // Track the handle so it gets cleaned up if the VM is disposed
759
+ // before the caller disposes the exception.
760
+ this._ownedHandles.add(exc);
761
+ throw new JSException(exc);
762
+ }
763
+ return result;
764
+ }
765
+ /**
766
+ * Evaluate JavaScript code and return the result as a handle.
767
+ * If the code throws, a `JSException` (which extends `Error`) is thrown
768
+ * on the host side, matching standard JavaScript semantics.
769
+ *
770
+ * @param code - The JavaScript source code to evaluate.
771
+ * @param filename - Optional filename for error stack traces (default `'<eval>'`).
772
+ * @param flags - Optional bitwise OR of `EvalFlags.*` constants.
773
+ * For example, pass `EvalFlags.ASYNC` to allow top-level `await`; the
774
+ * returned handle will be a Promise that resolves to the completion value.
775
+ * With `EvalFlags.TYPE_MODULE` the returned handle is a Promise that
776
+ * resolves to the module's namespace object (its exports).
777
+ */
778
+ evalCode(code, filename = '<eval>', flags = 0) {
779
+ this.assertNotDisposed();
780
+ const codeStr = this.writeString(code);
781
+ const fnStr = this.writeString(filename);
782
+ const resultPtr = this.exports.qjs_eval(codeStr.ptr, codeStr.len, fnStr.ptr, flags);
783
+ this.exports.wasm_free(codeStr.ptr);
784
+ this.exports.wasm_free(fnStr.ptr);
785
+ return this.throwIfException(new JSValueHandle(this, resultPtr));
786
+ }
787
+ /**
788
+ * Compile JavaScript source code to bytecode without executing it.
789
+ * The returned `Uint8Array` can be stored, transferred, or later executed
790
+ * with `evalBytecode()`.
791
+ *
792
+ * @param code - The JavaScript source code to compile.
793
+ * @param filename - Optional filename for error stack traces (default `'<compile>'`).
794
+ * @param evalFlags - Optional bitwise OR of `EvalFlags.*` constants.
795
+ * Use `EvalFlags.TYPE_MODULE` to compile as a module.
796
+ * @param compileFlags - Optional bitwise OR of `CompileFlags.*` constants.
797
+ * Use `CompileFlags.STRIP_SOURCE` and/or `CompileFlags.STRIP_DEBUG` to
798
+ * reduce bytecode size.
799
+ */
800
+ compile(code, filename = '<compile>', evalFlags = 0, compileFlags = 0) {
801
+ this.assertNotDisposed();
802
+ const codeStr = this.writeString(code);
803
+ const fnStr = this.writeString(filename);
804
+ // Allocate space for the output length (size_t = 4 bytes in wasm32)
805
+ const outLenPtr = this.exports.wasm_malloc(4);
806
+ const bufPtr = this.exports.qjs_compile(codeStr.ptr, codeStr.len, fnStr.ptr, evalFlags, compileFlags, outLenPtr);
807
+ this.exports.wasm_free(codeStr.ptr);
808
+ this.exports.wasm_free(fnStr.ptr);
809
+ if (bufPtr === 0) {
810
+ this.exports.wasm_free(outLenPtr);
811
+ // Compilation failed; throw the QuickJS exception
812
+ const exc = this.getException();
813
+ throw new Error(`Compilation error: ${exc.toString()}`);
814
+ }
815
+ const outLen = new Uint32Array(this.exports.memory.buffer, outLenPtr, 1)[0];
816
+ this.exports.wasm_free(outLenPtr);
817
+ // Copy the bytecode out of WASM memory before freeing
818
+ const bytecode = new Uint8Array(this.exports.memory.buffer, bufPtr, outLen).slice();
819
+ this.exports.wasm_free(bufPtr);
820
+ return bytecode;
821
+ }
822
+ /**
823
+ * Execute previously compiled bytecode (from `compile()`).
824
+ * Returns the evaluation result as a handle.
825
+ *
826
+ * For module bytecode (compiled with `EvalFlags.TYPE_MODULE`), the
827
+ * returned handle is a Promise that resolves to the module's namespace
828
+ * object (its exports).
829
+ *
830
+ * @param bytecode - The bytecode `Uint8Array` from `compile()`.
831
+ */
832
+ evalBytecode(bytecode) {
833
+ this.assertNotDisposed();
834
+ const bufPtr = this.exports.wasm_malloc(bytecode.byteLength);
835
+ new Uint8Array(this.exports.memory.buffer, bufPtr, bytecode.byteLength).set(bytecode);
836
+ const resultPtr = this.exports.qjs_eval_bytecode(bufPtr, bytecode.byteLength);
837
+ this.exports.wasm_free(bufPtr);
838
+ return this.throwIfException(new JSValueHandle(this, resultPtr));
839
+ }
840
+ /**
841
+ * Execute all pending microtask jobs (promise reactions, etc.)
842
+ * Returns the number of jobs executed.
843
+ */
844
+ executePendingJobs() {
845
+ this.assertNotDisposed();
846
+ let count = 0;
847
+ while (this.exports.qjs_is_job_pending()) {
848
+ const result = this.exports.qjs_execute_pending_job();
849
+ if (result < 0) {
850
+ const exc = this.getException();
851
+ throw new Error(`Job execution error: ${exc.toString()}`);
852
+ }
853
+ count++;
854
+ }
855
+ return count;
856
+ }
857
+ /**
858
+ * Explicitly trigger garbage collection. QuickJS runs GC automatically,
859
+ * but this can be useful to reclaim memory at a known point or before
860
+ * taking a snapshot.
861
+ */
862
+ runGC() {
863
+ this.assertNotDisposed();
864
+ this.exports.qjs_run_gc();
865
+ }
866
+ /**
867
+ * The GC threshold in bytes. When allocated memory exceeds this value,
868
+ * garbage collection is triggered automatically. Set to 0 to disable
869
+ * automatic GC.
870
+ */
871
+ get gcThreshold() {
872
+ this.assertNotDisposed();
873
+ return this.exports.qjs_get_gc_threshold();
874
+ }
875
+ set gcThreshold(threshold) {
876
+ this.assertNotDisposed();
877
+ this.exports.qjs_set_gc_threshold(threshold);
878
+ }
879
+ /**
880
+ * Get detailed memory usage statistics from the QuickJS runtime.
881
+ * Returns counts and sizes for atoms, strings, objects, functions, etc.
882
+ */
883
+ getMemoryUsage() {
884
+ this.assertNotDisposed();
885
+ // Allocate a buffer for 26 int64 fields (26 * 8 = 208 bytes)
886
+ const bufPtr = this.exports.wasm_malloc(26 * 8);
887
+ this.exports.qjs_compute_memory_usage(bufPtr);
888
+ const view = new BigInt64Array(this.exports.memory.buffer, bufPtr, 26);
889
+ const result = {
890
+ mallocSize: Number(view[0]),
891
+ mallocLimit: Number(view[1]),
892
+ memoryUsedSize: Number(view[2]),
893
+ mallocCount: Number(view[3]),
894
+ memoryUsedCount: Number(view[4]),
895
+ atomCount: Number(view[5]),
896
+ atomSize: Number(view[6]),
897
+ strCount: Number(view[7]),
898
+ strSize: Number(view[8]),
899
+ objCount: Number(view[9]),
900
+ objSize: Number(view[10]),
901
+ propCount: Number(view[11]),
902
+ propSize: Number(view[12]),
903
+ shapeCount: Number(view[13]),
904
+ shapeSize: Number(view[14]),
905
+ jsFuncCount: Number(view[15]),
906
+ jsFuncSize: Number(view[16]),
907
+ jsFuncCodeSize: Number(view[17]),
908
+ jsFuncPc2lineCount: Number(view[18]),
909
+ jsFuncPc2lineSize: Number(view[19]),
910
+ cFuncCount: Number(view[20]),
911
+ arrayCount: Number(view[21]),
912
+ fastArrayCount: Number(view[22]),
913
+ fastArrayElements: Number(view[23]),
914
+ binaryObjectCount: Number(view[24]),
915
+ binaryObjectSize: Number(view[25]),
916
+ };
917
+ this.exports.wasm_free(bufPtr);
918
+ return result;
919
+ }
920
+ /**
921
+ * Get the global object. Prefer the cached `vm.global` property.
922
+ */
923
+ getGlobal() {
924
+ this.assertNotDisposed();
925
+ return new JSValueHandle(this, this.exports.qjs_get_global());
926
+ }
927
+ /**
928
+ * Create a new QuickJS string value.
929
+ */
930
+ newString(str) {
931
+ this.assertNotDisposed();
932
+ const { ptr, len } = this.writeString(str);
933
+ const resultPtr = this.exports.qjs_new_string(ptr, len);
934
+ this.exports.wasm_free(ptr);
935
+ return new JSValueHandle(this, resultPtr);
936
+ }
937
+ /**
938
+ * Create a new QuickJS number value.
939
+ */
940
+ newNumber(num) {
941
+ this.assertNotDisposed();
942
+ return new JSValueHandle(this, this.exports.qjs_new_number(num));
943
+ }
944
+ /**
945
+ * Create a new QuickJS BigInt value.
946
+ */
947
+ newBigInt(val) {
948
+ this.assertNotDisposed();
949
+ // Split the bigint into lo/hi 32-bit halves
950
+ const lo = Number(val & 0xffffffffn);
951
+ const hi = Number((val >> 32n) & 0xffffffffn);
952
+ return new JSValueHandle(this, this.exports.qjs_new_big_int64(lo, hi));
953
+ }
954
+ /**
955
+ * Create a new QuickJS object value.
956
+ */
957
+ newObject() {
958
+ this.assertNotDisposed();
959
+ return new JSValueHandle(this, this.exports.qjs_new_object());
960
+ }
961
+ /**
962
+ * Create a new QuickJS array value.
963
+ */
964
+ newArray() {
965
+ this.assertNotDisposed();
966
+ return new JSValueHandle(this, this.exports.qjs_new_array());
967
+ }
968
+ /**
969
+ * Create a global symbol (`Symbol.for(description)`).
970
+ * Global symbols with the same description are always the same symbol,
971
+ * even across snapshot/restore.
972
+ */
973
+ newSymbolFor(description) {
974
+ this.assertNotDisposed();
975
+ const { ptr, len } = this.writeString(description);
976
+ const result = new JSValueHandle(this, this.exports.qjs_new_symbol(ptr, len, 1));
977
+ this.exports.wasm_free(ptr);
978
+ return result;
979
+ }
980
+ /**
981
+ * Create a new QuickJS ArrayBuffer by copying data from a host buffer.
982
+ */
983
+ newArrayBuffer(data) {
984
+ this.assertNotDisposed();
985
+ const bytes = data instanceof ArrayBuffer ? new Uint8Array(data) : data;
986
+ const ptr = this.exports.wasm_malloc(bytes.length);
987
+ if (ptr === 0)
988
+ throw new Error('wasm_malloc failed');
989
+ new Uint8Array(this.exports.memory.buffer).set(bytes, ptr);
990
+ const result = new JSValueHandle(this, this.exports.qjs_new_array_buffer(ptr, bytes.length));
991
+ this.exports.wasm_free(ptr);
992
+ return result;
993
+ }
994
+ /**
995
+ * Create a new QuickJS Uint8Array by copying data from a host buffer.
996
+ */
997
+ newUint8Array(data) {
998
+ this.assertNotDisposed();
999
+ const ptr = this.exports.wasm_malloc(data.length);
1000
+ if (ptr === 0)
1001
+ throw new Error('wasm_malloc failed');
1002
+ new Uint8Array(this.exports.memory.buffer).set(data, ptr);
1003
+ const result = new JSValueHandle(this, this.exports.qjs_new_uint8_array(ptr, data.length));
1004
+ this.exports.wasm_free(ptr);
1005
+ return result;
1006
+ }
1007
+ /**
1008
+ * Get undefined. Prefer the cached `vm.undefined` property.
1009
+ */
1010
+ getUndefined() {
1011
+ this.assertNotDisposed();
1012
+ return new JSValueHandle(this, this.exports.qjs_get_undefined());
1013
+ }
1014
+ /**
1015
+ * Get null. Prefer the cached `vm.null` property.
1016
+ */
1017
+ getNull() {
1018
+ this.assertNotDisposed();
1019
+ return new JSValueHandle(this, this.exports.qjs_get_null());
1020
+ }
1021
+ /**
1022
+ * Get true. Prefer the cached `vm.true` property.
1023
+ */
1024
+ getTrue() {
1025
+ this.assertNotDisposed();
1026
+ return new JSValueHandle(this, this.exports.qjs_get_true());
1027
+ }
1028
+ /**
1029
+ * Get false. Prefer the cached `vm.false` property.
1030
+ */
1031
+ getFalse() {
1032
+ this.assertNotDisposed();
1033
+ return new JSValueHandle(this, this.exports.qjs_get_false());
1034
+ }
1035
+ /**
1036
+ * Create a new QuickJS function backed by a host callback.
1037
+ *
1038
+ * When the function is called inside QuickJS, the host callback is invoked
1039
+ * with the `this` value and arguments as JSValueHandles.
1040
+ */
1041
+ newFunction(name, fn) {
1042
+ this.assertNotDisposed();
1043
+ if (this.hostCallbacks.has(name)) {
1044
+ throw new Error(`Host callback with name "${name}" is already registered`);
1045
+ }
1046
+ this.hostCallbacks.set(name, fn);
1047
+ const { ptr: namePtr, len: nameLen } = this.writeString(name);
1048
+ const resultPtr = this.exports.qjs_new_host_function(namePtr, nameLen, 0);
1049
+ this.exports.wasm_free(namePtr);
1050
+ return new JSValueHandle(this, resultPtr);
1051
+ }
1052
+ /**
1053
+ * Run `fn` with a handle scope: every handle created during the call is
1054
+ * disposed when it returns, except those passed to `scope.escape()`.
1055
+ *
1056
+ * This is the bulk alternative to disposing handles individually, for code
1057
+ * that creates many intermediates, such as walking a large value:
1058
+ *
1059
+ * ```ts
1060
+ * const name = vm.withScope((scope) => {
1061
+ * const user = root.getProp('user'); // freed automatically
1062
+ * const profile = user.getProp('profile'); // freed automatically
1063
+ * return scope.escape(profile.getProp('name'));
1064
+ * });
1065
+ * ```
1066
+ *
1067
+ * Scopes nest: `escape()` transfers the handle to the enclosing scope when
1068
+ * there is one, so it is still cleaned up at the outer boundary.
1069
+ *
1070
+ * `fn` must be synchronous. Handles created after an `await` are outside
1071
+ * the scope, because it closes as soon as `fn` returns.
1072
+ *
1073
+ * Host callbacks are safe to trigger inside a scope: the `this`/argument
1074
+ * handles the trampoline passes to a callback wrap C-owned pointers and
1075
+ * are exempt from scope tracking (see `handleHostCall`), so the scope
1076
+ * frees only handles the host actually owns. Handles a callback CREATES
1077
+ * (including `dup()`s of its arguments) are tracked normally.
1078
+ */
1079
+ withScope(fn) {
1080
+ this.assertNotDisposed();
1081
+ const enclosing = this._activeScope;
1082
+ const tracked = new Set();
1083
+ this._activeScope = tracked;
1084
+ const scope = {
1085
+ escape: (handle) => {
1086
+ tracked.delete(handle);
1087
+ enclosing?.add(handle);
1088
+ return handle;
1089
+ },
1090
+ };
1091
+ try {
1092
+ return fn(scope);
1093
+ }
1094
+ finally {
1095
+ this._activeScope = enclosing;
1096
+ for (const handle of tracked)
1097
+ handle.dispose();
1098
+ }
1099
+ }
1100
+ /**
1101
+ * Export a handle as a snapshot-portable token.
1102
+ *
1103
+ * A handle's heap box lives in the VM's linear memory, so a
1104
+ * `snapshot()` taken while the handle is alive carries it, and a VM
1105
+ * restored from that snapshot has the identical box at the identical
1106
+ * offset. `importHandle(token)` on the restored VM (or on this VM)
1107
+ * re-materializes an owned handle for the same guest value without
1108
+ * evaluating any guest code.
1109
+ *
1110
+ * Contract:
1111
+ * - the handle must stay undisposed until after `snapshot()`; its
1112
+ * box (and the reference it holds) must be part of the memory image;
1113
+ * - the token is only meaningful to THIS VM and VMs restored from a
1114
+ * snapshot of it taken while the handle was alive;
1115
+ * - `importHandle` duplicates the underlying value (fresh reference,
1116
+ * fresh box), so it can be called any number of times and each
1117
+ * returned handle is independently owned and disposable. The
1118
+ * exported box's own reference is intentionally never released on
1119
+ * restored VMs (one retained reference per VM image, reclaimed
1120
+ * with the VM).
1121
+ *
1122
+ * The intended use is boot-time capture: snapshot a VM after capturing
1123
+ * references to pristine intrinsics but BEFORE evaluating untrusted or
1124
+ * user code, then restore per task and import the captured handles,
1125
+ * guaranteeing the references predate anything user code patched,
1126
+ * without re-running capture code in the restored VM (where user-
1127
+ * patched globals could observe it). See vercel/workflow's host-side
1128
+ * serde for a worked example.
1129
+ */
1130
+ exportHandle(handle) {
1131
+ this.assertNotDisposed();
1132
+ if (handle.vm !== this) {
1133
+ throw new Error('exportHandle: handle belongs to a different VM');
1134
+ }
1135
+ if (handle.disposed) {
1136
+ throw new Error('exportHandle: handle is disposed');
1137
+ }
1138
+ if (handle._isBorrowed) {
1139
+ // Host-callback this/argument handles wrap boxes OWNED BY THE C
1140
+ // TRAMPOLINE, freed when the callback returns; a token minted
1141
+ // from one would point at freed memory in every restored VM.
1142
+ // Callbacks that need to persist an argument must dup() it first
1143
+ // (the duplicate is an owned box) and export the duplicate.
1144
+ throw new Error('exportHandle: cannot export a borrowed handle (host-callback ' +
1145
+ 'this/argument); its box is freed when the callback returns. ' +
1146
+ 'dup() it and export the duplicate.');
1147
+ }
1148
+ return handle.ptr;
1149
+ }
1150
+ /**
1151
+ * Re-materialize a handle from a token produced by `exportHandle`,
1152
+ * on this VM, or on a VM restored from a snapshot taken while the
1153
+ * exported handle was alive. Returns a NEW owned handle (the
1154
+ * underlying value's refcount is incremented); dispose it like any
1155
+ * other handle. See `exportHandle` for the full contract.
1156
+ */
1157
+ importHandle(token) {
1158
+ this.assertNotDisposed();
1159
+ // Best-effort validation before handing the value to qjs_dup_value,
1160
+ // which dereferences it as a raw JSValue* inside the WASM instance.
1161
+ // A malformed token (0, negative, fractional, out of address range)
1162
+ // would otherwise read arbitrary memory. A well-formed but FORGED
1163
+ // token remains undefined behavior: like any raw pointer, tokens
1164
+ // are only meaningful under the exportHandle contract.
1165
+ if (!Number.isInteger(token) ||
1166
+ token <= 0 ||
1167
+ token >= this.exports.memory.buffer.byteLength) {
1168
+ throw new Error(`importHandle: invalid token ${token}`);
1169
+ }
1170
+ return new JSValueHandle(this, this.exports.qjs_dup_value(token));
1171
+ }
1172
+ /**
1173
+ * Create a QuickJS function backed by a host callback whose registration is
1174
+ * tied to the returned handle: disposing the handle unregisters the
1175
+ * callback.
1176
+ *
1177
+ * Use this for short-lived callbacks (e.g. a visitor passed to
1178
+ * `Map.prototype.forEach`) where the name is an implementation detail.
1179
+ * `newFunction()` keeps its callback registered for the lifetime of the VM
1180
+ * (by design, so that names can be re-registered after a snapshot is
1181
+ * restored), which makes it unsuitable for callbacks created in a loop.
1182
+ *
1183
+ * The guest must not retain the function past disposal: calling it after
1184
+ * the handle is disposed throws, because the callback is gone. Ephemeral
1185
+ * functions do not survive snapshot/restore.
1186
+ */
1187
+ newEphemeralFunction(fn) {
1188
+ this.assertNotDisposed();
1189
+ const name = `__ephemeral:${this.nextInternalId++}`;
1190
+ this.hostCallbacks.set(name, fn);
1191
+ const { ptr: namePtr, len: nameLen } = this.writeString(name);
1192
+ const resultPtr = this.exports.qjs_new_host_function(namePtr, nameLen, 0);
1193
+ this.exports.wasm_free(namePtr);
1194
+ const handle = new JSValueHandle(this, resultPtr);
1195
+ handle._onDispose = () => {
1196
+ this.hostCallbacks.delete(name);
1197
+ };
1198
+ return handle;
1199
+ }
1200
+ /**
1201
+ * Remove a host callback registered with `newFunction()` or
1202
+ * `registerHostCallback()`. Returns true if a callback was removed.
1203
+ *
1204
+ * Any QuickJS function still referencing the name will throw when called,
1205
+ * so only unregister once the guest can no longer reach it.
1206
+ */
1207
+ unregisterHostCallback(name) {
1208
+ return this.hostCallbacks.delete(name);
1209
+ }
1210
+ /**
1211
+ * Create an internal host function that bypasses the duplicate-name check.
1212
+ * Used for ephemeral callbacks (promise settle handlers, resolvePromise, etc.)
1213
+ * that are not intended to survive snapshot/restore.
1214
+ */
1215
+ newInternalFunction(name, fn) {
1216
+ this.hostCallbacks.set(name, fn);
1217
+ const { ptr: namePtr, len: nameLen } = this.writeString(name);
1218
+ const resultPtr = this.exports.qjs_new_host_function(namePtr, nameLen, 0);
1219
+ this.exports.wasm_free(namePtr);
1220
+ return new JSValueHandle(this, resultPtr);
1221
+ }
1222
+ /**
1223
+ * Create a new promise.
1224
+ *
1225
+ * Returns a Deferred with:
1226
+ * - `handle` - the QuickJS promise object
1227
+ * - `settled` - a host Promise that resolves when the QuickJS promise settles
1228
+ * - `resolve(value)` - resolve the promise with a QuickJS value
1229
+ * - `reject(value)` - reject the promise with a QuickJS value
1230
+ */
1231
+ newPromise() {
1232
+ this.assertNotDisposed();
1233
+ const resolveOutPtr = this.exports.wasm_malloc(4);
1234
+ const rejectOutPtr = this.exports.wasm_malloc(4);
1235
+ const promisePtr = this.exports.qjs_new_promise(resolveOutPtr, rejectOutPtr);
1236
+ const view = new DataView(this.exports.memory.buffer);
1237
+ const resolvePtr = view.getUint32(resolveOutPtr, true);
1238
+ const rejectPtr = view.getUint32(rejectOutPtr, true);
1239
+ this.exports.wasm_free(resolveOutPtr);
1240
+ this.exports.wasm_free(rejectOutPtr);
1241
+ const promiseHandle = new JSValueHandle(this, promisePtr);
1242
+ const resolveHandle = new JSValueHandle(this, resolvePtr);
1243
+ const rejectHandle = new JSValueHandle(this, rejectPtr);
1244
+ const vm = this;
1245
+ // Track resolve/reject handles so they can be freed on VM dispose
1246
+ // if the promise is never resolved/rejected
1247
+ vm._ownedHandles.add(resolveHandle);
1248
+ vm._ownedHandles.add(rejectHandle);
1249
+ // Lazily-created settled promise: only attaches .then() handler when accessed
1250
+ let _settled = null;
1251
+ return {
1252
+ handle: promiseHandle,
1253
+ get settled() {
1254
+ if (!_settled) {
1255
+ let settledResolve;
1256
+ _settled = new Promise((res) => {
1257
+ settledResolve = res;
1258
+ });
1259
+ const settleName = `__settle:${vm.nextInternalId++}`;
1260
+ const onSettleFn = vm.newInternalFunction(settleName, () => {
1261
+ settledResolve();
1262
+ vm.hostCallbacks.delete(settleName);
1263
+ return vm.undefined;
1264
+ });
1265
+ vm.promiseThenRaw(promiseHandle, onSettleFn, onSettleFn).dispose();
1266
+ onSettleFn.dispose();
1267
+ }
1268
+ return _settled;
1269
+ },
1270
+ resolve(value) {
1271
+ vm.callFunctionRaw(resolveHandle, vm.undefined, value).dispose();
1272
+ vm._ownedHandles.delete(resolveHandle);
1273
+ resolveHandle.dispose();
1274
+ },
1275
+ reject(value) {
1276
+ vm.callFunctionRaw(rejectHandle, vm.undefined, value).dispose();
1277
+ vm._ownedHandles.delete(rejectHandle);
1278
+ rejectHandle.dispose();
1279
+ },
1280
+ };
1281
+ }
1282
+ /**
1283
+ * Resolve a promise handle. Returns a host-side Promise that resolves
1284
+ * with the settled value/error of the QuickJS promise.
1285
+ *
1286
+ * If the handle is not a promise, it is treated as an already-fulfilled value.
1287
+ *
1288
+ * The returned host Promise resolves to `{ value: JSValueHandle }` on
1289
+ * fulfillment or `{ error: JSValueHandle }` on rejection.
1290
+ */
1291
+ resolvePromise(promiseHandle) {
1292
+ this.assertNotDisposed();
1293
+ // If the handle is not a promise, treat it as a fulfilled value
1294
+ if (!this.exports.qjs_is_promise(promiseHandle.ptr)) {
1295
+ return Promise.resolve({ value: promiseHandle.dup() });
1296
+ }
1297
+ // Check if already settled
1298
+ const state = this.exports.qjs_promise_state(promiseHandle.ptr);
1299
+ if (state === 1) {
1300
+ // fulfilled
1301
+ return Promise.resolve({ value: new JSValueHandle(this, this.exports.qjs_promise_result(promiseHandle.ptr)) });
1302
+ }
1303
+ else if (state === 2) {
1304
+ // rejected
1305
+ return Promise.resolve({ error: new JSValueHandle(this, this.exports.qjs_promise_result(promiseHandle.ptr)) });
1306
+ }
1307
+ // Pending: attach a .then/.catch to get notified
1308
+ return new Promise((hostResolve) => {
1309
+ const id = this.nextInternalId++;
1310
+ const fulfilledName = `__onFulfilled:${id}`;
1311
+ const rejectedName = `__onRejected:${id}`;
1312
+ const onFulfilled = this.newInternalFunction(fulfilledName, (...args) => {
1313
+ const val = args[0]?.dup() ?? this.undefined;
1314
+ this.hostCallbacks.delete(fulfilledName);
1315
+ this.hostCallbacks.delete(rejectedName);
1316
+ hostResolve({ value: val });
1317
+ return this.undefined;
1318
+ });
1319
+ const onRejected = this.newInternalFunction(rejectedName, (...args) => {
1320
+ const val = args[0]?.dup() ?? this.undefined;
1321
+ this.hostCallbacks.delete(fulfilledName);
1322
+ this.hostCallbacks.delete(rejectedName);
1323
+ hostResolve({ error: val });
1324
+ return this.undefined;
1325
+ });
1326
+ // Subscribe via the engine-level primitive: JS_PromiseThen does not
1327
+ // consult Promise.prototype.then or Symbol.species, so guest code
1328
+ // that patches either cannot intercept the subscription (or run at
1329
+ // all during it).
1330
+ this.promiseThenRaw(promiseHandle, onFulfilled, onRejected).dispose();
1331
+ onFulfilled.dispose();
1332
+ onRejected.dispose();
1333
+ });
1334
+ }
1335
+ /**
1336
+ * Subscribe to a promise without executing guest code, via quickjs-ng's
1337
+ * JS_PromiseThen: no Promise.prototype.then lookup, no Symbol.species.
1338
+ * Returns the chained promise. Handler handles are borrowed (caller
1339
+ * still owns and disposes them).
1340
+ * @internal
1341
+ */
1342
+ promiseThenRaw(promise, onFulfilled, onRejected) {
1343
+ return new JSValueHandle(this, this.exports.qjs_promise_then(promise.ptr, onFulfilled.ptr, onRejected.ptr));
1344
+ }
1345
+ /**
1346
+ * Mark a promise as handled: an eventual (or already-recorded) rejection
1347
+ * will not be reported to `onUnhandledRejection`. Useful when the host
1348
+ * observes a rejection through other means (e.g. `resolvePromise()`) and
1349
+ * wants to suppress the unhandled-rejection callback for it.
1350
+ *
1351
+ * No-op if the handle is not a promise.
1352
+ */
1353
+ markPromiseHandled(promise) {
1354
+ this.assertNotDisposed();
1355
+ this.exports.qjs_promise_mark_as_handled(promise.ptr);
1356
+ }
1357
+ /**
1358
+ * Call a QuickJS function. If the function throws, a `JSException`
1359
+ * is thrown on the host side.
1360
+ */
1361
+ callFunction(func, thisVal, ...args) {
1362
+ return this.throwIfException(this.callFunctionRaw(func, thisVal, ...args));
1363
+ }
1364
+ /**
1365
+ * Invoke a QuickJS constructor with `new`, i.e. `new ctor(...args)`.
1366
+ * If the constructor throws (including when `ctor` is not a constructor),
1367
+ * a `JSException` is thrown on the host side.
1368
+ *
1369
+ * This is the counterpart to `callFunction` for building values inside
1370
+ * the VM from the host, e.g. `new Date(iso)` on a constructor captured
1371
+ * before any user code ran.
1372
+ */
1373
+ construct(ctor, ...args) {
1374
+ this.assertNotDisposed();
1375
+ const argc = args.length;
1376
+ let argvPtr = 0;
1377
+ if (argc > 0) {
1378
+ argvPtr = this.exports.wasm_malloc(argc * 4);
1379
+ const view = new DataView(this.exports.memory.buffer);
1380
+ for (let i = 0; i < argc; i++) {
1381
+ view.setUint32(argvPtr + i * 4, args[i].ptr, true);
1382
+ }
1383
+ }
1384
+ const resultPtr = this.exports.qjs_call_constructor(ctor.ptr, argc, argvPtr);
1385
+ if (argvPtr)
1386
+ this.exports.wasm_free(argvPtr);
1387
+ return this.throwIfException(new JSValueHandle(this, resultPtr));
1388
+ }
1389
+ /**
1390
+ * Internal: call a QuickJS function without throwing on exception.
1391
+ * Used by promise plumbing where exceptions are handled differently.
1392
+ */
1393
+ callFunctionRaw(func, thisVal, ...args) {
1394
+ this.assertNotDisposed();
1395
+ const argc = args.length;
1396
+ let argvPtr = 0;
1397
+ if (argc > 0) {
1398
+ argvPtr = this.exports.wasm_malloc(argc * 4);
1399
+ const view = new DataView(this.exports.memory.buffer);
1400
+ for (let i = 0; i < argc; i++) {
1401
+ view.setUint32(argvPtr + i * 4, args[i].ptr, true);
1402
+ }
1403
+ }
1404
+ const resultPtr = this.exports.qjs_call(func.ptr, thisVal.ptr, argc, argvPtr);
1405
+ if (argvPtr)
1406
+ this.exports.wasm_free(argvPtr);
1407
+ return new JSValueHandle(this, resultPtr);
1408
+ }
1409
+ /**
1410
+ * Set a property on an object. Accepts string or JSValueHandle as key.
1411
+ * JSValueHandle keys support symbols (including `Symbol.for()`).
1412
+ */
1413
+ setProp(obj, key, value) {
1414
+ this.assertNotDisposed();
1415
+ if (typeof key === 'string') {
1416
+ const { ptr: namePtr } = this.writeString(key);
1417
+ this.exports.qjs_set_prop_string(obj.ptr, namePtr, value.ptr);
1418
+ this.exports.wasm_free(namePtr);
1419
+ }
1420
+ else {
1421
+ this.exports.qjs_set_prop_value(obj.ptr, key.ptr, value.ptr);
1422
+ }
1423
+ }
1424
+ /**
1425
+ * Define a property on an object with explicit property descriptor flags.
1426
+ * Unlike `setProp`, this allows controlling `writable`, `enumerable`, and
1427
+ * `configurable` attributes, matching `Object.defineProperty()` semantics.
1428
+ * Accepts string or JSValueHandle as key (JSValueHandle keys support symbols).
1429
+ *
1430
+ * All flags default to `false` when not specified.
1431
+ */
1432
+ defineProp(obj, key, value, descriptor) {
1433
+ this.assertNotDisposed();
1434
+ let flags = 0;
1435
+ if (descriptor?.configurable)
1436
+ flags |= 1; // JS_PROP_CONFIGURABLE
1437
+ if (descriptor?.writable)
1438
+ flags |= 2; // JS_PROP_WRITABLE
1439
+ if (descriptor?.enumerable)
1440
+ flags |= 4; // JS_PROP_ENUMERABLE
1441
+ if (typeof key === 'string') {
1442
+ const { ptr: namePtr } = this.writeString(key);
1443
+ this.exports.qjs_define_prop_string(obj.ptr, namePtr, value.ptr, flags);
1444
+ this.exports.wasm_free(namePtr);
1445
+ }
1446
+ else {
1447
+ this.exports.qjs_define_prop_value(obj.ptr, key.ptr, value.ptr, flags);
1448
+ }
1449
+ }
1450
+ /**
1451
+ * Get a property from an object using a JSValueHandle key.
1452
+ * Supports symbol keys (including `Symbol.for()`).
1453
+ */
1454
+ getProp(obj, key) {
1455
+ this.assertNotDisposed();
1456
+ return new JSValueHandle(this, this.exports.qjs_get_prop_value(obj.ptr, key.ptr));
1457
+ }
1458
+ /**
1459
+ * Get the current exception, if any.
1460
+ */
1461
+ getException() {
1462
+ this.assertNotDisposed();
1463
+ return new JSValueHandle(this, this.exports.qjs_get_exception());
1464
+ }
1465
+ /**
1466
+ * Create a new QuickJS Error object.
1467
+ * Accepts a string message or a native Error object.
1468
+ */
1469
+ newError(messageOrError) {
1470
+ this.assertNotDisposed();
1471
+ const errPtr = this.exports.qjs_new_error();
1472
+ const errHandle = new JSValueHandle(this, errPtr);
1473
+ if (typeof messageOrError === 'string') {
1474
+ const msgHandle = this.newString(messageOrError);
1475
+ errHandle.setProp('message', msgHandle);
1476
+ msgHandle.dispose();
1477
+ }
1478
+ else {
1479
+ const msgHandle = this.newString(messageOrError.message);
1480
+ errHandle.setProp('message', msgHandle);
1481
+ msgHandle.dispose();
1482
+ if (messageOrError.name) {
1483
+ const nameHandle = this.newString(messageOrError.name);
1484
+ errHandle.setProp('name', nameHandle);
1485
+ nameHandle.dispose();
1486
+ }
1487
+ if (messageOrError.stack) {
1488
+ const stackHandle = this.newString(messageOrError.stack);
1489
+ errHandle.setProp('stack', stackHandle);
1490
+ stackHandle.dispose();
1491
+ }
1492
+ }
1493
+ return errHandle;
1494
+ }
1495
+ /**
1496
+ * Get the typeof a handle as a string.
1497
+ */
1498
+ typeof(handle) {
1499
+ this.assertNotDisposed();
1500
+ const e = this.exports;
1501
+ if (e.qjs_is_undefined(handle.ptr))
1502
+ return 'undefined';
1503
+ if (e.qjs_is_null(handle.ptr))
1504
+ return 'object'; // typeof null === 'object'
1505
+ if (e.qjs_is_bool(handle.ptr))
1506
+ return 'boolean';
1507
+ if (e.qjs_is_number(handle.ptr))
1508
+ return 'number';
1509
+ if (e.qjs_is_big_int(handle.ptr))
1510
+ return 'bigint';
1511
+ if (e.qjs_is_string(handle.ptr))
1512
+ return 'string';
1513
+ if (e.qjs_is_symbol(handle.ptr))
1514
+ return 'symbol';
1515
+ if (e.qjs_is_function(handle.ptr))
1516
+ return 'function';
1517
+ if (e.qjs_is_object(handle.ptr))
1518
+ return 'object';
1519
+ return 'unknown';
1520
+ }
1521
+ /**
1522
+ * Convert a QuickJS handle to a host JavaScript value.
1523
+ * Handles strings, numbers, booleans, null, undefined, bigint, arrays,
1524
+ * errors, functions, and plain objects. Circular references in objects
1525
+ * are returned as `undefined`.
1526
+ */
1527
+ dump(handle) {
1528
+ this.assertNotDisposed();
1529
+ return this._dump(handle, new Map());
1530
+ }
1531
+ _dump(handle, visited) {
1532
+ const e = this.exports;
1533
+ if (e.qjs_is_undefined(handle.ptr))
1534
+ return undefined;
1535
+ if (e.qjs_is_null(handle.ptr))
1536
+ return null;
1537
+ if (e.qjs_is_bool(handle.ptr))
1538
+ return e.qjs_get_bool(handle.ptr) !== 0;
1539
+ if (e.qjs_is_number(handle.ptr))
1540
+ return e.qjs_get_float64(handle.ptr);
1541
+ if (e.qjs_is_string(handle.ptr))
1542
+ return handle.toString();
1543
+ if (e.qjs_is_big_int(handle.ptr))
1544
+ return handle.toBigInt();
1545
+ if (e.qjs_is_symbol(handle.ptr)) {
1546
+ const descOutPtr = e.wasm_malloc(4);
1547
+ const kind = e.qjs_get_symbol_description(handle.ptr, descOutPtr);
1548
+ const view = new DataView(e.memory.buffer);
1549
+ const descPtr = view.getUint32(descOutPtr, true);
1550
+ e.wasm_free(descOutPtr);
1551
+ if (kind === 1) {
1552
+ // Global symbol: reconstruct as Symbol.for(description)
1553
+ const descHandle = new JSValueHandle(this, descPtr);
1554
+ const description = descHandle.toString();
1555
+ descHandle.dispose();
1556
+ return Symbol.for(description);
1557
+ }
1558
+ else if (kind === 2) {
1559
+ // Local (anonymous) symbol: can't be reconstructed on host
1560
+ const descHandle = new JSValueHandle(this, descPtr);
1561
+ descHandle.dispose();
1562
+ return undefined;
1563
+ }
1564
+ return undefined;
1565
+ }
1566
+ if (e.qjs_is_array_buffer(handle.ptr))
1567
+ return handle.toArrayBuffer();
1568
+ if (e.qjs_is_exception(handle.ptr)) {
1569
+ const exc = this.getException();
1570
+ const msg = exc.toString();
1571
+ exc.dispose();
1572
+ return new Error(msg);
1573
+ }
1574
+ // Functions cannot be meaningfully serialized
1575
+ if (e.qjs_is_function(handle.ptr))
1576
+ return undefined;
1577
+ // Detect circular references using the underlying JS object pointer.
1578
+ // If we've already visited this object, return the same host object
1579
+ // (preserving the circular structure on the host side).
1580
+ if (e.qjs_is_object(handle.ptr)) {
1581
+ const objPtr = e.qjs_get_value_ptr(handle.ptr);
1582
+ if (objPtr) {
1583
+ const existing = visited.get(objPtr);
1584
+ if (existing !== undefined)
1585
+ return existing;
1586
+ }
1587
+ }
1588
+ // Check for typed arrays (before the regular array check; typed arrays are not Array.isArray)
1589
+ if (e.qjs_is_object(handle.ptr)) {
1590
+ const byteOffsetPtr = e.wasm_malloc(4);
1591
+ const byteLengthPtr = e.wasm_malloc(4);
1592
+ const bytesPerElemPtr = e.wasm_malloc(4);
1593
+ const abPtr = e.qjs_get_typed_array_buffer(handle.ptr, byteOffsetPtr, byteLengthPtr, bytesPerElemPtr);
1594
+ const abHandle = new JSValueHandle(this, abPtr);
1595
+ if (e.qjs_is_exception(abHandle.ptr) === 0) {
1596
+ const view = new DataView(e.memory.buffer);
1597
+ const byteOffset = view.getUint32(byteOffsetPtr, true);
1598
+ const byteLength = view.getUint32(byteLengthPtr, true);
1599
+ const bytesPerElement = view.getUint32(bytesPerElemPtr, true);
1600
+ e.wasm_free(byteOffsetPtr);
1601
+ e.wasm_free(byteLengthPtr);
1602
+ e.wasm_free(bytesPerElemPtr);
1603
+ const abLenPtr = e.wasm_malloc(4);
1604
+ const abDataPtr = e.qjs_get_array_buffer(abHandle.ptr, abLenPtr);
1605
+ e.wasm_free(abLenPtr);
1606
+ abHandle.dispose();
1607
+ if (abDataPtr !== 0) {
1608
+ const rawBytes = new Uint8Array(e.memory.buffer, abDataPtr + byteOffset, byteLength).slice();
1609
+ switch (bytesPerElement) {
1610
+ case 1: return rawBytes;
1611
+ case 2: return new Uint16Array(rawBytes.buffer);
1612
+ case 4: return new Uint32Array(rawBytes.buffer);
1613
+ case 8: return new Float64Array(rawBytes.buffer);
1614
+ default: return rawBytes;
1615
+ }
1616
+ }
1617
+ }
1618
+ else {
1619
+ abHandle.dispose();
1620
+ e.wasm_free(byteOffsetPtr);
1621
+ e.wasm_free(byteLengthPtr);
1622
+ e.wasm_free(bytesPerElemPtr);
1623
+ }
1624
+ }
1625
+ if (e.qjs_is_array(handle.ptr)) {
1626
+ const lenHandle = handle.getProp('length');
1627
+ const len = e.qjs_get_float64(lenHandle.ptr);
1628
+ lenHandle.dispose();
1629
+ const arr = [];
1630
+ // Register the array in the visited map BEFORE populating it,
1631
+ // so circular references within the array resolve to this same array.
1632
+ const objPtr = e.qjs_get_value_ptr(handle.ptr);
1633
+ if (objPtr)
1634
+ visited.set(objPtr, arr);
1635
+ for (let i = 0; i < len; i++) {
1636
+ const elemPtr = e.qjs_get_prop_uint32(handle.ptr, i);
1637
+ const elemHandle = new JSValueHandle(this, elemPtr);
1638
+ arr.push(this._dump(elemHandle, visited));
1639
+ elemHandle.dispose();
1640
+ }
1641
+ return arr;
1642
+ }
1643
+ if (e.qjs_is_error(handle.ptr)) {
1644
+ const nameHandle = handle.getProp('name');
1645
+ const msgHandle = handle.getProp('message');
1646
+ const stackHandle = handle.getProp('stack');
1647
+ const name = nameHandle.isUndefined ? 'Error' : nameHandle.toString();
1648
+ const message = msgHandle.isUndefined ? '' : msgHandle.toString();
1649
+ const stack = stackHandle.isUndefined ? undefined : stackHandle.toString();
1650
+ nameHandle.dispose();
1651
+ msgHandle.dispose();
1652
+ stackHandle.dispose();
1653
+ const err = new Error(message);
1654
+ err.name = name;
1655
+ if (stack !== undefined) {
1656
+ err.stack = stack;
1657
+ }
1658
+ return err;
1659
+ }
1660
+ if (e.qjs_is_object(handle.ptr)) {
1661
+ const keysPtr = e.qjs_get_own_property_names(handle.ptr);
1662
+ const keysHandle = new JSValueHandle(this, keysPtr);
1663
+ if (e.qjs_is_exception(keysHandle.ptr) !== 0) {
1664
+ keysHandle.dispose();
1665
+ return {};
1666
+ }
1667
+ const lenHandle = keysHandle.getProp('length');
1668
+ const len = e.qjs_get_float64(lenHandle.ptr);
1669
+ lenHandle.dispose();
1670
+ const obj = {};
1671
+ // Register the object in the visited map BEFORE populating it,
1672
+ // so circular references resolve to this same object.
1673
+ const objPtr = e.qjs_get_value_ptr(handle.ptr);
1674
+ if (objPtr)
1675
+ visited.set(objPtr, obj);
1676
+ for (let i = 0; i < len; i++) {
1677
+ const keyPtr = e.qjs_get_prop_uint32(keysHandle.ptr, i);
1678
+ const keyHandle = new JSValueHandle(this, keyPtr);
1679
+ const key = keyHandle.toString();
1680
+ keyHandle.dispose();
1681
+ const valHandle = handle.getProp(key);
1682
+ obj[key] = this._dump(valHandle, visited);
1683
+ valHandle.dispose();
1684
+ }
1685
+ keysHandle.dispose();
1686
+ return obj;
1687
+ }
1688
+ return undefined;
1689
+ }
1690
+ /**
1691
+ * Convert a host JavaScript value to a QuickJS handle.
1692
+ */
1693
+ hostToHandle(value) {
1694
+ this.assertNotDisposed();
1695
+ if (value === undefined)
1696
+ return this.undefined;
1697
+ if (value === null)
1698
+ return this.null;
1699
+ if (value === true)
1700
+ return this.true;
1701
+ if (value === false)
1702
+ return this.false;
1703
+ if (typeof value === 'number')
1704
+ return this.newNumber(value);
1705
+ if (typeof value === 'string')
1706
+ return this.newString(value);
1707
+ if (typeof value === 'bigint')
1708
+ return this.newBigInt(value);
1709
+ if (typeof value === 'symbol') {
1710
+ const key = Symbol.keyFor(value);
1711
+ if (key !== undefined) {
1712
+ return this.newSymbolFor(key);
1713
+ }
1714
+ // Local symbols can't be transferred to QuickJS
1715
+ throw new Error(`Cannot convert local symbol to QuickJS handle. Use Symbol.for() for cross-boundary symbols.`);
1716
+ }
1717
+ if (value instanceof Promise) {
1718
+ const deferred = this.newPromise();
1719
+ value.then((r) => {
1720
+ deferred.resolve(this.hostToHandle(r));
1721
+ this.executePendingJobs();
1722
+ }, (err) => {
1723
+ deferred.reject(this.hostToHandle(err));
1724
+ this.executePendingJobs();
1725
+ });
1726
+ return deferred.handle;
1727
+ }
1728
+ if (value instanceof Error) {
1729
+ return this.newError(value);
1730
+ }
1731
+ if (value instanceof ArrayBuffer) {
1732
+ return this.newArrayBuffer(value);
1733
+ }
1734
+ if (value instanceof Uint8Array) {
1735
+ return this.newUint8Array(value);
1736
+ }
1737
+ if (ArrayBuffer.isView(value)) {
1738
+ // Other typed arrays: convert via Uint8Array view of the underlying buffer
1739
+ return this.newArrayBuffer(new Uint8Array(value.buffer, value.byteOffset, value.byteLength));
1740
+ }
1741
+ if (Array.isArray(value)) {
1742
+ const arr = this.newArray();
1743
+ for (let i = 0; i < value.length; i++) {
1744
+ const elemHandle = this.hostToHandle(value[i]);
1745
+ this.exports.qjs_set_prop_uint32(arr.ptr, i, elemHandle.ptr);
1746
+ elemHandle.dispose();
1747
+ }
1748
+ return arr;
1749
+ }
1750
+ if (typeof value === 'object' && value !== null) {
1751
+ const obj = this.newObject();
1752
+ for (const [key, val] of Object.entries(value)) {
1753
+ const valHandle = this.hostToHandle(val);
1754
+ obj.setProp(key, valHandle);
1755
+ valHandle.dispose();
1756
+ }
1757
+ return obj;
1758
+ }
1759
+ return this.undefined;
1760
+ }
1761
+ // ---- Snapshot / Restore ----
1762
+ /**
1763
+ * Snapshot the entire VM state.
1764
+ *
1765
+ * Returns a snapshot containing the full WASM linear memory. Use
1766
+ * `QuickJS.serializeSnapshot()` to convert to a versioned binary
1767
+ * buffer for persistent storage.
1768
+ */
1769
+ snapshot() {
1770
+ this.assertNotDisposed();
1771
+ return {
1772
+ memory: new Uint8Array(this.exports.memory.buffer).slice(),
1773
+ stackPointer: this.exports.__stack_pointer.value,
1774
+ runtimePtr: this.exports.qjs_get_runtime_ptr(),
1775
+ contextPtr: this.exports.qjs_get_context_ptr(),
1776
+ extensions: this.loadedExtensions.map((ext) => ({
1777
+ name: ext.name,
1778
+ memoryBase: ext.memoryBase,
1779
+ tableBase: ext.tableBase,
1780
+ initFn: ext.initFn,
1781
+ })),
1782
+ };
1783
+ }
1784
+ /**
1785
+ * Re-register a host callback after restoring from a snapshot.
1786
+ * The name must match the name passed to `newFunction()` before the snapshot.
1787
+ */
1788
+ registerHostCallback(name, fn) {
1789
+ this.hostCallbacks.set(name, fn);
1790
+ }
1791
+ /**
1792
+ * Dispose the VM, releasing all references to the WASM instance
1793
+ * so it can be garbage collected by the host JS engine.
1794
+ */
1795
+ dispose() {
1796
+ if (!this.disposed) {
1797
+ this.disposed = true;
1798
+ // Release references so the WASM instance and its linear memory
1799
+ // can be garbage collected even if someone holds onto this QuickJS object.
1800
+ this._global = null;
1801
+ this._undefined = null;
1802
+ this._null = null;
1803
+ this._true = null;
1804
+ this._false = null;
1805
+ this._ownedHandles.clear();
1806
+ this.hostCallbacks.clear();
1807
+ this._activeScope = null;
1808
+ this.exports = null;
1809
+ this.instance = null;
1810
+ this.module = null;
1811
+ }
1812
+ }
1813
+ /**
1814
+ * Support for `using` declarations (Explicit Resource Management).
1815
+ * Automatically disposes the VM when it goes out of scope.
1816
+ *
1817
+ * ```typescript
1818
+ * using vm = await QuickJS.create(wasmBytes);
1819
+ * vm.evalCode('1 + 2');
1820
+ * // vm is automatically disposed here
1821
+ * ```
1822
+ */
1823
+ [Symbol.dispose]() {
1824
+ this.dispose();
1825
+ }
1826
+ assertNotDisposed() {
1827
+ if (this.disposed) {
1828
+ throw new Error('QuickJS instance has been disposed');
1829
+ }
1830
+ }
1831
+ // ---- Internal accessors for JSValueHandle ----
1832
+ /** @internal */
1833
+ _getExports() {
1834
+ return this.exports;
1835
+ }
1836
+ /** @internal */
1837
+ _getMemory() {
1838
+ return this.exports.memory;
1839
+ }
1840
+ /** @internal */
1841
+ _writeString(str) {
1842
+ return this.writeString(str);
1843
+ }
1844
+ /** @internal */
1845
+ _readCString(ptr) {
1846
+ return this.readCString(ptr);
1847
+ }
1848
+ }
1849
+ // ---- lossless string helpers ----
1850
+ /**
1851
+ * Whether a string-typed property key survives the NUL-terminated
1852
+ * C-string key APIs: embedded U+0000 truncates the key, and an UNPAIRED
1853
+ * surrogate cannot be UTF-8 encoded (paired surrogates, such as those
1854
+ * that encode emoji, are fine). Keys that don't survive are routed through length-aware
1855
+ * guest string values instead.
1856
+ */
1857
+ function stringKeyNeedsValuePath(key) {
1858
+ return key.includes('\u0000') || LONE_SURROGATE_RE.test(key);
1859
+ }
1860
+ const wtf8Decoder = new TextDecoder();
1861
+ const wtf8Encoder = new TextEncoder();
1862
+ const LONE_SURROGATE_RE = /(?:[\uD800-\uDBFF](?![\uDC00-\uDFFF]))|(?:(?<![\uD800-\uDBFF])[\uDC00-\uDFFF])/;
1863
+ /**
1864
+ * Encode a JS string to WTF-8 bytes. Well-formed strings (including
1865
+ * paired surrogates, such as emoji) take the native TextEncoder; strings with
1866
+ * LONE surrogates take a manual encode that writes each unpaired
1867
+ * surrogate as the 3-byte sequence quickjs's tolerant UTF-8 decoder
1868
+ * accepts; TextEncoder would replace them with U+FFFD, silently
1869
+ * corrupting every host→guest string (sources, property keys,
1870
+ * newString values).
1871
+ */
1872
+ function encodeWtf8(str) {
1873
+ if (!LONE_SURROGATE_RE.test(str))
1874
+ return wtf8Encoder.encode(str);
1875
+ const bytes = [];
1876
+ for (let i = 0; i < str.length; i++) {
1877
+ const code = str.charCodeAt(i);
1878
+ if (code < 0x80) {
1879
+ bytes.push(code);
1880
+ }
1881
+ else if (code < 0x800) {
1882
+ bytes.push(0xc0 | (code >> 6), 0x80 | (code & 0x3f));
1883
+ }
1884
+ else if (code >= 0xd800 && code <= 0xdbff && i + 1 < str.length) {
1885
+ const next = str.charCodeAt(i + 1);
1886
+ if (next >= 0xdc00 && next <= 0xdfff) {
1887
+ // Well-formed pair: 4-byte UTF-8.
1888
+ const cp = 0x10000 + ((code - 0xd800) << 10) + (next - 0xdc00);
1889
+ bytes.push(0xf0 | (cp >> 18), 0x80 | ((cp >> 12) & 0x3f), 0x80 | ((cp >> 6) & 0x3f), 0x80 | (cp & 0x3f));
1890
+ i++;
1891
+ continue;
1892
+ }
1893
+ // Lone high surrogate: 3-byte WTF-8.
1894
+ bytes.push(0xe0 | (code >> 12), 0x80 | ((code >> 6) & 0x3f), 0x80 | (code & 0x3f));
1895
+ }
1896
+ else {
1897
+ // BMP char or lone (low / trailing high) surrogate: 3-byte form.
1898
+ bytes.push(0xe0 | (code >> 12), 0x80 | ((code >> 6) & 0x3f), 0x80 | (code & 0x3f));
1899
+ }
1900
+ }
1901
+ return new Uint8Array(bytes);
1902
+ }
1903
+ /**
1904
+ * Decode WTF-8 bytes to a JS string. WTF-8 is UTF-8 extended with 3-byte
1905
+ * sequences for surrogate code points (0xED 0xA0-0xBF 0x80-0xBF), which
1906
+ * is how quickjs's JS_ToCStringLen2 encodes lone surrogates ("keep
1907
+ * unmatched surrogate code points"). TextDecoder replaces those
1908
+ * sequences with U+FFFD, so they are detected first and the rare strings
1909
+ * containing them take a manual decode; everything else (the
1910
+ * overwhelming majority) uses the native decoder.
1911
+ */
1912
+ function decodeWtf8(bytes) {
1913
+ let hasSurrogateSequence = false;
1914
+ for (let i = 0; i < bytes.length - 1; i++) {
1915
+ if (bytes[i] === 0xed && bytes[i + 1] >= 0xa0 && bytes[i + 1] <= 0xbf) {
1916
+ hasSurrogateSequence = true;
1917
+ break;
1918
+ }
1919
+ }
1920
+ if (!hasSurrogateSequence)
1921
+ return wtf8Decoder.decode(bytes);
1922
+ let out = '';
1923
+ let i = 0;
1924
+ while (i < bytes.length) {
1925
+ const b0 = bytes[i];
1926
+ if (b0 < 0x80) {
1927
+ out += String.fromCharCode(b0);
1928
+ i += 1;
1929
+ }
1930
+ else if (b0 < 0xe0) {
1931
+ out += String.fromCharCode(((b0 & 0x1f) << 6) | (bytes[i + 1] & 0x3f));
1932
+ i += 2;
1933
+ }
1934
+ else if (b0 < 0xf0) {
1935
+ // 3-byte sequence: may decode into the surrogate range, which is
1936
+ // exactly the WTF-8 extension: emit the code unit as-is.
1937
+ out += String.fromCharCode(((b0 & 0x0f) << 12) | ((bytes[i + 1] & 0x3f) << 6) | (bytes[i + 2] & 0x3f));
1938
+ i += 3;
1939
+ }
1940
+ else {
1941
+ const cp = ((b0 & 0x07) << 18) |
1942
+ ((bytes[i + 1] & 0x3f) << 12) |
1943
+ ((bytes[i + 2] & 0x3f) << 6) |
1944
+ (bytes[i + 3] & 0x3f);
1945
+ out += String.fromCodePoint(cp);
1946
+ i += 4;
1947
+ }
1948
+ }
1949
+ return out;
1950
+ }
1951
+ // ---- JSException ----
1952
+ /**
1953
+ * An exception thrown from QuickJS code. Extends `Error` so it works with
1954
+ * standard error handling (`instanceof Error`, `.message`, `.stack`), and
1955
+ * also exposes a `handle` property, a live `JSValueHandle` to the QuickJS
1956
+ * exception value, allowing direct inspection of custom properties.
1957
+ *
1958
+ * The `handle` must be disposed when you're done with it (or use `using`).
1959
+ * If the error propagates uncaught, the handle will be cleaned up when the
1960
+ * VM is disposed.
1961
+ */
1962
+ export class JSException extends Error {
1963
+ /**
1964
+ * A live handle to the QuickJS exception value. You can read custom
1965
+ * properties, call methods, etc. Must be disposed when done.
1966
+ */
1967
+ handle;
1968
+ // Cached values so they survive handle disposal / VM teardown.
1969
+ // Using # fields keeps them out of console.log / Object.keys output.
1970
+ #name;
1971
+ #message;
1972
+ #stack;
1973
+ /** @internal */
1974
+ constructor(handle) {
1975
+ const env_1 = { stack: [], error: void 0, hasError: false };
1976
+ try {
1977
+ super();
1978
+ this.handle = handle;
1979
+ // V8 installs a lazy `stack` accessor on Error instances that shadows
1980
+ // our prototype getter. Delete it so our getter takes effect.
1981
+ delete this.stack;
1982
+ // Read error properties eagerly and cache them.
1983
+ const msgHandle = __addDisposableResource(env_1, handle.getProp('message'), false);
1984
+ this.#name = handle.getProp('name').consume(h => h.isUndefined ? 'Error' : h.toString());
1985
+ this.#message = msgHandle.isUndefined ? handle.toString() : msgHandle.toString();
1986
+ this.#stack = handle.getProp('stack').consume(h => h.isUndefined ? undefined : h.toString());
1987
+ }
1988
+ catch (e_1) {
1989
+ env_1.error = e_1;
1990
+ env_1.hasError = true;
1991
+ }
1992
+ finally {
1993
+ __disposeResources(env_1);
1994
+ }
1995
+ }
1996
+ get name() {
1997
+ return this.#name;
1998
+ }
1999
+ set name(v) {
2000
+ this.#name = v;
2001
+ }
2002
+ get message() {
2003
+ return this.#message;
2004
+ }
2005
+ set message(v) {
2006
+ this.#message = v;
2007
+ }
2008
+ get stack() {
2009
+ return this.#stack;
2010
+ }
2011
+ set stack(v) {
2012
+ this.#stack = v;
2013
+ }
2014
+ dispose() {
2015
+ this.handle.dispose();
2016
+ }
2017
+ [Symbol.dispose]() {
2018
+ this.handle.dispose();
2019
+ }
2020
+ }
2021
+ // ---- JSValue Handle ----
2022
+ /**
2023
+ * A handle to a JSValue inside the QuickJS WASM instance.
2024
+ */
2025
+ export class JSValueHandle {
2026
+ /** The QuickJS VM instance this handle belongs to. */
2027
+ vm;
2028
+ /** @internal */
2029
+ ptr;
2030
+ disposed_ = false;
2031
+ /**
2032
+ * When true, this handle is a cached singleton (e.g. `undefined`, `null`,
2033
+ * `true`, `false`, the global object) and `dispose()` is a no-op. This
2034
+ * prevents code that routinely disposes handles (such as the object/array
2035
+ * branches of `hostToHandle`) from freeing the shared heap `JSValue*` that
2036
+ * the cached singleton still references, which would corrupt later reads.
2037
+ * @internal
2038
+ */
2039
+ singleton;
2040
+ /**
2041
+ * When true, this handle wraps a `JSValue*` OWNED BY THE C CALLER: the
2042
+ * `this`/argument handles the host-call trampoline passes to a host
2043
+ * callback (`handleHostCall`). The C side frees those values after the
2044
+ * call returns, so `dispose()` is a no-op and the handle is never
2045
+ * registered with an active `withScope()` (either would double-free
2046
+ * the guest value and corrupt the heap). A callback that needs to
2047
+ * retain an argument past its own invocation must `dup()` it; the
2048
+ * duplicate takes a fresh reference and behaves like any owned handle.
2049
+ * @internal
2050
+ */
2051
+ borrowed;
2052
+ /**
2053
+ * Extra cleanup to run when this handle is disposed. Used by
2054
+ * `newEphemeralFunction()` to unregister its host callback.
2055
+ * @internal
2056
+ */
2057
+ _onDispose;
2058
+ constructor(vm, ptr, singleton = false, borrowed = false) {
2059
+ this.vm = vm;
2060
+ this.ptr = ptr;
2061
+ this.singleton = singleton;
2062
+ this.borrowed = borrowed;
2063
+ // Singletons are shared and outlive any scope; borrowed handles wrap
2064
+ // C-owned pointers that a scope must never free.
2065
+ if (!singleton && !borrowed)
2066
+ vm._activeScope?.add(this);
2067
+ }
2068
+ /**
2069
+ * Whether this handle wraps a C-owned pointer (host-callback
2070
+ * `this`/arguments). Borrowed handles must never be exported as
2071
+ * snapshot tokens: the trampoline frees their boxes after the
2072
+ * callback returns. @internal
2073
+ */
2074
+ get _isBorrowed() {
2075
+ return this.borrowed;
2076
+ }
2077
+ /**
2078
+ * Whether `dispose()` has been called on this handle.
2079
+ *
2080
+ * Note that handle methods do not currently guard against use after
2081
+ * disposal: reading from a disposed handle reads freed memory. Check this
2082
+ * when a handle's lifetime is managed elsewhere (e.g. by `withScope()`).
2083
+ */
2084
+ get disposed() {
2085
+ // singletons are never freed, so they are never "disposed"
2086
+ return this.disposed_;
2087
+ }
2088
+ get isUndefined() {
2089
+ return this.vm._getExports().qjs_is_undefined(this.ptr) !== 0;
2090
+ }
2091
+ get isNull() {
2092
+ return this.vm._getExports().qjs_is_null(this.ptr) !== 0;
2093
+ }
2094
+ /**
2095
+ * Get the promise state: 0 = pending, 1 = fulfilled, 2 = rejected
2096
+ */
2097
+ get isBool() {
2098
+ return this.vm._getExports().qjs_is_bool(this.ptr) !== 0;
2099
+ }
2100
+ get isNumber() {
2101
+ return this.vm._getExports().qjs_is_number(this.ptr) !== 0;
2102
+ }
2103
+ get isString() {
2104
+ return this.vm._getExports().qjs_is_string(this.ptr) !== 0;
2105
+ }
2106
+ get isSymbol() {
2107
+ return this.vm._getExports().qjs_is_symbol(this.ptr) !== 0;
2108
+ }
2109
+ get isBigInt() {
2110
+ return this.vm._getExports().qjs_is_big_int(this.ptr) !== 0;
2111
+ }
2112
+ get isObject() {
2113
+ return this.vm._getExports().qjs_is_object(this.ptr) !== 0;
2114
+ }
2115
+ get isArray() {
2116
+ return this.vm._getExports().qjs_is_array(this.ptr) !== 0;
2117
+ }
2118
+ get isFunction() {
2119
+ return this.vm._getExports().qjs_is_function(this.ptr) !== 0;
2120
+ }
2121
+ get isError() {
2122
+ return this.vm._getExports().qjs_is_error(this.ptr) !== 0;
2123
+ }
2124
+ get isPromise() {
2125
+ return this.vm._getExports().qjs_is_promise(this.ptr) !== 0;
2126
+ }
2127
+ get isArrayBuffer() {
2128
+ return this.vm._getExports().qjs_is_array_buffer(this.ptr) !== 0;
2129
+ }
2130
+ /**
2131
+ * Whether this value is a Proxy exotic object.
2132
+ *
2133
+ * This is an engine-level check: it never fires proxy traps and cannot
2134
+ * be determined (or spoofed) from within guest JavaScript. Use
2135
+ * {@link getProxyTarget} / {@link getProxyHandler} to introspect a
2136
+ * detected proxy without executing guest code.
2137
+ */
2138
+ get isProxy() {
2139
+ return this.vm._getExports().qjs_is_proxy(this.ptr) !== 0;
2140
+ }
2141
+ /**
2142
+ * Whether this value is a Map (engine brand check: trap-free,
2143
+ * spoof-proof, and unaffected by prototype/constructor mutation).
2144
+ * A Proxy wrapping a Map returns false.
2145
+ */
2146
+ get isMap() {
2147
+ return this.vm._getExports().qjs_is_map(this.ptr) !== 0;
2148
+ }
2149
+ /**
2150
+ * Whether this value is a Set (engine brand check: trap-free,
2151
+ * spoof-proof, and unaffected by prototype/constructor mutation).
2152
+ * A Proxy wrapping a Set returns false.
2153
+ */
2154
+ get isSet() {
2155
+ return this.vm._getExports().qjs_is_set(this.ptr) !== 0;
2156
+ }
2157
+ /**
2158
+ * Whether this value is a Date (engine brand check: trap-free,
2159
+ * spoof-proof, and unaffected by prototype/constructor mutation).
2160
+ * A Proxy wrapping a Date returns false.
2161
+ */
2162
+ get isDate() {
2163
+ return this.vm._getExports().qjs_is_date(this.ptr) !== 0;
2164
+ }
2165
+ /**
2166
+ * Whether this value is a RegExp (engine brand check: trap-free,
2167
+ * spoof-proof, and unaffected by prototype/constructor mutation).
2168
+ * A Proxy wrapping a RegExp returns false.
2169
+ */
2170
+ get isRegExp() {
2171
+ return this.vm._getExports().qjs_is_regexp(this.ptr) !== 0;
2172
+ }
2173
+ /** Whether this value is a WeakRef (engine brand check). */
2174
+ get isWeakRef() {
2175
+ return this.vm._getExports().qjs_is_weak_ref(this.ptr) !== 0;
2176
+ }
2177
+ /** Whether this value is a WeakMap (engine brand check). */
2178
+ get isWeakMap() {
2179
+ return this.vm._getExports().qjs_is_weak_map(this.ptr) !== 0;
2180
+ }
2181
+ /** Whether this value is a WeakSet (engine brand check). */
2182
+ get isWeakSet() {
2183
+ return this.vm._getExports().qjs_is_weak_set(this.ptr) !== 0;
2184
+ }
2185
+ /** Whether this value is a DataView (engine brand check). */
2186
+ get isDataView() {
2187
+ return this.vm._getExports().qjs_is_data_view(this.ptr) !== 0;
2188
+ }
2189
+ /**
2190
+ * A numeric identity for the underlying heap value, or 0 for values that
2191
+ * are not heap-allocated (numbers, booleans, `null`, `undefined`).
2192
+ *
2193
+ * Two handles to the same underlying object always report the same
2194
+ * identity, and two live handles to different objects always report
2195
+ * different identities, so this is the value to key a `Map` on when
2196
+ * deduplicating or detecting cycles across handles (`dump()` uses it for
2197
+ * exactly that).
2198
+ *
2199
+ * The identity is only meaningful while the value is alive; it is an
2200
+ * address, so it may be reused after every handle to the value has been
2201
+ * disposed. Do not persist it, and do not treat it as unforgeable: a
2202
+ * number read out of the guest can trivially collide with one.
2203
+ */
2204
+ get identity() {
2205
+ return this.vm._getExports().qjs_get_value_ptr(this.ptr);
2206
+ }
2207
+ /**
2208
+ * Extract the value as a boolean, applying JavaScript truthiness
2209
+ * (equivalent to `!!value` inside the VM).
2210
+ */
2211
+ toBoolean() {
2212
+ return this.vm._getExports().qjs_get_bool(this.ptr) !== 0;
2213
+ }
2214
+ /**
2215
+ * The internal QuickJS class ID of this value, or 0 for non-objects.
2216
+ * Useful as a generic engine-level brand when no dedicated `is*`
2217
+ * getter exists. Class IDs are stable within a VM instance but are an
2218
+ * engine implementation detail, so prefer the dedicated getters.
2219
+ */
2220
+ get classId() {
2221
+ return this.vm._getExports().qjs_get_class_id(this.ptr);
2222
+ }
2223
+ /**
2224
+ * The engine-level class name of this value, e.g. `"Object"`, `"Map"`,
2225
+ * `"Date"`, `"RegExp"`, or the registered name of an extension-defined
2226
+ * class like `"URL"`, or `undefined` for non-objects and unnamed
2227
+ * internal classes.
2228
+ *
2229
+ * Unlike `constructorName` (which reads the `constructor` and `name`
2230
+ * properties and can therefore fire getters/proxy traps and be spoofed),
2231
+ * this is trap-free: it reads the engine's class table directly, never
2232
+ * executes guest code, and cannot be forged by reassigning prototypes or
2233
+ * constructors. Note that the engine registers the Proxy class under the
2234
+ * name `"Object"` (mirroring `Object.prototype.toString`), so use `isProxy`
2235
+ * to detect proxies and `getProxyTarget()` to read the target's brand.
2236
+ */
2237
+ get className() {
2238
+ const h = new JSValueHandle(this.vm, this.vm._getExports().qjs_get_class_name(this.ptr));
2239
+ // qjs_get_class_name can return JS_EXCEPTION (e.g. OOM while
2240
+ // materializing the name atom as a string); surface it instead of
2241
+ // stringifying the exception sentinel and leaving the real error
2242
+ // pending on the context.
2243
+ if (this.vm._getExports().qjs_is_exception(h.ptr) !== 0) {
2244
+ h.dispose();
2245
+ throw new JSException(this.vm.getException());
2246
+ }
2247
+ try {
2248
+ return h.isUndefined ? undefined : h.toString();
2249
+ }
2250
+ finally {
2251
+ h.dispose();
2252
+ }
2253
+ }
2254
+ get promiseState() {
2255
+ return this.vm._getExports().qjs_promise_state(this.ptr);
2256
+ }
2257
+ /**
2258
+ * Get the typeof this value as a string.
2259
+ * Returns the same values as the native `typeof` operator.
2260
+ */
2261
+ get typeof() {
2262
+ return this.vm.typeof(this);
2263
+ }
2264
+ /**
2265
+ * Get the length property of this value (for arrays, strings, etc.).
2266
+ */
2267
+ get length() {
2268
+ const h = this.getProp('length');
2269
+ const n = h.toNumber();
2270
+ h.dispose();
2271
+ return n;
2272
+ }
2273
+ /**
2274
+ * Get the constructor name of this object, or undefined if unavailable.
2275
+ */
2276
+ get constructorName() {
2277
+ const ctor = this.getProp('constructor');
2278
+ if (ctor.isUndefined || ctor.isNull) {
2279
+ ctor.dispose();
2280
+ return undefined;
2281
+ }
2282
+ const name = ctor.getProp('name');
2283
+ ctor.dispose();
2284
+ if (name.isUndefined || name.isNull) {
2285
+ name.dispose();
2286
+ return undefined;
2287
+ }
2288
+ const result = name.toString();
2289
+ name.dispose();
2290
+ return result;
2291
+ }
2292
+ /**
2293
+ * Get the own enumerable string property names (equivalent to Object.keys()).
2294
+ */
2295
+ keys() {
2296
+ const e = this.vm._getExports();
2297
+ const keysPtr = e.qjs_get_own_property_names(this.ptr);
2298
+ const keysHandle = new JSValueHandle(this.vm, keysPtr);
2299
+ if (e.qjs_is_exception(keysHandle.ptr) !== 0) {
2300
+ keysHandle.dispose();
2301
+ return [];
2302
+ }
2303
+ const lenHandle = keysHandle.getProp('length');
2304
+ const len = e.qjs_get_float64(lenHandle.ptr);
2305
+ lenHandle.dispose();
2306
+ const result = [];
2307
+ for (let i = 0; i < len; i++) {
2308
+ const keyPtr = e.qjs_get_prop_uint32(keysHandle.ptr, i);
2309
+ const keyHandle = new JSValueHandle(this.vm, keyPtr);
2310
+ result.push(keyHandle.toString());
2311
+ keyHandle.dispose();
2312
+ }
2313
+ keysHandle.dispose();
2314
+ return result;
2315
+ }
2316
+ /**
2317
+ * Get all own property names including non-enumerable ones
2318
+ * (equivalent to Object.getOwnPropertyNames()).
2319
+ */
2320
+ getOwnPropertyNames() {
2321
+ const e = this.vm._getExports();
2322
+ const keysPtr = e.qjs_get_own_property_names_all(this.ptr);
2323
+ const keysHandle = new JSValueHandle(this.vm, keysPtr);
2324
+ if (e.qjs_is_exception(keysHandle.ptr) !== 0) {
2325
+ keysHandle.dispose();
2326
+ return [];
2327
+ }
2328
+ const lenHandle = keysHandle.getProp('length');
2329
+ const len = e.qjs_get_float64(lenHandle.ptr);
2330
+ lenHandle.dispose();
2331
+ const result = [];
2332
+ for (let i = 0; i < len; i++) {
2333
+ const keyPtr = e.qjs_get_prop_uint32(keysHandle.ptr, i);
2334
+ const keyHandle = new JSValueHandle(this.vm, keyPtr);
2335
+ result.push(keyHandle.toString());
2336
+ keyHandle.dispose();
2337
+ }
2338
+ keysHandle.dispose();
2339
+ return result;
2340
+ }
2341
+ /**
2342
+ * Get ALL own property keys (strings and symbols), including
2343
+ * non-enumerable (equivalent to Reflect.ownKeys()).
2344
+ *
2345
+ * String keys are returned as strings; symbol keys are returned as
2346
+ * JSValueHandles which the caller must dispose.
2347
+ *
2348
+ * Trap-free for ordinary objects; fires the `ownKeys` trap for a
2349
+ * Proxy (check {@link isProxy} first if that matters).
2350
+ */
2351
+ getOwnPropertyKeys() {
2352
+ const e = this.vm._getExports();
2353
+ const keysPtr = e.qjs_get_own_property_keys(this.ptr);
2354
+ const keysHandle = new JSValueHandle(this.vm, keysPtr);
2355
+ if (e.qjs_is_exception(keysHandle.ptr) !== 0) {
2356
+ keysHandle.dispose();
2357
+ return [];
2358
+ }
2359
+ const lenHandle = keysHandle.getProp('length');
2360
+ const len = e.qjs_get_float64(lenHandle.ptr);
2361
+ lenHandle.dispose();
2362
+ const result = [];
2363
+ for (let i = 0; i < len; i++) {
2364
+ const keyPtr = e.qjs_get_prop_uint32(keysHandle.ptr, i);
2365
+ const keyHandle = new JSValueHandle(this.vm, keyPtr);
2366
+ if (keyHandle.isSymbol) {
2367
+ result.push(keyHandle);
2368
+ }
2369
+ else {
2370
+ result.push(keyHandle.toString());
2371
+ keyHandle.dispose();
2372
+ }
2373
+ }
2374
+ keysHandle.dispose();
2375
+ return result;
2376
+ }
2377
+ /**
2378
+ * Get the own property descriptor for a key WITHOUT invoking getters
2379
+ * (equivalent to Object.getOwnPropertyDescriptor()).
2380
+ *
2381
+ * This is the safe way to inspect a property that may be an accessor:
2382
+ * a data property yields `{ value, writable, enumerable, configurable }`,
2383
+ * an accessor property yields `{ get, set, enumerable, configurable }`
2384
+ * where `get`/`set` are handles to the accessor functions themselves
2385
+ * (never invoked). Returns undefined if there is no such own property.
2386
+ *
2387
+ * The `value`/`get`/`set` handles are owned by the caller and must be
2388
+ * disposed.
2389
+ *
2390
+ * Trap-free for ordinary objects; fires the `getOwnPropertyDescriptor`
2391
+ * trap for a Proxy (check {@link isProxy} first if that matters).
2392
+ */
2393
+ getOwnPropertyDescriptor(key) {
2394
+ const env_2 = { stack: [], error: void 0, hasError: false };
2395
+ try {
2396
+ const e = this.vm._getExports();
2397
+ let keyHandle;
2398
+ let keyPtr;
2399
+ if (typeof key === 'string') {
2400
+ keyHandle = this.vm.newString(key);
2401
+ keyPtr = keyHandle.ptr;
2402
+ }
2403
+ else {
2404
+ keyPtr = key.ptr;
2405
+ }
2406
+ const descPtr = e.qjs_get_own_property_descriptor(this.ptr, keyPtr);
2407
+ keyHandle?.dispose();
2408
+ if (descPtr === 0)
2409
+ return undefined; /* no such own property */
2410
+ const descHandle = __addDisposableResource(env_2, new JSValueHandle(this.vm, descPtr), false);
2411
+ if (e.qjs_is_exception(descHandle.ptr) !== 0) {
2412
+ throw new JSException(this.vm.getException());
2413
+ }
2414
+ const enumerable = descHandle.getProp('enumerable').consume(h => e.qjs_get_bool(h.ptr) !== 0);
2415
+ const configurable = descHandle.getProp('configurable').consume(h => e.qjs_get_bool(h.ptr) !== 0);
2416
+ if (descHandle.hasOwnProperty('value')) {
2417
+ return {
2418
+ value: descHandle.getProp('value'),
2419
+ writable: descHandle.getProp('writable').consume(h => e.qjs_get_bool(h.ptr) !== 0),
2420
+ enumerable,
2421
+ configurable,
2422
+ };
2423
+ }
2424
+ return {
2425
+ get: descHandle.getProp('get'),
2426
+ set: descHandle.getProp('set'),
2427
+ enumerable,
2428
+ configurable,
2429
+ };
2430
+ }
2431
+ catch (e_2) {
2432
+ env_2.error = e_2;
2433
+ env_2.hasError = true;
2434
+ }
2435
+ finally {
2436
+ __disposeResources(env_2);
2437
+ }
2438
+ }
2439
+ /**
2440
+ * Check if a property is an own property (equivalent to Object.prototype.hasOwnProperty).
2441
+ */
2442
+ hasOwnProperty(name) {
2443
+ if (stringKeyNeedsValuePath(name)) {
2444
+ const env_3 = { stack: [], error: void 0, hasError: false };
2445
+ try {
2446
+ const keyHandle = __addDisposableResource(env_3, this.vm.newString(name), false);
2447
+ return (this.vm._getExports().qjs_has_own_property_value(this.ptr, keyHandle.ptr) === 1);
2448
+ }
2449
+ catch (e_3) {
2450
+ env_3.error = e_3;
2451
+ env_3.hasError = true;
2452
+ }
2453
+ finally {
2454
+ __disposeResources(env_3);
2455
+ }
2456
+ }
2457
+ const { ptr: namePtr } = this.vm._writeString(name);
2458
+ const result = this.vm._getExports().qjs_has_own_property(this.ptr, namePtr);
2459
+ this.vm._getExports().wasm_free(namePtr);
2460
+ return result === 1;
2461
+ }
2462
+ /**
2463
+ * Check if a property is enumerable (equivalent to Object.prototype.propertyIsEnumerable).
2464
+ */
2465
+ propertyIsEnumerable(name) {
2466
+ if (stringKeyNeedsValuePath(name)) {
2467
+ const env_4 = { stack: [], error: void 0, hasError: false };
2468
+ try {
2469
+ const keyHandle = __addDisposableResource(env_4, this.vm.newString(name), false);
2470
+ return (this.vm._getExports().qjs_property_is_enumerable_value(this.ptr, keyHandle.ptr) === 1);
2471
+ }
2472
+ catch (e_4) {
2473
+ env_4.error = e_4;
2474
+ env_4.hasError = true;
2475
+ }
2476
+ finally {
2477
+ __disposeResources(env_4);
2478
+ }
2479
+ }
2480
+ const { ptr: namePtr } = this.vm._writeString(name);
2481
+ const result = this.vm._getExports().qjs_property_is_enumerable(this.ptr, namePtr);
2482
+ this.vm._getExports().wasm_free(namePtr);
2483
+ return result === 1;
2484
+ }
2485
+ /**
2486
+ * Get the prototype of this object (equivalent to Object.getPrototypeOf()).
2487
+ */
2488
+ getPrototypeOf() {
2489
+ const protoPtr = this.vm._getExports().qjs_get_prototype_of(this.ptr);
2490
+ return new JSValueHandle(this.vm, protoPtr);
2491
+ }
2492
+ /**
2493
+ * Get the `[[ProxyTarget]]` of this Proxy without firing any traps.
2494
+ * Throws {@link JSException} if this value is not a Proxy; check
2495
+ * {@link isProxy} first. Note the target may itself be a Proxy.
2496
+ */
2497
+ getProxyTarget() {
2498
+ const ptr = this.vm._getExports().qjs_get_proxy_target(this.ptr);
2499
+ const handle = new JSValueHandle(this.vm, ptr);
2500
+ if (this.vm._getExports().qjs_is_exception(handle.ptr) !== 0) {
2501
+ handle.dispose();
2502
+ throw new JSException(this.vm.getException());
2503
+ }
2504
+ return handle;
2505
+ }
2506
+ /**
2507
+ * Get the `[[ProxyHandler]]` of this Proxy without firing any traps.
2508
+ * Throws {@link JSException} if this value is not a Proxy; check
2509
+ * {@link isProxy} first.
2510
+ */
2511
+ getProxyHandler() {
2512
+ const ptr = this.vm._getExports().qjs_get_proxy_handler(this.ptr);
2513
+ const handle = new JSValueHandle(this.vm, ptr);
2514
+ if (this.vm._getExports().qjs_is_exception(handle.ptr) !== 0) {
2515
+ handle.dispose();
2516
+ throw new JSException(this.vm.getException());
2517
+ }
2518
+ return handle;
2519
+ }
2520
+ /**
2521
+ * Get a property by name.
2522
+ */
2523
+ getProp(name) {
2524
+ if (stringKeyNeedsValuePath(name)) {
2525
+ const env_5 = { stack: [], error: void 0, hasError: false };
2526
+ try {
2527
+ // A NUL or lone surrogate in the key cannot cross the C-string
2528
+ // API; go through a length-aware guest string key instead.
2529
+ const keyHandle = __addDisposableResource(env_5, this.vm.newString(name), false);
2530
+ return this.vm.getProp(this, keyHandle);
2531
+ }
2532
+ catch (e_5) {
2533
+ env_5.error = e_5;
2534
+ env_5.hasError = true;
2535
+ }
2536
+ finally {
2537
+ __disposeResources(env_5);
2538
+ }
2539
+ }
2540
+ const { ptr: namePtr } = this.vm._writeString(name);
2541
+ const resultPtr = this.vm._getExports().qjs_get_prop_string(this.ptr, namePtr);
2542
+ this.vm._getExports().wasm_free(namePtr);
2543
+ return new JSValueHandle(this.vm, resultPtr);
2544
+ }
2545
+ /**
2546
+ * Set a property by name.
2547
+ */
2548
+ setProp(name, value) {
2549
+ if (stringKeyNeedsValuePath(name)) {
2550
+ const env_6 = { stack: [], error: void 0, hasError: false };
2551
+ try {
2552
+ const keyHandle = __addDisposableResource(env_6, this.vm.newString(name), false);
2553
+ this.vm.setProp(this, keyHandle, value);
2554
+ return;
2555
+ }
2556
+ catch (e_6) {
2557
+ env_6.error = e_6;
2558
+ env_6.hasError = true;
2559
+ }
2560
+ finally {
2561
+ __disposeResources(env_6);
2562
+ }
2563
+ }
2564
+ const { ptr: namePtr } = this.vm._writeString(name);
2565
+ this.vm._getExports().qjs_set_prop_string(this.ptr, namePtr, value.ptr);
2566
+ this.vm._getExports().wasm_free(namePtr);
2567
+ }
2568
+ /**
2569
+ * Define a property with explicit property descriptor flags.
2570
+ * Unlike `setProp`, this allows controlling `writable`, `enumerable`, and
2571
+ * `configurable` attributes, matching `Object.defineProperty()` semantics.
2572
+ * Accepts string or JSValueHandle as key (JSValueHandle keys support symbols).
2573
+ *
2574
+ * All flags default to `false` when not specified.
2575
+ */
2576
+ defineProp(key, value, descriptor) {
2577
+ let flags = 0;
2578
+ if (descriptor?.configurable)
2579
+ flags |= 1; // JS_PROP_CONFIGURABLE
2580
+ if (descriptor?.writable)
2581
+ flags |= 2; // JS_PROP_WRITABLE
2582
+ if (descriptor?.enumerable)
2583
+ flags |= 4; // JS_PROP_ENUMERABLE
2584
+ if (typeof key === 'string') {
2585
+ if (stringKeyNeedsValuePath(key)) {
2586
+ const env_7 = { stack: [], error: void 0, hasError: false };
2587
+ try {
2588
+ const keyHandle = __addDisposableResource(env_7, this.vm.newString(key), false);
2589
+ this.vm._getExports().qjs_define_prop_value(this.ptr, keyHandle.ptr, value.ptr, flags);
2590
+ return;
2591
+ }
2592
+ catch (e_7) {
2593
+ env_7.error = e_7;
2594
+ env_7.hasError = true;
2595
+ }
2596
+ finally {
2597
+ __disposeResources(env_7);
2598
+ }
2599
+ }
2600
+ const { ptr: namePtr } = this.vm._writeString(key);
2601
+ this.vm._getExports().qjs_define_prop_string(this.ptr, namePtr, value.ptr, flags);
2602
+ this.vm._getExports().wasm_free(namePtr);
2603
+ }
2604
+ else {
2605
+ this.vm._getExports().qjs_define_prop_value(this.ptr, key.ptr, value.ptr, flags);
2606
+ }
2607
+ }
2608
+ /**
2609
+ * Extract the value as a number.
2610
+ */
2611
+ toNumber() {
2612
+ return this.vm._getExports().qjs_get_float64(this.ptr);
2613
+ }
2614
+ /**
2615
+ * Extract the value as a BigInt.
2616
+ */
2617
+ toBigInt() {
2618
+ const e = this.vm._getExports();
2619
+ const loPtr = e.wasm_malloc(4);
2620
+ const hiPtr = e.wasm_malloc(4);
2621
+ const ret = e.qjs_get_big_int64(this.ptr, loPtr, hiPtr);
2622
+ if (ret !== 0) {
2623
+ e.wasm_free(loPtr);
2624
+ e.wasm_free(hiPtr);
2625
+ throw new Error('Failed to convert value to BigInt');
2626
+ }
2627
+ const view = new DataView(e.memory.buffer);
2628
+ const lo = view.getUint32(loPtr, true);
2629
+ const hi = view.getInt32(hiPtr, true); // signed for the high word
2630
+ e.wasm_free(loPtr);
2631
+ e.wasm_free(hiPtr);
2632
+ return (BigInt(hi) << 32n) | BigInt(lo);
2633
+ }
2634
+ /**
2635
+ * Extract the value as an ArrayBuffer (copies from WASM memory).
2636
+ * Works on ArrayBuffer values. For typed arrays, gets the underlying buffer.
2637
+ */
2638
+ toArrayBuffer() {
2639
+ const e = this.vm._getExports();
2640
+ const lenOutPtr = e.wasm_malloc(4);
2641
+ if (e.qjs_is_array_buffer(this.ptr)) {
2642
+ const dataPtr = e.qjs_get_array_buffer(this.ptr, lenOutPtr);
2643
+ if (dataPtr === 0) {
2644
+ e.wasm_free(lenOutPtr);
2645
+ throw new Error('Failed to get ArrayBuffer data');
2646
+ }
2647
+ const view = new DataView(e.memory.buffer);
2648
+ const len = view.getUint32(lenOutPtr, true);
2649
+ e.wasm_free(lenOutPtr);
2650
+ // Copy out of WASM memory
2651
+ return new Uint8Array(e.memory.buffer, dataPtr, len).slice().buffer;
2652
+ }
2653
+ // Try typed array → underlying ArrayBuffer
2654
+ e.wasm_free(lenOutPtr);
2655
+ const byteOffsetPtr = e.wasm_malloc(4);
2656
+ const byteLengthPtr = e.wasm_malloc(4);
2657
+ const bytesPerElemPtr = e.wasm_malloc(4);
2658
+ const abPtr = e.qjs_get_typed_array_buffer(this.ptr, byteOffsetPtr, byteLengthPtr, bytesPerElemPtr);
2659
+ const abHandle = new JSValueHandle(this.vm, abPtr);
2660
+ if (this.vm._getExports().qjs_is_exception(abHandle.ptr) !== 0) {
2661
+ abHandle.dispose();
2662
+ e.wasm_free(byteOffsetPtr);
2663
+ e.wasm_free(byteLengthPtr);
2664
+ e.wasm_free(bytesPerElemPtr);
2665
+ throw new Error('Value is not an ArrayBuffer or typed array');
2666
+ }
2667
+ const view = new DataView(e.memory.buffer);
2668
+ const byteOffset = view.getUint32(byteOffsetPtr, true);
2669
+ const byteLength = view.getUint32(byteLengthPtr, true);
2670
+ e.wasm_free(byteOffsetPtr);
2671
+ e.wasm_free(byteLengthPtr);
2672
+ e.wasm_free(bytesPerElemPtr);
2673
+ // Get the raw data from the underlying ArrayBuffer
2674
+ const abLenPtr = e.wasm_malloc(4);
2675
+ const abDataPtr = e.qjs_get_array_buffer(abHandle.ptr, abLenPtr);
2676
+ e.wasm_free(abLenPtr);
2677
+ abHandle.dispose();
2678
+ if (abDataPtr === 0) {
2679
+ throw new Error('Failed to get ArrayBuffer data from typed array');
2680
+ }
2681
+ // Copy the relevant slice out of WASM memory
2682
+ return new Uint8Array(e.memory.buffer, abDataPtr + byteOffset, byteLength).slice().buffer;
2683
+ }
2684
+ /**
2685
+ * Extract the value as a Uint8Array (copies from WASM memory).
2686
+ * Works on Uint8Array, ArrayBuffer, and other typed array values.
2687
+ */
2688
+ toUint8Array() {
2689
+ return new Uint8Array(this.toArrayBuffer());
2690
+ }
2691
+ /**
2692
+ * Extract the value as a string. Works on any value.
2693
+ *
2694
+ * For values that are not already strings this performs a JavaScript
2695
+ * string conversion, which **executes guest code**: `toString()` /
2696
+ * `valueOf()` / `Symbol.toPrimitive` on the value or its prototype chain,
2697
+ * and proxy traps. Guard with `isString` when the caller must not run guest
2698
+ * code, and call a captured intrinsic (e.g. `URL.prototype.toString` via
2699
+ * `vm.callFunction`) when a specific conversion is wanted.
2700
+ */
2701
+ toString() {
2702
+ // Length-aware + WTF-8 (qjs_get_string_len): embedded U+0000 code
2703
+ // units survive (the byte length is explicit, not NUL-scanned) and
2704
+ // lone surrogates survive (quickjs encodes unmatched surrogate code
2705
+ // points as 3-byte WTF-8 sequences, decoded back to their original
2706
+ // code units). The previous JS_ToCString/NUL-terminated read
2707
+ // silently truncated at the first NUL and replaced lone surrogates
2708
+ // with U+FFFD, and the two corruptions could cancel each other's
2709
+ // length changes, defeating length-based detection downstream.
2710
+ const e = this.vm._getExports();
2711
+ const lenPtr = e.wasm_malloc(4);
2712
+ // Same failure check as writeString(): a 0 return would make
2713
+ // qjs_get_string_len write the length to address 0 and the DataView
2714
+ // read below read from it: silent corruption instead of an error.
2715
+ if (lenPtr === 0)
2716
+ throw new Error('wasm_malloc failed');
2717
+ try {
2718
+ const cstrPtr = e.qjs_get_string_len(this.ptr, lenPtr);
2719
+ if (cstrPtr === 0)
2720
+ return '<null>';
2721
+ const len = new DataView(e.memory.buffer).getUint32(lenPtr, true);
2722
+ const bytes = new Uint8Array(e.memory.buffer, cstrPtr, len);
2723
+ const str = decodeWtf8(bytes);
2724
+ e.qjs_free_cstring(cstrPtr);
2725
+ return str;
2726
+ }
2727
+ finally {
2728
+ e.wasm_free(lenPtr);
2729
+ }
2730
+ }
2731
+ /**
2732
+ * Use this handle, then dispose it. Returns the callback's return value.
2733
+ */
2734
+ consume(fn) {
2735
+ try {
2736
+ return fn(this);
2737
+ }
2738
+ finally {
2739
+ this.dispose();
2740
+ }
2741
+ }
2742
+ /**
2743
+ * Duplicate this handle (increment refcount).
2744
+ */
2745
+ dup() {
2746
+ return new JSValueHandle(this.vm, this.vm._getExports().qjs_dup_value(this.ptr));
2747
+ }
2748
+ /**
2749
+ * Dispose this handle, freeing the heap-allocated JSValue.
2750
+ * Safe to call after the VM has been disposed (becomes a no-op).
2751
+ */
2752
+ dispose() {
2753
+ // Cached singleton handles (undefined/null/true/false/global) share a
2754
+ // single heap-allocated JSValue that the VM keeps referencing. Freeing it
2755
+ // here would leave the cached handle pointing at freed memory, so disposing
2756
+ // a singleton is intentionally a no-op. Borrowed handles (host-callback
2757
+ // `this`/arguments) wrap pointers owned by the C caller, which frees them
2758
+ // itself after the call returns; freeing here would double-free.
2759
+ if (this.singleton || this.borrowed)
2760
+ return;
2761
+ if (!this.disposed_) {
2762
+ this.disposed_ = true;
2763
+ this._onDispose?.();
2764
+ this._onDispose = undefined;
2765
+ // If the VM is already disposed, the WASM instance is gone;
2766
+ // no need to (and we can't) call qjs_free_value.
2767
+ const exports = this.vm._getExports();
2768
+ if (exports) {
2769
+ exports.qjs_free_value(this.ptr);
2770
+ }
2771
+ }
2772
+ }
2773
+ /**
2774
+ * Support for `using` declarations (Explicit Resource Management).
2775
+ * Automatically disposes the handle when it goes out of scope.
2776
+ *
2777
+ * ```typescript
2778
+ * using result = vm.evalCode('1 + 2');
2779
+ * console.log(result.toNumber()); // 3
2780
+ * // result is automatically disposed here
2781
+ * ```
2782
+ */
2783
+ [Symbol.dispose]() {
2784
+ this.dispose();
2785
+ }
2786
+ }
2787
+ //# sourceMappingURL=index.js.map