quickjs-emscripten-sync 1.8.4 → 1.9.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/src/index.ts CHANGED
@@ -2,6 +2,7 @@ import type {
2
2
  QuickJSDeferredPromise,
3
3
  QuickJSHandle,
4
4
  QuickJSContext,
5
+ QuickJSAsyncContext,
5
6
  SuccessOrFail,
6
7
  VmCallResult,
7
8
  Intrinsics,
@@ -11,9 +12,20 @@ import { wrapContext, QuickJSContextEx } from "./contextex";
11
12
  import { defaultRegisteredObjects } from "./default";
12
13
  import marshal from "./marshal";
13
14
  import unmarshal from "./unmarshal";
15
+ import unmarshalHostRef from "./unmarshal/hostref";
14
16
  import { complexity, isES2015Class, isObject, walkObject } from "./util";
15
17
  import VMMap from "./vmmap";
16
- import { call, isHandleObject, json, consumeAll, mayConsume, handleFrom } from "./vmutil";
18
+ import {
19
+ call,
20
+ isHandleObject,
21
+ json,
22
+ consume,
23
+ consumeAll,
24
+ mayConsume,
25
+ handleFrom,
26
+ enableFnCache,
27
+ disposeFnCache,
28
+ } from "./vmutil";
17
29
  import { wrap, wrapHandle, unwrap, unwrapHandle, Wrapped } from "./wrapper";
18
30
 
19
31
  export {
@@ -53,6 +65,10 @@ export type Options = {
53
65
  compat?: boolean;
54
66
  /** Experimental: use QuickJSContextEx, which wraps existing QuickJSContext. */
55
67
  experimentalContextEx?: boolean;
68
+ /** Globally enable sync mode (default `true`). When `false`, objects are not wrapped with proxies and marshalled handles are disposed after use, so `arena.sync` has no effect but objects are not retained for their whole lifetime. Useful to avoid memory growth when frequently exchanging short-lived objects. */
69
+ syncEnabled?: boolean;
70
+ /** A callback that returns whether an object should be passed to the VM by reference (as an opaque HostRef) instead of being marshalled by value/proxy. The guest cannot read such objects, but can hold them and pass them back to the host, where they resolve to the original object. */
71
+ marshalByReference?: (target: any) => boolean;
56
72
  };
57
73
 
58
74
  /**
@@ -80,6 +96,7 @@ export class Arena {
80
96
  }
81
97
 
82
98
  this.context = options?.experimentalContextEx ? wrapContext(ctx) : ctx;
99
+ enableFnCache(this.context);
83
100
  this._options = options;
84
101
  this._symbolHandle = ctx.unwrapResult(ctx.evalCode(`Symbol()`));
85
102
  this._map = new VMMap(ctx);
@@ -94,9 +111,15 @@ export class Arena {
94
111
  this._map.dispose();
95
112
  this._registeredMap.dispose();
96
113
  this._symbolHandle.dispose();
114
+ disposeFnCache(this.context);
97
115
  this.context.disposeEx?.();
98
116
  }
99
117
 
118
+ /** Allows `using arena = new Arena(...)` to dispose the arena automatically. */
119
+ [Symbol.dispose]() {
120
+ this.dispose();
121
+ }
122
+
100
123
  /**
101
124
  * Evaluate JS code in the VM and get the result as an object on the host side. It also converts and re-throws error objects when an error is thrown during evaluation.
102
125
  */
@@ -150,7 +173,7 @@ export class Arena {
150
173
  if ("value" in result) {
151
174
  return result.value;
152
175
  }
153
- throw this._unwrapIfNotSynced(result.error.consume(this._unmarshal));
176
+ throw this._unwrapIfNotSynced(consume(result.error, this._unmarshal));
154
177
  }
155
178
 
156
179
  /**
@@ -355,12 +378,12 @@ export class Arena {
355
378
  if ("value" in result) {
356
379
  return result.value;
357
380
  }
358
- throw this._unwrapIfNotSynced(result.error.consume(this._unmarshal));
381
+ throw this._unwrapIfNotSynced(consume(result.error, this._unmarshal));
359
382
  }
360
383
 
361
384
  _unwrapResultAndUnmarshal(result: VmCallResult<QuickJSHandle> | undefined): any {
362
385
  if (!result) return;
363
- return this._unwrapIfNotSynced(this._unwrapResult(result).consume(this._unmarshal));
386
+ return this._unwrapIfNotSynced(consume(this._unwrapResult(result), this._unmarshal));
364
387
  }
365
388
 
366
389
  _isMarshalable = (t: unknown): boolean | "json" => {
@@ -368,6 +391,19 @@ export class Arena {
368
391
  return (typeof im === "function" ? im(this._unwrap(t)) : im) ?? "json";
369
392
  };
370
393
 
394
+ _marshalByReference = (t: unknown): boolean => {
395
+ return !!this._options?.marshalByReference?.(this._unwrap(t));
396
+ };
397
+
398
+ // Register an opaque HostRef handle by its host value without wrapping it.
399
+ _registerHostRef = (t: unknown, handle: QuickJSHandle): QuickJSHandle => {
400
+ const u = this._unwrap(t);
401
+ const existing = this._map.get(u);
402
+ if (existing) return existing;
403
+ this._map.set(u, handle);
404
+ return handle;
405
+ };
406
+
371
407
  _marshalFind = (t: unknown) => {
372
408
  const unwrappedT = this._unwrap(t);
373
409
  const handle =
@@ -405,21 +441,29 @@ export class Arena {
405
441
  return [registered, false];
406
442
  }
407
443
 
408
- const handle = marshal(this._wrap(target) ?? target, {
444
+ // Pass-by-reference objects must reference the original, not a proxy wrapper.
445
+ const marshalTarget = this._marshalByReference(target)
446
+ ? this._unwrap(target)
447
+ : this._wrap(target) ?? target;
448
+
449
+ const handle = marshal(marshalTarget, {
409
450
  ctx: this.context,
410
451
  unmarshal: this._unmarshal,
411
452
  isMarshalable: this._isMarshalable,
453
+ marshalByReference: this._marshalByReference,
454
+ registerHostRef: this._registerHostRef,
412
455
  find: this._marshalFind,
413
456
  pre: this._marshalPre,
414
457
  preApply: this._marshalPreApply,
415
458
  custom: this._options?.customMarshaller,
416
459
  });
417
460
 
418
- return [handle, !this._map.hasHandle(handle)];
461
+ const syncEnabled = this._options?.syncEnabled ?? true;
462
+ return [handle, !syncEnabled || !this._map.hasHandle(handle)];
419
463
  };
420
464
 
421
465
  _preUnmarshal = (t: any, h: QuickJSHandle): Wrapped<any> => {
422
- return this._register(t, h, undefined, true)?.[0];
466
+ return this._register(t, h, undefined, this._options?.syncEnabled ?? true)?.[0];
423
467
  };
424
468
 
425
469
  _unmarshalFind = (h: QuickJSHandle): unknown => {
@@ -432,6 +476,12 @@ export class Arena {
432
476
  return registered;
433
477
  }
434
478
 
479
+ // Resolve opaque HostRefs before wrapping, since a proxy would hide them.
480
+ if (this._options?.marshalByReference) {
481
+ const ref = unmarshalHostRef(this.context, handle);
482
+ if (ref) return ref.value;
483
+ }
484
+
435
485
  const [wrappedHandle] = this._wrapHandle(handle);
436
486
  return unmarshal(wrappedHandle ?? handle, {
437
487
  ctx: this.context,
@@ -439,6 +489,7 @@ export class Arena {
439
489
  find: this._unmarshalFind,
440
490
  pre: this._preUnmarshal,
441
491
  custom: this._options?.customUnmarshaller,
492
+ hostRef: !!this._options?.marshalByReference,
442
493
  });
443
494
  };
444
495
 
@@ -487,6 +538,7 @@ export class Arena {
487
538
  this._marshal,
488
539
  this._syncMode,
489
540
  this._options?.isWrappable,
541
+ this._options?.syncEnabled ?? true,
490
542
  );
491
543
  }
492
544
 
@@ -508,6 +560,7 @@ export class Arena {
508
560
  this._unmarshal,
509
561
  this._syncMode,
510
562
  this._options?.isHandleWrappable,
563
+ this._options?.syncEnabled ?? true,
511
564
  );
512
565
  }
513
566
 
@@ -515,3 +568,28 @@ export class Arena {
515
568
  return unwrapHandle(this.context, target, this._symbolHandle);
516
569
  }
517
570
  }
571
+
572
+ /**
573
+ * An Arena backed by a {@link QuickJSAsyncContext}. In addition to everything
574
+ * `Arena` offers, it can evaluate code asynchronously with `evalCodeAsync`,
575
+ * which lets the VM await host promises (e.g. async module loaders or async
576
+ * functions exposed from the host) without manually pumping pending jobs.
577
+ */
578
+ export class AsyncArena extends Arena {
579
+ asyncContext: QuickJSAsyncContext;
580
+
581
+ constructor(ctx: QuickJSAsyncContext, options?: Options) {
582
+ super(ctx, options);
583
+ this.asyncContext = ctx;
584
+ }
585
+
586
+ /**
587
+ * Evaluate JS code asynchronously in the VM and get the result on the host
588
+ * side. Like `evalCode`, it converts and re-throws errors thrown during
589
+ * evaluation. Use this when the code awaits host-provided promises.
590
+ */
591
+ async evalCodeAsync<T = any>(code: string, filename?: string): Promise<T> {
592
+ const result = await this.asyncContext.evalCodeAsync(code, filename);
593
+ return this._unwrapResultAndUnmarshal(result);
594
+ }
595
+ }
@@ -1,6 +1,6 @@
1
1
  import type { QuickJSContext, QuickJSHandle } from "quickjs-emscripten";
2
2
 
3
- import { call } from "../vmutil";
3
+ import { call, consumeAll } from "../vmutil";
4
4
 
5
5
  export default function marshalCustom(
6
6
  ctx: QuickJSContext,
@@ -33,4 +33,18 @@ export function date(target: unknown, ctx: QuickJSContext): QuickJSHandle | unde
33
33
  return handle;
34
34
  }
35
35
 
36
- export const defaultCustom = [symbol, date];
36
+ export function arrayBuffer(target: unknown, ctx: QuickJSContext): QuickJSHandle | undefined {
37
+ if (target instanceof ArrayBuffer) {
38
+ return ctx.newArrayBuffer(target.slice(0));
39
+ }
40
+ if (ArrayBuffer.isView(target)) {
41
+ // TypedArray or DataView: copy the viewed bytes and rebuild in the VM.
42
+ const bytes = new Uint8Array(target.buffer, target.byteOffset, target.byteLength).slice();
43
+ return consumeAll(
44
+ [ctx.newArrayBuffer(bytes.buffer), ctx.newString(target.constructor.name)],
45
+ ([buf, name]) => call(ctx, `(buf, name) => new globalThis[name](buf)`, undefined, buf, name),
46
+ );
47
+ }
48
+ }
49
+
50
+ export const defaultCustom = [symbol, date, arrayBuffer];
@@ -0,0 +1,18 @@
1
+ import type { QuickJSContext, QuickJSHandle } from "quickjs-emscripten";
2
+
3
+ /**
4
+ * Marshal a host object as an opaque HostRef handle instead of deep-copying or
5
+ * proxying it. The guest cannot read the object, but can hold it and pass it
6
+ * back to the host, where it unmarshals to the original object by reference.
7
+ */
8
+ export default function marshalHostRef(
9
+ ctx: QuickJSContext,
10
+ target: unknown,
11
+ // Registers handle <-> target without wrapping (a proxy would hide the opaque
12
+ // HostRef). Keeps identity and lets the arena dispose the handle on teardown.
13
+ register: (target: unknown, handle: QuickJSHandle) => QuickJSHandle,
14
+ ): QuickJSHandle | undefined {
15
+ if (typeof target !== "object" && typeof target !== "function") return;
16
+ if (target === null) return;
17
+ return register(target, ctx.newHostRef(target as object).handle);
18
+ }
@@ -2,7 +2,9 @@ import type { QuickJSDeferredPromise, QuickJSHandle, QuickJSContext } from "quic
2
2
 
3
3
  import marshalCustom, { defaultCustom } from "./custom";
4
4
  import marshalFunction from "./function";
5
+ import marshalHostRef from "./hostref";
5
6
  import marshalJSON from "./json";
7
+ import marshalMapSet from "./mapset";
6
8
  import marshalObject from "./object";
7
9
  import marshalPrimitive from "./primitive";
8
10
  import marshalPromise from "./promise";
@@ -11,6 +13,8 @@ export type Options = {
11
13
  ctx: QuickJSContext;
12
14
  unmarshal: (handle: QuickJSHandle) => unknown;
13
15
  isMarshalable?: (target: unknown) => boolean | "json";
16
+ marshalByReference?: (target: unknown) => boolean;
17
+ registerHostRef?: (target: unknown, handle: QuickJSHandle) => QuickJSHandle;
14
18
  find: (target: unknown) => QuickJSHandle | undefined;
15
19
  pre: (
16
20
  target: unknown,
@@ -36,6 +40,13 @@ export function marshal(target: unknown, options: Options): QuickJSHandle {
36
40
  if (handle) return handle;
37
41
  }
38
42
 
43
+ // Opt-in pass-by-reference: hand the object to the VM as an opaque HostRef
44
+ // instead of marshalling its contents. Bypasses isMarshalable on purpose.
45
+ if (options.marshalByReference?.(target) && options.registerHostRef) {
46
+ const handle = marshalHostRef(ctx, target, options.registerHostRef);
47
+ if (handle) return handle;
48
+ }
49
+
39
50
  const marshalable = isMarshalable?.(target);
40
51
  if (marshalable === false) {
41
52
  return ctx.undefined;
@@ -52,6 +63,7 @@ export function marshal(target: unknown, options: Options): QuickJSHandle {
52
63
  marshalCustom(ctx, target, pre2, [...defaultCustom, ...(options.custom ?? [])]) ??
53
64
  marshalPromise(ctx, target, marshal2, pre2) ??
54
65
  marshalFunction(ctx, target, marshal2, unmarshal, pre2, options.preApply) ??
66
+ marshalMapSet(ctx, target, marshal2, pre2) ??
55
67
  marshalObject(ctx, target, marshal2, pre2) ??
56
68
  ctx.undefined
57
69
  );
@@ -0,0 +1,28 @@
1
+ import type { QuickJSContext, QuickJSHandle } from "quickjs-emscripten";
2
+
3
+ import { call } from "../vmutil";
4
+
5
+ export default function marshalMapSet(
6
+ ctx: QuickJSContext,
7
+ target: unknown,
8
+ marshal: (target: unknown) => QuickJSHandle,
9
+ preMarshal: (target: unknown, handle: QuickJSHandle) => QuickJSHandle | undefined,
10
+ ): QuickJSHandle | undefined {
11
+ if (target instanceof Map) {
12
+ const raw = call(ctx, "() => new Map()");
13
+ const handle = preMarshal(target, raw) ?? raw;
14
+ for (const [k, v] of target) {
15
+ call(ctx, "(m, k, v) => m.set(k, v)", undefined, raw, marshal(k), marshal(v)).dispose();
16
+ }
17
+ return handle;
18
+ }
19
+
20
+ if (target instanceof Set) {
21
+ const raw = call(ctx, "() => new Set()");
22
+ const handle = preMarshal(target, raw) ?? raw;
23
+ for (const v of target) {
24
+ call(ctx, "(s, v) => s.add(v)", undefined, raw, marshal(v)).dispose();
25
+ }
26
+ return handle;
27
+ }
28
+ }
@@ -1,7 +1,5 @@
1
1
  import type { QuickJSContext, QuickJSHandle } from "quickjs-emscripten";
2
2
 
3
- // import { call } from "../vmutil";
4
-
5
3
  export default function marshalPrimitive(
6
4
  ctx: QuickJSContext,
7
5
  target: unknown,
@@ -15,17 +13,10 @@ export default function marshalPrimitive(
15
13
  return ctx.newString(target);
16
14
  case "boolean":
17
15
  return target ? ctx.true : ctx.false;
16
+ case "bigint":
17
+ return ctx.newBigInt(target);
18
18
  case "object":
19
19
  return target === null ? ctx.null : undefined;
20
-
21
- // BigInt is not supported by quickjs-emscripten
22
- // case "bigint":
23
- // return call(
24
- // ctx,
25
- // `s => BigInt(s)`,
26
- // undefined,
27
- // ctx.newString(target.toString())
28
- // );
29
20
  }
30
21
 
31
22
  return undefined;
@@ -35,11 +35,13 @@ export default function marshalProperties(
35
35
  });
36
36
  };
37
37
 
38
- const desc = Object.getOwnPropertyDescriptors(target);
39
- Object.entries(desc).forEach(([k, v]) => cb(k, v));
40
- Object.getOwnPropertySymbols(desc).forEach(k => cb(k, (desc as any)[k]));
38
+ try {
39
+ const desc = Object.getOwnPropertyDescriptors(target);
40
+ Object.entries(desc).forEach(([k, v]) => cb(k, v));
41
+ Object.getOwnPropertySymbols(desc).forEach(k => cb(k, (desc as any)[k]));
41
42
 
42
- call(ctx, `Object.defineProperties`, undefined, handle, descs).dispose();
43
-
44
- descs.dispose();
43
+ call(ctx, `Object.defineProperties`, undefined, handle, descs).dispose();
44
+ } finally {
45
+ descs.dispose();
46
+ }
45
47
  }
@@ -1,6 +1,6 @@
1
1
  import type { QuickJSContext, QuickJSHandle } from "quickjs-emscripten";
2
2
 
3
- import { call } from "../vmutil";
3
+ import { call, consume } from "../vmutil";
4
4
 
5
5
  export default function unmarshalCustom(
6
6
  ctx: QuickJSContext,
@@ -23,9 +23,40 @@ export function symbol(handle: QuickJSHandle, ctx: QuickJSContext): symbol | und
23
23
  }
24
24
 
25
25
  export function date(handle: QuickJSHandle, ctx: QuickJSContext): Date | undefined {
26
- if (!ctx.dump(call(ctx, "a => a instanceof Date", undefined, handle))) return;
27
- const t = ctx.getNumber(call(ctx, "a => a.getTime()", undefined, handle));
26
+ if (!consume(call(ctx, "a => a instanceof Date", undefined, handle), h => ctx.dump(h))) return;
27
+ const t = consume(call(ctx, "a => a.getTime()", undefined, handle), h => ctx.getNumber(h));
28
28
  return new Date(t);
29
29
  }
30
30
 
31
- export const defaultCustom = [symbol, date];
31
+ export function arrayBuffer(
32
+ handle: QuickJSHandle,
33
+ ctx: QuickJSContext,
34
+ ): ArrayBuffer | ArrayBufferView | undefined {
35
+ if (consume(call(ctx, "a => a instanceof ArrayBuffer", undefined, handle), h => ctx.dump(h))) {
36
+ const lifetime = ctx.getArrayBuffer(handle);
37
+ const copy = lifetime.value.slice();
38
+ lifetime.dispose();
39
+ return copy.buffer;
40
+ }
41
+
42
+ if (consume(call(ctx, "a => ArrayBuffer.isView(a)", undefined, handle), h => ctx.dump(h))) {
43
+ const name = consume(call(ctx, "a => a.constructor.name", undefined, handle), h =>
44
+ ctx.getString(h),
45
+ );
46
+ const Ctor = (globalThis as any)[name];
47
+ if (typeof Ctor !== "function") return;
48
+ const bufHandle = call(
49
+ ctx,
50
+ "a => a.buffer.slice(a.byteOffset, a.byteOffset + a.byteLength)",
51
+ undefined,
52
+ handle,
53
+ );
54
+ const lifetime = ctx.getArrayBuffer(bufHandle);
55
+ const bytes = lifetime.value.slice();
56
+ lifetime.dispose();
57
+ bufHandle.dispose();
58
+ return new Ctor(bytes.buffer);
59
+ }
60
+ }
61
+
62
+ export const defaultCustom = [symbol, date, arrayBuffer];
@@ -1,6 +1,6 @@
1
1
  import type { QuickJSContext, QuickJSHandle } from "quickjs-emscripten";
2
2
 
3
- import { call, mayConsumeAll } from "../vmutil";
3
+ import { call, mayConsumeAll, unwrapResult } from "../vmutil";
4
4
 
5
5
  import unmarshalProperties from "./properties";
6
6
 
@@ -26,7 +26,7 @@ export default function unmarshalFunction(
26
26
  return this;
27
27
  }
28
28
 
29
- const resultHandle = ctx.unwrapResult(ctx.callFunction(handle, thisHandle, ...argHandles));
29
+ const resultHandle = unwrapResult(ctx, ctx.callFunction(handle, thisHandle, ...argHandles));
30
30
 
31
31
  const [result, alreadyExists] = unmarshal(resultHandle);
32
32
  if (alreadyExists) resultHandle.dispose();
@@ -0,0 +1,21 @@
1
+ import type { QuickJSContext, QuickJSHandle } from "quickjs-emscripten";
2
+
3
+ /**
4
+ * If `handle` is an opaque HostRef, resolve it back to the original host value.
5
+ * Returns a wrapper so the caller can tell "resolved to a falsy value" apart
6
+ * from "not a HostRef". `toHostRef` is cheap for non-HostRef handles.
7
+ */
8
+ export default function unmarshalHostRef(
9
+ ctx: QuickJSContext,
10
+ handle: QuickJSHandle,
11
+ ): { value: unknown } | undefined {
12
+ const ref = ctx.toHostRef(handle);
13
+ if (!ref) return;
14
+ try {
15
+ return { value: ref.value };
16
+ } finally {
17
+ // toHostRef returns a HostRef wrapping a *dup* of the handle, so disposing
18
+ // it does not affect the caller's handle.
19
+ ref.dispose();
20
+ }
21
+ }
@@ -2,6 +2,8 @@ import type { QuickJSContext, QuickJSHandle } from "quickjs-emscripten";
2
2
 
3
3
  import unmarshalCustom, { defaultCustom } from "./custom";
4
4
  import unmarshalFunction from "./function";
5
+ import unmarshalHostRef from "./hostref";
6
+ import unmarshalMapSet from "./mapset";
5
7
  import unmarshalObject from "./object";
6
8
  import unmarshalPrimitive from "./primitive";
7
9
  import unmarshalPromise from "./promise";
@@ -13,6 +15,8 @@ export type Options = {
13
15
  find: (handle: QuickJSHandle) => unknown | undefined;
14
16
  pre: <T = unknown>(target: T, handle: QuickJSHandle) => T | undefined;
15
17
  custom?: Iterable<(obj: QuickJSHandle, ctx: QuickJSContext) => any>;
18
+ /** When true, resolve opaque HostRef handles back to their host value. */
19
+ hostRef?: boolean;
16
20
  };
17
21
 
18
22
  export function unmarshal(handle: QuickJSHandle, options: Options): any {
@@ -37,8 +41,24 @@ function unmarshalInner(handle: QuickJSHandle, options: Options): [any, boolean]
37
41
 
38
42
  const unmarshal2 = (h: QuickJSHandle) => unmarshalInner(h, options);
39
43
 
44
+ // Opaque HostRef handles resolve back to the original host value by reference.
45
+ if (options.hostRef) {
46
+ const ref = unmarshalHostRef(ctx, handle);
47
+ if (ref) return [ref.value, true];
48
+ }
49
+
50
+ // Custom types (Symbol, Date, ArrayBuffer, TypedArray, ...) are unmarshalled
51
+ // by value and not tracked in the map, so their source handle is not owned by
52
+ // anyone else and the caller must dispose it.
53
+ const custom = unmarshalCustom(ctx, handle, pre, [...defaultCustom, ...(options.custom ?? [])]);
54
+ if (custom) return [custom, true];
55
+
56
+ // Map/Set are unmarshalled by value (snapshot copy), so the source handle is
57
+ // not tracked and the caller must dispose it.
58
+ const mapSet = unmarshalMapSet(ctx, handle, unmarshal2, pre);
59
+ if (mapSet) return [mapSet, true];
60
+
40
61
  const result =
41
- unmarshalCustom(ctx, handle, pre, [...defaultCustom, ...(options.custom ?? [])]) ??
42
62
  unmarshalPromise(ctx, handle, marshal, pre) ??
43
63
  unmarshalFunction(ctx, handle, marshal, unmarshal2, pre) ??
44
64
  unmarshalObject(ctx, handle, unmarshal2, pre);
@@ -0,0 +1,43 @@
1
+ import type { QuickJSContext, QuickJSHandle } from "quickjs-emscripten";
2
+
3
+ import { call, consume } from "../vmutil";
4
+
5
+ export default function unmarshalMapSet(
6
+ ctx: QuickJSContext,
7
+ handle: QuickJSHandle,
8
+ unmarshal: (handle: QuickJSHandle) => [unknown, boolean],
9
+ preUnmarshal: <T>(target: T, handle: QuickJSHandle) => T | undefined,
10
+ ): Map<any, any> | Set<any> | undefined {
11
+ const isMap = consume(call(ctx, "a => a instanceof Map", undefined, handle), h => ctx.dump(h));
12
+ const isSet =
13
+ !isMap && consume(call(ctx, "a => a instanceof Set", undefined, handle), h => ctx.dump(h));
14
+ if (!isMap && !isSet) return;
15
+
16
+ const result: Map<any, any> | Set<any> = isMap ? new Map() : new Set();
17
+ preUnmarshal(result, handle);
18
+
19
+ const iterator = ctx.unwrapResult(ctx.getIterator(handle));
20
+ try {
21
+ for (const elResult of iterator) {
22
+ const el = ctx.unwrapResult(elResult);
23
+ if (isMap) {
24
+ const keyHandle = ctx.getProp(el, 0);
25
+ const valueHandle = ctx.getProp(el, 1);
26
+ const [key, disposeKey] = unmarshal(keyHandle);
27
+ const [value, disposeValue] = unmarshal(valueHandle);
28
+ if (disposeKey) keyHandle.dispose();
29
+ if (disposeValue) valueHandle.dispose();
30
+ (result as Map<any, any>).set(key, value);
31
+ el.dispose();
32
+ } else {
33
+ const [value, disposeValue] = unmarshal(el);
34
+ if (disposeValue) el.dispose();
35
+ (result as Set<any>).add(value);
36
+ }
37
+ }
38
+ } finally {
39
+ if (iterator.alive) iterator.dispose();
40
+ }
41
+
42
+ return result;
43
+ }
@@ -1,6 +1,6 @@
1
1
  import type { QuickJSContext, QuickJSHandle } from "quickjs-emscripten";
2
2
 
3
- import { call } from "../vmutil";
3
+ import { call, consume } from "../vmutil";
4
4
 
5
5
  import unmarshalProperties from "./properties";
6
6
 
@@ -10,31 +10,27 @@ export default function unmarshalObject(
10
10
  unmarshal: (handle: QuickJSHandle) => [unknown, boolean],
11
11
  preUnmarshal: <T>(target: T, handle: QuickJSHandle) => T | undefined,
12
12
  ): object | undefined {
13
- if (
14
- ctx.typeof(handle) !== "object" ||
15
- // null check
16
- ctx
17
- .unwrapResult(ctx.evalCode("o => o === null"))
18
- .consume(n => ctx.dump(ctx.unwrapResult(ctx.callFunction(n, ctx.undefined, handle))))
19
- )
20
- return;
13
+ if (ctx.typeof(handle) !== "object" || ctx.sameValue(handle, ctx.null)) return;
21
14
 
22
- const raw = call(ctx, "Array.isArray", undefined, handle).consume(r => ctx.dump(r)) ? [] : {};
15
+ const raw = consume(call(ctx, "Array.isArray", undefined, handle), r => ctx.dump(r)) ? [] : {};
23
16
  const obj = preUnmarshal(raw, handle) ?? raw;
24
17
 
25
- const prototype = call(
26
- ctx,
27
- `o => {
18
+ const prototype = consume(
19
+ call(
20
+ ctx,
21
+ `o => {
28
22
  const p = Object.getPrototypeOf(o);
29
23
  return !p || p === Object.prototype || p === Array.prototype ? undefined : p;
30
24
  }`,
31
- undefined,
32
- handle,
33
- ).consume(prototype => {
34
- if (ctx.typeof(prototype) === "undefined") return;
35
- const [proto] = unmarshal(prototype);
36
- return proto;
37
- });
25
+ undefined,
26
+ handle,
27
+ ),
28
+ prototype => {
29
+ if (ctx.typeof(prototype) === "undefined") return;
30
+ const [proto] = unmarshal(prototype);
31
+ return proto;
32
+ },
33
+ );
38
34
  if (typeof prototype === "object") {
39
35
  Object.setPrototypeOf(obj, prototype);
40
36
  }
@@ -7,24 +7,13 @@ export default function unmarshalPrimitive(
7
7
  const ty = ctx.typeof(handle);
8
8
  if (ty === "undefined" || ty === "number" || ty === "string" || ty === "boolean") {
9
9
  return [ctx.dump(handle), true];
10
- } else if (ty === "object") {
11
- const isNull = ctx
12
- .unwrapResult(ctx.evalCode("a => a === null"))
13
- .consume(n => ctx.dump(ctx.unwrapResult(ctx.callFunction(n, ctx.undefined, handle))));
14
- if (isNull) {
15
- return [null, true];
16
- }
17
10
  }
18
-
19
- // BigInt is not supported by quickjs-emscripten
20
- // if (ty === "bigint") {
21
- // const str = ctx
22
- // .getProp(handle, "toString")
23
- // .consume(toString => vm.unwrapResult(vm.callFunction(toString, handle)))
24
- // .consume(str => ctx.getString(str));
25
- // const bi = BigInt(str);
26
- // return [bi, true];
27
- // }
11
+ if (ty === "bigint") {
12
+ return [ctx.getBigInt(handle), true];
13
+ }
14
+ if (ty === "object" && ctx.sameValue(handle, ctx.null)) {
15
+ return [null, true];
16
+ }
28
17
 
29
18
  return [undefined, false];
30
19
  }
@@ -1,6 +1,6 @@
1
1
  import type { QuickJSContext, QuickJSHandle } from "quickjs-emscripten";
2
2
 
3
- import { call } from "../vmutil";
3
+ import { call, consume } from "../vmutil";
4
4
 
5
5
  export default function unmarshalProperties(
6
6
  ctx: QuickJSContext,
@@ -8,8 +8,8 @@ export default function unmarshalProperties(
8
8
  target: object | ((...args: any[]) => any),
9
9
  unmarshal: (handle: QuickJSHandle) => [unknown, boolean],
10
10
  ) {
11
- ctx
12
- .newFunction("", (key, value) => {
11
+ consume(
12
+ ctx.newFunction("", (key, value) => {
13
13
  const [keyName] = unmarshal(key);
14
14
  if (typeof keyName !== "string" && typeof keyName !== "number" && typeof keyName !== "symbol")
15
15
  return;
@@ -43,8 +43,8 @@ export default function unmarshalProperties(
43
43
  }, {});
44
44
 
45
45
  Object.defineProperty(target, keyName, desc);
46
- })
47
- .consume(fn => {
46
+ }),
47
+ fn => {
48
48
  call(
49
49
  ctx,
50
50
  `(o, fn) => {
@@ -56,5 +56,6 @@ export default function unmarshalProperties(
56
56
  handle,
57
57
  fn,
58
58
  ).dispose();
59
- });
59
+ },
60
+ );
60
61
  }