quickjs-emscripten-sync 1.9.1 → 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.
@@ -402,6 +447,15 @@ Only the `set`, `deleteProperty`, and `defineProperty` operations on objects are
402
447
 
403
448
  `Date`, `Map`, `Set`, `ArrayBuffer`, and typed arrays are marshalled by value (a snapshot copy is created on the other side). They are not proxied, so later mutations are not synchronized, and self-referential `Map`/`Set` are not supported.
404
449
 
450
+ ### `this` on plain calls
451
+
452
+ When an exposed host function is called plainly from the VM (`fn()`, not `obj.fn()`), the host function receives `this === undefined`. This differs from plain JavaScript, where a non-strict function called this way would see `this === globalThis`. The VM global object is intentionally **not** marshalled to the host: doing so would eagerly deep-copy the entire global graph on the first call and would leak `globalThis` across the boundary. Method calls are unaffected — `obj.fn()` still receives `obj` as `this`.
453
+
454
+ ```js
455
+ arena.expose({ whoAmI() { return this; } });
456
+ arena.evalCode(`whoAmI()`); // undefined (not globalThis)
457
+ ```
458
+
405
459
  ## License
406
460
 
407
461
  [MIT License](LICENSE)
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;
@@ -211,6 +258,7 @@ export declare class Arena {
211
258
  _disposeTransient: (handle: QuickJSHandle) => void;
212
259
  _marshalPreApply: (target: (...args: any[]) => any, that: unknown, args: unknown[]) => void;
213
260
  _marshal: (target: any) => [QuickJSHandle, boolean];
261
+ _prepareMarshalReturn: (h: QuickJSHandle) => QuickJSHandle;
214
262
  _preUnmarshal: (t: any, h: QuickJSHandle) => Wrapped<any>;
215
263
  _unmarshalFind: (h: QuickJSHandle) => unknown;
216
264
  _unmarshal: (handle: QuickJSHandle) => any;
@@ -236,8 +284,14 @@ export declare class AsyncArena extends Arena {
236
284
  * Evaluate JS code asynchronously in the VM and get the result on the host
237
285
  * side. Like `evalCode`, it converts and re-throws errors thrown during
238
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 }`)
239
293
  */
240
- evalCodeAsync<T = any>(code: string, filename?: string): Promise<T>;
294
+ evalCodeAsync<T = any>(code: string, filename?: string, options?: number | ContextEvalOptions): Promise<T>;
241
295
  }
242
296
 
243
297
  export declare function call(ctx: QuickJSContext, code: string, thisArg?: QuickJSHandle, ...args: QuickJSHandle[]): QuickJSHandle;
@@ -249,11 +303,15 @@ export declare function complexity(value: any, max?: number): number;
249
303
 
250
304
  export declare function consumeAll<T extends QuickJSHandle[], K>(handles: T, cb: (handles: T) => K): K;
251
305
 
306
+ export { ContextEvalOptions }
307
+
252
308
  /**
253
309
  * Default value of registeredObjects option of the Arena class constructor.
254
310
  */
255
311
  export declare const defaultRegisteredObjects: [any, string][];
256
312
 
313
+ export { InterruptHandler }
314
+
257
315
  export { Intrinsics }
258
316
 
259
317
  export declare function isES2015Class(cls: any): cls is new (...args: any[]) => any;
@@ -290,6 +348,34 @@ export declare type Options = {
290
348
  syncEnabled?: boolean;
291
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. */
292
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);
293
379
  };
294
380
 
295
381
  declare type Options_2 = {
@@ -303,6 +389,9 @@ declare type Options_2 = {
303
389
  disposeTransient?: (handle: QuickJSHandle) => void;
304
390
  pre: (target: unknown, handle: QuickJSHandle | QuickJSDeferredPromise, mode: true | "json" | undefined) => QuickJSHandle | undefined;
305
391
  preApply?: (target: (...args: any[]) => any, thisArg: unknown, args: unknown[]) => any;
392
+ prepareReturn?: (handle: QuickJSHandle) => QuickJSHandle;
393
+ unwrap?: (target: unknown) => unknown;
394
+ asyncify?: (target: unknown) => boolean;
306
395
  custom?: Iterable<(obj: unknown, ctx: QuickJSContext) => QuickJSHandle | undefined>;
307
396
  };
308
397
 
@@ -364,6 +453,13 @@ export declare class VMMap {
364
453
 
365
454
  export declare function walkObject(value: any, callback?: (target: any, set: Set<any>) => boolean | void): Set<any>;
366
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
+
367
463
  declare type Wrapped<T> = T & {
368
464
  __qes_wrapped: never;
369
465
  };