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/src/wrapper.ts CHANGED
@@ -54,7 +54,8 @@ export function wrap<T = any>(
54
54
  (receiverHandle, keyHandle, valueHandle) => {
55
55
  const [handle2, unwrapped] = unwrapHandle(ctx, receiverHandle, proxyKeySymbolHandle);
56
56
  if (unwrapped) {
57
- handle2.consume(h => ctx.setProp(h, keyHandle, valueHandle));
57
+ // `consume` disposes the unwrapped handle even if `setProp` throws.
58
+ consume(handle2, h => ctx.setProp(h, keyHandle, valueHandle));
58
59
  } else {
59
60
  ctx.setProp(handle2, keyHandle, valueHandle);
60
61
  }
@@ -72,7 +73,8 @@ export function wrap<T = any>(
72
73
  if (sync === "host" || !ctx.alive) return true;
73
74
 
74
75
  if (unwrapped) {
75
- handle2.consume(h => call(ctx, `(a, b) => delete a[b]`, undefined, h, keyHandle));
76
+ // `consume` disposes the unwrapped handle even if the call throws.
77
+ consume(handle2, h => call(ctx, `(a, b) => delete a[b]`, undefined, h, keyHandle));
76
78
  } else {
77
79
  call(ctx, `(a, b) => delete a[b]`, undefined, handle2, keyHandle);
78
80
  }
@@ -103,7 +105,8 @@ export function wrap<T = any>(
103
105
  keyHandle,
104
106
  descHandle,
105
107
  ).dispose();
106
- if (unwrapped) handle2.consume(define);
108
+ // `consume` disposes the unwrapped handle even if `define` throws.
109
+ if (unwrapped) consume(handle2, define);
107
110
  else define(handle2);
108
111
  },
109
112
  );
@@ -113,23 +116,34 @@ export function wrap<T = any>(
113
116
  return rec;
114
117
  }
115
118
 
116
- export function wrapHandle(
119
+ export type WrapHandle = {
120
+ /** Wrap a VM handle with the shared proxyFuncs, like the old `wrapHandle`. */
121
+ wrapHandle: (handle: QuickJSHandle) => [Wrapped<QuickJSHandle> | undefined, boolean];
122
+ /** Dispose the shared proxyFuncs handle. Call exactly once, at Arena dispose. */
123
+ dispose: () => void;
124
+ };
125
+
126
+ /**
127
+ * Build a `wrapHandle` function that shares ONE `proxyFuncs` handle across every
128
+ * handle it wraps, instead of allocating a fresh VM function per wrapped object.
129
+ *
130
+ * The callbacks inside proxyFuncs (getSyncMode/setter/deleter/definer) receive
131
+ * the target handle as an argument, so the closure only captures values stable
132
+ * for the whole Arena lifetime (`ctx`, `unmarshal`, `syncMode`, `proxyKeySymbol`).
133
+ * The proxyFuncs handle is created lazily on first use and must be disposed once
134
+ * via `dispose()`; the VM-side Proxy keeps its own reference to it, so it stays
135
+ * valid for every proxy created during the Arena's lifetime.
136
+ */
137
+ export function createWrapHandle(
117
138
  ctx: QuickJSContext,
118
- handle: QuickJSHandle,
119
139
  proxyKeySymbol: symbol,
120
140
  proxyKeySymbolHandle: QuickJSHandle,
121
141
  unmarshal: (handle: QuickJSHandle) => any,
122
142
  syncMode?: (target: QuickJSHandle) => SyncMode | undefined,
123
143
  wrappable?: (target: QuickJSHandle, ctx: QuickJSContext) => boolean,
124
144
  syncEnabled = true,
125
- ): [Wrapped<QuickJSHandle> | undefined, boolean] {
126
- if (!isHandleObject(ctx, handle) || (wrappable && !wrappable(handle, ctx)))
127
- return [undefined, false];
128
-
129
- if (isHandleWrapped(ctx, handle, proxyKeySymbolHandle)) return [handle, false];
130
-
131
- // Sync globally disabled: skip the VM-side proxy.
132
- if (!syncEnabled) return [handle as Wrapped<QuickJSHandle>, false];
145
+ ): WrapHandle {
146
+ let proxyFuncs: QuickJSHandle | undefined;
133
147
 
134
148
  const getSyncMode = (h: QuickJSHandle) => {
135
149
  const res = syncMode?.(unmarshal(h));
@@ -162,26 +176,57 @@ export function wrapHandle(
162
176
  Object.defineProperty(unwrap(target, proxyKeySymbol), key, desc);
163
177
  };
164
178
 
165
- const proxyFuncs = ctx.newFunction("proxyFuncs", (t, ...args) => {
166
- const name = ctx.getNumber(t);
167
- switch (name) {
168
- case 1:
169
- return getSyncMode(args[0]);
170
- case 2:
171
- return setter(args[0], args[1], args[2]);
172
- case 3:
173
- return deleter(args[0], args[1]);
174
- case 4:
175
- return definer(args[0], args[1], args[2]);
176
- }
177
- return ctx.undefined;
178
- });
179
- // Use the exception-safe consume so proxyFuncs is disposed even if compiling
180
- // the proxy below throws (e.g. under memory pressure).
181
- return consume(proxyFuncs, proxyFuncs => [
182
- call(
179
+ const ensureProxyFuncs = (): QuickJSHandle => {
180
+ if (proxyFuncs && proxyFuncs.alive) return proxyFuncs;
181
+ proxyFuncs = ctx.newFunction("proxyFuncs", (t, ...args) => {
182
+ const name = ctx.getNumber(t);
183
+ switch (name) {
184
+ case 1:
185
+ return getSyncMode(args[0]);
186
+ case 2:
187
+ return setter(args[0], args[1], args[2]);
188
+ case 3:
189
+ return deleter(args[0], args[1]);
190
+ case 4:
191
+ return definer(args[0], args[1], args[2]);
192
+ }
193
+ return ctx.undefined;
194
+ });
195
+ return proxyFuncs;
196
+ };
197
+
198
+ const wrapHandleFn = (
199
+ handle: QuickJSHandle,
200
+ ): [Wrapped<QuickJSHandle> | undefined, boolean] => {
201
+ if (!isHandleObject(ctx, handle) || (wrappable && !wrappable(handle, ctx)))
202
+ return [undefined, false];
203
+
204
+ // Sync globally disabled: skip the VM-side proxy. (When wrapped or built-in
205
+ // the old path also returned `[handle, false]`, so this order is equivalent
206
+ // and avoids the extra check entirely.)
207
+ if (!syncEnabled) return [handle as Wrapped<QuickJSHandle>, false];
208
+
209
+ // The "already wrapped / must-not-wrap built-in" test is folded into the
210
+ // proxy-creating VM call: it returns `null` when it declines (built-in with
211
+ // internal slots, or `target[sym]` already set), so we pay ONE roundtrip
212
+ // instead of a separate `isHandleWrapped` call plus the wrap. A declined
213
+ // handle is treated as already-wrapped by returning the ORIGINAL handle.
214
+ //
215
+ // The shared proxyFuncs is borrowed (not consumed) by `call`, and the
216
+ // VM-side Proxy keeps its own reference, so it stays alive for the whole
217
+ // Arena lifetime and is disposed once via `dispose()` even if `call` throws.
218
+ const result = call(
183
219
  ctx,
184
220
  `(target, sym, proxyFuncs) => {
221
+ if (
222
+ (target instanceof Promise) ||
223
+ (target instanceof Date) ||
224
+ (target instanceof ArrayBuffer) ||
225
+ (ArrayBuffer.isView(target)) ||
226
+ (target instanceof Map) ||
227
+ (target instanceof Set) ||
228
+ target[sym]
229
+ ) return null;
185
230
  const rec = new Proxy(target, {
186
231
  get(obj, key, receiver) {
187
232
  return key === sym ? obj : Reflect.get(obj, key, receiver)
@@ -222,10 +267,58 @@ export function wrapHandle(
222
267
  undefined,
223
268
  handle,
224
269
  proxyKeySymbolHandle,
225
- proxyFuncs,
226
- ) as Wrapped<QuickJSHandle>,
227
- true,
228
- ]);
270
+ ensureProxyFuncs(),
271
+ );
272
+
273
+ // Declined: the VM function returned null. Detect it with cheap host-side
274
+ // checks (no callFunction), dispose the null result, and treat the original
275
+ // handle as already-wrapped.
276
+ if (ctx.sameValue(result, ctx.null)) {
277
+ result.dispose();
278
+ return [handle as Wrapped<QuickJSHandle>, false];
279
+ }
280
+
281
+ return [result as Wrapped<QuickJSHandle>, true];
282
+ };
283
+
284
+ return {
285
+ wrapHandle: wrapHandleFn,
286
+ dispose: () => {
287
+ if (proxyFuncs && proxyFuncs.alive) proxyFuncs.dispose();
288
+ proxyFuncs = undefined;
289
+ },
290
+ };
291
+ }
292
+
293
+ /**
294
+ * Standalone wrap of a single handle. Builds a one-shot {@link createWrapHandle}
295
+ * and disposes its proxyFuncs right after; the VM-side Proxy keeps its own
296
+ * reference, so the wrapped handle stays valid.
297
+ */
298
+ export function wrapHandle(
299
+ ctx: QuickJSContext,
300
+ handle: QuickJSHandle,
301
+ proxyKeySymbol: symbol,
302
+ proxyKeySymbolHandle: QuickJSHandle,
303
+ unmarshal: (handle: QuickJSHandle) => any,
304
+ syncMode?: (target: QuickJSHandle) => SyncMode | undefined,
305
+ wrappable?: (target: QuickJSHandle, ctx: QuickJSContext) => boolean,
306
+ syncEnabled = true,
307
+ ): [Wrapped<QuickJSHandle> | undefined, boolean] {
308
+ const w = createWrapHandle(
309
+ ctx,
310
+ proxyKeySymbol,
311
+ proxyKeySymbolHandle,
312
+ unmarshal,
313
+ syncMode,
314
+ wrappable,
315
+ syncEnabled,
316
+ );
317
+ try {
318
+ return w.wrapHandle(handle);
319
+ } finally {
320
+ w.dispose();
321
+ }
229
322
  }
230
323
 
231
324
  export function unwrap<T>(obj: T, key: string | symbol): T {
@@ -237,8 +330,22 @@ export function unwrapHandle(
237
330
  handle: QuickJSHandle,
238
331
  key: QuickJSHandle,
239
332
  ): [QuickJSHandle, boolean] {
240
- if (!isHandleWrapped(ctx, handle, key)) return [handle, false];
241
- return [ctx.getProp(handle, key), true];
333
+ // Fold the `isHandleWrapped` test and the property read into ONE VM call:
334
+ // return the unwrapped target (`a[sym]`) or null. This pays a single roundtrip
335
+ // instead of `isHandleWrapped` + `getProp`. A null result means "not wrapped",
336
+ // so the original handle is used as-is.
337
+ const result = call(
338
+ ctx,
339
+ `(a, s) => (typeof a === "object" && a !== null || typeof a === "function") && a[s] || null`,
340
+ undefined,
341
+ handle,
342
+ key,
343
+ );
344
+ if (ctx.sameValue(result, ctx.null)) {
345
+ result.dispose();
346
+ return [handle, false];
347
+ }
348
+ return [result, true];
242
349
  }
243
350
 
244
351
  export function isWrapped<T>(obj: T, key: string | symbol): obj is Wrapped<T> {