diff --git a/packages/codemode/src/interpreter/runtime.ts b/packages/codemode/src/interpreter/runtime.ts index fed5c1189ff..90e08dde5c3 100644 --- a/packages/codemode/src/interpreter/runtime.ts +++ b/packages/codemode/src/interpreter/runtime.ts @@ -615,26 +615,36 @@ const collectPatternNames = (pattern: AstNode, out: Array = []): Array { private readonly active = new Set() - private readonly settled = new Map>() - private readonly unobserved = new Set() + private readonly ids = new WeakMap() + private readonly observed = new WeakSet() + private readonly failures = new Map() + private nextID = 0 constructor(private readonly scope: Scope.Scope) {} create(effect: Effect.Effect): Effect.Effect { return Effect.map(Effect.forkIn(effect, this.scope, { startImmediately: true }), (fiber) => { const promise = new SandboxPromise(fiber) + const id = this.nextID++ this.active.add(promise) - this.unobserved.add(promise) + this.ids.set(promise, id) fiber.addObserver((exit) => { this.active.delete(promise) - this.settled.set(promise, exit) + if (Exit.isSuccess(exit) || Cause.hasInterruptsOnly(exit.cause) || this.observed.has(promise)) { + this.ids.delete(promise) + return + } + this.failures.set(id, normalizeError(Cause.squash(exit.cause))) }) return promise }) } observe(promise: SandboxPromise): Effect.Effect> { - this.unobserved.delete(promise) + this.observed.add(promise) + const id = this.ids.get(promise) + this.ids.delete(promise) + if (id !== undefined) this.failures.delete(id) return Fiber.await(promise.fiber) } @@ -645,10 +655,8 @@ class PromiseRuntime { for (const promise of [...self.active]) yield* Fiber.await(promise.fiber) } - for (const promise of self.unobserved) { - const exit = self.settled.get(promise) - if (exit === undefined || Exit.isSuccess(exit) || Cause.hasInterruptsOnly(exit.cause)) continue - const failure = normalizeError(Cause.squash(exit.cause)) + const failure = [...self.failures].sort(([left], [right]) => left - right)[0]?.[1] + if (failure !== undefined) { throw new InterpreterRuntimeError( `Unhandled rejection from an un-awaited promise: ${failure.message}`, undefined, diff --git a/packages/codemode/test/promise.test.ts b/packages/codemode/test/promise.test.ts index 683644a7cb7..bff15809256 100644 --- a/packages/codemode/test/promise.test.ts +++ b/packages/codemode/test/promise.test.ts @@ -610,6 +610,21 @@ describe("Promise.resolve / Promise.reject", () => { ).toBe("nope") }) + test("a rejection observed after settlement is handled", async () => { + expect( + await value(` + const rejected = Promise.reject(new Error("handled")) + await tools.host.sleepy({ id: 1 }) + try { + await rejected + return "no" + } catch (error) { + return error.message + } + `), + ).toBe("handled") + }) + test("an abandoned rejected promise is reported as unhandled", async () => { const diagnostic = await error(` Promise.reject(new Error("abandoned")) @@ -653,6 +668,20 @@ describe("timeout interruption of forked calls", () => { expect(result.error.kind).toBe("TimeoutExceeded") expect(trace.interrupted).toBe(2) }) + + test("a non-settling race loser times out and is interrupted once", async () => { + const trace = makeTrace() + const result = await run(`return await Promise.race(["winner", tools.host.sleepy({ id: 1, ms: 60000 })])`, { + trace, + limits: { timeoutMs: 100 }, + }) + expect(result.ok).toBe(false) + if (result.ok) return + expect(result.error.kind).toBe("TimeoutExceeded") + expect(trace.starts).toEqual([1]) + expect(trace.completed).toBe(0) + expect(trace.interrupted).toBe(1) + }) }) describe("unsupported promise surface", () => {