quickjs-emscripten-sync 1.10.0 → 1.11.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
@@ -6,13 +6,17 @@ import type {
6
6
  SuccessOrFail,
7
7
  VmCallResult,
8
8
  Intrinsics,
9
+ ContextEvalOptions,
10
+ InterruptHandler,
9
11
  } from "quickjs-emscripten";
10
12
 
11
- import { wrapContext, QuickJSContextEx } from "./contextex";
13
+ import type { QuickJSContextEx } from "./contextex";
14
+ import { wrapContext } from "./contextex";
12
15
  import { defaultRegisteredObjects } from "./default";
13
16
  import marshal from "./marshal";
14
17
  import unmarshal from "./unmarshal";
15
18
  import unmarshalHostRef from "./unmarshal/hostref";
19
+ import unmarshalPrimitive from "./unmarshal/primitive";
16
20
  import { complexity, isES2015Class, isObject, walkObject } from "./util";
17
21
  import VMMap from "./vmmap";
18
22
  import {
@@ -26,7 +30,8 @@ import {
26
30
  enableFnCache,
27
31
  disposeFnCache,
28
32
  } from "./vmutil";
29
- import { wrap, wrapHandle, unwrap, unwrapHandle, Wrapped } from "./wrapper";
33
+ import type { Wrapped, WrapHandle } from "./wrapper";
34
+ import { wrap, createWrapHandle, unwrap, unwrapHandle } from "./wrapper";
30
35
 
31
36
  export {
32
37
  VMMap,
@@ -43,7 +48,7 @@ export {
43
48
  consumeAll,
44
49
  };
45
50
 
46
- export type { Intrinsics };
51
+ export type { Intrinsics, ContextEvalOptions, InterruptHandler };
47
52
 
48
53
  export type Options = {
49
54
  /** A callback that returns a boolean value that determines whether an object is marshalled or not. If false, no marshaling will be done and undefined will be passed to the QuickJS VM, otherwise marshaling will be done. By default, all objects will be marshalled. */
@@ -69,6 +74,34 @@ export type Options = {
69
74
  syncEnabled?: boolean;
70
75
  /** 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
76
  marshalByReference?: (target: any) => boolean;
77
+ /**
78
+ * Marshal selected host functions as *asyncified* functions. Normally an async
79
+ * host function is marshalled like any other function, so the guest receives a
80
+ * marshalled `Promise`. When a function is asyncified, the QuickJS VM instead
81
+ * suspends until the host promise settles and the guest receives the resolved
82
+ * value synchronously (as if the call were blocking).
83
+ *
84
+ * - `true`: asyncify every host function that is *declared* async, auto-detected
85
+ * via `Object.prototype.toString.call(fn) === "[object AsyncFunction]"`. Note
86
+ * that functions transpiled to ES5 (e.g. by Babel/TypeScript targeting older
87
+ * runtimes) are plain functions returning a promise and are **not** detected —
88
+ * use the predicate form for those.
89
+ * - predicate: asyncify exactly the functions for which it returns `true`. It is
90
+ * called with the *unwrapped* target (the original function, not a sync proxy),
91
+ * like `isMarshalable`.
92
+ * - absent/`false`: current behavior; async functions yield a `Promise` in the guest.
93
+ *
94
+ * Constraints (Emscripten Asyncify):
95
+ * - Asyncified functions only work when evaluation is entered through an async
96
+ * entry point ({@link AsyncArena.evalCodeAsync}). Calling one during a
97
+ * synchronous `evalCode` fails with `Error: Function unexpectedly returned a Promise`.
98
+ * - Only ONE suspended call is allowed at a time; concurrent suspension throws a
99
+ * runtime error from quickjs-emscripten.
100
+ * - If the context does not support `newAsyncifiedFunction` (a plain sync context
101
+ * from `getQuickJS().newContext()`), this option is silently ignored and async
102
+ * functions behave as today (Promise).
103
+ */
104
+ asyncify?: boolean | ((target: (...args: any[]) => any) => boolean);
72
105
  };
73
106
 
74
107
  /**
@@ -91,6 +124,7 @@ export class Arena {
91
124
  _temporalSync = new Set<any>();
92
125
  _symbol = Symbol();
93
126
  _symbolHandle: QuickJSHandle;
127
+ _wrapHandleImpl: WrapHandle;
94
128
  _options?: Options;
95
129
 
96
130
  /** Constructs a new Arena instance. It requires a quickjs-emscripten context initialized with `quickjs.newContext()`. */
@@ -107,6 +141,17 @@ export class Arena {
107
141
  enableFnCache(this.context);
108
142
  this._options = options;
109
143
  this._symbolHandle = ctx.unwrapResult(ctx.evalCode(`Symbol()`));
144
+ // One proxyFuncs handle shared by every wrapped handle for the Arena's
145
+ // lifetime, instead of allocating a fresh VM function per wrap.
146
+ this._wrapHandleImpl = createWrapHandle(
147
+ this.context,
148
+ this._symbol,
149
+ this._symbolHandle,
150
+ this._unmarshal,
151
+ this._syncMode,
152
+ this._options?.isHandleWrappable,
153
+ this._options?.syncEnabled ?? true,
154
+ );
110
155
  this._map = new VMMap(ctx);
111
156
  this._registeredMap = new VMMap(ctx);
112
157
  this.registerAll(options?.registeredObjects ?? defaultRegisteredObjects);
@@ -122,6 +167,7 @@ export class Arena {
122
167
  this._transientHandles.clear();
123
168
  this._map.dispose();
124
169
  this._registeredMap.dispose();
170
+ this._wrapHandleImpl.dispose();
125
171
  this._symbolHandle.dispose();
126
172
  disposeFnCache(this.context);
127
173
  this.context.disposeEx?.();
@@ -134,9 +180,15 @@ export class Arena {
134
180
 
135
181
  /**
136
182
  * 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.
183
+ *
184
+ * @param code - The JS code to evaluate
185
+ * @param filename - Optional filename used in error stacks and for debugging
186
+ * @param options - Optional eval options forwarded to quickjs-emscripten's
187
+ * `evalCode`, either an `EvalFlags` bitmask (`number`) or a
188
+ * {@link ContextEvalOptions} object (e.g. `{ strict: true }`)
137
189
  */
138
- evalCode<T = any>(code: string): T {
139
- const handle = this.context.evalCode(code);
190
+ evalCode<T = any>(code: string, filename?: string, options?: number | ContextEvalOptions): T {
191
+ const handle = this.context.evalCode(code, filename, options);
140
192
  return this._unwrapResultAndUnmarshal(handle);
141
193
  }
142
194
 
@@ -147,6 +199,10 @@ export class Arena {
147
199
  *
148
200
  * @param code - The ES module code to evaluate
149
201
  * @param filename - Optional filename for debugging purposes (default: "module.js")
202
+ * @param options - Optional {@link ContextEvalOptions}. `type` is always forced
203
+ * to `"module"`, so any `type` in `options` is overridden. A numeric
204
+ * `EvalFlags` bitmask cannot be merged with the forced module type and is
205
+ * therefore not supported here; if a number is passed it is ignored.
150
206
  * @returns The module's exports object, or a Promise resolving to exports if using top-level await
151
207
  *
152
208
  * @example
@@ -172,8 +228,15 @@ export class Arena {
172
228
  * console.log(exports.data); // 123
173
229
  * ```
174
230
  */
175
- evalModule<T = any>(code: string, filename = "module.js"): T | Promise<T> {
176
- const handle = this.context.evalCode(code, filename, { type: "module" });
231
+ evalModule<T = any>(
232
+ code: string,
233
+ filename = "module.js",
234
+ options?: number | ContextEvalOptions,
235
+ ): T | Promise<T> {
236
+ const handle = this.context.evalCode(code, filename, {
237
+ ...(typeof options === "number" ? {} : options),
238
+ type: "module",
239
+ });
177
240
  return this._unwrapResultAndUnmarshal(handle);
178
241
  }
179
242
 
@@ -236,6 +299,45 @@ export class Arena {
236
299
  this.context.runtime.setMaxStackSize(stackSize);
237
300
  }
238
301
 
302
+ /**
303
+ * Set a callback which is regularly called by the QuickJS engine when it is
304
+ * executing code. This callback can be used to implement an execution
305
+ * timeout: return `true` from the handler to interrupt the running code.
306
+ *
307
+ * This is useful for preventing runaway CPU usage (e.g. infinite loops) in
308
+ * untrusted code.
309
+ *
310
+ * @param cb - The interrupt handler. Return `true` to interrupt execution.
311
+ *
312
+ * @example
313
+ * ```js
314
+ * // Interrupt evaluation after 1 second
315
+ * const deadline = Date.now() + 1000;
316
+ * arena.setInterruptHandler(() => Date.now() > deadline);
317
+ *
318
+ * try {
319
+ * arena.evalCode(`while (true) {}`);
320
+ * } catch (e) {
321
+ * console.log("Execution interrupted");
322
+ * }
323
+ * ```
324
+ */
325
+ setInterruptHandler(cb: InterruptHandler): void {
326
+ this.context.runtime.setInterruptHandler(cb);
327
+ }
328
+
329
+ /**
330
+ * Remove the interrupt handler previously set with `setInterruptHandler`.
331
+ *
332
+ * @example
333
+ * ```js
334
+ * arena.removeInterruptHandler();
335
+ * ```
336
+ */
337
+ removeInterruptHandler(): void {
338
+ this.context.runtime.removeInterruptHandler();
339
+ }
340
+
239
341
  /**
240
342
  * Get detailed memory usage statistics for this runtime.
241
343
  *
@@ -407,6 +509,15 @@ export class Arena {
407
509
  return !!this._options?.marshalByReference?.(this._unwrap(t));
408
510
  };
409
511
 
512
+ _asyncify = (t: unknown): boolean => {
513
+ const a = this._options?.asyncify;
514
+ if (!a) return false;
515
+ const target = this._unwrap(t);
516
+ if (typeof a === "function") return typeof target === "function" && a(target as any);
517
+ // `a === true`: auto-detect functions declared with `async`.
518
+ return Object.prototype.toString.call(target) === "[object AsyncFunction]";
519
+ };
520
+
410
521
  // Register an opaque HostRef handle by its host value without wrapping it.
411
522
  _registerHostRef = (t: unknown, handle: QuickJSHandle): QuickJSHandle => {
412
523
  const u = this._unwrap(t);
@@ -491,6 +602,8 @@ export class Arena {
491
602
  disposeTransient: this._disposeTransient,
492
603
  preApply: this._marshalPreApply,
493
604
  prepareReturn: this._prepareMarshalReturn,
605
+ unwrap: t => this._unwrap(t),
606
+ asyncify: this._options?.asyncify ? this._asyncify : undefined,
494
607
  custom: this._options?.customMarshaller,
495
608
  });
496
609
 
@@ -499,7 +612,11 @@ export class Arena {
499
612
  this._transientHandles.delete(handle);
500
613
 
501
614
  const syncEnabled = this._options?.syncEnabled ?? true;
502
- return [handle, !syncEnabled || !this._map.hasHandle(handle)];
615
+ // A non-object (primitive) handle is never retained in `_map` (registered
616
+ // primitives already returned above via `_registeredMap`), so skip the
617
+ // `hasHandle` VM roundtrip and mark it disposable directly.
618
+ if (!syncEnabled || !isObject(target)) return [handle, true];
619
+ return [handle, !this._map.hasHandle(handle)];
503
620
  };
504
621
 
505
622
  _prepareMarshalReturn = (h: QuickJSHandle): QuickJSHandle => {
@@ -510,7 +627,12 @@ export class Arena {
510
627
  // `x === fn()` identity across calls. Hand the VM a dup instead and keep
511
628
  // ours alive. With sync off, handles are not retained, so this is a no-op.
512
629
  const syncEnabled = this._options?.syncEnabled ?? true;
513
- return syncEnabled && h.alive && this._map.hasHandle(h) ? h.dup() : h;
630
+ // Only object handles are retained in `_map`; a primitive return can never
631
+ // be in the map, so `isHandleObject` (a cheap typeof) skips the `hasHandle`
632
+ // VM roundtrip for primitive returns from host functions.
633
+ return syncEnabled && h.alive && isHandleObject(this.context, h) && this._map.hasHandle(h)
634
+ ? h.dup()
635
+ : h;
514
636
  };
515
637
 
516
638
  _preUnmarshal = (t: any, h: QuickJSHandle): Wrapped<any> => {
@@ -522,6 +644,14 @@ export class Arena {
522
644
  };
523
645
 
524
646
  _unmarshal = (handle: QuickJSHandle): any => {
647
+ // Primitives (undefined/number/string/boolean/bigint/null) are resolved by a
648
+ // cheap host-side `ctx.typeof`, skipping the `_registeredMap` VM lookup,
649
+ // HostRef resolution, and `_wrapHandle`. The VMMap only ever keys objects and
650
+ // symbols, so a primitive handle can never match `getByHandle`; symbols fall
651
+ // through `unmarshalPrimitive` and still reach the lookup below.
652
+ const [primitive, ok] = unmarshalPrimitive(this.context, handle);
653
+ if (ok) return primitive;
654
+
525
655
  const registered = this._registeredMap.getByHandle(handle);
526
656
  if (typeof registered !== "undefined") {
527
657
  return registered;
@@ -533,15 +663,29 @@ export class Arena {
533
663
  if (ref) return ref.value;
534
664
  }
535
665
 
536
- const [wrappedHandle] = this._wrapHandle(handle);
537
- return unmarshal(wrappedHandle ?? handle, {
538
- ctx: this.context,
539
- marshal: this._marshal,
540
- find: this._unmarshalFind,
541
- pre: this._preUnmarshal,
542
- custom: this._options?.customUnmarshaller,
543
- hostRef: !!this._options?.marshalByReference,
544
- });
666
+ const [wrappedHandle, wrapped] = this._wrapHandle(handle);
667
+ let done = false;
668
+ try {
669
+ const result = unmarshal(wrappedHandle ?? handle, {
670
+ ctx: this.context,
671
+ marshal: this._marshal,
672
+ find: this._unmarshalFind,
673
+ pre: this._preUnmarshal,
674
+ custom: this._options?.customUnmarshaller,
675
+ hostRef: !!this._options?.marshalByReference,
676
+ });
677
+ done = true;
678
+ return result;
679
+ } finally {
680
+ // A mid-flight abort (e.g. an OOM inside the unmarshal chain) can throw
681
+ // before the freshly created proxy handle was registered in the map,
682
+ // orphaning it. Dispose it here; if it was already registered the map
683
+ // skips it (alive check) at dispose. Disposing a handle never allocates,
684
+ // so this is safe even under an active memory limit.
685
+ if (!done && wrapped && wrappedHandle?.alive && wrappedHandle !== handle) {
686
+ wrappedHandle.dispose();
687
+ }
688
+ }
545
689
  };
546
690
 
547
691
  _register(
@@ -555,24 +699,41 @@ export class Arena {
555
699
  }
556
700
 
557
701
  let wrappedT = this._wrap(t);
558
- const [wrappedH] = this._wrapHandle(h);
702
+ // `wrappedHNew` is true when `_wrapHandle` allocated a fresh VM proxy (which
703
+ // we own) rather than returning the original handle. If registration fails
704
+ // or throws mid-flight (e.g. an OOM in `map.set`), that proxy would be
705
+ // orphaned, so the finally below disposes it.
706
+ const [wrappedH, wrappedHNew] = this._wrapHandle(h);
559
707
  const isPromise = t instanceof Promise;
560
708
  if (!wrappedH || (!wrappedT && !isPromise)) return; // t or h is not an object
561
709
  if (isPromise) wrappedT = t;
562
710
 
563
711
  const unwrappedT = this._unwrap(t);
564
- const [unwrappedH, unwrapped] = this._unwrapHandle(h);
565
-
566
- const res = map.set(wrappedT, wrappedH, unwrappedT, unwrappedH);
567
- if (!res) {
568
- // already registered
569
- if (unwrapped) unwrappedH.dispose();
570
- throw new Error("already registered");
571
- } else if (sync) {
572
- this._sync.add(unwrappedT);
712
+ let unwrappedH: QuickJSHandle | undefined;
713
+ let unwrapped = false;
714
+ let done = false;
715
+ try {
716
+ [unwrappedH, unwrapped] = this._unwrapHandle(h);
717
+
718
+ const res = map.set(wrappedT, wrappedH, unwrappedT, unwrappedH);
719
+ if (!res) {
720
+ // already registered
721
+ throw new Error("already registered");
722
+ } else if (sync) {
723
+ this._sync.add(unwrappedT);
724
+ }
725
+
726
+ done = true;
727
+ return [wrappedT, wrappedH];
728
+ } finally {
729
+ if (!done) {
730
+ // `map.set` did not take ownership (it threw or returned false): dispose
731
+ // the handles we created so they are not orphaned. A non-new `wrappedH`
732
+ // and a non-wrapped `unwrappedH` are the caller's handle `h`, left alone.
733
+ if (wrappedHNew && wrappedH.alive) wrappedH.dispose();
734
+ if (unwrapped && unwrappedH?.alive) unwrappedH.dispose();
735
+ }
573
736
  }
574
-
575
- return [wrappedT, wrappedH];
576
737
  }
577
738
 
578
739
  _syncMode = (obj: any): "both" | undefined => {
@@ -603,16 +764,7 @@ export class Arena {
603
764
  };
604
765
 
605
766
  _wrapHandle(handle: QuickJSHandle): [Wrapped<QuickJSHandle> | undefined, boolean] {
606
- return wrapHandle(
607
- this.context,
608
- handle,
609
- this._symbol,
610
- this._symbolHandle,
611
- this._unmarshal,
612
- this._syncMode,
613
- this._options?.isHandleWrappable,
614
- this._options?.syncEnabled ?? true,
615
- );
767
+ return this._wrapHandleImpl.wrapHandle(handle);
616
768
  }
617
769
 
618
770
  _unwrapHandle(target: QuickJSHandle): [QuickJSHandle, boolean] {
@@ -638,9 +790,19 @@ export class AsyncArena extends Arena {
638
790
  * Evaluate JS code asynchronously in the VM and get the result on the host
639
791
  * side. Like `evalCode`, it converts and re-throws errors thrown during
640
792
  * evaluation. Use this when the code awaits host-provided promises.
793
+ *
794
+ * @param code - The JS code to evaluate
795
+ * @param filename - Optional filename used in error stacks and for debugging
796
+ * @param options - Optional eval options forwarded to quickjs-emscripten's
797
+ * `evalCodeAsync`, either an `EvalFlags` bitmask (`number`) or a
798
+ * {@link ContextEvalOptions} object (e.g. `{ strict: true }`)
641
799
  */
642
- async evalCodeAsync<T = any>(code: string, filename?: string): Promise<T> {
643
- const result = await this.asyncContext.evalCodeAsync(code, filename);
800
+ async evalCodeAsync<T = any>(
801
+ code: string,
802
+ filename?: string,
803
+ options?: number | ContextEvalOptions,
804
+ ): Promise<T> {
805
+ const result = await this.asyncContext.evalCodeAsync(code, filename, options);
644
806
  return this._unwrapResultAndUnmarshal(result);
645
807
  }
646
808
  }
@@ -1,6 +1,6 @@
1
1
  import type { QuickJSContext, QuickJSHandle } from "quickjs-emscripten";
2
2
 
3
- import { call, consumeAll } from "../vmutil";
3
+ import { call, consume, consumeAll } from "../vmutil";
4
4
 
5
5
  export default function marshalCustom(
6
6
  ctx: QuickJSContext,
@@ -13,24 +13,33 @@ export default function marshalCustom(
13
13
  handle = c(target, ctx);
14
14
  if (handle) break;
15
15
  }
16
- return handle ? preMarshal(target, handle) ?? handle : undefined;
16
+ if (!handle) return undefined;
17
+ // Own the custom-marshalled handle until `preMarshal` registers it; dispose it
18
+ // if `preMarshal` throws mid-flight so it is not orphaned.
19
+ let owned = true;
20
+ try {
21
+ const result = preMarshal(target, handle) ?? handle;
22
+ owned = false;
23
+ return result;
24
+ } finally {
25
+ if (owned && handle.alive) handle.dispose();
26
+ }
17
27
  }
18
28
 
19
29
  export function symbol(target: unknown, ctx: QuickJSContext): QuickJSHandle | undefined {
20
30
  if (typeof target !== "symbol") return;
21
- const handle = call(
22
- ctx,
23
- "d => Symbol(d)",
24
- undefined,
25
- target.description ? ctx.newString(target.description) : ctx.undefined,
26
- );
27
- return handle;
31
+ // `call` does not dispose its arguments, so the description string handle must
32
+ // be consumed here or it leaks on every symbol marshal.
33
+ return target.description
34
+ ? consume(ctx.newString(target.description), d => call(ctx, "d => Symbol(d)", undefined, d))
35
+ : call(ctx, "d => Symbol(d)", undefined, ctx.undefined);
28
36
  }
29
37
 
30
38
  export function date(target: unknown, ctx: QuickJSContext): QuickJSHandle | undefined {
31
39
  if (!(target instanceof Date)) return;
32
- const handle = call(ctx, "d => new Date(d)", undefined, ctx.newNumber(target.getTime()));
33
- return handle;
40
+ // `call` does not dispose its arguments, so the time number handle is consumed
41
+ // here rather than leaked.
42
+ return consume(ctx.newNumber(target.getTime()), d => call(ctx, "d => new Date(d)", undefined, d));
34
43
  }
35
44
 
36
45
  export function arrayBuffer(target: unknown, ctx: QuickJSContext): QuickJSHandle | undefined {
@@ -1,4 +1,5 @@
1
- import { getQuickJS, QuickJSHandle } from "quickjs-emscripten";
1
+ import type { QuickJSHandle } from "quickjs-emscripten";
2
+ import { getQuickJS } from "quickjs-emscripten";
2
3
  import { expect, test, vi } from "vitest";
3
4
 
4
5
  import { json, call } from "../vmutil";
@@ -8,8 +9,10 @@ import marshalFunction from "./function";
8
9
  test("normal func", async () => {
9
10
  const ctx = (await getQuickJS()).newContext();
10
11
 
11
- const marshal = vi.fn(v => json(ctx, v));
12
- const unmarshal = vi.fn(v => (ctx.sameValue(v, ctx.global) ? undefined : ctx.dump(v)));
12
+ const marshal = vi.fn((v) => json(ctx, v));
13
+ const unmarshal = vi.fn((v) =>
14
+ ctx.sameValue(v, ctx.global) ? undefined : ctx.dump(v),
15
+ );
13
16
  const preMarshal = vi.fn((_, a) => a);
14
17
  const innerfn = vi.fn((..._args: any[]) => "hoge");
15
18
  const fn = (...args: any[]) => innerfn(...args);
@@ -43,7 +46,7 @@ test("normal func", async () => {
43
46
 
44
47
  test("func which has properties", async () => {
45
48
  const ctx = (await getQuickJS()).newContext();
46
- const marshal = vi.fn(v => json(ctx, v));
49
+ const marshal = vi.fn((v) => json(ctx, v));
47
50
 
48
51
  const fn = () => {};
49
52
  fn.hoge = "foo";
@@ -52,7 +55,7 @@ test("func which has properties", async () => {
52
55
  ctx,
53
56
  fn,
54
57
  marshal,
55
- v => ctx.dump(v),
58
+ (v) => ctx.dump(v),
56
59
  (_, a) => a,
57
60
  );
58
61
  if (!handle) throw new Error("handle is undefined");
@@ -93,16 +96,20 @@ test("class", async () => {
93
96
  if (!handle) throw new Error("handle is undefined");
94
97
 
95
98
  const newA = ctx.unwrapResult(ctx.evalCode(`A => new A(100)`));
96
- const instance = ctx.unwrapResult(ctx.callFunction(newA, ctx.undefined, handle));
99
+ const instance = ctx.unwrapResult(
100
+ ctx.callFunction(newA, ctx.undefined, handle),
101
+ );
97
102
 
98
103
  expect(ctx.dump(ctx.getProp(handle, "name"))).toBe("A");
99
104
  expect(ctx.dump(ctx.getProp(handle, "length"))).toBe(1);
100
- expect(ctx.dump(call(ctx, "(cls, i) => i instanceof cls", undefined, handle, instance))).toBe(
101
- true,
102
- );
105
+ expect(
106
+ ctx.dump(
107
+ call(ctx, "(cls, i) => i instanceof cls", undefined, handle, instance),
108
+ ),
109
+ ).toBe(true);
103
110
  expect(ctx.dump(ctx.getProp(instance, "a"))).toBe(100);
104
111
 
105
- disposables.forEach(d => d.dispose());
112
+ disposables.forEach((d) => d.dispose());
106
113
  instance.dispose();
107
114
  newA.dispose();
108
115
  handle.dispose();
@@ -117,19 +124,34 @@ test("preApply", async () => {
117
124
  if (typeof v === "number") return ctx.newNumber(v);
118
125
  return ctx.null;
119
126
  };
120
- const unmarshal = (v: QuickJSHandle) => (ctx.typeof(v) === "object" ? that : ctx.dump(v));
121
- const preApply = vi.fn((a: (...args: any[]) => any, b: any, c: any[]) => a.apply(b, c) + "!");
127
+ const unmarshal = (v: QuickJSHandle) =>
128
+ ctx.typeof(v) === "object" ? that : ctx.dump(v);
129
+ const preApply = vi.fn(
130
+ (a: (...args: any[]) => any, b: any, c: any[]) => a.apply(b, c) + "!",
131
+ );
122
132
  const that = {};
123
133
  const thatHandle = ctx.newObject();
124
134
 
125
135
  const fn = () => "foo";
126
- const handle = marshalFunction(ctx, fn, marshal, unmarshal, (_, a) => a, preApply);
136
+ const handle = marshalFunction(
137
+ ctx,
138
+ fn,
139
+ marshal,
140
+ unmarshal,
141
+ (_, a) => a,
142
+ preApply,
143
+ );
127
144
  if (!handle) throw new Error("handle is undefined");
128
145
 
129
146
  expect(preApply).toBeCalledTimes(0);
130
147
 
131
148
  const res = ctx.unwrapResult(
132
- ctx.callFunction(handle, thatHandle, ctx.newNumber(100), ctx.newString("hoge")),
149
+ ctx.callFunction(
150
+ handle,
151
+ thatHandle,
152
+ ctx.newNumber(100),
153
+ ctx.newString("hoge"),
154
+ ),
133
155
  );
134
156
 
135
157
  expect(preApply).toBeCalledTimes(1);