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.
- package/README.md +45 -0
- package/dist/index.d.ts +97 -3
- package/dist/quickjs-emscripten-sync.js +715 -550
- package/dist/quickjs-emscripten-sync.umd.cjs +33 -14
- package/package.json +7 -7
- package/src/contextex.ts +19 -7
- package/src/faultinjection.test.ts +265 -0
- package/src/index.test.ts +282 -0
- package/src/index.ts +204 -42
- package/src/marshal/custom.ts +20 -11
- package/src/marshal/function.test.ts +36 -14
- package/src/marshal/function.ts +100 -51
- package/src/marshal/index.ts +10 -0
- 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/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 +14 -6
- package/src/vmutil.test.ts +2 -1
- package/src/wrapper.test.ts +5 -3
- package/src/wrapper.ts +144 -37
package/src/index.test.ts
CHANGED
|
@@ -73,6 +73,70 @@ describe("readme", () => {
|
|
|
73
73
|
});
|
|
74
74
|
});
|
|
75
75
|
|
|
76
|
+
describe("class constructors (#92)", () => {
|
|
77
|
+
test("new on an exposed class inside the VM", async () => {
|
|
78
|
+
const ctx = (await getQuickJS()).newContext();
|
|
79
|
+
const arena = new Arena(ctx, { isMarshalable: true });
|
|
80
|
+
|
|
81
|
+
class Cls {
|
|
82
|
+
hoge = "";
|
|
83
|
+
|
|
84
|
+
constructor() {
|
|
85
|
+
this.hoge = "foo";
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
arena.expose({ Cls });
|
|
89
|
+
|
|
90
|
+
const instance = arena.evalCode(`new Cls()`) as { hoge: string };
|
|
91
|
+
expect(instance.hoge).toBe("foo");
|
|
92
|
+
expect(arena.evalCode(`new Cls() instanceof Cls`)).toBe(true);
|
|
93
|
+
|
|
94
|
+
arena.dispose();
|
|
95
|
+
ctx.dispose();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("constructor arguments reach the host", async () => {
|
|
99
|
+
const ctx = (await getQuickJS()).newContext();
|
|
100
|
+
const arena = new Arena(ctx, { isMarshalable: true });
|
|
101
|
+
|
|
102
|
+
class Cls {
|
|
103
|
+
value: number;
|
|
104
|
+
|
|
105
|
+
constructor(a: number, b: number) {
|
|
106
|
+
this.value = a + b;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
arena.expose({ Cls });
|
|
110
|
+
|
|
111
|
+
const instance = arena.evalCode(`new Cls(2, 3)`) as { value: number };
|
|
112
|
+
expect(instance.value).toBe(5);
|
|
113
|
+
|
|
114
|
+
arena.dispose();
|
|
115
|
+
ctx.dispose();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("new on a synced class inside the VM", async () => {
|
|
119
|
+
const ctx = (await getQuickJS()).newContext();
|
|
120
|
+
const arena = new Arena(ctx, { isMarshalable: true });
|
|
121
|
+
|
|
122
|
+
class Cls {
|
|
123
|
+
hoge = "";
|
|
124
|
+
|
|
125
|
+
constructor(v = "foo") {
|
|
126
|
+
this.hoge = v;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
arena.expose({ Cls: arena.sync(Cls) });
|
|
130
|
+
|
|
131
|
+
const instance = arena.evalCode(`new Cls("bar")`) as { hoge: string };
|
|
132
|
+
expect(instance.hoge).toBe("bar");
|
|
133
|
+
expect(arena.evalCode(`new Cls() instanceof Cls`)).toBe(true);
|
|
134
|
+
|
|
135
|
+
arena.dispose();
|
|
136
|
+
ctx.dispose();
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
76
140
|
describe("evalCode", () => {
|
|
77
141
|
test("simple object and function", async () => {
|
|
78
142
|
const ctx = (await getQuickJS()).newContext();
|
|
@@ -254,6 +318,36 @@ describe("evalCode", () => {
|
|
|
254
318
|
arena.dispose();
|
|
255
319
|
ctx.dispose();
|
|
256
320
|
});
|
|
321
|
+
|
|
322
|
+
test("options are forwarded (strict mode)", async () => {
|
|
323
|
+
const ctx = (await getQuickJS()).newContext();
|
|
324
|
+
const arena = new Arena(ctx, { isMarshalable: true });
|
|
325
|
+
|
|
326
|
+
// Assigning to an undeclared variable succeeds in sloppy mode...
|
|
327
|
+
expect(arena.evalCode("x = 1")).toBe(1);
|
|
328
|
+
// ...but throws a ReferenceError in strict mode.
|
|
329
|
+
expect(() => arena.evalCode("y = 1", undefined, { strict: true })).toThrow(ReferenceError);
|
|
330
|
+
|
|
331
|
+
arena.dispose();
|
|
332
|
+
ctx.dispose();
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
test("filename appears in error stacks", async () => {
|
|
336
|
+
const ctx = (await getQuickJS()).newContext();
|
|
337
|
+
const arena = new Arena(ctx, { isMarshalable: true });
|
|
338
|
+
|
|
339
|
+
try {
|
|
340
|
+
arena.evalCode(`throw new Error("boom")`, "my-special-file.js");
|
|
341
|
+
throw new Error("should have thrown");
|
|
342
|
+
} catch (e) {
|
|
343
|
+
expect(e).toBeInstanceOf(Error);
|
|
344
|
+
expect((e as Error).message).toBe("boom");
|
|
345
|
+
expect((e as Error).stack).toContain("my-special-file.js");
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
arena.dispose();
|
|
349
|
+
ctx.dispose();
|
|
350
|
+
});
|
|
257
351
|
});
|
|
258
352
|
|
|
259
353
|
describe("expose without sync", () => {
|
|
@@ -836,6 +930,30 @@ describe("evalModule", () => {
|
|
|
836
930
|
arena.dispose();
|
|
837
931
|
ctx.dispose();
|
|
838
932
|
});
|
|
933
|
+
|
|
934
|
+
test("user options are merged and type is forced to module", async () => {
|
|
935
|
+
const ctx = (await getQuickJS()).newContext();
|
|
936
|
+
const arena = new Arena(ctx, { isMarshalable: true });
|
|
937
|
+
|
|
938
|
+
// `strict` is passed through, and a user-supplied `type` is overridden with
|
|
939
|
+
// "module" so `export` still works and the exports are returned.
|
|
940
|
+
const exports = arena.evalModule(
|
|
941
|
+
`
|
|
942
|
+
export const value = 42;
|
|
943
|
+
export function greet(name) {
|
|
944
|
+
return "Hi, " + name;
|
|
945
|
+
}
|
|
946
|
+
`,
|
|
947
|
+
"with-options.js",
|
|
948
|
+
{ strict: true, type: "global" },
|
|
949
|
+
);
|
|
950
|
+
|
|
951
|
+
expect(exports.value).toBe(42);
|
|
952
|
+
expect(exports.greet("World")).toBe("Hi, World");
|
|
953
|
+
|
|
954
|
+
arena.dispose();
|
|
955
|
+
ctx.dispose();
|
|
956
|
+
});
|
|
839
957
|
});
|
|
840
958
|
|
|
841
959
|
describe("memory management", () => {
|
|
@@ -1035,6 +1153,27 @@ describe("memory management", () => {
|
|
|
1035
1153
|
arena.dispose();
|
|
1036
1154
|
ctx.dispose();
|
|
1037
1155
|
});
|
|
1156
|
+
|
|
1157
|
+
test("setInterruptHandler interrupts infinite loops", async () => {
|
|
1158
|
+
const ctx = (await getQuickJS()).newContext();
|
|
1159
|
+
const arena = new Arena(ctx, { isMarshalable: true });
|
|
1160
|
+
|
|
1161
|
+
// Interrupt after the handler has been called a number of times.
|
|
1162
|
+
let calls = 0;
|
|
1163
|
+
arena.setInterruptHandler(() => ++calls > 1000);
|
|
1164
|
+
|
|
1165
|
+
expect(() => {
|
|
1166
|
+
arena.evalCode(`while (true) {}`);
|
|
1167
|
+
}).toThrow();
|
|
1168
|
+
expect(calls).toBeGreaterThan(1000);
|
|
1169
|
+
|
|
1170
|
+
// After removing the handler, normal evaluation works again.
|
|
1171
|
+
arena.removeInterruptHandler();
|
|
1172
|
+
expect(arena.evalCode(`1 + 2`)).toBe(3);
|
|
1173
|
+
|
|
1174
|
+
arena.dispose();
|
|
1175
|
+
ctx.dispose();
|
|
1176
|
+
});
|
|
1038
1177
|
});
|
|
1039
1178
|
|
|
1040
1179
|
describe("intrinsics configuration", () => {
|
|
@@ -1116,6 +1255,149 @@ describe("AsyncArena", () => {
|
|
|
1116
1255
|
});
|
|
1117
1256
|
});
|
|
1118
1257
|
|
|
1258
|
+
describe("asyncify (#32)", () => {
|
|
1259
|
+
test("without the option an async fn yields a Promise in the guest", async () => {
|
|
1260
|
+
const ctx = await newAsyncContext();
|
|
1261
|
+
const arena = new AsyncArena(ctx, { isMarshalable: true });
|
|
1262
|
+
|
|
1263
|
+
arena.expose({ f: async () => "result" });
|
|
1264
|
+
expect(await arena.evalCodeAsync(`typeof f()`)).toBe("object");
|
|
1265
|
+
|
|
1266
|
+
arena.dispose();
|
|
1267
|
+
ctx.dispose();
|
|
1268
|
+
});
|
|
1269
|
+
|
|
1270
|
+
test("asyncify: true resolves the value synchronously in the guest", async () => {
|
|
1271
|
+
const ctx = await newAsyncContext();
|
|
1272
|
+
const arena = new AsyncArena(ctx, { isMarshalable: true, asyncify: true });
|
|
1273
|
+
|
|
1274
|
+
arena.expose({
|
|
1275
|
+
f: async () => {
|
|
1276
|
+
await new Promise(r => setTimeout(r, 1));
|
|
1277
|
+
return "result";
|
|
1278
|
+
},
|
|
1279
|
+
g: async () => ({ a: 1, b: [2, 3] }),
|
|
1280
|
+
});
|
|
1281
|
+
|
|
1282
|
+
expect(await arena.evalCodeAsync(`typeof f()`)).toBe("string");
|
|
1283
|
+
expect(await arena.evalCodeAsync(`f()`)).toBe("result");
|
|
1284
|
+
expect(await arena.evalCodeAsync(`const o = g(); [typeof o, o.a, o.b[1]]`)).toEqual([
|
|
1285
|
+
"object",
|
|
1286
|
+
1,
|
|
1287
|
+
3,
|
|
1288
|
+
]);
|
|
1289
|
+
|
|
1290
|
+
arena.dispose();
|
|
1291
|
+
ctx.dispose();
|
|
1292
|
+
});
|
|
1293
|
+
|
|
1294
|
+
test("asyncify: true passes arguments through", async () => {
|
|
1295
|
+
const ctx = await newAsyncContext();
|
|
1296
|
+
const arena = new AsyncArena(ctx, { isMarshalable: true, asyncify: true });
|
|
1297
|
+
|
|
1298
|
+
arena.expose({ add: async (a: number, b: number) => a + b });
|
|
1299
|
+
expect(await arena.evalCodeAsync(`add(2, 3)`)).toBe(5);
|
|
1300
|
+
|
|
1301
|
+
arena.dispose();
|
|
1302
|
+
ctx.dispose();
|
|
1303
|
+
});
|
|
1304
|
+
|
|
1305
|
+
test("predicate form only asyncifies matching functions", async () => {
|
|
1306
|
+
const ctx = await newAsyncContext();
|
|
1307
|
+
const arena = new AsyncArena(ctx, {
|
|
1308
|
+
isMarshalable: true,
|
|
1309
|
+
asyncify: target => (target as any).asyncified === true,
|
|
1310
|
+
});
|
|
1311
|
+
|
|
1312
|
+
const yes = async () => "sync-value";
|
|
1313
|
+
(yes as any).asyncified = true;
|
|
1314
|
+
const no = async () => "promise-value";
|
|
1315
|
+
|
|
1316
|
+
arena.expose({ yes, no });
|
|
1317
|
+
|
|
1318
|
+
expect(await arena.evalCodeAsync(`typeof yes()`)).toBe("string");
|
|
1319
|
+
expect(await arena.evalCodeAsync(`yes()`)).toBe("sync-value");
|
|
1320
|
+
expect(await arena.evalCodeAsync(`typeof no()`)).toBe("object");
|
|
1321
|
+
|
|
1322
|
+
arena.dispose();
|
|
1323
|
+
ctx.dispose();
|
|
1324
|
+
});
|
|
1325
|
+
|
|
1326
|
+
test("a rejecting asyncified fn surfaces as a catchable VM error", async () => {
|
|
1327
|
+
const ctx = await newAsyncContext();
|
|
1328
|
+
const arena = new AsyncArena(ctx, { isMarshalable: true, asyncify: true });
|
|
1329
|
+
|
|
1330
|
+
arena.expose({
|
|
1331
|
+
boom: async () => {
|
|
1332
|
+
throw new Error("kaboom");
|
|
1333
|
+
},
|
|
1334
|
+
});
|
|
1335
|
+
|
|
1336
|
+
// Uncaught in the guest: the error propagates back to the host.
|
|
1337
|
+
await expect(arena.evalCodeAsync(`boom()`)).rejects.toThrow("kaboom");
|
|
1338
|
+
|
|
1339
|
+
// Caught in the guest: the guest can handle it synchronously.
|
|
1340
|
+
expect(
|
|
1341
|
+
await arena.evalCodeAsync(`
|
|
1342
|
+
try {
|
|
1343
|
+
boom();
|
|
1344
|
+
"no-throw";
|
|
1345
|
+
} catch (e) {
|
|
1346
|
+
"caught:" + e.message;
|
|
1347
|
+
}
|
|
1348
|
+
`),
|
|
1349
|
+
).toBe("caught:kaboom");
|
|
1350
|
+
|
|
1351
|
+
arena.dispose();
|
|
1352
|
+
ctx.dispose();
|
|
1353
|
+
});
|
|
1354
|
+
|
|
1355
|
+
test("sequential asyncified calls in one evalCodeAsync work", async () => {
|
|
1356
|
+
const ctx = await newAsyncContext();
|
|
1357
|
+
const arena = new AsyncArena(ctx, { isMarshalable: true, asyncify: true });
|
|
1358
|
+
|
|
1359
|
+
arena.expose({
|
|
1360
|
+
first: async () => 10,
|
|
1361
|
+
second: async (n: number) => n + 5,
|
|
1362
|
+
});
|
|
1363
|
+
|
|
1364
|
+
expect(await arena.evalCodeAsync(`const a = first(); const b = second(a); a + b`)).toBe(25);
|
|
1365
|
+
|
|
1366
|
+
arena.dispose();
|
|
1367
|
+
ctx.dispose();
|
|
1368
|
+
});
|
|
1369
|
+
|
|
1370
|
+
test("calling an asyncified fn during synchronous evalCode fails", async () => {
|
|
1371
|
+
const ctx = await newAsyncContext();
|
|
1372
|
+
const arena = new AsyncArena(ctx, { isMarshalable: true, asyncify: true });
|
|
1373
|
+
|
|
1374
|
+
arena.expose({ f: async () => "result" });
|
|
1375
|
+
|
|
1376
|
+
// Asyncify can only suspend through an async entry point; a synchronous
|
|
1377
|
+
// evalCode cannot unwind the stack, so it throws.
|
|
1378
|
+
expect(() => arena.evalCode(`f()`)).toThrow("Function unexpectedly returned a Promise");
|
|
1379
|
+
|
|
1380
|
+
arena.dispose();
|
|
1381
|
+
ctx.dispose();
|
|
1382
|
+
});
|
|
1383
|
+
|
|
1384
|
+
test("sync context fallback: asyncify does not break, async fn behaves as today", async () => {
|
|
1385
|
+
const ctx = (await getQuickJS()).newContext();
|
|
1386
|
+
const arena = new Arena(ctx, { isMarshalable: true, asyncify: true });
|
|
1387
|
+
|
|
1388
|
+
arena.expose({ f: async () => "result" });
|
|
1389
|
+
// No newAsyncifiedFunction on a plain context: falls back to Promise marshalling.
|
|
1390
|
+
expect(arena.evalCode(`globalThis.__p = f(); typeof __p`)).toBe("object");
|
|
1391
|
+
// Let the host promise settle into the VM promise before disposing, so the
|
|
1392
|
+
// marshalled resolution does not touch a disposed context.
|
|
1393
|
+
await new Promise(r => setTimeout(r));
|
|
1394
|
+
arena.executePendingJobs();
|
|
1395
|
+
|
|
1396
|
+
arena.dispose();
|
|
1397
|
+
ctx.dispose();
|
|
1398
|
+
});
|
|
1399
|
+
});
|
|
1400
|
+
|
|
1119
1401
|
describe("Symbol.dispose", () => {
|
|
1120
1402
|
test("using disposes the arena", async () => {
|
|
1121
1403
|
const ctx = (await getQuickJS()).newContext();
|