quickjs-emscripten-sync 1.8.4 → 1.9.1

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
@@ -1,26 +1,35 @@
1
1
  # quickjs-emscripten-sync
2
2
 
3
- [![CI](https://github.com/reearth/quickjs-emscripten-sync/actions/workflows/ci.yml/badge.svg)](https://github.com/reearth/quickjs-emscripten-sync/actions/workflows/main.yml) [![codecov](https://codecov.io/gh/reearth/quickjs-emscripten-sync/branch/main/graph/badge.svg)](https://codecov.io/gh/reearth/quickjs-emscripten-sync)
3
+ [![CI](https://github.com/reearth/quickjs-emscripten-sync/actions/workflows/ci.yml/badge.svg)](https://github.com/reearth/quickjs-emscripten-sync/actions/workflows/ci.yml) [![codecov](https://codecov.io/gh/reearth/quickjs-emscripten-sync/branch/main/graph/badge.svg)](https://codecov.io/gh/reearth/quickjs-emscripten-sync)
4
4
 
5
- Build a secure plugin system in web browsers
5
+ **Build a secure plugin system for web browsers.**
6
6
 
7
- This library wraps [quickjs-emscripten](https://github.com/justjake/quickjs-emscripten) and provides a way to sync object state between the browser and sandboxed QuickJS.
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
- - Exchange and sync values between the browser (host) and QuickJS seamlessly
10
- - Primitives (number, boolean, string, symbol)
11
- - Arrays
12
- - Functions
13
- - Classes and instances
14
- - Objects with prototypes and any property descriptors
9
+ ## Features
10
+
11
+ - Exchange and synchronize values between the host and QuickJS seamlessly:
12
+ - Primitives (number, boolean, string, symbol, bigint)
13
+ - Arrays, and objects with prototypes and any property descriptors
14
+ - Functions, classes, and instances
15
15
  - Promises
16
- - Expose objects as a global object in QuickJS
17
- - Marshaling limitation for specific objects
18
- - Register a pair of objects that will be considered the same between the browser and QuickJS
16
+ - `Date`
17
+ - `Map` and `Set` (by value)
18
+ - `ArrayBuffer`, typed arrays, and `DataView` (by value)
19
+ - Expose host objects as globals inside the VM.
20
+ - Fine-grained control over which objects may be marshalled (for security).
21
+ - Pass objects opaquely by reference, or register host/VM object pairs to be treated as identical.
22
+
23
+ ## Installation
19
24
 
20
25
  ```
21
26
  npm install quickjs-emscripten quickjs-emscripten-sync
22
27
  ```
23
28
 
29
+ `quickjs-emscripten` is a peer dependency.
30
+
31
+ ## Quick start
32
+
24
33
  ```js
25
34
  import { getQuickJS } from "quickjs-emscripten";
26
35
  import { Arena } from "quickjs-emscripten-sync";
@@ -36,7 +45,7 @@ class Cls {
36
45
  const ctx = (await getQuickJS()).newContext();
37
46
  const arena = new Arena(ctx, { isMarshalable: true });
38
47
 
39
- // We can pass objects to the context and run code safely
48
+ // Pass host objects to the VM and run code against them safely.
40
49
  const exposed = {
41
50
  Cls,
42
51
  cls: new Cls(),
@@ -44,100 +53,74 @@ const exposed = {
44
53
  };
45
54
  arena.expose(exposed);
46
55
 
47
- arena.evalCode(`cls instanceof Cls`); // returns true
48
- arena.evalCode(`cls.field`); // returns 0
49
- arena.evalCode(`cls.method()`); // returns 1
50
- arena.evalCode(`cls.field`); // returns 1
56
+ arena.evalCode(`cls instanceof Cls`); // true
57
+ arena.evalCode(`cls.field`); // 0
58
+ arena.evalCode(`cls.method()`); // 1
59
+ arena.evalCode(`cls.field`); // 1
51
60
 
52
- arena.evalCode(`syncedCls.field`); // returns 0
53
- exposed.syncedCls.method(); // returns 1
54
- arena.evalCode(`syncedCls.field`); // returns 1
61
+ // Changes to a synced object are reflected on both sides.
62
+ arena.evalCode(`syncedCls.field`); // 0
63
+ exposed.syncedCls.method(); // 1
64
+ arena.evalCode(`syncedCls.field`); // 1
55
65
 
66
+ // Always dispose the arena before disposing the context.
56
67
  arena.dispose();
57
68
  ctx.dispose();
58
69
  ```
59
70
 
60
- [Example code](src/index.test.ts) is available as the unit test code.
71
+ More runnable examples can be found in the [unit tests](src/index.test.ts).
61
72
 
62
- ## Operating Environment
73
+ ## Operating environment
63
74
 
64
75
  - Web browsers that support WebAssembly
65
76
  - Node.js
66
77
 
67
- If you want to run quickjs-emscripten and quickjs-emscripten-sync in a web browser, they have to be bundled with a bundler tool such as webpack, because quickjs-emscripten is now written in CommonJS format and web browsers cannot load it directly.
78
+ To run in a web browser, bundle your code with a tool such as webpack, Vite, or Rollup, since the WebAssembly module cannot be loaded directly via a `<script>` tag.
68
79
 
69
- ## Usage
80
+ ## How it works
70
81
 
71
- ```js
72
- import { getQuickJS } from "quickjs-emscripten";
73
- import { Arena } from "quickjs-emscripten-sync";
82
+ Running untrusted JS in quickjs-emscripten is safe, but it requires you to manage a large number of handles and their lifetimes by hand. Any handle that is not freed before the context is destroyed causes an error.
74
83
 
75
- (async function() {
76
- const ctx = (await getQuickJS()).newContext();
77
-
78
- // init Arena
79
- // ⚠️ Marshaling is opt-in for security reasons.
80
- // ⚠️ Be careful when activating marshalling.
81
- const arena = new Arena(ctx, { isMarshalable: true });
82
-
83
- // expose objects as global objects in QuickJS context
84
- arena.expose({
85
- console: {
86
- log: console.log
87
- }
88
- });
89
- arena.evalCode(`console.log("hello, world");`); // run console.log
90
- arena.evalCode(`1 + 1`); // 2
91
-
92
- // expose objects but also enable sync
93
- const data = arena.sync({ hoge: "foo" });
94
- arena.expose({ data });
95
-
96
- arena.evalCode(`data.hoge = "bar"`);
97
- // eval code and operations to exposed objects are automatically synced
98
- console.log(data.hoge); // "bar"
99
- data.hoge = "changed!";
100
- console.log(arena.evalCode(`data.hoge`)); // "changed!"
101
-
102
- // Don't forget calling arena.dispose() before disposing QuickJS context!
103
- arena.dispose();
104
- ctx.dispose();
105
- })();
106
- ```
84
+ quickjs-emscripten-sync hides this complexity behind the `Arena` class:
107
85
 
108
- ## Marshaling Limitations
86
+ - It tracks every handle generated through the context and frees them for you when the arena is disposed.
87
+ - It **marshals** host objects into VM handles and **unmarshals** VM handles back into host objects, recursing through properties and the prototype chain so the conversion is transparent. When a function is called, its arguments and `this` are converted for the side where the function is defined, and the return value is converted back for the caller.
88
+ - Most objects are wrapped in proxies during conversion, so that `set`, `delete`, and `defineProperty` operations are synchronized between the host and the VM.
109
89
 
110
- Objects are automatically converted when they cross between the host and the QuickJS context. The conversion of a host's object to a handle is called marshaling, and the conversion of a handle to a host's object is called unmarshaling.
90
+ > **Marshal** = converting a host object into a VM handle.
91
+ > **Unmarshal** = converting a VM handle back into a host object.
111
92
 
112
- And for marshalling, it is possible to control whether the conversion is performed or not.
93
+ ## Controlling what gets marshalled
113
94
 
114
- For example, exposing the host's global object to QuickJS is very heavy and dangerous. This exposure can be limited and controlled with the `isMarshalable` option. If `false` is returned, just `undefined` is passed to QuickJS.
95
+ You can control whether (and how) host objects are marshalled into the VM. This matters for security: exposing the host's global object to the VM, for example, is both heavy and dangerous.
96
+
97
+ Use the `isMarshalable` option to limit it. When the callback returns `false`, `undefined` is passed to the VM instead of the object.
115
98
 
116
99
  ```js
117
100
  import { Arena, complexity } from "quickjs-emscripten-sync";
118
101
 
119
102
  const arena = new Arena(ctx, {
120
103
  isMarshalable: (target: any) => {
121
- // prevent passing globalThis to QuickJS
104
+ // Never pass globalThis to the VM.
122
105
  if (target === window) return false;
123
- // complexity is a helper function to detect whether the object is heavy
106
+ // complexity() helps detect objects that are too heavy to pass.
124
107
  if (complexity(target, 30) >= 30) return false;
125
- return true; // other objects are OK
126
- }
108
+ return true; // anything else is fine
109
+ },
127
110
  });
128
111
 
129
- arena.evalCode(`a => a === undefined`)({}); // false
130
- arena.evalCode(`a => a === undefined`)(window); // true
112
+ arena.evalCode(`a => a === undefined`)({}); // false
113
+ arena.evalCode(`a => a === undefined`)(window); // true
131
114
  arena.evalCode(`a => a === undefined`)(document); // true
132
115
  ```
133
116
 
134
- The `complexity` function is useful to detect whether the object is too heavy to be passed to QuickJS.
117
+ See [`isMarshalable`](#options) for all accepted values.
135
118
 
136
- ## Security Warning
119
+ ## Security
137
120
 
138
- QuickJS has an environment isolated from the browser, so any code can be executed safely, but there are edge cases where some exposed objects by quickjs-emscripten-sync may break security.
121
+ ⚠️ QuickJS runs in an environment isolated from the browser, so untrusted code can generally be executed safely. However, there are edge cases where objects you expose through quickjs-emscripten-sync can break that isolation.
139
122
 
140
- quickjs-emscripten-sync cannot prevent such dangerous case, so **PLEASE be very careful and deliberate about what you expose to QuickJS!**
123
+ quickjs-emscripten-sync cannot detect every such case, so **be very careful and deliberate about what you expose to the VM.**
141
124
 
142
125
  ### Case 1: Prototype pollution
143
126
 
@@ -146,165 +129,259 @@ import { set } from "lodash-es";
146
129
 
147
130
  arena.expose({
148
131
  danger: (keys, value) => {
149
- // This function may cause prototype pollution in the browser by QuickJS
150
- set({}, keys, value)
151
- }
132
+ // Calling this from the VM can pollute prototypes in the host.
133
+ set({}, keys, value);
134
+ },
152
135
  });
153
136
 
154
137
  arena.evalCode(`danger("__proto__.a", () => { /* injected */ })`);
155
138
  ```
156
139
 
157
- ### Case 2: Unintended HTTP request
158
-
159
- It is very dangerous to expose or use directly or indirectly the `window` object, `localStorage`, `fetch`, `XMLHttpRequest` ...
140
+ ### Case 2: Unintended HTTP requests and DOM access
160
141
 
161
- This is because it enables the execution of unintended code such as XSS attacks, such as reading local storage, sending unintended HTTP requests, and manipulating DOM objects.
142
+ Exposing `window`, `localStorage`, `fetch`, `XMLHttpRequest`, and similar APIs directly or indirectly is very dangerous. It lets sandboxed code read local storage, send arbitrary HTTP requests, manipulate the DOM, and mount XSS-style attacks.
162
143
 
163
144
  ```js
164
145
  arena.expose({
165
- // This function may cause unintended HTTP requests
146
+ // Calling this from the VM can trigger unintended HTTP requests.
166
147
  danger: (url, body) => {
167
148
  fetch(url, {
168
149
  method: "POST",
169
- headers: {
170
- "Content-Type": "application/json"
171
- },
172
- body: JSON.stringify(body)
150
+ headers: { "Content-Type": "application/json" },
151
+ body: JSON.stringify(body),
173
152
  });
174
- }
153
+ },
175
154
  });
176
155
 
177
156
  arena.evalCode(`danger("/api", { dangerous: true })`);
178
157
  ```
179
158
 
180
- By default, quickjs-emscripten-sync doesn't prevent any marshaling, even in such cases. And there are many built-in objects in the host, so please note that it's hard to prevent all dangerous cases with the `isMarshalable` option alone.
159
+ By default, quickjs-emscripten-sync does not block any marshalling. Because the host has many built-in objects, the `isMarshalable` option alone cannot prevent every dangerous case design your exposed surface carefully.
181
160
 
182
161
  ## API
183
162
 
184
163
  ### `Arena`
185
164
 
186
- The Arena class manages all generated handles at once by quickjs-emscripten and automatically converts objects between the host and the QuickJS context.
165
+ `Arena` manages all handles generated by quickjs-emscripten and automatically converts objects between the host and the VM.
187
166
 
188
167
  #### `new Arena(ctx: QuickJSContext, options?: Options)`
189
168
 
190
- Constructs a new Arena instance. It requires a quickjs-emscripten context initialized with `quickjs.newContext()`.
169
+ Creates a new arena. `ctx` must be a context created with `quickjs.newContext()`.
191
170
 
192
- Options accepted:
171
+ > ⚠️ Marshalling is opt-in for security reasons. Enable it deliberately.
172
+
173
+ ##### Options
193
174
 
194
175
  ```ts
195
176
  type Options = {
196
- /** 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. */
177
+ /**
178
+ * Controls whether and how an object is marshalled. By default, objects are
179
+ * marshalled via JSON. See the table below for accepted values.
180
+ */
197
181
  isMarshalable?: boolean | "json" | ((target: any) => boolean | "json");
198
- /** Pre-registered pairs of objects that will be considered the same between the host and the QuickJS VM. This will be used automatically during the conversion. By default, it will be registered automatically with `defaultRegisteredObjects`.
182
+
183
+ /**
184
+ * Pre-registered pairs of objects that are treated as identical between the
185
+ * host and the VM, and reused automatically during conversion. Defaults to
186
+ * `defaultRegisteredObjects`.
199
187
  *
200
- * Instead of a string, you can also pass a QuickJSHandle directly. In that case, however, you have to dispose of them manually when destroying the VM.
188
+ * Instead of a code string you may pass a QuickJSHandle directly; in that
189
+ * case you must dispose of it yourself when destroying the VM.
201
190
  */
202
191
  registeredObjects?: Iterable<[any, QuickJSHandle | string]>;
203
- /** Register functions to convert an object to a QuickJS handle. */
204
- customMarshaller?: Iterable<
205
- (target: unknown, ctx: QuickJSContext) => QuickJSHandle | undefined
206
- >;
207
- /** Register functions to convert a QuickJS handle to an object. */
208
- customUnmarshaller?: Iterable<
209
- (target: QuickJSHandle, ctx: QuickJSContext) => any
210
- >;
211
- /** A callback that returns a boolean value that determines whether an object is wrappable by proxies. If returns false, note that the object cannot be synchronized between the host and the QuickJS even if arena.sync is used. */
192
+
193
+ /** Custom functions that convert a host object into a QuickJS handle. */
194
+ customMarshaller?: Iterable<(target: unknown, ctx: QuickJSContext) => QuickJSHandle | undefined>;
195
+
196
+ /** Custom functions that convert a QuickJS handle into a host object. */
197
+ customUnmarshaller?: Iterable<(target: QuickJSHandle, ctx: QuickJSContext) => any>;
198
+
199
+ /**
200
+ * Returns whether an object may be wrapped with a proxy. If it returns
201
+ * `false`, the object cannot be synchronized even when `arena.sync` is used.
202
+ */
212
203
  isWrappable?: (target: any) => boolean;
213
- /** A callback that returns a boolean value that determines whether an QuickJS handle is wrappable by proxies. If returns false, note that the handle cannot be synchronized between the host and the QuickJS even if arena.sync is used. */
204
+
205
+ /**
206
+ * Returns whether a QuickJS handle may be wrapped with a proxy. If it returns
207
+ * `false`, the handle cannot be synchronized even when `arena.sync` is used.
208
+ */
214
209
  isHandleWrappable?: (handle: QuickJSHandle, ctx: QuickJSContext) => boolean;
215
- /** Compatibility with quickjs-emscripten prior to v0.15. Inject code for compatibility into context at Arena class initialization time. */
210
+
211
+ /** Compatibility shim for quickjs-emscripten prior to v0.15. */
216
212
  compat?: boolean;
217
- }
213
+
214
+ /**
215
+ * Enables sync mode globally (default `true`). When `false`, objects are not
216
+ * wrapped with proxies and marshalled handles are disposed right after use:
217
+ * `arena.sync` has no effect, but objects are not retained for the arena's
218
+ * whole lifetime. Useful to avoid memory growth when frequently exchanging
219
+ * short-lived objects.
220
+ */
221
+ syncEnabled?: boolean;
222
+
223
+ /**
224
+ * Returns whether an object should be passed to the VM by reference (as an
225
+ * opaque HostRef) instead of being marshalled by value or proxy. See
226
+ * "Passing objects by reference" below.
227
+ */
228
+ marshalByReference?: (target: any) => boolean;
229
+ };
218
230
  ```
219
231
 
220
- Notes:
232
+ ###### `isMarshalable`
233
+
234
+ Determines how objects are marshalled from the host into the VM. **Keep this as restrictive as possible — loosening it can reduce your application's security.** See [Security](#security).
221
235
 
222
- **`isMarshalable`**: Determines how marshalling will be done when sending objects from the host to the context. **Make sure to set the marshalling to be the minimum necessary as it may reduce the security of your application.** [Please read the section on security above.](#security-warning)
236
+ | Value | Behaviour |
237
+ | --- | --- |
238
+ | `"json"` | **Default.** The object is serialized to JSON on the host and parsed in the VM. Functions and classes are lost. Safe. |
239
+ | `false` | The object is never marshalled; `undefined` is passed instead. Safe. |
240
+ | `(target) => boolean \| "json"` | **Recommended.** Decide per object. Return `true` to fully marshal, `"json"` for JSON, or `false` to skip. |
241
+ | `true` | The object is always fully marshalled. **Risky — not recommended.** |
223
242
 
224
- - `"json"` (**default**, safety): Target object will be serialized as JSON in host and then parsed in context. Functions and classes will be lost in the process.
225
- - `false` (safety): Target object will not be always marshalled as `undefined`.
226
- - `(target: any) => boolean | "json"` (recoomended): You can control marshalling mode for each objects. If you want to do marshalling, usually use this method. Allow partial marshalling by returning `true` only for some objects.
227
- - `true` (**risky and not recommended**): Target object will be always marshaled. This setting may reduce security.
243
+ ###### `registeredObjects`
228
244
 
229
- **`registeredObjects`**: You can pre-register a pair of objects that will be considered the same between the host and the QuickJS context. This will be used automatically during the conversion. By default, it will be registered automatically with [`defaultRegisteredObjects`](src/default.ts). If you want to add a new pair to this, please do the following:
245
+ Pre-register host/VM object pairs that should be treated as identical during conversion. Defaults to [`defaultRegisteredObjects`](src/default.ts). To extend it:
230
246
 
231
247
  ```js
232
248
  import { defaultRegisteredObjects } from "quickjs-emscripten-sync";
233
249
 
234
250
  const arena = new Arena(ctx, {
235
- registeredObjects: [
236
- ...defaultRegisteredObjects,
237
- [Math, "Math"]
238
- ]
251
+ registeredObjects: [...defaultRegisteredObjects, [Math, "Math"]],
239
252
  });
240
253
  ```
241
254
 
242
- Instead of a string, you can also pass a QuickJSHandle directly. In that case, however, you have to dispose of them manually when destroying the context.
255
+ Instead of a code string you may pass a QuickJSHandle directly; in that case you must dispose of it yourself when destroying the context.
243
256
 
244
- #### `dispose()`
257
+ ###### `marshalByReference`
245
258
 
246
- Dispose of the arena and managed handles. This method won't dispose the context itself, so the context has to be disposed of manually.
259
+ Return `true` for objects you want to pass to the VM as an opaque reference (a [HostRef](https://github.com/justjake/quickjs-emscripten)) instead of marshalling their contents. The VM cannot read or mutate such objects, but it can hold them and pass them back to the host, where they resolve to the **original** object (identity is preserved). This is useful for handing the sandbox a host resource — a class instance, a DOM node, and so on that it should carry around opaquely rather than copy.
247
260
 
248
- #### `evalCode<T = any>(code: string): T | undefined`
261
+ ```js
262
+ const secret = { token: "..." };
263
+ const arena = new Arena(ctx, {
264
+ isMarshalable: true,
265
+ marshalByReference: target => target === secret,
266
+ });
249
267
 
250
- Evaluate JS code in the context 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.
268
+ arena.expose({
269
+ getSecret: () => secret,
270
+ useSecret: s => s.token, // the host receives the original `secret`
271
+ });
251
272
 
252
- #### `executePendingJobs(): number`
273
+ arena.evalCode(`useSecret(getSecret())`); // "..." (the VM never sees the contents)
274
+ ```
253
275
 
254
- Almost same as `ctx.runtime.executePendingJobs()`, but it converts and re-throws error objects when an error is thrown during evaluation.
276
+ #### `evalCode<T = any>(code: string): T`
277
+
278
+ Evaluate JS code in the VM and return the result as a host object. Errors thrown during evaluation are converted and re-thrown on the host.
279
+
280
+ #### `evalModule<T = any>(code: string, filename?: string): T | Promise<T>`
281
+
282
+ Evaluate ES module code and return the module's exports. Requires quickjs-emscripten >= 0.29.0. Returns a promise if the module uses top-level `await`.
255
283
 
256
284
  #### `expose(obj: { [k: string]: any })`
257
285
 
258
- Expose objects as global objects in the context.
286
+ Expose host objects as globals in the VM. Exposed objects are not synchronized by default; to sync one, wrap it with `sync` first and expose the wrapped object.
259
287
 
260
- By default, exposed objects are not synchronized between the host and the context.
261
- If you want to sync an objects, first wrap the object with `sync` method, and then expose the wrapped object.
288
+ ```js
289
+ arena.expose({ console: { log: console.log } });
290
+ arena.evalCode(`console.log("hello, world")`);
291
+ ```
262
292
 
263
293
  #### `sync<T>(target: T): T`
264
294
 
265
- Enables sync for the object between the host and the context and returns objects wrapped with proxies.
295
+ Enable synchronization for an object and return a proxy-wrapped version of it. **Use the returned value** — mutating the original object does not propagate changes. Conversely, `set` and `delete` on the wrapped object (from either side) are synchronized.
266
296
 
267
- The return value is necessary in order to reflect changes to the object from the host to the context. Please note that setting a value in the field or deleting a field in the original object will not synchronize it.
297
+ ```js
298
+ const data = arena.sync({ hoge: "foo" });
299
+ arena.expose({ data });
300
+
301
+ arena.evalCode(`data.hoge = "bar"`);
302
+ console.log(data.hoge); // "bar"
303
+
304
+ data.hoge = "changed!";
305
+ console.log(arena.evalCode(`data.hoge`)); // "changed!"
306
+ ```
268
307
 
269
308
  #### `register(target: any, code: string | QuickJSHandle)`
270
309
 
271
- Register a pair of objects that will be considered the same between the host and the QuickJS context.
310
+ Register a single host/VM object pair to be treated as identical.
272
311
 
273
- #### `unregisterAll(targets: Iterable<[any, string | QuickJSHandle]>)`
312
+ #### `registerAll(map: Iterable<[any, string | QuickJSHandle]>)`
274
313
 
275
- Execute `register` methods for each pair.
314
+ Call `register` for each pair.
276
315
 
277
- #### `unregister(target: any)`
316
+ #### `unregister(target: any, dispose?: boolean)`
278
317
 
279
- Unregister a pair of objects that were registered with `registeredObjects` option and `register` method.
318
+ Remove a pair registered via the `registeredObjects` option or `register`.
280
319
 
281
- #### `unregisterAll(targets: Iterable<any>)`
320
+ #### `unregisterAll(targets: Iterable<any>, dispose?: boolean)`
282
321
 
283
- Execute `unregister` methods for each target.
322
+ Call `unregister` for each target.
284
323
 
285
- ### `defaultRegisteredObjects: [any, string][]`
324
+ #### `dispose()`
286
325
 
287
- Default value of registeredObjects option of the Arena class constructor.
326
+ Dispose of the arena and the handles it manages. This does **not** dispose the context itself — dispose that manually, and always after the arena.
288
327
 
289
- ### `complexity(target: any, max?: number): number`
328
+ `Arena` also implements `Symbol.dispose`, so a `using` declaration disposes it automatically:
329
+
330
+ ```js
331
+ {
332
+ using arena = new Arena(ctx, { isMarshalable: true });
333
+ arena.evalCode(`1 + 1`);
334
+ } // arena.dispose() runs here
335
+ ctx.dispose();
336
+ ```
337
+
338
+ #### `executePendingJobs(maxJobsToExecute?: number): number`
339
+
340
+ Like `ctx.runtime.executePendingJobs()`, but converts and re-throws errors thrown during evaluation.
290
341
 
291
- Measure the complexity of an object as you traverse the field and prototype chain. If max is specified, when the complexity reaches max, the traversal is terminated and it returns the max. In this function, one object and function are counted as a complexity of 1, and primitives are not counted as a complexity.
342
+ #### Runtime limits and stats
292
343
 
293
- ## Advanced
344
+ These forward to the underlying runtime and are useful for sandboxing untrusted code:
294
345
 
295
- ### How to work
346
+ - `setMemoryLimit(limitBytes: number): void` — cap runtime memory (`-1` to remove the limit).
347
+ - `setMaxStackSize(stackSize: number): void` — cap stack size in bytes (`0` to remove the limit).
348
+ - `getMemoryUsage(): object` — detailed memory statistics.
349
+ - `dumpMemoryUsage(): string` — a human-readable memory report.
296
350
 
297
- quickjs-emscripten can execute JS code safely, but it requires to deal with a lot of handles and lifetimes. Also, when destroying the context, any un-destroyed handle will result in an error.
351
+ ### `AsyncArena`
298
352
 
299
- quickjs-emscripten-sync will automatically manage all handles once generated by QuickJS context in an Arena class.
300
- And it will automatically "marshal" objects as handles and "unmarshal" handles as objects to enable seamless data exchange between the browser and QuickJS. It recursively traverses the object properties and prototype chain to transform objects. A function is called after its arguments and this arg are automatically converted for the environment in which the function is defined. The return value will be automatically converted to match the environment of the caller.
301
- Most objects are wrapped by proxies during conversion, allowing "set" and "delete" operations on objects to be synchronized between the browser and QuickJS.
353
+ `AsyncArena` extends `Arena` for use with a [`QuickJSAsyncContext`](https://github.com/justjake/quickjs-emscripten). It adds `evalCodeAsync`, the async counterpart to `evalCode`, so code that relies on asynchronous module loading can be evaluated.
302
354
 
303
- ### Limitations
355
+ ```js
356
+ import { newAsyncContext } from "quickjs-emscripten";
357
+ import { AsyncArena } from "quickjs-emscripten-sync";
358
+
359
+ const ctx = await newAsyncContext();
360
+ const arena = new AsyncArena(ctx, { isMarshalable: true });
361
+
362
+ await arena.evalCodeAsync(`1 + 2`); // 3
363
+
364
+ arena.dispose();
365
+ ctx.dispose();
366
+ ```
367
+
368
+ #### `evalCodeAsync<T = any>(code: string, filename?: string): Promise<T>`
369
+
370
+ Evaluate JS code asynchronously and return the result on the host. Like `evalCode`, it converts and re-throws errors thrown during evaluation.
304
371
 
305
- #### Class constructor
372
+ ### `defaultRegisteredObjects: [any, string][]`
373
+
374
+ The default value of the `registeredObjects` option.
375
+
376
+ ### `complexity(target: any, max?: number): number`
377
+
378
+ Measure the complexity of an object by traversing its fields and prototype chain. Each object and function counts as 1; primitives are not counted. If `max` is given, traversal stops once the count reaches `max` and returns `max` — handy for cheaply detecting objects that are too heavy to marshal.
306
379
 
307
- When initializing a new instance, it is not possible to fully proxy this arg (a.k.a. `new.target`) inside the class constructor. Therefore, after the constructor call, the fields set for this are re-set to this on the context side. Therefore, there may be some edge cases where the constructor may not work properly.
380
+ ## Limitations
381
+
382
+ ### Class constructors
383
+
384
+ When a class is instantiated inside the VM, `this` (and `new.target`) cannot be fully proxied during the constructor call. quickjs-emscripten-sync runs the host constructor and then copies the resulting fields onto the VM-side `this`, so constructors that rely on the live `this` during construction may behave unexpectedly in edge cases.
308
385
 
309
386
  ```js
310
387
  class Cls {
@@ -317,9 +394,13 @@ arena.expose({ Cls });
317
394
  arena.evalCode(`new Cls()`); // Cls { hoge: "foo" }
318
395
  ```
319
396
 
320
- #### Operation synchronization
397
+ ### Operation synchronization
398
+
399
+ Only the `set`, `deleteProperty`, and `defineProperty` operations on objects are synchronized. Other operations (for example `Object.setPrototypeOf`) are not propagated to the other side.
400
+
401
+ ### Marshalling by value
321
402
 
322
- For now, only the `set` and `deleteProperty` operations on objects are subject to synchronization. The result of `Object.defineProperty` on a proxied object will not be synchronized to the other side.
403
+ `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.
323
404
 
324
405
  ## License
325
406