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 +54 -0
- package/dist/index.d.ts +99 -3
- package/dist/quickjs-emscripten-sync.js +742 -559
- package/dist/quickjs-emscripten-sync.umd.cjs +33 -14
- package/package.json +7 -7
- package/src/contextex.ts +19 -7
- package/src/edge.test.ts +44 -2
- package/src/faultinjection.test.ts +265 -0
- package/src/identity.test.ts +57 -0
- package/src/index.test.ts +302 -0
- package/src/index.ts +215 -41
- package/src/marshal/custom.ts +20 -11
- package/src/marshal/function.test.ts +42 -18
- package/src/marshal/function.ts +106 -40
- package/src/marshal/index.ts +25 -1
- package/src/marshal/mapset.ts +30 -16
- package/src/marshal/object.ts +23 -14
- package/src/marshal/promise.test.ts +13 -11
- package/src/marshal/promise.ts +14 -6
- package/src/marshal/properties.test.ts +14 -5
- package/src/marshal/properties.ts +60 -12
- package/src/remarshalleak.test.ts +50 -0
- package/src/unmarshal/custom.test.ts +6 -3
- package/src/unmarshal/function.test.ts +46 -20
- package/src/unmarshal/index.test.ts +19 -10
- package/src/unmarshal/mapset.ts +24 -9
- package/src/unmarshal/object.test.ts +18 -9
- package/src/unmarshal/promise.test.ts +5 -4
- package/src/unmarshal/promise.ts +6 -2
- package/src/unmarshal/properties.test.ts +3 -2
- package/src/unmarshal/properties.ts +70 -36
- package/src/vmmap.ts +19 -7
- package/src/vmutil.test.ts +2 -1
- package/src/wrapper.test.ts +5 -3
- package/src/wrapper.ts +144 -37
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
):
|
|
126
|
-
|
|
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
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
226
|
-
)
|
|
227
|
-
|
|
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
|
-
|
|
241
|
-
return [
|
|
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> {
|