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/README.md CHANGED
@@ -6,6 +6,9 @@
6
6
 
7
7
  quickjs-emscripten-sync wraps [quickjs-emscripten](https://github.com/justjake/quickjs-emscripten) and keeps object state in sync between the host (browser or Node.js) and a sandboxed QuickJS VM, so you can exchange values across the boundary as if they were plain JavaScript objects.
8
8
 
9
+ > [!TIP]
10
+ > **Want a higher-level, batteries-included plugin system?** Check out [**@reearth/zushi**](https://github.com/reearth/zushi) — a framework-agnostic plugin runtime that runs untrusted plugin code in an isolated WASM backend (QuickJS built in), exposes a host-defined API, and renders plugin UI in sandboxed iframes. quickjs-emscripten-sync provides the low-level host↔VM sync primitives; zushi builds a full plugin runtime on top.
11
+
9
12
  ## Features
10
13
 
11
14
  - Exchange and synchronize values between the host and QuickJS seamlessly:
@@ -226,6 +229,14 @@ type Options = {
226
229
  * "Passing objects by reference" below.
227
230
  */
228
231
  marshalByReference?: (target: any) => boolean;
232
+
233
+ /**
234
+ * Marshal selected async host functions as *asyncified* functions, so the VM
235
+ * suspends until the host promise settles and the guest receives the resolved
236
+ * value synchronously. Only effective with an `AsyncArena`. See "Asyncified
237
+ * host functions" below.
238
+ */
239
+ asyncify?: boolean | ((target: (...args: any[]) => any) => boolean);
229
240
  };
230
241
  ```
231
242
 
@@ -369,6 +380,40 @@ ctx.dispose();
369
380
 
370
381
  Evaluate JS code asynchronously and return the result on the host. Like `evalCode`, it converts and re-throws errors thrown during evaluation.
371
382
 
383
+ #### Asyncified host functions
384
+
385
+ Normally an async host function is marshalled like any other function, so the guest receives a marshalled `Promise`:
386
+
387
+ ```js
388
+ const ctx = await newAsyncContext();
389
+ const arena = new AsyncArena(ctx, { isMarshalable: true });
390
+
391
+ arena.expose({ fetchSync: async () => "result" });
392
+ await arena.evalCodeAsync(`typeof fetchSync()`); // "object" (a Promise)
393
+ ```
394
+
395
+ With the `asyncify` option, selected async functions are marshalled through QuickJS's [Asyncify](https://emscripten.org/docs/porting/asyncify.html) support instead. When the guest calls one, the VM suspends until the host promise settles and the guest receives the **resolved value synchronously**, as if the call were blocking:
396
+
397
+ ```js
398
+ const arena = new AsyncArena(ctx, { isMarshalable: true, asyncify: true });
399
+
400
+ arena.expose({ fetchSync: async () => "result" });
401
+ await arena.evalCodeAsync(`const v = fetchSync(); typeof v`); // "string"
402
+ await arena.evalCodeAsync(`fetchSync()`); // "result"
403
+ ```
404
+
405
+ `asyncify` accepts:
406
+
407
+ - `true` — asyncify every host function that is *declared* async, auto-detected via `Object.prototype.toString.call(fn) === "[object AsyncFunction]"`. Functions transpiled to ES5 (e.g. Babel/TypeScript targeting older runtimes) are plain functions that return a promise and are **not** detected — use the predicate form for those.
408
+ - `(target) => boolean` — asyncify exactly the functions for which it returns `true`. Called with the unwrapped target, like `isMarshalable`.
409
+ - absent / `false` — current behavior; async functions yield a `Promise` in the guest.
410
+
411
+ Constraints (imposed by Emscripten Asyncify):
412
+
413
+ - Asyncified functions only work when evaluation is entered through an async entry point (`evalCodeAsync`). Calling one during a synchronous `evalCode` throws `Error: Function unexpectedly returned a Promise`.
414
+ - Only **one** suspended call is allowed at a time. Concurrent suspension (e.g. two asyncified calls awaiting simultaneously) throws a runtime error from quickjs-emscripten. Sequential calls are fine.
415
+ - The option requires a `QuickJSAsyncContext` (from `newAsyncContext()`). On a plain synchronous context it is silently ignored and async functions behave as before (Promise).
416
+
372
417
  ### `defaultRegisteredObjects: [any, string][]`
373
418
 
374
419
  The default value of the `registeredObjects` option.
package/dist/index.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { ContextEvalOptions } from 'quickjs-emscripten';
2
+ import { InterruptHandler } from 'quickjs-emscripten';
1
3
  import { Intrinsics } from 'quickjs-emscripten';
2
4
  import { QuickJSAsyncContext } from 'quickjs-emscripten';
3
5
  import { QuickJSContext } from 'quickjs-emscripten';
@@ -19,6 +21,7 @@ export declare class Arena {
19
21
  _temporalSync: Set<any>;
20
22
  _symbol: symbol;
21
23
  _symbolHandle: QuickJSHandle;
24
+ _wrapHandleImpl: WrapHandle;
22
25
  _options?: Options;
23
26
  /** Constructs a new Arena instance. It requires a quickjs-emscripten context initialized with `quickjs.newContext()`. */
24
27
  constructor(ctx: QuickJSContext, options?: Options);
@@ -30,8 +33,14 @@ export declare class Arena {
30
33
  [Symbol.dispose](): void;
31
34
  /**
32
35
  * 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.
36
+ *
37
+ * @param code - The JS code to evaluate
38
+ * @param filename - Optional filename used in error stacks and for debugging
39
+ * @param options - Optional eval options forwarded to quickjs-emscripten's
40
+ * `evalCode`, either an `EvalFlags` bitmask (`number`) or a
41
+ * {@link ContextEvalOptions} object (e.g. `{ strict: true }`)
33
42
  */
34
- evalCode<T = any>(code: string): T;
43
+ evalCode<T = any>(code: string, filename?: string, options?: number | ContextEvalOptions): T;
35
44
  /**
36
45
  * Evaluate ES module code in the VM and get the module's exports.
37
46
  *
@@ -39,6 +48,10 @@ export declare class Arena {
39
48
  *
40
49
  * @param code - The ES module code to evaluate
41
50
  * @param filename - Optional filename for debugging purposes (default: "module.js")
51
+ * @param options - Optional {@link ContextEvalOptions}. `type` is always forced
52
+ * to `"module"`, so any `type` in `options` is overridden. A numeric
53
+ * `EvalFlags` bitmask cannot be merged with the forced module type and is
54
+ * therefore not supported here; if a number is passed it is ignored.
42
55
  * @returns The module's exports object, or a Promise resolving to exports if using top-level await
43
56
  *
44
57
  * @example
@@ -64,7 +77,7 @@ export declare class Arena {
64
77
  * console.log(exports.data); // 123
65
78
  * ```
66
79
  */
67
- evalModule<T = any>(code: string, filename?: string): T | Promise<T>;
80
+ evalModule<T = any>(code: string, filename?: string, options?: number | ContextEvalOptions): T | Promise<T>;
68
81
  /**
69
82
  * Almost same as `vm.executePendingJobs()`, but it converts and re-throws error objects when an error is thrown during evaluation.
70
83
  */
@@ -111,6 +124,39 @@ export declare class Arena {
111
124
  * ```
112
125
  */
113
126
  setMaxStackSize(stackSize: number): void;
127
+ /**
128
+ * Set a callback which is regularly called by the QuickJS engine when it is
129
+ * executing code. This callback can be used to implement an execution
130
+ * timeout: return `true` from the handler to interrupt the running code.
131
+ *
132
+ * This is useful for preventing runaway CPU usage (e.g. infinite loops) in
133
+ * untrusted code.
134
+ *
135
+ * @param cb - The interrupt handler. Return `true` to interrupt execution.
136
+ *
137
+ * @example
138
+ * ```js
139
+ * // Interrupt evaluation after 1 second
140
+ * const deadline = Date.now() + 1000;
141
+ * arena.setInterruptHandler(() => Date.now() > deadline);
142
+ *
143
+ * try {
144
+ * arena.evalCode(`while (true) {}`);
145
+ * } catch (e) {
146
+ * console.log("Execution interrupted");
147
+ * }
148
+ * ```
149
+ */
150
+ setInterruptHandler(cb: InterruptHandler): void;
151
+ /**
152
+ * Remove the interrupt handler previously set with `setInterruptHandler`.
153
+ *
154
+ * @example
155
+ * ```js
156
+ * arena.removeInterruptHandler();
157
+ * ```
158
+ */
159
+ removeInterruptHandler(): void;
114
160
  /**
115
161
  * Get detailed memory usage statistics for this runtime.
116
162
  *
@@ -204,6 +250,7 @@ export declare class Arena {
204
250
  _unwrapResultAndUnmarshal(result: VmCallResult<QuickJSHandle> | undefined): any;
205
251
  _isMarshalable: (t: unknown) => boolean | "json";
206
252
  _marshalByReference: (t: unknown) => boolean;
253
+ _asyncify: (t: unknown) => boolean;
207
254
  _registerHostRef: (t: unknown, handle: QuickJSHandle) => QuickJSHandle;
208
255
  _marshalFind: (t: unknown) => QuickJSHandle | undefined;
209
256
  _marshalPre: (t: unknown, h: QuickJSHandle | QuickJSDeferredPromise, mode: true | "json" | undefined) => Wrapped<QuickJSHandle> | undefined;
@@ -237,8 +284,14 @@ export declare class AsyncArena extends Arena {
237
284
  * Evaluate JS code asynchronously in the VM and get the result on the host
238
285
  * side. Like `evalCode`, it converts and re-throws errors thrown during
239
286
  * evaluation. Use this when the code awaits host-provided promises.
287
+ *
288
+ * @param code - The JS code to evaluate
289
+ * @param filename - Optional filename used in error stacks and for debugging
290
+ * @param options - Optional eval options forwarded to quickjs-emscripten's
291
+ * `evalCodeAsync`, either an `EvalFlags` bitmask (`number`) or a
292
+ * {@link ContextEvalOptions} object (e.g. `{ strict: true }`)
240
293
  */
241
- evalCodeAsync<T = any>(code: string, filename?: string): Promise<T>;
294
+ evalCodeAsync<T = any>(code: string, filename?: string, options?: number | ContextEvalOptions): Promise<T>;
242
295
  }
243
296
 
244
297
  export declare function call(ctx: QuickJSContext, code: string, thisArg?: QuickJSHandle, ...args: QuickJSHandle[]): QuickJSHandle;
@@ -250,11 +303,15 @@ export declare function complexity(value: any, max?: number): number;
250
303
 
251
304
  export declare function consumeAll<T extends QuickJSHandle[], K>(handles: T, cb: (handles: T) => K): K;
252
305
 
306
+ export { ContextEvalOptions }
307
+
253
308
  /**
254
309
  * Default value of registeredObjects option of the Arena class constructor.
255
310
  */
256
311
  export declare const defaultRegisteredObjects: [any, string][];
257
312
 
313
+ export { InterruptHandler }
314
+
258
315
  export { Intrinsics }
259
316
 
260
317
  export declare function isES2015Class(cls: any): cls is new (...args: any[]) => any;
@@ -291,6 +348,34 @@ export declare type Options = {
291
348
  syncEnabled?: boolean;
292
349
  /** 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. */
293
350
  marshalByReference?: (target: any) => boolean;
351
+ /**
352
+ * Marshal selected host functions as *asyncified* functions. Normally an async
353
+ * host function is marshalled like any other function, so the guest receives a
354
+ * marshalled `Promise`. When a function is asyncified, the QuickJS VM instead
355
+ * suspends until the host promise settles and the guest receives the resolved
356
+ * value synchronously (as if the call were blocking).
357
+ *
358
+ * - `true`: asyncify every host function that is *declared* async, auto-detected
359
+ * via `Object.prototype.toString.call(fn) === "[object AsyncFunction]"`. Note
360
+ * that functions transpiled to ES5 (e.g. by Babel/TypeScript targeting older
361
+ * runtimes) are plain functions returning a promise and are **not** detected —
362
+ * use the predicate form for those.
363
+ * - predicate: asyncify exactly the functions for which it returns `true`. It is
364
+ * called with the *unwrapped* target (the original function, not a sync proxy),
365
+ * like `isMarshalable`.
366
+ * - absent/`false`: current behavior; async functions yield a `Promise` in the guest.
367
+ *
368
+ * Constraints (Emscripten Asyncify):
369
+ * - Asyncified functions only work when evaluation is entered through an async
370
+ * entry point ({@link AsyncArena.evalCodeAsync}). Calling one during a
371
+ * synchronous `evalCode` fails with `Error: Function unexpectedly returned a Promise`.
372
+ * - Only ONE suspended call is allowed at a time; concurrent suspension throws a
373
+ * runtime error from quickjs-emscripten.
374
+ * - If the context does not support `newAsyncifiedFunction` (a plain sync context
375
+ * from `getQuickJS().newContext()`), this option is silently ignored and async
376
+ * functions behave as today (Promise).
377
+ */
378
+ asyncify?: boolean | ((target: (...args: any[]) => any) => boolean);
294
379
  };
295
380
 
296
381
  declare type Options_2 = {
@@ -305,6 +390,8 @@ declare type Options_2 = {
305
390
  pre: (target: unknown, handle: QuickJSHandle | QuickJSDeferredPromise, mode: true | "json" | undefined) => QuickJSHandle | undefined;
306
391
  preApply?: (target: (...args: any[]) => any, thisArg: unknown, args: unknown[]) => any;
307
392
  prepareReturn?: (handle: QuickJSHandle) => QuickJSHandle;
393
+ unwrap?: (target: unknown) => unknown;
394
+ asyncify?: (target: unknown) => boolean;
308
395
  custom?: Iterable<(obj: unknown, ctx: QuickJSContext) => QuickJSHandle | undefined>;
309
396
  };
310
397
 
@@ -366,6 +453,13 @@ export declare class VMMap {
366
453
 
367
454
  export declare function walkObject(value: any, callback?: (target: any, set: Set<any>) => boolean | void): Set<any>;
368
455
 
456
+ declare type WrapHandle = {
457
+ /** Wrap a VM handle with the shared proxyFuncs, like the old `wrapHandle`. */
458
+ wrapHandle: (handle: QuickJSHandle) => [Wrapped<QuickJSHandle> | undefined, boolean];
459
+ /** Dispose the shared proxyFuncs handle. Call exactly once, at Arena dispose. */
460
+ dispose: () => void;
461
+ };
462
+
369
463
  declare type Wrapped<T> = T & {
370
464
  __qes_wrapped: never;
371
465
  };