Skip to content

Commit 92942c5

Browse files
committed
## Core Upgrade
- Upgrade quickjs-emscripten from 0.25.0 to 0.29.0 - Fix Disposable interface implementation in vmutil.ts to support Symbol.dispose (required by 0.29.0) ## Module System Enhancements (0.29.0) - Update evalModule() to return module exports (previously returned void) - Add comprehensive documentation and examples for accessing module exports - Add 5 new tests for module export functionality: - exported values and objects - exported functions - default exports - class static method exports - top-level await support ## Promise State Inspection (0.29.1) - Add getPromiseState() method for synchronous promise state checking - Returns pending, fulfilled, or rejected states - Add 4 tests covering various promise state scenarios - Useful for determining when to sync promise results without async operations ## Intrinsics Configuration Support (0.26.0) - Export Intrinsics, JSPromiseState, and JSPromiseStateEnum types - Add documentation showing how to configure intrinsics at context creation - Enables security sandboxing by disabling dangerous features (eval, Proxy, etc.) - Add test demonstrating intrinsics configuration usage
1 parent 1c81bc3 commit 92942c5

File tree

2 files changed

+153
-0
lines changed

2 files changed

+153
-0
lines changed

src/index.test.ts

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1012,3 +1012,119 @@ describe("memory management", () => {
10121012
ctx.dispose();
10131013
});
10141014
});
1015+
1016+
describe("intrinsics configuration", () => {
1017+
test("intrinsics can be configured when creating context", async () => {
1018+
const quickjs = await getQuickJS();
1019+
const runtime = quickjs.newRuntime();
1020+
1021+
// Example: disable eval for sandboxing
1022+
const ctx = runtime.newContext({ intrinsics: { Eval: false } });
1023+
const arena = new Arena(ctx, { isMarshalable: true });
1024+
1025+
// This test demonstrates that intrinsics are configured at context creation
1026+
// The actual restrictions would be enforced by quickjs-emscripten
1027+
expect(arena).toBeDefined();
1028+
expect(arena.context).toBeDefined();
1029+
1030+
arena.dispose();
1031+
ctx.dispose();
1032+
runtime.dispose();
1033+
});
1034+
});
1035+
1036+
describe("promise state inspection", () => {
1037+
test("getPromiseState returns fulfilled for resolved promise", async () => {
1038+
const ctx = (await getQuickJS()).newContext();
1039+
const arena = new Arena(ctx, { isMarshalable: true });
1040+
1041+
const handle = arena.context.unwrapResult(arena.context.evalCode("Promise.resolve(42)"));
1042+
1043+
// Promise.resolve creates an already fulfilled promise
1044+
expect(arena.getPromiseState(handle)).toBe("fulfilled");
1045+
1046+
handle.dispose();
1047+
arena.dispose();
1048+
ctx.dispose();
1049+
});
1050+
1051+
test("getPromiseState returns pending then fulfilled", async () => {
1052+
const ctx = (await getQuickJS()).newContext();
1053+
const arena = new Arena(ctx, { isMarshalable: true });
1054+
1055+
// Create a promise that requires job execution to resolve
1056+
const handle = arena.context.unwrapResult(
1057+
arena.context.evalCode(`
1058+
new Promise((resolve) => {
1059+
globalThis.resolveIt = resolve;
1060+
})
1061+
`),
1062+
);
1063+
1064+
expect(arena.getPromiseState(handle)).toBe("pending");
1065+
1066+
// Now resolve it
1067+
arena.evalCode("globalThis.resolveIt(123)");
1068+
arena.executePendingJobs();
1069+
1070+
expect(arena.getPromiseState(handle)).toBe("fulfilled");
1071+
1072+
handle.dispose();
1073+
arena.dispose();
1074+
ctx.dispose();
1075+
});
1076+
1077+
test("getPromiseState with chained promises", async () => {
1078+
const ctx = (await getQuickJS()).newContext();
1079+
const arena = new Arena(ctx, { isMarshalable: true });
1080+
1081+
// Create a pending promise
1082+
const handle = arena.context.unwrapResult(
1083+
arena.context.evalCode(`
1084+
new Promise((resolve) => {
1085+
globalThis.doResolve = resolve;
1086+
}).then(x => x * 2)
1087+
`),
1088+
);
1089+
1090+
expect(arena.getPromiseState(handle)).toBe("pending");
1091+
1092+
// Resolve the original promise
1093+
arena.evalCode("globalThis.doResolve(10)");
1094+
arena.executePendingJobs();
1095+
1096+
expect(arena.getPromiseState(handle)).toBe("fulfilled");
1097+
1098+
handle.dispose();
1099+
arena.dispose();
1100+
ctx.dispose();
1101+
});
1102+
1103+
test("getPromiseState detects state changes", async () => {
1104+
const ctx = (await getQuickJS()).newContext();
1105+
const arena = new Arena(ctx, { isMarshalable: true });
1106+
1107+
// Set up a promise that can be controlled
1108+
arena.evalCode(`
1109+
globalThis.promiseState = new Promise((resolve, reject) => {
1110+
globalThis.doResolve = resolve;
1111+
globalThis.doReject = reject;
1112+
});
1113+
`);
1114+
1115+
const handle = arena.context.getProp(arena.context.global, "promiseState");
1116+
1117+
// Initially pending
1118+
expect(arena.getPromiseState(handle)).toBe("pending");
1119+
1120+
// Resolve it
1121+
arena.evalCode("globalThis.doResolve(42)");
1122+
arena.executePendingJobs();
1123+
1124+
expect(arena.getPromiseState(handle)).toBe("fulfilled");
1125+
1126+
handle.dispose();
1127+
arena.dispose();
1128+
ctx.dispose();
1129+
});
1130+
});

src/index.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import type {
44
QuickJSContext,
55
SuccessOrFail,
66
VmCallResult,
7+
Intrinsics,
8+
JSPromiseState,
9+
JSPromiseStateEnum,
710
} from "quickjs-emscripten";
811

912
import { wrapContext, QuickJSContextEx } from "./contextex";
@@ -31,6 +34,8 @@ export {
3134
consumeAll,
3235
};
3336

37+
export type { Intrinsics, JSPromiseState, JSPromiseStateEnum };
38+
3439
export type Options = {
3540
/** 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. */
3641
isMarshalable?: boolean | "json" | ((target: any) => boolean | "json");
@@ -266,6 +271,38 @@ export class Arena {
266271
return this.context.runtime.dumpMemoryUsage();
267272
}
268273

274+
/**
275+
* Get the state of a VM Promise synchronously.
276+
*
277+
* Note: This only works with promises that were created in the VM and accessed
278+
* via their internal handle. For marshaled promises from evalCode/evalModule,
279+
* the promise is already resolved on the host side.
280+
*
281+
* Requires quickjs-emscripten >= 0.29.1
282+
*
283+
* @param promiseHandle - The QuickJS handle to the promise
284+
* @returns The state of the promise: "pending", "fulfilled", or "rejected"
285+
*
286+
* @example
287+
* ```js
288+
* const handle = arena.context.unwrapResult(
289+
* arena.context.evalCode('Promise.resolve(42)')
290+
* );
291+
*
292+
* console.log(arena.getPromiseState(handle)); // "pending" (before job execution)
293+
*
294+
* arena.executePendingJobs();
295+
*
296+
* console.log(arena.getPromiseState(handle)); // "fulfilled"
297+
*
298+
* handle.dispose();
299+
* ```
300+
*/
301+
getPromiseState(promiseHandle: QuickJSHandle): "pending" | "fulfilled" | "rejected" {
302+
const state = this.context.getPromiseState(promiseHandle);
303+
return state.type;
304+
}
305+
269306
/**
270307
* Expose objects as global objects in the VM.
271308
*

0 commit comments

Comments
 (0)