quickjs-emscripten-sync 1.0.0 → 1.3.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.
Files changed (75) hide show
  1. package/LICENSE +2 -2
  2. package/README.md +303 -0
  3. package/dist/index.d.ts +129 -29
  4. package/dist/quickjs-emscripten-sync.es.js +951 -0
  5. package/dist/quickjs-emscripten-sync.umd.js +64 -0
  6. package/package.json +28 -40
  7. package/src/default.ts +6 -2
  8. package/src/index.test.ts +266 -51
  9. package/src/index.ts +123 -66
  10. package/src/marshal/function.test.ts +11 -10
  11. package/src/marshal/function.ts +3 -3
  12. package/src/marshal/index.test.ts +75 -10
  13. package/src/marshal/index.ts +28 -14
  14. package/src/marshal/json.test.ts +94 -0
  15. package/src/marshal/json.ts +15 -0
  16. package/src/marshal/object.test.ts +49 -6
  17. package/src/marshal/object.ts +2 -2
  18. package/src/marshal/primitive.test.ts +2 -0
  19. package/src/marshal/primitive.ts +8 -2
  20. package/src/marshal/promise.test.ts +86 -0
  21. package/src/marshal/promise.ts +26 -0
  22. package/src/marshal/properties.test.ts +7 -5
  23. package/src/marshal/properties.ts +2 -2
  24. package/src/marshal/symbol.test.ts +3 -1
  25. package/src/unmarshal/function.test.ts +28 -21
  26. package/src/unmarshal/function.ts +31 -23
  27. package/src/unmarshal/index.test.ts +86 -86
  28. package/src/unmarshal/index.ts +5 -4
  29. package/src/unmarshal/object.test.ts +36 -8
  30. package/src/unmarshal/object.ts +8 -4
  31. package/src/unmarshal/primitive.test.ts +2 -0
  32. package/src/unmarshal/primitive.ts +1 -2
  33. package/src/unmarshal/promise.test.ts +44 -0
  34. package/src/unmarshal/promise.ts +40 -0
  35. package/src/unmarshal/properties.test.ts +4 -2
  36. package/src/unmarshal/properties.ts +11 -9
  37. package/src/unmarshal/symbol.test.ts +3 -1
  38. package/src/util.test.ts +22 -5
  39. package/src/util.ts +15 -0
  40. package/src/vmmap.test.ts +22 -0
  41. package/src/vmmap.ts +6 -2
  42. package/src/vmutil.test.ts +116 -8
  43. package/src/vmutil.ts +76 -8
  44. package/src/wrapper.test.ts +44 -31
  45. package/src/wrapper.ts +47 -30
  46. package/dist/default.d.ts +0 -4
  47. package/dist/index.js +0 -8
  48. package/dist/marshal/array.d.ts +0 -2
  49. package/dist/marshal/function.d.ts +0 -2
  50. package/dist/marshal/index.d.ts +0 -11
  51. package/dist/marshal/object.d.ts +0 -2
  52. package/dist/marshal/primitive.d.ts +0 -2
  53. package/dist/marshal/properties.d.ts +0 -2
  54. package/dist/marshal/symbol.d.ts +0 -2
  55. package/dist/quickjs-emscripten-sync.cjs.development.js +0 -1302
  56. package/dist/quickjs-emscripten-sync.cjs.development.js.map +0 -1
  57. package/dist/quickjs-emscripten-sync.cjs.production.min.js +0 -2
  58. package/dist/quickjs-emscripten-sync.cjs.production.min.js.map +0 -1
  59. package/dist/quickjs-emscripten-sync.esm.js +0 -1285
  60. package/dist/quickjs-emscripten-sync.esm.js.map +0 -1
  61. package/dist/unmarshal/array.d.ts +0 -2
  62. package/dist/unmarshal/function.d.ts +0 -2
  63. package/dist/unmarshal/index.d.ts +0 -9
  64. package/dist/unmarshal/object.d.ts +0 -2
  65. package/dist/unmarshal/primitive.d.ts +0 -2
  66. package/dist/unmarshal/properties.d.ts +0 -2
  67. package/dist/unmarshal/symbol.d.ts +0 -2
  68. package/dist/util.d.ts +0 -7
  69. package/dist/vmmap.d.ts +0 -35
  70. package/dist/vmutil.d.ts +0 -7
  71. package/dist/wrapper.d.ts +0 -11
  72. package/src/marshal/array.test.ts +0 -40
  73. package/src/marshal/array.ts +0 -24
  74. package/src/unmarshal/array.test.ts +0 -45
  75. package/src/unmarshal/array.ts +0 -32
package/src/index.ts CHANGED
@@ -1,5 +1,9 @@
1
- import { QuickJSHandle, QuickJSVm } from "quickjs-emscripten";
2
- import {
1
+ import type {
2
+ QuickJSDeferredPromise,
3
+ QuickJSHandle,
4
+ QuickJSVm,
5
+ } from "quickjs-emscripten";
6
+ import type {
3
7
  SuccessOrFail,
4
8
  VmCallResult,
5
9
  } from "quickjs-emscripten/dist/vm-interface";
@@ -9,11 +13,18 @@ import marshal from "./marshal";
9
13
  import unmarshal from "./unmarshal";
10
14
  import { wrap, wrapHandle, unwrap, unwrapHandle, Wrapped } from "./wrapper";
11
15
  import { complexity, isES2015Class, isObject, walkObject } from "./util";
12
- import { call, eq, isHandleObject, send, consumeAll } from "./vmutil";
16
+ import {
17
+ call,
18
+ eq,
19
+ isHandleObject,
20
+ json,
21
+ consumeAll,
22
+ mayConsume,
23
+ handleFrom,
24
+ } from "./vmutil";
13
25
  import { defaultRegisteredObjects } from "./default";
14
26
 
15
27
  export {
16
- Arena,
17
28
  VMMap,
18
29
  defaultRegisteredObjects,
19
30
  marshal,
@@ -25,14 +36,14 @@ export {
25
36
  call,
26
37
  eq,
27
38
  isHandleObject,
28
- send,
39
+ json,
29
40
  consumeAll,
30
41
  };
31
42
 
32
43
  export type Options = {
33
44
  /** 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. */
34
- isMarshalable?: (target: any) => boolean;
35
- /** You can pre-register a pair of objects that will be considered the same between the browser and the QuickJS VM. This will be used automatically during the conversion. By default, it will be registered automatically with `defaultRegisteredObjects`.
45
+ isMarshalable?: boolean | "json" | ((target: any) => boolean | "json");
46
+ /** You can pre-register a pair 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`.
36
47
  *
37
48
  * 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.
38
49
  */
@@ -40,9 +51,9 @@ export type Options = {
40
51
  };
41
52
 
42
53
  /**
43
- * The Arena class manages all generated handles at once by quickjs-emscripten and automatically converts objects between browser and QuickJS.
54
+ * The Arena class manages all generated handles at once by quickjs-emscripten and automatically converts objects between the host and the QuickJS VM.
44
55
  */
45
- export default class Arena {
56
+ export class Arena {
46
57
  vm: QuickJSVm;
47
58
  _map: VMMap;
48
59
  _registeredMap: VMMap;
@@ -73,10 +84,10 @@ export default class Arena {
73
84
  }
74
85
 
75
86
  /**
76
- * Evaluate JS code in the VM and get the result as an object on the browser side. It also converts and re-throws error objects when an error is thrown during evaluation.
87
+ * Evaluate JS code in the VM 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.
77
88
  */
78
89
  evalCode<T = any>(code: string): T {
79
- let handle = this.vm.evalCode(code);
90
+ const handle = this.vm.evalCode(code);
80
91
  return this._unwrapResultAndUnmarshal(handle);
81
92
  }
82
93
 
@@ -88,31 +99,39 @@ export default class Arena {
88
99
  if ("value" in result) {
89
100
  return result.value;
90
101
  }
91
- throw result.error.consume(err => this._unmarshal(err));
102
+ throw this._unwrapIfNotSynced(result.error.consume(this._unmarshal));
92
103
  }
93
104
 
94
105
  /**
95
106
  * Expose objects as global objects in the VM.
96
107
  *
97
- * If sync is true, this function returns objects wrapped with proxies. This is necessary in order to reflect changes to the object from the browser side to the VM side. Please note that setting a value in the field or deleting a field in the original object will not synchronize it.
108
+ * By default, exposed objects are not synchronized between the host and the VM.
109
+ * If you want to sync an objects, first wrap the object with sync method, and then expose the wrapped object.
110
+ */
111
+ expose(obj: { [k: string]: any }) {
112
+ for (const [key, value] of Object.entries(obj)) {
113
+ mayConsume(this._marshal(value), (handle) => {
114
+ this.vm.setProp(this.vm.global, key, handle);
115
+ });
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Enables sync for the object between the host and the VM and returns objects wrapped with proxies.
121
+ *
122
+ * The return value is necessary in order to reflect changes to the object from the host to the VM. Please note that setting a value in the field or deleting a field in the original object will not synchronize it.
98
123
  */
99
- expose<T extends { [k: string]: any }>(obj: T, sync?: boolean): T {
100
- const newobject = Object.entries(obj).map(([key, value]) => {
101
- const value2 = sync ? this._wrap(value) : value;
102
- const handle = this._marshal(value2);
103
- if (sync) {
104
- walkObject(value2, v => {
105
- this._sync.add(this._unwrap(v));
106
- });
107
- }
108
- this.vm.setProp(this.vm.global, key, handle);
109
- return [key, value2] as const;
124
+ sync<T>(target: T): T {
125
+ const wrapped = this._wrap(target);
126
+ if (typeof wrapped === "undefined") return target;
127
+ walkObject(wrapped, (v) => {
128
+ this._sync.add(this._unwrap(v));
110
129
  });
111
- return Object.fromEntries(newobject) as T;
130
+ return wrapped;
112
131
  }
113
132
 
114
133
  /**
115
- * Register a pair of objects that will be considered the same between the browser and the QuickJS VM.
134
+ * Register a pair of objects that will be considered the same between the host and the QuickJS VM.
116
135
  *
117
136
  * Instead of a string, you can also pass a QuickJSHandle directly. In that case, however, when you have to dispose them manually when destroying the VM.
118
137
  */
@@ -122,6 +141,7 @@ export default class Arena {
122
141
  typeof handleOrCode === "string"
123
142
  ? this._unwrapResult(this.vm.evalCode(handleOrCode))
124
143
  : handleOrCode;
144
+ if (eq(this.vm, handle, this.vm.undefined)) return;
125
145
  if (typeof handleOrCode === "string") {
126
146
  this._registeredMapDispose.add(target);
127
147
  }
@@ -170,52 +190,85 @@ export default class Arena {
170
190
  if ("value" in result) {
171
191
  return result.value;
172
192
  }
173
- throw result.error.consume(err => this._unmarshal(err));
193
+ throw this._unwrapIfNotSynced(result.error.consume(this._unmarshal));
174
194
  }
175
195
 
176
196
  _unwrapResultAndUnmarshal(
177
197
  result: VmCallResult<QuickJSHandle> | undefined
178
198
  ): any {
179
199
  if (!result) return;
180
- return this._unwrapResult(result).consume(h => this._unmarshal(h));
200
+ return this._unwrapIfNotSynced(
201
+ this._unwrapResult(result).consume(this._unmarshal)
202
+ );
181
203
  }
182
204
 
183
- _marshal(target: any): QuickJSHandle {
205
+ _isMarshalable = (t: unknown): boolean | "json" => {
206
+ const im = this._options?.isMarshalable;
207
+ return (typeof im === "function" ? im(this._unwrap(t)) : im) ?? "json";
208
+ };
209
+
210
+ _marshalFind = (t: unknown) => {
211
+ const unwrappedT = this._unwrap(t);
212
+ const handle =
213
+ this._registeredMap.get(t) ??
214
+ (unwrappedT !== t ? this._registeredMap.get(unwrappedT) : undefined) ??
215
+ this._map.get(t) ??
216
+ (unwrappedT !== t ? this._map.get(unwrappedT) : undefined);
217
+ return handle;
218
+ };
219
+
220
+ _marshalPre = (
221
+ t: unknown,
222
+ h: QuickJSHandle | QuickJSDeferredPromise,
223
+ mode: true | "json" | undefined
224
+ ): Wrapped<QuickJSHandle> | undefined => {
225
+ if (mode === "json") return;
226
+ return this._register(t, handleFrom(h), this._map)?.[1];
227
+ };
228
+
229
+ _marshalPreApply = (
230
+ target: Function,
231
+ that: unknown,
232
+ args: unknown[]
233
+ ): void => {
234
+ const unwrapped = isObject(that) ? this._unwrap(that) : undefined;
235
+ // override sync mode of this object while calling the function
236
+ if (unwrapped) this._temporalSync.add(unwrapped);
237
+ try {
238
+ return target.apply(that, args);
239
+ } finally {
240
+ // restore sync mode
241
+ if (unwrapped) this._temporalSync.delete(unwrapped);
242
+ }
243
+ };
244
+
245
+ _marshal = (target: any): [QuickJSHandle, boolean] => {
184
246
  const registered = this._registeredMap.get(target);
185
247
  if (registered) {
186
- return registered;
248
+ return [registered, false];
187
249
  }
188
250
 
189
- const map = new VMMap(this.vm);
190
- map.merge(this._map);
191
-
192
251
  const handle = marshal(this._wrap(target) ?? target, {
193
252
  vm: this.vm,
194
- unmarshal: h => this._unmarshal(h),
195
- isMarshalable: t =>
196
- this._options?.isMarshalable?.(this._unwrap(t)) ?? true,
197
- find: t => this._registeredMap.get(t) ?? map.get(t),
198
- pre: (t, h) => this._register(t, h, map)?.[1],
199
- preApply: (target, that, args) => {
200
- const unwrapped = isObject(that) ? this._unwrap(that) : undefined;
201
- // override sync mode of this object while calling the function
202
- if (unwrapped) this._temporalSync.add(unwrapped);
203
- try {
204
- return target.apply(that, args);
205
- } finally {
206
- // restore sync mode
207
- if (unwrapped) this._temporalSync.delete(unwrapped);
208
- }
209
- },
253
+ unmarshal: this._unmarshal,
254
+ isMarshalable: this._isMarshalable,
255
+ find: this._marshalFind,
256
+ pre: this._marshalPre,
257
+ preApply: this._marshalPreApply,
210
258
  });
211
259
 
212
- this._map.merge(map);
213
- map.clear();
214
- map.dispose();
215
- return handle;
216
- }
260
+ return [handle, !this._map.hasHandle(handle)];
261
+ };
262
+
263
+ _preUnmarshal = (t: any, h: QuickJSHandle): Wrapped<any> => {
264
+ return this._register(t, h, undefined, true)?.[0];
265
+ };
266
+
267
+ _unmarshalFind = (h: QuickJSHandle): unknown => {
268
+ return this._registeredMap.getByHandle(h) ?? this._map.getByHandle(h);
269
+ };
217
270
 
218
- _unmarshal(handle: QuickJSHandle): any {
271
+ _unmarshal = (handle: QuickJSHandle): any => {
219
272
  const registered = this._registeredMap.getByHandle(handle);
220
273
  if (typeof registered !== "undefined") {
221
274
  return registered;
@@ -224,12 +277,11 @@ export default class Arena {
224
277
  const [wrappedHandle] = this._wrapHandle(handle);
225
278
  return unmarshal(wrappedHandle ?? handle, {
226
279
  vm: this.vm,
227
- marshal: (v: any) => this._marshal(v),
228
- find: h => this._registeredMap.getByHandle(h) ?? this._map.getByHandle(h),
229
- pre: (t: any, h: QuickJSHandle) =>
230
- this._register(t, h, undefined, true)?.[0],
280
+ marshal: this._marshal,
281
+ find: this._unmarshalFind,
282
+ pre: this._preUnmarshal,
231
283
  });
232
- }
284
+ };
233
285
 
234
286
  _register(
235
287
  t: any,
@@ -260,12 +312,12 @@ export default class Arena {
260
312
  return [wrappedT, wrappedH];
261
313
  }
262
314
 
263
- _syncMode(obj: any) {
315
+ _syncMode = (obj: any): "both" | undefined => {
264
316
  const obj2 = this._unwrap(obj);
265
317
  return this._sync.has(obj2) || this._temporalSync.has(obj2)
266
318
  ? "both"
267
319
  : undefined;
268
- }
320
+ };
269
321
 
270
322
  _wrap<T>(target: T): Wrapped<T> | undefined {
271
323
  return wrap(
@@ -273,8 +325,8 @@ export default class Arena {
273
325
  target,
274
326
  this._symbol,
275
327
  this._symbolHandle,
276
- t => this._marshal(t),
277
- t => this._syncMode(t)
328
+ this._marshal,
329
+ this._syncMode
278
330
  );
279
331
  }
280
332
 
@@ -282,6 +334,11 @@ export default class Arena {
282
334
  return unwrap(target, this._symbol);
283
335
  }
284
336
 
337
+ _unwrapIfNotSynced = <T>(target: T): T => {
338
+ const unwrapped = this._unwrap(target);
339
+ return this._sync.has(unwrapped) ? target : unwrapped;
340
+ };
341
+
285
342
  _wrapHandle(
286
343
  handle: QuickJSHandle
287
344
  ): [Wrapped<QuickJSHandle> | undefined, boolean] {
@@ -290,8 +347,8 @@ export default class Arena {
290
347
  handle,
291
348
  this._symbol,
292
349
  this._symbolHandle,
293
- h => this._unmarshal(h),
294
- t => this._syncMode(t)
350
+ this._unmarshal,
351
+ this._syncMode
295
352
  );
296
353
  }
297
354
 
@@ -1,16 +1,17 @@
1
1
  import { getQuickJS, QuickJSHandle } from "quickjs-emscripten";
2
- import { send, eq, call } from "../vmutil";
2
+ import { json, eq, call } from "../vmutil";
3
3
  import marshalFunction from "./function";
4
+ import { expect, test, vi } from "vitest";
4
5
 
5
6
  test("normal func", async () => {
6
7
  const vm = (await getQuickJS()).createVm();
7
8
 
8
- const marshal = jest.fn(v => send(vm, v));
9
- const unmarshal = jest.fn(v =>
9
+ const marshal = vi.fn((v) => json(vm, v));
10
+ const unmarshal = vi.fn((v) =>
10
11
  eq(vm, v, vm.global) ? undefined : vm.dump(v)
11
12
  );
12
- const preMarshal = jest.fn((_, a) => a);
13
- const innerfn = jest.fn((..._args: any[]) => "hoge");
13
+ const preMarshal = vi.fn((_, a) => a);
14
+ const innerfn = vi.fn((..._args: any[]) => "hoge");
14
15
  const fn = (...args: any[]) => innerfn(...args);
15
16
 
16
17
  const handle = marshalFunction(vm, fn, marshal, unmarshal, preMarshal);
@@ -40,7 +41,7 @@ test("normal func", async () => {
40
41
 
41
42
  test("func which has properties", async () => {
42
43
  const vm = (await getQuickJS()).createVm();
43
- const marshal = jest.fn(v => send(vm, v));
44
+ const marshal = vi.fn((v) => json(vm, v));
44
45
 
45
46
  const fn = () => {};
46
47
  fn.hoge = "foo";
@@ -49,7 +50,7 @@ test("func which has properties", async () => {
49
50
  vm,
50
51
  fn,
51
52
  marshal,
52
- v => vm.dump(v),
53
+ (v) => vm.dump(v),
53
54
  (_, a) => a
54
55
  );
55
56
  if (!handle) throw new Error("handle is undefined");
@@ -101,7 +102,7 @@ test("class", async () => {
101
102
  ).toBe(true);
102
103
  expect(vm.dump(vm.getProp(instance, "a"))).toBe(100);
103
104
 
104
- disposables.forEach(d => d.dispose());
105
+ disposables.forEach((d) => d.dispose());
105
106
  instance.dispose();
106
107
  newA.dispose();
107
108
  handle.dispose();
@@ -118,7 +119,7 @@ test("preApply", async () => {
118
119
  };
119
120
  const unmarshal = (v: QuickJSHandle) =>
120
121
  vm.typeof(v) === "object" ? that : vm.dump(v);
121
- const preApply = jest.fn(
122
+ const preApply = vi.fn(
122
123
  (a: Function, b: any, c: any[]) => a.apply(b, c) + "!"
123
124
  );
124
125
  const that = {};
@@ -152,7 +153,7 @@ test("preApply", async () => {
152
153
 
153
154
  test("undefined", async () => {
154
155
  const vm = (await getQuickJS()).createVm();
155
- const f = jest.fn();
156
+ const f = vi.fn();
156
157
 
157
158
  expect(marshalFunction(vm, undefined, f, f, f)).toBe(undefined);
158
159
  expect(marshalFunction(vm, null, f, f, f)).toBe(undefined);
@@ -17,9 +17,9 @@ export default function marshalFunction(
17
17
  if (typeof target !== "function") return;
18
18
 
19
19
  const raw = vm
20
- .newFunction(target.name, function(...argHandles) {
20
+ .newFunction(target.name, function (...argHandles) {
21
21
  const that = unmarshal(this);
22
- const args = argHandles.map(a => unmarshal(a));
22
+ const args = argHandles.map((a) => unmarshal(a));
23
23
 
24
24
  if (isES2015Class(target) && isObject(that)) {
25
25
  // Class constructors cannot be invoked without new expression, and new.target is not changed
@@ -34,7 +34,7 @@ export default function marshalFunction(
34
34
  preApply ? preApply(target, that, args) : target.apply(that, args)
35
35
  );
36
36
  })
37
- .consume(handle2 =>
37
+ .consume((handle2) =>
38
38
  // fucntions created by vm.newFunction are not callable as a class constrcutor
39
39
  call(
40
40
  vm,
@@ -1,7 +1,10 @@
1
1
  import { getQuickJS } from "quickjs-emscripten";
2
+ import { expect, test, vi } from "vitest";
3
+
2
4
  import VMMap from "../vmmap";
3
- import { instanceOf, call } from "../vmutil";
5
+ import { instanceOf, call, handleFrom, fn } from "../vmutil";
4
6
  import marshal from ".";
7
+ import { newDeferred } from "../util";
5
8
 
6
9
  test("primitive, array, object", async () => {
7
10
  const { vm, map, marshal, dispose } = await setup();
@@ -67,7 +70,7 @@ test("arrow function", async () => {
67
70
  test("function", async () => {
68
71
  const { vm, map, marshal, dispose } = await setup();
69
72
 
70
- const bar = function(a: number, b: { hoge: number }) {
73
+ const bar = function (a: number, b: { hoge: number }) {
71
74
  return a + b.hoge;
72
75
  };
73
76
  const handle = marshal(bar);
@@ -90,6 +93,42 @@ test("function", async () => {
90
93
  dispose();
91
94
  });
92
95
 
96
+ test("promise", async () => {
97
+ const { vm, marshal, dispose } = await setup();
98
+ const register = fn(
99
+ vm,
100
+ `promise => { promise.then(d => notify("resolve", d), d => notify("reject", d)); }`
101
+ );
102
+
103
+ let notified: any;
104
+ vm.newFunction("notify", (...handles) => {
105
+ notified = handles.map((h) => vm.dump(h));
106
+ }).consume((h) => {
107
+ vm.setProp(vm.global, "notify", h);
108
+ });
109
+
110
+ const deferred = newDeferred();
111
+ const handle = marshal(deferred.promise);
112
+ register(undefined, handle);
113
+
114
+ deferred.resolve("foo");
115
+ await deferred.promise;
116
+ expect(vm.unwrapResult(vm.executePendingJobs())).toBe(1);
117
+ expect(notified).toEqual(["resolve", "foo"]);
118
+
119
+ const deferred2 = newDeferred();
120
+ const handle2 = marshal(deferred2.promise);
121
+ register(undefined, handle2);
122
+
123
+ deferred2.reject("bar");
124
+ await expect(deferred2.promise).rejects.toBe("bar");
125
+ expect(vm.unwrapResult(vm.executePendingJobs())).toBe(1);
126
+ expect(notified).toEqual(["reject", "bar"]);
127
+
128
+ register.dispose();
129
+ dispose();
130
+ });
131
+
93
132
  test("class", async () => {
94
133
  const { vm, map, marshal, dispose } = await setup();
95
134
 
@@ -117,6 +156,7 @@ test("class", async () => {
117
156
  }
118
157
  }
119
158
 
159
+ expect(A.name).toBe("_A"); // class A's name is _A in vitest
120
160
  const handle = marshal(A);
121
161
  if (!map) throw new Error("map is undefined");
122
162
 
@@ -126,15 +166,15 @@ test("class", async () => {
126
166
  expect(map.has(A.a)).toBe(true);
127
167
  expect(map.has(A.prototype.hoge)).toBe(true);
128
168
  expect(
129
- map.has(Object.getOwnPropertyDescriptor(A.prototype, "foo")!.get)
169
+ map.has(Object.getOwnPropertyDescriptor(A.prototype, "foo")?.get)
130
170
  ).toBe(true);
131
171
  expect(
132
- map.has(Object.getOwnPropertyDescriptor(A.prototype, "foo")!.set)
172
+ map.has(Object.getOwnPropertyDescriptor(A.prototype, "foo")?.set)
133
173
  ).toBe(true);
134
174
 
135
175
  expect(vm.typeof(handle)).toBe("function");
136
176
  expect(vm.dump(vm.getProp(handle, "length"))).toBe(1);
137
- expect(vm.dump(vm.getProp(handle, "name"))).toBe("A");
177
+ expect(vm.dump(vm.getProp(handle, "name"))).toBe("_A");
138
178
  const staticA = vm.getProp(handle, "a");
139
179
  expect(instanceOf(vm, staticA, handle)).toBe(true);
140
180
  expect(vm.dump(vm.getProp(staticA, "a"))).toBe(100);
@@ -172,7 +212,7 @@ test("class", async () => {
172
212
  });
173
213
 
174
214
  test("marshalable", async () => {
175
- const isMarshalable = jest.fn((a: any) => a !== globalThis);
215
+ const isMarshalable = vi.fn((a: any) => a !== globalThis);
176
216
  const { vm, marshal, dispose } = await setup({
177
217
  isMarshalable,
178
218
  });
@@ -186,10 +226,34 @@ test("marshalable", async () => {
186
226
  dispose();
187
227
  });
188
228
 
229
+ test("marshalable json", async () => {
230
+ const isMarshalable = vi.fn(() => "json" as const);
231
+ const { vm, marshal, dispose } = await setup({
232
+ isMarshalable,
233
+ });
234
+
235
+ class Hoge {}
236
+ const target = {
237
+ a: { c: () => 1, d: new Date(), e: [() => 1, 1, new Hoge()] },
238
+ b: 1,
239
+ };
240
+ const handle = marshal(target);
241
+
242
+ expect(vm.dump(handle)).toEqual({
243
+ a: { d: target.a.d.toISOString(), e: [null, 1, {}] },
244
+ b: 1,
245
+ });
246
+ expect(isMarshalable).toBeCalledTimes(1);
247
+ expect(isMarshalable).toBeCalledWith(target);
248
+ expect(isMarshalable).toReturnWith("json");
249
+
250
+ dispose();
251
+ });
252
+
189
253
  const setup = async ({
190
254
  isMarshalable,
191
255
  }: {
192
- isMarshalable?: (target: any) => boolean;
256
+ isMarshalable?: (target: any) => boolean | "json";
193
257
  } = {}) => {
194
258
  const vm = (await getQuickJS()).createVm();
195
259
  const map = new VMMap(vm);
@@ -199,13 +263,14 @@ const setup = async ({
199
263
  marshal: (v: any) =>
200
264
  marshal(v, {
201
265
  vm,
202
- unmarshal: h => map.getByHandle(h) ?? vm.dump(h),
266
+ unmarshal: (h) => map.getByHandle(h) ?? vm.dump(h),
203
267
  isMarshalable,
204
- pre: (t, h) => {
268
+ pre: (t, d) => {
269
+ const h = handleFrom(d);
205
270
  map.set(t, h);
206
271
  return h;
207
272
  },
208
- find: t => map.get(t),
273
+ find: (t) => map.get(t),
209
274
  }),
210
275
  dispose: () => {
211
276
  map.dispose();
@@ -1,16 +1,25 @@
1
- import { QuickJSHandle, QuickJSVm } from "quickjs-emscripten";
2
- import marshalArray from "./array";
1
+ import type {
2
+ QuickJSDeferredPromise,
3
+ QuickJSHandle,
4
+ QuickJSVm,
5
+ } from "quickjs-emscripten";
3
6
  import marshalFunction from "./function";
4
7
  import marshalObject from "./object";
5
8
  import marshalPrimitive from "./primitive";
6
9
  import marshalSymbol from "./symbol";
10
+ import marshalJSON from "./json";
11
+ import marshalPromise from "./promise";
7
12
 
8
13
  export type Options = {
9
14
  vm: QuickJSVm;
10
15
  unmarshal: (handle: QuickJSHandle) => unknown;
11
- isMarshalable?: (target: unknown) => boolean;
16
+ isMarshalable?: (target: unknown) => boolean | "json";
12
17
  find: (target: unknown) => QuickJSHandle | undefined;
13
- pre: (target: unknown, handle: QuickJSHandle) => QuickJSHandle | undefined;
18
+ pre: (
19
+ target: unknown,
20
+ handle: QuickJSHandle | QuickJSDeferredPromise,
21
+ mode: true | "json" | undefined
22
+ ) => QuickJSHandle | undefined;
14
23
  preApply?: (target: Function, thisArg: unknown, args: unknown[]) => any;
15
24
  };
16
25
 
@@ -29,20 +38,25 @@ export function marshal(target: unknown, options: Options): QuickJSHandle {
29
38
  if (handle) return handle;
30
39
  }
31
40
 
32
- if (isMarshalable?.(target) === false) {
41
+ const marshalable = isMarshalable?.(target);
42
+ if (marshalable === false) {
33
43
  return vm.undefined;
34
44
  }
35
45
 
36
- const marshal2 = (t: unknown) => marshal(t, options);
37
-
38
- const result =
39
- marshalSymbol(vm, target, pre) ??
40
- marshalArray(vm, target, marshal2, pre) ??
41
- marshalFunction(vm, target, marshal2, unmarshal, pre, options.preApply) ??
42
- marshalObject(vm, target, marshal2, pre) ??
43
- vm.undefined;
46
+ const pre2 = (target: any, handle: QuickJSHandle | QuickJSDeferredPromise) =>
47
+ pre(target, handle, marshalable);
48
+ if (marshalable === "json") {
49
+ return marshalJSON(vm, target, pre2);
50
+ }
44
51
 
45
- return result;
52
+ const marshal2 = (t: unknown) => marshal(t, options);
53
+ return (
54
+ marshalSymbol(vm, target, pre2) ??
55
+ marshalPromise(vm, target, marshal2, pre2) ??
56
+ marshalFunction(vm, target, marshal2, unmarshal, pre2, options.preApply) ??
57
+ marshalObject(vm, target, marshal2, pre2) ??
58
+ vm.undefined
59
+ );
46
60
  }
47
61
 
48
62
  export default marshal;