quickjs-emscripten-sync 1.8.3 → 1.9.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/vmmap.ts CHANGED
@@ -1,18 +1,29 @@
1
1
  import type { QuickJSContext, QuickJSHandle } from "quickjs-emscripten";
2
2
 
3
+ import { unwrapResult } from "./vmutil";
4
+
5
+ /**
6
+ * Bidirectional map between host values and QuickJS handles.
7
+ *
8
+ * Each registered pair gets a numeric id. A value may be registered under two
9
+ * keys (e.g. a proxy-wrapped object and the underlying object), each with its
10
+ * own handle, so every lookup direction is backed by an explicit map and id
11
+ * reverse-lookups, keeping `delete` O(1).
12
+ */
3
13
  export default class VMMap {
4
14
  ctx: QuickJSContext;
5
- _map1 = new Map<any, number>();
6
- _map2 = new Map<any, number>();
7
- _map3 = new Map<number, QuickJSHandle>();
8
- _map4 = new Map<number, QuickJSHandle>();
9
- _counterMap = new Map<number, any>();
15
+ _keyToId = new Map<any, number>();
16
+ _key2ToId = new Map<any, number>();
17
+ _idToHandle = new Map<number, QuickJSHandle>();
18
+ _idToHandle2 = new Map<number, QuickJSHandle>();
19
+ _idToKey = new Map<number, any>();
20
+ _idToKey2 = new Map<number, any>();
10
21
  _disposables = new Set<QuickJSHandle>();
11
22
  _mapGet: QuickJSHandle;
12
23
  _mapSet: QuickJSHandle;
13
24
  _mapDelete: QuickJSHandle;
14
25
  _mapClear: QuickJSHandle;
15
- _counter = Number.MIN_SAFE_INTEGER;
26
+ _nextId = Number.MIN_SAFE_INTEGER;
16
27
 
17
28
  constructor(ctx: QuickJSContext) {
18
29
  this.ctx = ctx;
@@ -68,18 +79,19 @@ export default class VMMap {
68
79
  return v === handle || v === handle2;
69
80
  }
70
81
 
71
- const counter = this._counter++;
72
- this._map1.set(key, counter);
73
- this._map3.set(counter, handle);
74
- this._counterMap.set(counter, key);
82
+ const id = this._nextId++;
83
+ this._keyToId.set(key, id);
84
+ this._idToHandle.set(id, handle);
85
+ this._idToKey.set(id, key);
75
86
  if (key2) {
76
- this._map2.set(key2, counter);
87
+ this._key2ToId.set(key2, id);
88
+ this._idToKey2.set(id, key2);
77
89
  if (handle2) {
78
- this._map4.set(counter, handle2);
90
+ this._idToHandle2.set(id, handle2);
79
91
  }
80
92
  }
81
93
 
82
- this.ctx.newNumber(counter).consume(c => {
94
+ this.ctx.newNumber(id).consume(c => {
83
95
  this._call(this._mapSet, undefined, handle, c, handle2 ?? this.ctx.undefined);
84
96
  });
85
97
 
@@ -104,8 +116,8 @@ export default class VMMap {
104
116
  }
105
117
 
106
118
  get(key: any) {
107
- const num = this._map1.get(key) ?? this._map2.get(key);
108
- const handle = typeof num === "number" ? this._map3.get(num) : undefined;
119
+ const id = this._keyToId.get(key) ?? this._key2ToId.get(key);
120
+ const handle = typeof id === "number" ? this._idToHandle.get(id) : undefined;
109
121
 
110
122
  if (!handle) return;
111
123
  if (!handle.alive) {
@@ -120,7 +132,7 @@ export default class VMMap {
120
132
  if (!handle.alive) {
121
133
  return;
122
134
  }
123
- return this._counterMap.get(this.ctx.getNumber(this._call(this._mapGet, undefined, handle)));
135
+ return this._idToKey.get(this.ctx.getNumber(this._call(this._mapGet, undefined, handle)));
124
136
  }
125
137
 
126
138
  has(key: any) {
@@ -132,46 +144,29 @@ export default class VMMap {
132
144
  }
133
145
 
134
146
  keys() {
135
- return this._map1.keys();
147
+ return this._keyToId.keys();
136
148
  }
137
149
 
138
150
  delete(key: any, dispose?: boolean) {
139
- const num = this._map1.get(key) ?? this._map2.get(key);
140
- if (typeof num === "undefined") return;
151
+ const id = this._keyToId.get(key) ?? this._key2ToId.get(key);
152
+ if (typeof id === "undefined") return;
141
153
 
142
- const handle = this._map3.get(num);
143
- const handle2 = this._map4.get(num);
154
+ const handle = this._idToHandle.get(id);
155
+ const handle2 = this._idToHandle2.get(id);
144
156
  this._call(
145
157
  this._mapDelete,
146
158
  undefined,
147
159
  ...[handle, handle2].filter((h): h is QuickJSHandle => !!h?.alive),
148
160
  );
149
161
 
150
- this._map1.delete(key);
151
- this._map2.delete(key);
152
- this._map3.delete(num);
153
- this._map4.delete(num);
154
-
155
- for (const [k, v] of this._map1) {
156
- if (v === num) {
157
- this._map1.delete(k);
158
- break;
159
- }
160
- }
161
-
162
- for (const [k, v] of this._map2) {
163
- if (v === num) {
164
- this._map2.delete(k);
165
- break;
166
- }
167
- }
168
-
169
- for (const [k, v] of this._counterMap) {
170
- if (v === key) {
171
- this._counterMap.delete(k);
172
- break;
173
- }
174
- }
162
+ const key1 = this._idToKey.get(id);
163
+ const key2 = this._idToKey2.get(id);
164
+ if (typeof key1 !== "undefined") this._keyToId.delete(key1);
165
+ if (typeof key2 !== "undefined") this._key2ToId.delete(key2);
166
+ this._idToHandle.delete(id);
167
+ this._idToHandle2.delete(id);
168
+ this._idToKey.delete(id);
169
+ this._idToKey2.delete(id);
175
170
 
176
171
  if (dispose) {
177
172
  if (handle?.alive) handle.dispose();
@@ -187,12 +182,13 @@ export default class VMMap {
187
182
  }
188
183
 
189
184
  clear() {
190
- this._counter = 0;
191
- this._map1.clear();
192
- this._map2.clear();
193
- this._map3.clear();
194
- this._map4.clear();
195
- this._counterMap.clear();
185
+ this._nextId = 0;
186
+ this._keyToId.clear();
187
+ this._key2ToId.clear();
188
+ this._idToHandle.clear();
189
+ this._idToHandle2.clear();
190
+ this._idToKey.clear();
191
+ this._idToKey2.clear();
196
192
  if (this._mapClear.alive) {
197
193
  this._call(this._mapClear, undefined);
198
194
  }
@@ -204,12 +200,12 @@ export default class VMMap {
204
200
  v.dispose();
205
201
  }
206
202
  }
207
- for (const v of this._map3.values()) {
203
+ for (const v of this._idToHandle.values()) {
208
204
  if (v.alive) {
209
205
  v.dispose();
210
206
  }
211
207
  }
212
- for (const v of this._map4.values()) {
208
+ for (const v of this._idToHandle2.values()) {
213
209
  if (v.alive) {
214
210
  v.dispose();
215
211
  }
@@ -219,37 +215,31 @@ export default class VMMap {
219
215
  }
220
216
 
221
217
  get size() {
222
- return this._map1.size;
218
+ return this._keyToId.size;
223
219
  }
224
220
 
225
221
  [Symbol.iterator](): Iterator<[any, QuickJSHandle, any, QuickJSHandle | undefined]> {
226
- const keys = this._map1.keys();
222
+ const keys = this._keyToId.keys();
227
223
  return {
228
224
  next: () => {
229
-
230
225
  while (true) {
231
226
  const k1 = keys.next();
232
227
  if (k1.done) return { value: undefined, done: true };
233
- const n = this._map1.get(k1.value);
234
- if (typeof n === "undefined") continue;
235
- const v1 = this._map3.get(n);
236
- const v2 = this._map4.get(n);
228
+ const id = this._keyToId.get(k1.value);
229
+ if (typeof id === "undefined") continue;
230
+ const v1 = this._idToHandle.get(id);
231
+ const v2 = this._idToHandle2.get(id);
237
232
  if (!v1) continue;
238
- const k2 = this._get2(n);
233
+ const k2 = this._idToKey2.get(id);
239
234
  return { value: [k1.value, v1, k2, v2], done: false };
240
235
  }
241
236
  },
242
237
  };
243
238
  }
244
239
 
245
- _get2(num: number) {
246
- for (const [k, v] of this._map2) {
247
- if (v === num) return k;
248
- }
249
- }
250
-
251
240
  _call(fn: QuickJSHandle, thisArg: QuickJSHandle | undefined, ...args: QuickJSHandle[]) {
252
- return this.ctx.unwrapResult(
241
+ return unwrapResult(
242
+ this.ctx,
253
243
  this.ctx.callFunction(
254
244
  fn,
255
245
  typeof thisArg === "undefined" ? this.ctx.undefined : thisArg,
package/src/vmutil.ts CHANGED
@@ -3,38 +3,108 @@ import type {
3
3
  QuickJSContext,
4
4
  QuickJSHandle,
5
5
  QuickJSDeferredPromise,
6
+ SuccessOrFail,
6
7
  } from "quickjs-emscripten";
7
8
 
9
+ /**
10
+ * Unwrap a VM result, disposing the error handle on failure.
11
+ *
12
+ * `ctx.unwrapResult` throws the error handle without disposing it, which leaks
13
+ * the handle. Under memory pressure the leak cascades: reading the error needs
14
+ * the VM, which is already exhausted, so cleanup is skipped entirely. Here we
15
+ * read the error host-side, always dispose the handle, then throw a host Error.
16
+ */
17
+ export function unwrapResult<T>(ctx: QuickJSContext, result: SuccessOrFail<T, QuickJSHandle>): T {
18
+ if ("error" in result && result.error) {
19
+ const { error } = result;
20
+ let dumped: any;
21
+ try {
22
+ dumped = ctx.dump(error);
23
+ } catch {
24
+ // VM may be unable to read the error (e.g. out of memory); fall back below.
25
+ } finally {
26
+ if (error.alive) error.dispose();
27
+ }
28
+ const err = new Error(
29
+ typeof dumped === "object" && dumped && "message" in dumped
30
+ ? String(dumped.message)
31
+ : dumped !== undefined
32
+ ? String(dumped)
33
+ : "quickjs-emscripten-sync: VM evaluation failed",
34
+ );
35
+ if (typeof dumped === "object" && dumped) {
36
+ if ("name" in dumped) err.name = String(dumped.name);
37
+ if ("stack" in dumped) err.stack = String(dumped.stack);
38
+ }
39
+ throw err;
40
+ }
41
+ return result.value;
42
+ }
43
+
8
44
  export function fn(
9
45
  ctx: QuickJSContext,
10
46
  code: string,
11
47
  ): ((thisArg: QuickJSHandle | undefined, ...args: QuickJSHandle[]) => QuickJSHandle) & Disposable {
12
- const handle = ctx.unwrapResult(ctx.evalCode(code));
48
+ const handle = unwrapResult(ctx, ctx.evalCode(code));
13
49
  const f: any = (thisArg: QuickJSHandle | undefined, ...args: QuickJSHandle[]): any => {
14
- return ctx.unwrapResult(ctx.callFunction(handle, thisArg ?? ctx.undefined, ...args));
50
+ return unwrapResult(ctx, ctx.callFunction(handle, thisArg ?? ctx.undefined, ...args));
15
51
  };
16
52
  const disposeFn = () => handle.dispose();
17
53
  f.dispose = disposeFn;
18
54
  f[Symbol.dispose] = disposeFn;
19
- f.alive = true;
20
55
  Object.defineProperty(f, "alive", {
21
56
  get: () => handle.alive,
22
57
  });
23
58
  return f;
24
59
  }
25
60
 
61
+ // Compiled functions for `call` can be cached per context, keyed by code: the
62
+ // code passed to `call` is always a constant literal, so recompiling the same
63
+ // helper on every call (isHandleObject, defineProperties, etc.) was a dominant
64
+ // cost. Caching is opt-in per context via `enableFnCache` because cached
65
+ // handles outlive a single call and must be disposed with `disposeFnCache`;
66
+ // the Arena enables it in its constructor and disposes it in `dispose`. For
67
+ // contexts without a cache, `call` keeps its original compile-and-dispose
68
+ // behaviour so standalone callers don't leak handles.
69
+ const fnCache = new WeakMap<QuickJSContext, Map<string, QuickJSHandle>>();
70
+
71
+ /** Enable per-context caching of compiled functions used by `call`. */
72
+ export function enableFnCache(ctx: QuickJSContext): void {
73
+ if (!fnCache.has(ctx)) fnCache.set(ctx, new Map());
74
+ }
75
+
76
+ /** Dispose all compiled functions cached for a context and disable caching. */
77
+ export function disposeFnCache(ctx: QuickJSContext): void {
78
+ const cache = fnCache.get(ctx);
79
+ if (!cache) return;
80
+ for (const handle of cache.values()) {
81
+ if (handle.alive) handle.dispose();
82
+ }
83
+ fnCache.delete(ctx);
84
+ }
85
+
26
86
  export function call(
27
87
  ctx: QuickJSContext,
28
88
  code: string,
29
89
  thisArg?: QuickJSHandle,
30
90
  ...args: QuickJSHandle[]
31
91
  ): QuickJSHandle {
32
- const f = fn(ctx, code);
33
- try {
34
- return f(thisArg, ...args);
35
- } finally {
36
- f.dispose();
92
+ const cache = fnCache.get(ctx);
93
+ if (!cache) {
94
+ const f = fn(ctx, code);
95
+ try {
96
+ return f(thisArg, ...args);
97
+ } finally {
98
+ f.dispose();
99
+ }
100
+ }
101
+
102
+ let handle = cache.get(code);
103
+ if (!handle || !handle.alive) {
104
+ handle = unwrapResult(ctx, ctx.evalCode(code));
105
+ cache.set(code, handle);
37
106
  }
107
+ return unwrapResult(ctx, ctx.callFunction(handle, thisArg ?? ctx.undefined, ...args));
38
108
  }
39
109
 
40
110
  export function instanceOf(ctx: QuickJSContext, a: QuickJSHandle, b: QuickJSHandle): boolean {
@@ -42,9 +112,8 @@ export function instanceOf(ctx: QuickJSContext, a: QuickJSHandle, b: QuickJSHand
42
112
  }
43
113
 
44
114
  export function isHandleObject(ctx: QuickJSContext, h: QuickJSHandle): boolean {
45
- return ctx.dump(
46
- call(ctx, `a => typeof a === "object" && a !== null || typeof a === "function"`, undefined, h),
47
- );
115
+ const type = ctx.typeof(h);
116
+ return type === "function" || (type === "object" && !ctx.sameValue(h, ctx.null));
48
117
  }
49
118
 
50
119
  export function json(ctx: QuickJSContext, target: any): QuickJSHandle {
@@ -53,6 +122,20 @@ export function json(ctx: QuickJSContext, target: any): QuickJSHandle {
53
122
  return call(ctx, `JSON.parse`, undefined, ctx.newString(json));
54
123
  }
55
124
 
125
+ /**
126
+ * Run `cb` with `handle`, then dispose `handle` even if `cb` throws.
127
+ *
128
+ * Unlike `handle.consume`, which skips disposal when its callback throws, this
129
+ * helper disposes in a `finally` so error paths don't leak the handle.
130
+ */
131
+ export function consume<T extends QuickJSHandle, K>(handle: T, cb: (handle: T) => K): K {
132
+ try {
133
+ return cb(handle);
134
+ } finally {
135
+ if (handle.alive) handle.dispose();
136
+ }
137
+ }
138
+
56
139
  export function consumeAll<T extends QuickJSHandle[], K>(handles: T, cb: (handles: T) => K): K {
57
140
  try {
58
141
  return cb(handles);
package/src/wrapper.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { QuickJSHandle, QuickJSContext } from "quickjs-emscripten";
2
2
 
3
3
  import { isObject } from "./util";
4
- import { call, isHandleObject, mayConsumeAll } from "./vmutil";
4
+ import { call, consume, isHandleObject, mayConsumeAll } from "./vmutil";
5
5
 
6
6
  export type SyncMode = "both" | "vm" | "host";
7
7
 
@@ -15,18 +15,28 @@ export function wrap<T = any>(
15
15
  marshal: (target: any) => [QuickJSHandle, boolean],
16
16
  syncMode?: (target: T) => SyncMode | undefined,
17
17
  wrappable?: (target: unknown) => boolean,
18
+ syncEnabled = true,
18
19
  ): Wrapped<T> | undefined {
19
- // promise and date cannot be wrapped
20
+ // These built-ins rely on internal slots or non-property access, so a proxy
21
+ // would break them; they are marshalled by value instead of being wrapped.
20
22
  if (
21
23
  !isObject(target) ||
22
24
  target instanceof Promise ||
23
25
  target instanceof Date ||
26
+ target instanceof ArrayBuffer ||
27
+ ArrayBuffer.isView(target) ||
28
+ target instanceof Map ||
29
+ target instanceof Set ||
24
30
  (wrappable && !wrappable(target))
25
31
  )
26
32
  return undefined;
27
33
 
28
34
  if (isWrapped(target, proxyKeySymbol)) return target;
29
35
 
36
+ // Sync globally disabled: skip the proxy, but still treat the object as
37
+ // "wrapped" so the rest of the pipeline handles it uniformly.
38
+ if (!syncEnabled) return target as Wrapped<T>;
39
+
30
40
  const rec = new Proxy(target as any, {
31
41
  get(obj, key) {
32
42
  return key === proxyKeySymbol ? obj : Reflect.get(obj, key);
@@ -34,7 +44,9 @@ export function wrap<T = any>(
34
44
  set(obj, key, value, receiver) {
35
45
  const v = unwrap(value, proxyKeySymbol);
36
46
  const sync = syncMode?.(receiver) ?? "host";
37
- if ((sync !== "vm" && !Reflect.set(obj, key, v, receiver)) || sync === "host" || !ctx.alive)
47
+ // Set on the target directly (not via `receiver`) so creating a new
48
+ // property does not re-enter the `defineProperty` trap.
49
+ if ((sync !== "vm" && !Reflect.set(obj, key, v)) || sync === "host" || !ctx.alive)
38
50
  return true;
39
51
 
40
52
  mayConsumeAll(
@@ -68,6 +80,35 @@ export function wrap<T = any>(
68
80
  return true;
69
81
  });
70
82
  },
83
+ defineProperty(obj, key, descriptor) {
84
+ const sync = syncMode?.(rec) ?? "host";
85
+ const desc: PropertyDescriptor = { ...descriptor };
86
+ if ("value" in desc) desc.value = unwrap(desc.value, proxyKeySymbol);
87
+ if (typeof desc.get === "function") desc.get = unwrap(desc.get, proxyKeySymbol);
88
+ if (typeof desc.set === "function") desc.set = unwrap(desc.set, proxyKeySymbol);
89
+
90
+ if (sync !== "vm" && !Reflect.defineProperty(obj, key, desc)) return false;
91
+ if (sync === "host" || !ctx.alive) return true;
92
+
93
+ mayConsumeAll(
94
+ [marshal(rec), marshal(key), marshal(desc)],
95
+ (recHandle, keyHandle, descHandle) => {
96
+ const [handle2, unwrapped] = unwrapHandle(ctx, recHandle, proxyKeySymbolHandle);
97
+ const define = (h: QuickJSHandle) =>
98
+ call(
99
+ ctx,
100
+ `(o, k, d) => { Object.defineProperty(o, k, d); }`,
101
+ undefined,
102
+ h,
103
+ keyHandle,
104
+ descHandle,
105
+ ).dispose();
106
+ if (unwrapped) handle2.consume(define);
107
+ else define(handle2);
108
+ },
109
+ );
110
+ return true;
111
+ },
71
112
  }) as Wrapped<T>;
72
113
  return rec;
73
114
  }
@@ -80,12 +121,16 @@ export function wrapHandle(
80
121
  unmarshal: (handle: QuickJSHandle) => any,
81
122
  syncMode?: (target: QuickJSHandle) => SyncMode | undefined,
82
123
  wrappable?: (target: QuickJSHandle, ctx: QuickJSContext) => boolean,
124
+ syncEnabled = true,
83
125
  ): [Wrapped<QuickJSHandle> | undefined, boolean] {
84
126
  if (!isHandleObject(ctx, handle) || (wrappable && !wrappable(handle, ctx)))
85
127
  return [undefined, false];
86
128
 
87
129
  if (isHandleWrapped(ctx, handle, proxyKeySymbolHandle)) return [handle, false];
88
130
 
131
+ // Sync globally disabled: skip the VM-side proxy.
132
+ if (!syncEnabled) return [handle as Wrapped<QuickJSHandle>, false];
133
+
89
134
  const getSyncMode = (h: QuickJSHandle) => {
90
135
  const res = syncMode?.(unmarshal(h));
91
136
  if (typeof res === "string") return ctx.newString(res);
@@ -108,23 +153,35 @@ export function wrapHandle(
108
153
  Reflect.deleteProperty(unwrap(target, proxyKeySymbol), key);
109
154
  };
110
155
 
111
- return ctx
112
- .newFunction("proxyFuncs", (t, ...args) => {
113
- const name = ctx.getNumber(t);
114
- switch (name) {
115
- case 1:
116
- return getSyncMode(args[0]);
117
- case 2:
118
- return setter(args[0], args[1], args[2]);
119
- case 3:
120
- return deleter(args[0], args[1]);
121
- }
122
- return ctx.undefined;
123
- })
124
- .consume(proxyFuncs => [
125
- call(
126
- ctx,
127
- `(target, sym, proxyFuncs) => {
156
+ const definer = (h: QuickJSHandle, keyHandle: QuickJSHandle, descHandle: QuickJSHandle) => {
157
+ const target = unmarshal(h);
158
+ if (!target) return;
159
+ const key = unmarshal(keyHandle);
160
+ if (key === "__proto__") return; // for security
161
+ const desc = unmarshal(descHandle);
162
+ Object.defineProperty(unwrap(target, proxyKeySymbol), key, desc);
163
+ };
164
+
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(
183
+ ctx,
184
+ `(target, sym, proxyFuncs) => {
128
185
  const rec = new Proxy(target, {
129
186
  get(obj, key, receiver) {
130
187
  return key === sym ? obj : Reflect.get(obj, key, receiver)
@@ -134,7 +191,7 @@ export function wrapHandle(
134
191
  ? value[sym] ?? value
135
192
  : value;
136
193
  const sync = proxyFuncs(1, receiver) ?? "vm";
137
- if (sync === "host" || Reflect.set(obj, key, v, receiver)) {
194
+ if (sync === "host" || Reflect.set(obj, key, v)) {
138
195
  if (sync !== "vm") {
139
196
  proxyFuncs(2, receiver, key, v);
140
197
  }
@@ -150,16 +207,25 @@ export function wrapHandle(
150
207
  }
151
208
  return true;
152
209
  },
210
+ defineProperty(obj, key, descriptor) {
211
+ const sync = proxyFuncs(1, rec) ?? "vm";
212
+ if (sync === "host" || Reflect.defineProperty(obj, key, descriptor)) {
213
+ if (sync !== "vm") {
214
+ proxyFuncs(4, rec, key, descriptor);
215
+ }
216
+ }
217
+ return true;
218
+ },
153
219
  });
154
220
  return rec;
155
221
  }`,
156
- undefined,
157
- handle,
158
- proxyKeySymbolHandle,
159
- proxyFuncs,
160
- ) as Wrapped<QuickJSHandle>,
161
- true,
162
- ]);
222
+ undefined,
223
+ handle,
224
+ proxyKeySymbolHandle,
225
+ proxyFuncs,
226
+ ) as Wrapped<QuickJSHandle>,
227
+ true,
228
+ ]);
163
229
  }
164
230
 
165
231
  export function unwrap<T>(obj: T, key: string | symbol): T {
@@ -187,8 +253,9 @@ export function isHandleWrapped(
187
253
  return !!ctx.dump(
188
254
  call(
189
255
  ctx,
190
- // promise and date cannot be wrapped
191
- `(a, s) => (a instanceof Promise) || (a instanceof Date) || (typeof a === "object" && a !== null || typeof a === "function") && !!a[s]`,
256
+ // Built-ins that must not be wrapped (internal slots / non-property access)
257
+ // report as "wrapped" so wrapHandle leaves them alone.
258
+ `(a, s) => (a instanceof Promise) || (a instanceof Date) || (a instanceof ArrayBuffer) || (ArrayBuffer.isView(a)) || (a instanceof Map) || (a instanceof Set) || (typeof a === "object" && a !== null || typeof a === "function") && !!a[s]`,
192
259
  undefined,
193
260
  handle,
194
261
  key,