quickjs-emscripten-sync 1.10.0 → 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.
@@ -1,4 +1,5 @@
1
- import { getQuickJS, QuickJSHandle } from "quickjs-emscripten";
1
+ import type { QuickJSHandle } from "quickjs-emscripten";
2
+ import { getQuickJS } from "quickjs-emscripten";
2
3
  import { expect, test, vi } from "vitest";
3
4
 
4
5
  import { json } from "../vmutil";
@@ -8,8 +9,11 @@ import unmarshalFunction from "./function";
8
9
  test("arrow function", async () => {
9
10
  const ctx = (await getQuickJS()).newContext();
10
11
  const marshal = vi.fn((v): [QuickJSHandle, boolean] => [json(ctx, v), false]);
11
- const unmarshal = vi.fn((v: QuickJSHandle): [unknown, boolean] => [ctx.dump(v), false]);
12
- const preUnmarshal = vi.fn(a => a);
12
+ const unmarshal = vi.fn((v: QuickJSHandle): [unknown, boolean] => [
13
+ ctx.dump(v),
14
+ false,
15
+ ]);
16
+ const preUnmarshal = vi.fn((a) => a);
13
17
 
14
18
  const handle = ctx.unwrapResult(ctx.evalCode(`(a, b) => a + b`));
15
19
  const func = unmarshalFunction(ctx, handle, marshal, unmarshal, preUnmarshal);
@@ -49,9 +53,11 @@ test("function", async () => {
49
53
  if (ty === "object" || ty === "function") disposables.push(v);
50
54
  return [ctx.dump(v), false];
51
55
  });
52
- const preUnmarshal = vi.fn(a => a);
56
+ const preUnmarshal = vi.fn((a) => a);
53
57
 
54
- const handle = ctx.unwrapResult(ctx.evalCode(`(function (a) { return this.a + a; })`));
58
+ const handle = ctx.unwrapResult(
59
+ ctx.evalCode(`(function (a) { return this.a + a; })`),
60
+ );
55
61
 
56
62
  const func = unmarshalFunction(ctx, handle, marshal, unmarshal, preUnmarshal);
57
63
  if (!func) throw new Error("func is undefined");
@@ -71,7 +77,7 @@ test("function", async () => {
71
77
  expect(preUnmarshal).toBeCalledTimes(1);
72
78
  expect(preUnmarshal).toBeCalledWith(func, handle);
73
79
 
74
- disposables.forEach(d => d.dispose());
80
+ disposables.forEach((d) => d.dispose());
75
81
  thatHandle.dispose();
76
82
  handle.dispose();
77
83
  ctx.dispose();
@@ -89,11 +95,19 @@ test("constructor", async () => {
89
95
  if (ty === "object" || ty === "function") disposables.push(v);
90
96
  return [ctx.dump(v), false];
91
97
  });
92
- const preUnmarshal = vi.fn(a => a);
93
-
94
- const handle = ctx.unwrapResult(ctx.evalCode(`(function (b) { this.a = b + 2; })`));
95
-
96
- const Cls = unmarshalFunction(ctx, handle, marshal, unmarshal, preUnmarshal) as any;
98
+ const preUnmarshal = vi.fn((a) => a);
99
+
100
+ const handle = ctx.unwrapResult(
101
+ ctx.evalCode(`(function (b) { this.a = b + 2; })`),
102
+ );
103
+
104
+ const Cls = unmarshalFunction(
105
+ ctx,
106
+ handle,
107
+ marshal,
108
+ unmarshal,
109
+ preUnmarshal,
110
+ ) as any;
97
111
  if (!Cls) throw new Error("Cls is undefined");
98
112
 
99
113
  const instance = new Cls(100);
@@ -113,7 +127,7 @@ test("constructor", async () => {
113
127
  expect(preUnmarshal).toBeCalledTimes(1);
114
128
  expect(preUnmarshal).toBeCalledWith(Cls, handle);
115
129
 
116
- disposables.forEach(d => d.dispose());
130
+ disposables.forEach((d) => d.dispose());
117
131
  handle.dispose();
118
132
  ctx.dispose();
119
133
  });
@@ -130,11 +144,19 @@ test("class", async () => {
130
144
  if (ty === "object" || ty === "function") disposables.push(v);
131
145
  return [ctx.dump(v), false];
132
146
  });
133
- const preUnmarshal = vi.fn(a => a);
134
-
135
- const handle = ctx.unwrapResult(ctx.evalCode(`(class A { constructor(a) { this.a = a + 1; } })`));
136
-
137
- const Cls = unmarshalFunction(ctx, handle, marshal, unmarshal, preUnmarshal) as any;
147
+ const preUnmarshal = vi.fn((a) => a);
148
+
149
+ const handle = ctx.unwrapResult(
150
+ ctx.evalCode(`(class A { constructor(a) { this.a = a + 1; } })`),
151
+ );
152
+
153
+ const Cls = unmarshalFunction(
154
+ ctx,
155
+ handle,
156
+ marshal,
157
+ unmarshal,
158
+ preUnmarshal,
159
+ ) as any;
138
160
  if (!Cls) throw new Error("Cls is undefined");
139
161
 
140
162
  const instance = new Cls(2);
@@ -154,7 +176,7 @@ test("class", async () => {
154
176
  expect(preUnmarshal).toBeCalledTimes(1);
155
177
  expect(preUnmarshal).toBeCalledWith(Cls, handle);
156
178
 
157
- disposables.forEach(d => d.dispose());
179
+ disposables.forEach((d) => d.dispose());
158
180
  handle.dispose();
159
181
  ctx.dispose();
160
182
  });
@@ -167,8 +189,12 @@ test("undefined", async () => {
167
189
  expect(unmarshalFunction(ctx, ctx.true, f, f, f)).toEqual(undefined);
168
190
  expect(unmarshalFunction(ctx, ctx.false, f, f, f)).toEqual(undefined);
169
191
  expect(unmarshalFunction(ctx, ctx.null, f, f, f)).toEqual(undefined);
170
- expect(unmarshalFunction(ctx, ctx.newString("hoge"), f, f, f)).toEqual(undefined);
171
- expect(unmarshalFunction(ctx, ctx.newNumber(-10), f, f, f)).toEqual(undefined);
192
+ expect(unmarshalFunction(ctx, ctx.newString("hoge"), f, f, f)).toEqual(
193
+ undefined,
194
+ );
195
+ expect(unmarshalFunction(ctx, ctx.newNumber(-10), f, f, f)).toEqual(
196
+ undefined,
197
+ );
172
198
 
173
199
  const obj = ctx.newObject();
174
200
  expect(unmarshalFunction(ctx, obj, f, f, f)).toEqual(undefined);
@@ -1,4 +1,5 @@
1
- import { getQuickJS, QuickJSHandle } from "quickjs-emscripten";
1
+ import type { QuickJSHandle } from "quickjs-emscripten";
2
+ import { getQuickJS } from "quickjs-emscripten";
2
3
  import { expect, test, vi } from "vitest";
3
4
 
4
5
  import VMMap from "../vmmap";
@@ -29,13 +30,19 @@ test("primitive, array, object", async () => {
29
30
  });
30
31
  expect(map.size).toBe(5);
31
32
  expect(map.getByHandle(handle)).toBe(target);
32
- ctx.getProp(handle, "aaa").consume(h => expect(map.getByHandle(h)).toBe(target.aaa));
33
33
  ctx
34
34
  .getProp(handle, "aaa")
35
- .consume(h => ctx.getProp(h, 2))
36
- .consume(h => expect(map.getByHandle(h)).toBe(target.aaa[2]));
37
- ctx.getProp(handle, "nested").consume(h => expect(map.getByHandle(h)).toBe(target.nested));
38
- ctx.getProp(handle, "bbb").consume(h => expect(map.getByHandle(h)).toBe(target.bbb));
35
+ .consume((h) => expect(map.getByHandle(h)).toBe(target.aaa));
36
+ ctx
37
+ .getProp(handle, "aaa")
38
+ .consume((h) => ctx.getProp(h, 2))
39
+ .consume((h) => expect(map.getByHandle(h)).toBe(target.aaa[2]));
40
+ ctx
41
+ .getProp(handle, "nested")
42
+ .consume((h) => expect(map.getByHandle(h)).toBe(target.nested));
43
+ ctx
44
+ .getProp(handle, "bbb")
45
+ .consume((h) => expect(map.getByHandle(h)).toBe(target.bbb));
39
46
 
40
47
  expect(marshal).toBeCalledTimes(0);
41
48
  expect(target.bbb()).toBe("bar");
@@ -65,7 +72,9 @@ test("object with symbol key", async () => {
65
72
  test("function", async () => {
66
73
  const { ctx, unmarshal, marshal, map, dispose } = await setup();
67
74
 
68
- const handle = ctx.unwrapResult(ctx.evalCode(`(function(a) { return a.a + "!"; })`));
75
+ const handle = ctx.unwrapResult(
76
+ ctx.evalCode(`(function(a) { return a.a + "!"; })`),
77
+ );
69
78
  const func = unmarshal(handle);
70
79
  const arg = { a: "hoge" };
71
80
  expect(func(arg)).toBe("hoge!");
@@ -156,7 +165,7 @@ const setup = async () => {
156
165
  const handle2 =
157
166
  typeof target === "function"
158
167
  ? ctx.newFunction(target.name, (...handles) => {
159
- target(...handles.map(h => ctx.dump(h)));
168
+ target(...handles.map((h) => ctx.dump(h)));
160
169
  })
161
170
  : json(ctx, target);
162
171
  const ty = ctx.typeof(handle2);
@@ -169,7 +178,7 @@ const setup = async () => {
169
178
  map,
170
179
  unmarshal: (handle: QuickJSHandle) =>
171
180
  unmarshal(handle, {
172
- find: h => map.getByHandle(h),
181
+ find: (h) => map.getByHandle(h),
173
182
  marshal,
174
183
  pre: (t, h) => {
175
184
  map.set(t, h);
@@ -179,7 +188,7 @@ const setup = async () => {
179
188
  }),
180
189
  marshal,
181
190
  dispose: () => {
182
- disposables.forEach(d => d.dispose());
191
+ disposables.forEach((d) => d.dispose());
183
192
  map.dispose();
184
193
  ctx.dispose();
185
194
  },
@@ -21,18 +21,33 @@ export default function unmarshalMapSet(
21
21
  for (const elResult of iterator) {
22
22
  const el = ctx.unwrapResult(elResult);
23
23
  if (isMap) {
24
+ // `disposeKey`/`disposeValue` start true so a mid-flight throw (e.g. OOM
25
+ // inside `unmarshal`) disposes the property handles instead of orphaning
26
+ // them; on success they are disposed only when redundant (already owned).
24
27
  const keyHandle = ctx.getProp(el, 0);
25
28
  const valueHandle = ctx.getProp(el, 1);
26
- const [key, disposeKey] = unmarshal(keyHandle);
27
- const [value, disposeValue] = unmarshal(valueHandle);
28
- if (disposeKey) keyHandle.dispose();
29
- if (disposeValue) valueHandle.dispose();
30
- (result as Map<any, any>).set(key, value);
31
- el.dispose();
29
+ let disposeKey = true;
30
+ let disposeValue = true;
31
+ try {
32
+ const [key, dk] = unmarshal(keyHandle);
33
+ disposeKey = dk;
34
+ const [value, dv] = unmarshal(valueHandle);
35
+ disposeValue = dv;
36
+ (result as Map<any, any>).set(key, value);
37
+ } finally {
38
+ if (disposeKey && keyHandle.alive) keyHandle.dispose();
39
+ if (disposeValue && valueHandle.alive) valueHandle.dispose();
40
+ if (el.alive) el.dispose();
41
+ }
32
42
  } else {
33
- const [value, disposeValue] = unmarshal(el);
34
- if (disposeValue) el.dispose();
35
- (result as Set<any>).add(value);
43
+ let disposeValue = true;
44
+ try {
45
+ const [value, dv] = unmarshal(el);
46
+ disposeValue = dv;
47
+ (result as Set<any>).add(value);
48
+ } finally {
49
+ if (disposeValue && el.alive) el.dispose();
50
+ }
36
51
  }
37
52
  }
38
53
  } finally {
@@ -1,12 +1,16 @@
1
- import { getQuickJS, QuickJSHandle } from "quickjs-emscripten";
1
+ import type { QuickJSHandle } from "quickjs-emscripten";
2
+ import { getQuickJS } from "quickjs-emscripten";
2
3
  import { expect, test, vi } from "vitest";
3
4
 
4
5
  import unmarshalObject from "./object";
5
6
 
6
7
  test("normal object", async () => {
7
8
  const ctx = (await getQuickJS()).newContext();
8
- const unmarshal = vi.fn((v: QuickJSHandle): [unknown, boolean] => [ctx.dump(v), false]);
9
- const preUnmarshal = vi.fn(a => a);
9
+ const unmarshal = vi.fn((v: QuickJSHandle): [unknown, boolean] => [
10
+ ctx.dump(v),
11
+ false,
12
+ ]);
13
+ const preUnmarshal = vi.fn((a) => a);
10
14
 
11
15
  const handle = ctx.unwrapResult(ctx.evalCode(`({ a: 1, b: true })`));
12
16
  const obj = unmarshalObject(ctx, handle, unmarshal, preUnmarshal);
@@ -31,7 +35,7 @@ test("properties", async () => {
31
35
  disposables.push(v);
32
36
  return [ctx.typeof(v) === "function" ? () => {} : ctx.dump(v), false];
33
37
  });
34
- const preUnmarshal = vi.fn(a => a);
38
+ const preUnmarshal = vi.fn((a) => a);
35
39
 
36
40
  const handle = ctx.unwrapResult(
37
41
  ctx.evalCode(`{
@@ -69,15 +73,18 @@ test("properties", async () => {
69
73
  expect(preUnmarshal).toBeCalledTimes(1);
70
74
  expect(preUnmarshal).toBeCalledWith(obj, handle);
71
75
 
72
- disposables.forEach(d => d.dispose());
76
+ disposables.forEach((d) => d.dispose());
73
77
  handle.dispose();
74
78
  ctx.dispose();
75
79
  });
76
80
 
77
81
  test("array", async () => {
78
82
  const ctx = (await getQuickJS()).newContext();
79
- const unmarshal = vi.fn((v: QuickJSHandle): [unknown, boolean] => [ctx.dump(v), false]);
80
- const preUnmarshal = vi.fn(a => a);
83
+ const unmarshal = vi.fn((v: QuickJSHandle): [unknown, boolean] => [
84
+ ctx.dump(v),
85
+ false,
86
+ ]);
87
+ const preUnmarshal = vi.fn((a) => a);
81
88
 
82
89
  const handle = ctx.unwrapResult(ctx.evalCode(`[1, true, "a"]`));
83
90
  const array = unmarshalObject(ctx, handle, unmarshal, preUnmarshal);
@@ -103,9 +110,11 @@ test("prototype", async () => {
103
110
  ctx.typeof(v) === "object" ? { a: () => 1 } : ctx.dump(v),
104
111
  false,
105
112
  ]);
106
- const preUnmarshal = vi.fn(a => a);
113
+ const preUnmarshal = vi.fn((a) => a);
107
114
 
108
- const handle = ctx.unwrapResult(ctx.evalCode(`Object.create({ a: () => 1 })`));
115
+ const handle = ctx.unwrapResult(
116
+ ctx.evalCode(`Object.create({ a: () => 1 })`),
117
+ );
109
118
  const obj = unmarshalObject(ctx, handle, unmarshal, preUnmarshal) as any;
110
119
  if (!obj) throw new Error("obj is undefined");
111
120
  expect(Object.getPrototypeOf(obj)).toEqual({ a: expect.any(Function) });
@@ -1,4 +1,5 @@
1
- import { Disposable, getQuickJS, type QuickJSHandle } from "quickjs-emscripten";
1
+ import { getQuickJS } from "quickjs-emscripten";
2
+ import type { Disposable, QuickJSHandle } from "quickjs-emscripten";
2
3
  import { expect, test, vi } from "vitest";
3
4
 
4
5
  import unmarshalPromise from "./promise";
@@ -7,13 +8,13 @@ const testPromise = (reject: boolean) => async () => {
7
8
  const ctx = (await getQuickJS()).newContext();
8
9
  const disposables: Disposable[] = [];
9
10
  const marshal = vi.fn((v): [QuickJSHandle, boolean] => {
10
- const f = ctx.newFunction(v.name, h => {
11
+ const f = ctx.newFunction(v.name, (h) => {
11
12
  v(ctx.dump(h));
12
13
  });
13
14
  disposables.push(f);
14
15
  return [f, false];
15
16
  });
16
- const preUnmarshal = vi.fn(a => a);
17
+ const preUnmarshal = vi.fn((a) => a);
17
18
 
18
19
  const deferred = ctx.newPromise();
19
20
  disposables.push(deferred);
@@ -36,7 +37,7 @@ const testPromise = (reject: boolean) => async () => {
36
37
  await expect(promise).resolves.toBe("hoge");
37
38
  }
38
39
 
39
- disposables.forEach(d => d.dispose());
40
+ disposables.forEach((d) => d.dispose());
40
41
  ctx.dispose();
41
42
  };
42
43
 
@@ -1,7 +1,7 @@
1
1
  import type { QuickJSContext, QuickJSHandle } from "quickjs-emscripten";
2
2
 
3
3
  import { newDeferred } from "../util";
4
- import { call, instanceOf } from "../vmutil";
4
+ import { call, consume, instanceOf } from "../vmutil";
5
5
 
6
6
  export default function unmarshalPromise<T = unknown>(
7
7
  ctx: QuickJSContext,
@@ -24,7 +24,11 @@ export default function unmarshalPromise<T = unknown>(
24
24
 
25
25
  function isPromiseHandle(ctx: QuickJSContext, handle: QuickJSHandle): boolean {
26
26
  if (!handle.owner) return false;
27
- return ctx.unwrapResult(ctx.evalCode("Promise")).consume(promise => {
27
+ // `consume` disposes the Promise constructor handle even if `instanceOf`
28
+ // throws mid-flight (e.g. an interrupt or OOM lands in the `a instanceof b`
29
+ // VM call); the raw `Lifetime.consume` would skip disposal on throw, orphaning
30
+ // the constructor handle.
31
+ return consume(ctx.unwrapResult(ctx.evalCode("Promise")), promise => {
28
32
  if (!handle.owner) return false;
29
33
  return instanceOf(ctx, handle, promise);
30
34
  });
@@ -1,4 +1,5 @@
1
- import { getQuickJS, QuickJSHandle } from "quickjs-emscripten";
1
+ import type { QuickJSHandle } from "quickjs-emscripten";
2
+ import { getQuickJS } from "quickjs-emscripten";
2
3
  import { expect, test, vi } from "vitest";
3
4
 
4
5
  import unmarshalProperties from "./properties";
@@ -47,7 +48,7 @@ test("works", async () => {
47
48
  expect(unmarshal).toReturnWith(["c", false]);
48
49
  expect(unmarshal).toReturnWith([expect.any(Function), false]); // get, set
49
50
 
50
- disposables.forEach(d => d.dispose());
51
+ disposables.forEach((d) => d.dispose());
51
52
  handle.dispose();
52
53
  ctx.dispose();
53
54
  });
@@ -2,6 +2,46 @@ import type { QuickJSContext, QuickJSHandle } from "quickjs-emscripten";
2
2
 
3
3
  import { call, consume } from "../vmutil";
4
4
 
5
+ // The VM-side iterator computes a numeric bitmask per property so the host can
6
+ // rebuild the PropertyDescriptor with at most one `ctx.getProp` (for the single
7
+ // value/get/set entry that is actually present) plus one `ctx.getNumber`,
8
+ // instead of the previous six `getProp` + six `typeof` calls per property.
9
+ //
10
+ // Bit layout (a "present" bit for every field, plus a "value" bit for the three
11
+ // boolean fields whose value we cannot recover from a handle):
12
+ // bit 0 (1) value present (typeof d.value !== "undefined")
13
+ // bit 1 (2) get present (typeof d.get !== "undefined")
14
+ // bit 2 (4) set present (typeof d.set !== "undefined")
15
+ // bit 3 (8) configurable present (typeof d.configurable === "boolean")
16
+ // bit 4 (16) configurable value (d.configurable === true)
17
+ // bit 5 (32) enumerable present (typeof d.enumerable === "boolean")
18
+ // bit 6 (64) enumerable value (d.enumerable === true)
19
+ // bit 7 (128) writable present (typeof d.writable === "boolean")
20
+ // bit 8 (256) writable value (d.writable === true)
21
+ //
22
+ // This mirrors the previous logic exactly: value/get/set are copied only when
23
+ // defined (an explicit `value: undefined` is treated as absent, so defaults
24
+ // apply, just as `typeof === "undefined"` skipped it before); a boolean field
25
+ // is copied only when actually a boolean, and absent boolean fields stay absent
26
+ // so `Object.defineProperty` applies its defaults. Data vs accessor descriptors
27
+ // are distinguished naturally: data descriptors carry a `writable` present bit
28
+ // and no get/set, accessors carry get/set and no `writable`.
29
+ const flagsFn = `(o, fn) => {
30
+ const descs = Object.getOwnPropertyDescriptors(o);
31
+ const emit = (k, d) => {
32
+ let f = 0;
33
+ if (typeof d.value !== "undefined") f |= 1;
34
+ if (typeof d.get !== "undefined") f |= 2;
35
+ if (typeof d.set !== "undefined") f |= 4;
36
+ if (typeof d.configurable === "boolean") { f |= 8; if (d.configurable) f |= 16; }
37
+ if (typeof d.enumerable === "boolean") { f |= 32; if (d.enumerable) f |= 64; }
38
+ if (typeof d.writable === "boolean") { f |= 128; if (d.writable) f |= 256; }
39
+ fn(k, d, f);
40
+ };
41
+ Object.entries(descs).forEach(([k, v]) => emit(k, v));
42
+ Object.getOwnPropertySymbols(descs).forEach(k => emit(k, descs[k]));
43
+ }`;
44
+
5
45
  export default function unmarshalProperties(
6
46
  ctx: QuickJSContext,
7
47
  handle: QuickJSHandle,
@@ -9,53 +49,47 @@ export default function unmarshalProperties(
9
49
  unmarshal: (handle: QuickJSHandle) => [unknown, boolean],
10
50
  ) {
11
51
  consume(
12
- ctx.newFunction("", (key, value) => {
52
+ ctx.newFunction("", (key, descHandle, flagsHandle) => {
13
53
  const [keyName] = unmarshal(key);
14
54
  if (typeof keyName !== "string" && typeof keyName !== "number" && typeof keyName !== "symbol")
15
55
  return;
16
56
 
17
- const desc = (
18
- [
19
- ["value", true],
20
- ["get", true],
21
- ["set", true],
22
- ["configurable", false],
23
- ["enumerable", false],
24
- ["writable", false],
25
- ] as const
26
- ).reduce<PropertyDescriptor>((desc, [key, unmarshable]) => {
27
- const h = ctx.getProp(value, key);
28
- const ty = ctx.typeof(h);
29
-
30
- if (ty === "undefined") return desc;
31
- if (!unmarshable && ty === "boolean") {
32
- desc[key] = ctx.dump(ctx.getProp(value, key));
33
- return desc;
34
- }
57
+ const flags = ctx.getNumber(flagsHandle);
58
+ const desc: PropertyDescriptor = {};
35
59
 
36
- const [v, alreadyExists] = unmarshal(h);
37
- if (alreadyExists) {
38
- h.dispose();
60
+ // Obtain value/get/set as host-owned handles via `ctx.getProp` exactly as
61
+ // before: these must NOT come from the (scope-borrowed, auto-disposed)
62
+ // callback arguments, because `unmarshal` may retain the handle in the
63
+ // Arena's VMMap for identity tracking. Dispose only when `unmarshal`
64
+ // reports the value already existed (so the fresh handle is redundant);
65
+ // otherwise ownership was transferred to the map and we must keep it.
66
+ const readHandle = (fieldKey: string): unknown => {
67
+ const h = ctx.getProp(descHandle, fieldKey);
68
+ let retained = false;
69
+ try {
70
+ const [v, alreadyExists] = unmarshal(h);
71
+ // When the value already existed, ownership stays with the map and the
72
+ // fresh handle is redundant; otherwise `unmarshal` transferred it to the
73
+ // map and we must keep it. A mid-flight throw in `unmarshal` also lands
74
+ // here with `retained` false, so the orphaned handle is disposed.
75
+ retained = !alreadyExists;
76
+ return v;
77
+ } finally {
78
+ if (!retained && h.alive) h.dispose();
39
79
  }
40
- desc[key] = v;
80
+ };
41
81
 
42
- return desc;
43
- }, {});
82
+ if (flags & 1) desc.value = readHandle("value");
83
+ if (flags & 2) desc.get = readHandle("get") as any;
84
+ if (flags & 4) desc.set = readHandle("set") as any;
85
+ if (flags & 8) desc.configurable = !!(flags & 16);
86
+ if (flags & 32) desc.enumerable = !!(flags & 64);
87
+ if (flags & 128) desc.writable = !!(flags & 256);
44
88
 
45
89
  Object.defineProperty(target, keyName, desc);
46
90
  }),
47
91
  fn => {
48
- call(
49
- ctx,
50
- `(o, fn) => {
51
- const descs = Object.getOwnPropertyDescriptors(o);
52
- Object.entries(descs).forEach(([k, v]) => fn(k, v));
53
- Object.getOwnPropertySymbols(descs).forEach(k => fn(k, descs[k]));
54
- }`,
55
- undefined,
56
- handle,
57
- fn,
58
- ).dispose();
92
+ call(ctx, flagsFn, undefined, handle, fn).dispose();
59
93
  },
60
94
  );
61
95
  }
package/src/vmmap.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { QuickJSContext, QuickJSHandle } from "quickjs-emscripten";
2
2
 
3
- import { unwrapResult } from "./vmutil";
3
+ import { consume, unwrapResult } from "./vmutil";
4
4
 
5
5
  /**
6
6
  * Bidirectional map between host values and QuickJS handles.
@@ -79,7 +79,19 @@ export default class VMMap {
79
79
  return v === handle || v === handle2;
80
80
  }
81
81
 
82
- const id = this._nextId++;
82
+ const id = this._nextId;
83
+
84
+ // Register on the VM side FIRST, before mutating any host-side bookkeeping.
85
+ // If this throws (e.g. an OOM lands inside `_mapSet`), no host map has been
86
+ // touched and `handle`/`handle2` are NOT retained here, so the caller still
87
+ // owns them and can dispose them: `set` stays atomic (all-or-nothing).
88
+ // `consume` disposes the id number handle even when the call throws (the raw
89
+ // `Lifetime.consume` skips disposal on throw, leaking it).
90
+ consume(this.ctx.newNumber(id), c => {
91
+ this._call(this._mapSet, undefined, handle, c, handle2 ?? this.ctx.undefined);
92
+ });
93
+
94
+ this._nextId++;
83
95
  this._keyToId.set(key, id);
84
96
  this._idToHandle.set(id, handle);
85
97
  this._idToKey.set(id, key);
@@ -91,10 +103,6 @@ export default class VMMap {
91
103
  }
92
104
  }
93
105
 
94
- this.ctx.newNumber(id).consume(c => {
95
- this._call(this._mapSet, undefined, handle, c, handle2 ?? this.ctx.undefined);
96
- });
97
-
98
106
  return true;
99
107
  }
100
108
 
@@ -1,4 +1,5 @@
1
- import { getQuickJS, QuickJSHandle } from "quickjs-emscripten";
1
+ import type { QuickJSHandle } from "quickjs-emscripten";
2
+ import { getQuickJS } from "quickjs-emscripten";
2
3
  import { expect, test, vi } from "vitest";
3
4
 
4
5
  import {
@@ -1,15 +1,17 @@
1
- import { QuickJSHandle, getQuickJS } from "quickjs-emscripten";
1
+ import type { QuickJSHandle} from "quickjs-emscripten";
2
+ import { getQuickJS } from "quickjs-emscripten";
2
3
  import { expect, test, vi } from "vitest";
3
4
 
4
5
  import { call, json } from "./vmutil";
6
+ import type {
7
+ SyncMode} from "./wrapper";
5
8
  import {
6
9
  wrap,
7
10
  unwrap,
8
11
  isWrapped,
9
12
  wrapHandle,
10
13
  unwrapHandle,
11
- isHandleWrapped,
12
- SyncMode,
14
+ isHandleWrapped
13
15
  } from "./wrapper";
14
16
 
15
17
  test("wrap, unwrap, isWrapped", async () => {