fix(codemode): preserve promise race losers
This commit is contained in:
parent
0800770b0d
commit
c3b9d03bdb
5 changed files with 53 additions and 73 deletions
|
|
@ -246,7 +246,7 @@ CodeMode executes a deliberately bounded JavaScript subset. It supports:
|
||||||
- Regular expressions - `/literals/` and `new RegExp(...)` with `test`/`exec` (stateful `lastIndex` for `g`), plus string `match`/`matchAll`/`replace`/`replaceAll`/`split`/`search` with patterns. Match results are arrays carrying `index` and named `groups` as own properties (`input` is omitted). `replace` and `replaceAll` accept function replacers with captures, offset, input, and named groups; callbacks run sequentially, may await tool calls, and have their results coerced to strings. Invalid patterns, invalid flags, and missing-`g` calls fail with catchable errors that say what was wrong and how to fix it (escaping hints, the exact `/pattern/g` to write). Patterns run on the host engine, so pathological backtracking is bounded only by the execution timeout.
|
- Regular expressions - `/literals/` and `new RegExp(...)` with `test`/`exec` (stateful `lastIndex` for `g`), plus string `match`/`matchAll`/`replace`/`replaceAll`/`split`/`search` with patterns. Match results are arrays carrying `index` and named `groups` as own properties (`input` is omitted). `replace` and `replaceAll` accept function replacers with captures, offset, input, and named groups; callbacks run sequentially, may await tool calls, and have their results coerced to strings. Invalid patterns, invalid flags, and missing-`g` calls fail with catchable errors that say what was wrong and how to fix it (escaping hints, the exact `/pattern/g` to write). Patterns run on the host engine, so pathological backtracking is bounded only by the execution timeout.
|
||||||
- `Map` and `Set` - construction from entries/arrays/strings, `get`/`set`/`add`/`has`/`delete`/`clear`/`size`/`forEach`, and `keys`/`values`/`entries` returning **arrays** (not iterators).
|
- `Map` and `Set` - construction from entries/arrays/strings, `get`/`set`/`add`/`has`/`delete`/`clear`/`size`/`forEach`, and `keys`/`values`/`entries` returning **arrays** (not iterators).
|
||||||
- URL helpers - `URL` resolution and mutation, linked `URLSearchParams`, `URL.canParse`/`URL.parse`, URI and URI-component encoding/decoding, and query parameter construction, lookup, mutation, sorting, callbacks, and materialization. URLSearchParams iteration methods return arrays, matching the Map/Set convention.
|
- URL helpers - `URL` resolution and mutation, linked `URLSearchParams`, `URL.canParse`/`URL.parse`, URI and URI-component encoding/decoding, and query parameter construction, lookup, mutation, sorting, callbacks, and materialization. URLSearchParams iteration methods return arrays, matching the Map/Set convention.
|
||||||
- First-class promises - an un-awaited `tools.ns.tool(...)` is a promise value whose call starts immediately on a supervised fiber; `await` resolves it (awaiting a non-promise value is a no-op, and `return tools.ns.tool(...)` resolves like an async-function return). Promises support `then`/`catch`/`finally`, including returned-promise flattening and rejection recovery. `Promise.all`, `Promise.allSettled`, and `Promise.race` accept any array mixing promises and plain values (built inline, beforehand, or via spread); `Promise.resolve`/`Promise.reject` construct settled promises. `Promise.allSettled` rejection reasons are the same plain `{ name?, message }` data a `catch` binding sees, and `Promise.race` interrupts its losing in-flight calls. At most 8 tool calls run concurrently. When a program completes, still-running un-awaited calls are awaited before the execution ends; a failure from a call that was never awaited surfaces as an unhandled-rejection diagnostic.
|
- First-class promises - an un-awaited `tools.ns.tool(...)` is a promise value whose call starts immediately on a supervised fiber; `await` resolves it (awaiting a non-promise value is a no-op, and `return tools.ns.tool(...)` resolves like an async-function return). Promises support `then`/`catch`/`finally`, including returned-promise flattening and rejection recovery. `Promise.all`, `Promise.allSettled`, and `Promise.race` accept any array mixing promises and plain values (built inline, beforehand, or via spread); `Promise.resolve`/`Promise.reject` construct settled promises. `Promise.allSettled` rejection reasons are the same plain `{ name?, message }` data a `catch` binding sees, and `Promise.race` lets losing promises continue. At most 8 tool calls run concurrently. When a program completes, still-running un-awaited calls are awaited before the execution ends; a failure from a call that was never awaited surfaces as an unhandled-rejection diagnostic.
|
||||||
- `throw value` and `throw new Error(message)` for explicit program failure. `Error` (and `TypeError`/`RangeError`/`SyntaxError`/`ReferenceError`/`EvalError`/`URIError`) are real constructors, callable with or without `new`; error values are plain `{ name, message }` data that additionally satisfy `instanceof Error` (a specific type matches itself and `Error`, as in JS). Every caught failure - thrown errors, interpreter runtime errors, and tool failures - is `instanceof Error` in a `catch` block; a thrown non-error value (`throw "text"`) is not, matching JS. Caught failures carry the `name` the equivalent real-JS failure would have - `JSON.parse` and invalid regex patterns produce a `SyntaxError` (satisfying `instanceof SyntaxError`), an unknown identifier a `ReferenceError`, assigning to a constant a `TypeError`, a bad `normalize` form a `RangeError`; failures with no specific analogue (including tool failures) are named `"Error"`. `instanceof` also recognizes `Date`, `RegExp`, `Map`, `Set`, `URL`, `URLSearchParams`, `Array`, `Object`, and `Promise`; any other right-hand side is a catchable error.
|
- `throw value` and `throw new Error(message)` for explicit program failure. `Error` (and `TypeError`/`RangeError`/`SyntaxError`/`ReferenceError`/`EvalError`/`URIError`) are real constructors, callable with or without `new`; error values are plain `{ name, message }` data that additionally satisfy `instanceof Error` (a specific type matches itself and `Error`, as in JS). Every caught failure - thrown errors, interpreter runtime errors, and tool failures - is `instanceof Error` in a `catch` block; a thrown non-error value (`throw "text"`) is not, matching JS. Caught failures carry the `name` the equivalent real-JS failure would have - `JSON.parse` and invalid regex patterns produce a `SyntaxError` (satisfying `instanceof SyntaxError`), an unknown identifier a `ReferenceError`, assigning to a constant a `TypeError`, a bad `normalize` form a `RangeError`; failures with no specific analogue (including tool failures) are named `"Error"`. `instanceof` also recognizes `Date`, `RegExp`, `Map`, `Set`, `URL`, `URLSearchParams`, `Array`, `Object`, and `Promise`; any other right-hand side is a catchable error.
|
||||||
|
|
||||||
Inside a program, standard-library values stay live everywhere: the internal data checkpoints (`Object.*` helpers, spread, coercion inputs) preserve the instances, so `Object.values({ d: date })[0].getTime()` and a spread copy of an object holding a Map keep working. Only at the host boundary (final result, tool arguments, `JSON.stringify`) do they serialize exactly as `JSON.stringify` would: Date and URL become strings (an invalid Date becomes `null`), while RegExp, Map, Set, and URLSearchParams become `{}`. Promise values never cross a data boundary: an un-awaited promise in a result or tool argument produces a diagnostic that says to await it, instead of serializing to `{}`.
|
Inside a program, standard-library values stay live everywhere: the internal data checkpoints (`Object.*` helpers, spread, coercion inputs) preserve the instances, so `Object.values({ d: date })[0].getTime()` and a spread copy of an object holding a Map keep working. Only at the host boundary (final result, tool arguments, `JSON.stringify`) do they serialize exactly as `JSON.stringify` would: Date and URL become strings (an invalid Date becomes `null`), while RegExp, Map, Set, and URLSearchParams become `{}`. Promise values never cross a data boundary: an un-awaited promise in a result or tool argument produces a diagnostic that says to await it, instead of serializing to `{}`.
|
||||||
|
|
|
||||||
|
|
@ -152,7 +152,7 @@ current omissions to implement, not intentional product boundaries.
|
||||||
- [ ] Decide whether thenable assimilation belongs in the bounded runtime. Promise resolution currently unwraps only
|
- [ ] Decide whether thenable assimilation belongs in the bounded runtime. Promise resolution currently unwraps only
|
||||||
`SandboxPromise`; arbitrary `{ then(resolve, reject) }` values remain data. Full assimilation requires internal
|
`SandboxPromise`; arbitrary `{ then(resolve, reject) }` values remain data. Full assimilation requires internal
|
||||||
callable resolver values, first-settlement arbitration, recursive adoption, and cycle detection.
|
callable resolver values, first-settlement arbitration, recursive adoption, and cycle detection.
|
||||||
- [ ] Propagate cancellation through chained promises, and consider `Promise.any`.
|
- [ ] Consider `Promise.any`.
|
||||||
- [ ] Support async iteration and `for await...of`. Define behavior first for the runtime's supported promise and
|
- [ ] Support async iteration and `for await...of`. Define behavior first for the runtime's supported promise and
|
||||||
collection values, then extend it to bounded host streams when a stream boundary exists.
|
collection values, then extend it to bounded host streams when a stream boundary exists.
|
||||||
- [ ] Support callback-bearing standard-library variants that models commonly generate: the mapper argument to
|
- [ ] Support callback-bearing standard-library variants that models commonly generate: the mapper argument to
|
||||||
|
|
|
||||||
|
|
@ -219,7 +219,7 @@ const normalizeError = (error: unknown): Diagnostic => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shared by catch bindings, Promise.allSettled rejection reasons, and Promise.race losers.
|
// Shared by catch bindings and Promise.allSettled rejection reasons.
|
||||||
const caughtErrorValue = (thrown: unknown): unknown => {
|
const caughtErrorValue = (thrown: unknown): unknown => {
|
||||||
if (thrown instanceof ProgramThrow) return thrown.value
|
if (thrown instanceof ProgramThrow) return thrown.value
|
||||||
if (thrown instanceof InterpreterRuntimeError) return createErrorValue(thrown.errorName, thrown.message)
|
if (thrown instanceof InterpreterRuntimeError) return createErrorValue(thrown.errorName, thrown.message)
|
||||||
|
|
@ -703,7 +703,7 @@ class Interpreter<R> {
|
||||||
// Awaits every fiber-backed promise the program abandoned (fire-and-forget tool calls), so
|
// Awaits every fiber-backed promise the program abandoned (fire-and-forget tool calls), so
|
||||||
// their work completes before the execution ends - mirroring a JS runtime waiting on
|
// their work completes before the execution ends - mirroring a JS runtime waiting on
|
||||||
// in-flight I/O at exit. A failure nobody could have handled becomes an unhandled-rejection
|
// in-flight I/O at exit. A failure nobody could have handled becomes an unhandled-rejection
|
||||||
// diagnostic (interrupted calls, e.g. Promise.race losers, are ignored).
|
// diagnostic.
|
||||||
private drainPendingSettlements(): Effect.Effect<void, unknown, never> {
|
private drainPendingSettlements(): Effect.Effect<void, unknown, never> {
|
||||||
const self = this
|
const self = this
|
||||||
return Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
|
|
@ -752,28 +752,13 @@ class Interpreter<R> {
|
||||||
|
|
||||||
// `await promise`: succeed with the fulfilled value or re-raise the failure so try/catch
|
// `await promise`: succeed with the fulfilled value or re-raise the failure so try/catch
|
||||||
// observes it exactly like a synchronous throw at the await site.
|
// observes it exactly like a synchronous throw at the await site.
|
||||||
private settlePromise(promise: SandboxPromise, node?: AstNode): Effect.Effect<unknown, unknown, never> {
|
private settlePromise(promise: SandboxPromise): Effect.Effect<unknown, unknown, never> {
|
||||||
const self = this
|
const self = this
|
||||||
return Effect.flatMap(this.observePromise(promise), (exit) => self.unwrapPromiseExit(promise, exit, node))
|
return Effect.flatMap(this.observePromise(promise), (exit) => self.unwrapPromiseExit(exit))
|
||||||
}
|
}
|
||||||
|
|
||||||
private unwrapPromiseExit(
|
private unwrapPromiseExit(exit: Exit.Exit<unknown, unknown>): Effect.Effect<unknown, unknown> {
|
||||||
promise: SandboxPromise | undefined,
|
|
||||||
exit: Exit.Exit<unknown, unknown>,
|
|
||||||
node?: AstNode,
|
|
||||||
): Effect.Effect<unknown, unknown> {
|
|
||||||
if (Exit.isSuccess(exit)) return Effect.succeed(exit.value)
|
if (Exit.isSuccess(exit)) return Effect.succeed(exit.value)
|
||||||
// A call Promise.race interrupted after losing settles as a catchable program failure;
|
|
||||||
// any other interruption is execution teardown (timeout/host) and must keep propagating
|
|
||||||
// as interruption rather than becoming program-visible data.
|
|
||||||
if (promise?.interrupted === true && Cause.hasInterruptsOnly(exit.cause)) {
|
|
||||||
return Effect.fail(
|
|
||||||
new InterpreterRuntimeError(
|
|
||||||
"This tool call was interrupted because another value settled a Promise.race first.",
|
|
||||||
node,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return Effect.failCause(exit.cause)
|
return Effect.failCause(exit.cause)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1533,7 +1518,7 @@ class Interpreter<R> {
|
||||||
// matching real JS semantics for non-thenables.
|
// matching real JS semantics for non-thenables.
|
||||||
const self = this
|
const self = this
|
||||||
return Effect.flatMap(this.evaluateExpression(getNode(node, "argument")), (value) =>
|
return Effect.flatMap(this.evaluateExpression(getNode(node, "argument")), (value) =>
|
||||||
value instanceof SandboxPromise ? self.settlePromise(value, node) : Effect.succeed(value),
|
value instanceof SandboxPromise ? self.settlePromise(value) : Effect.succeed(value),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
case "NewExpression":
|
case "NewExpression":
|
||||||
|
|
@ -2257,42 +2242,33 @@ class Interpreter<R> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
return yield* self.unwrapPromiseExit(winner.item, winner.exit, node)
|
return yield* self.unwrapPromiseExit(winner.exit)
|
||||||
}
|
}
|
||||||
return values
|
return values
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
case "allSettled": {
|
case "allSettled": {
|
||||||
const observations = items.map((item) =>
|
const observations = items.map((item) =>
|
||||||
item instanceof SandboxPromise
|
item instanceof SandboxPromise ? this.observePromise(item) : Effect.succeed(Exit.succeed(item as unknown)),
|
||||||
? Effect.map(this.observePromise(item), (exit) => ({ promise: item as SandboxPromise | undefined, exit }))
|
|
||||||
: Effect.succeed({ promise: undefined as SandboxPromise | undefined, exit: Exit.succeed(item as unknown) }),
|
|
||||||
)
|
)
|
||||||
return Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
const outcomes: Array<unknown> = []
|
const outcomes: Array<unknown> = []
|
||||||
for (const observation of observations) {
|
for (const observation of observations) {
|
||||||
const { exit, promise } = yield* observation
|
const exit = yield* observation
|
||||||
if (Exit.isSuccess(exit)) {
|
if (Exit.isSuccess(exit)) {
|
||||||
outcomes.push(
|
outcomes.push(
|
||||||
Object.assign(Object.create(null) as SafeObject, { status: "fulfilled", value: exit.value }),
|
Object.assign(Object.create(null) as SafeObject, { status: "fulfilled", value: exit.value }),
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const raceInterrupted = promise?.interrupted === true && Cause.hasInterruptsOnly(exit.cause)
|
if (Cause.hasInterruptsOnly(exit.cause)) {
|
||||||
if (Cause.hasInterruptsOnly(exit.cause) && !raceInterrupted) {
|
|
||||||
// Execution teardown (timeout/host interruption), not a program-level rejection.
|
// Execution teardown (timeout/host interruption), not a program-level rejection.
|
||||||
return yield* Effect.failCause(exit.cause)
|
return yield* Effect.failCause(exit.cause)
|
||||||
}
|
}
|
||||||
const thrown = raceInterrupted
|
|
||||||
? new InterpreterRuntimeError(
|
|
||||||
"This tool call was interrupted because another value settled a Promise.race first.",
|
|
||||||
node,
|
|
||||||
)
|
|
||||||
: Cause.squash(exit.cause)
|
|
||||||
outcomes.push(
|
outcomes.push(
|
||||||
Object.assign(Object.create(null) as SafeObject, {
|
Object.assign(Object.create(null) as SafeObject, {
|
||||||
status: "rejected",
|
status: "rejected",
|
||||||
reason: caughtErrorValue(thrown),
|
reason: caughtErrorValue(Cause.squash(exit.cause)),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -2312,20 +2288,21 @@ class Interpreter<R> {
|
||||||
: Effect.succeed({ index, exit: Exit.succeed(item as unknown) }),
|
: Effect.succeed({ index, exit: Exit.succeed(item as unknown) }),
|
||||||
)
|
)
|
||||||
return Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
// First settlement (fulfilled OR rejected) wins; the observations never fail, so
|
// First settlement (fulfilled OR rejected) wins. Losers continue like native
|
||||||
// racing them yields exactly that. Losing in-flight calls are then interrupted.
|
// promises, while a supervised drain keeps their failures handled and their work
|
||||||
|
// inside the execution lifetime.
|
||||||
const winner = yield* Effect.raceAll(observations)
|
const winner = yield* Effect.raceAll(observations)
|
||||||
for (const [index, item] of items.entries()) {
|
yield* self.createPromise(
|
||||||
if (index === winner.index || !(item instanceof SandboxPromise) || item.fiber === undefined) continue
|
Effect.asVoid(
|
||||||
item.interrupted = true
|
Effect.forEach(
|
||||||
yield* Fiber.interrupt(item.fiber)
|
items,
|
||||||
}
|
(item, index) =>
|
||||||
const winningItem = items[winner.index]
|
index !== winner.index && item instanceof SandboxPromise ? self.observePromise(item) : Effect.void,
|
||||||
return yield* self.unwrapPromiseExit(
|
{ concurrency: "unbounded" },
|
||||||
winningItem instanceof SandboxPromise ? winningItem : undefined,
|
),
|
||||||
winner.exit,
|
),
|
||||||
node,
|
|
||||||
)
|
)
|
||||||
|
return yield* self.unwrapPromiseExit(winner.exit)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2469,7 +2446,7 @@ class Interpreter<R> {
|
||||||
new InterpreterRuntimeError("Chaining cycle detected for promise.", node).as("TypeError"),
|
new InterpreterRuntimeError("Chaining cycle detected for promise.", node).as("TypeError"),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return result instanceof SandboxPromise ? this.settlePromise(result, node) : Effect.succeed(result)
|
return result instanceof SandboxPromise ? this.settlePromise(result) : Effect.succeed(result)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -2477,7 +2454,7 @@ class Interpreter<R> {
|
||||||
const onFulfilled = name === "then" ? callback(args[0]) : undefined
|
const onFulfilled = name === "then" ? callback(args[0]) : undefined
|
||||||
const onRejected = name === "then" ? callback(args[1]) : name === "catch" ? callback(args[0]) : undefined
|
const onRejected = name === "then" ? callback(args[1]) : name === "catch" ? callback(args[0]) : undefined
|
||||||
const onFinally = name === "finally" ? callback(args[0]) : undefined
|
const onFinally = name === "finally" ? callback(args[0]) : undefined
|
||||||
const settlement = Effect.exit(this.settlePromise(promise, node))
|
const settlement = Effect.exit(this.settlePromise(promise))
|
||||||
|
|
||||||
return Effect.map(
|
return Effect.map(
|
||||||
this.createPromise(
|
this.createPromise(
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import type { Effect, Fiber } from "effect"
|
import type { Effect, Fiber } from "effect"
|
||||||
|
|
||||||
export class SandboxPromise {
|
export class SandboxPromise {
|
||||||
interrupted = false
|
|
||||||
constructor(
|
constructor(
|
||||||
readonly fiber: Fiber.Fiber<unknown, unknown> | undefined,
|
readonly fiber: Fiber.Fiber<unknown, unknown> | undefined,
|
||||||
readonly immediate?: Effect.Effect<unknown, unknown>,
|
readonly immediate?: Effect.Effect<unknown, unknown>,
|
||||||
|
|
|
||||||
|
|
@ -412,45 +412,36 @@ describe("Promise.allSettled", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("Promise.race", () => {
|
describe("Promise.race", () => {
|
||||||
test("first settlement wins and losers are interrupted", async () => {
|
test("first settlement wins and losers continue", async () => {
|
||||||
const trace = makeTrace()
|
const trace = makeTrace()
|
||||||
const result = await value(
|
const result = await value(
|
||||||
`
|
`
|
||||||
const fast = tools.host.sleepy({ id: 1, ms: 10 })
|
const fast = tools.host.sleepy({ id: 1, ms: 10 })
|
||||||
const slow = tools.host.sleepy({ id: 2, ms: 5000 })
|
const slow = tools.host.sleepy({ id: 2, ms: 30 })
|
||||||
return await Promise.race([fast, slow])
|
return await Promise.race([fast, slow])
|
||||||
`,
|
`,
|
||||||
{ trace },
|
{ trace },
|
||||||
)
|
)
|
||||||
expect(result).toBe(1)
|
expect(result).toBe(1)
|
||||||
expect(trace.interrupted).toBe(1)
|
expect(trace.interrupted).toBe(0)
|
||||||
expect(trace.completed).toBe(1)
|
expect(trace.completed).toBe(2)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("awaiting an interrupted loser afterwards is a catchable program failure", async () => {
|
test("a losing chain continues and remains awaitable", async () => {
|
||||||
expect(
|
expect(
|
||||||
await value(`
|
await value(`
|
||||||
const fast = tools.host.sleepy({ id: 1, ms: 10 })
|
const slow = tools.host.sleepy({ id: 2, ms: 30 }).then((id) => id * 2)
|
||||||
const slow = tools.host.sleepy({ id: 2, ms: 5000 })
|
const winner = await Promise.race([slow, "fast"])
|
||||||
const winner = await Promise.race([fast, slow])
|
return [winner, await slow]
|
||||||
try {
|
|
||||||
await slow
|
|
||||||
return "no"
|
|
||||||
} catch (e) {
|
|
||||||
return { winner, caught: e.message }
|
|
||||||
}
|
|
||||||
`),
|
`),
|
||||||
).toEqual({
|
).toEqual(["fast", 4])
|
||||||
winner: 1,
|
|
||||||
caught: "This tool call was interrupted because another value settled a Promise.race first.",
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("a rejection can win the race", async () => {
|
test("a rejection can win the race", async () => {
|
||||||
expect(
|
expect(
|
||||||
await value(`
|
await value(`
|
||||||
try {
|
try {
|
||||||
await Promise.race([tools.host.fail({}), tools.host.sleepy({ id: 1, ms: 5000 })])
|
await Promise.race([tools.host.fail({}), tools.host.sleepy({ id: 1, ms: 30 })])
|
||||||
return "no"
|
return "no"
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return e.message
|
return e.message
|
||||||
|
|
@ -462,9 +453,22 @@ describe("Promise.race", () => {
|
||||||
test("a plain value wins over pending promises", async () => {
|
test("a plain value wins over pending promises", async () => {
|
||||||
const trace = makeTrace()
|
const trace = makeTrace()
|
||||||
expect(
|
expect(
|
||||||
await value(`return await Promise.race([tools.host.sleepy({ id: 1, ms: 5000 }), "immediate"])`, { trace }),
|
await value(`return await Promise.race([tools.host.sleepy({ id: 1, ms: 30 }), "immediate"])`, { trace }),
|
||||||
).toBe("immediate")
|
).toBe("immediate")
|
||||||
expect(trace.interrupted).toBe(1)
|
expect(trace.interrupted).toBe(0)
|
||||||
|
expect(trace.completed).toBe(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("a losing rejection remains handled", async () => {
|
||||||
|
expect(
|
||||||
|
await value(`
|
||||||
|
const rejectLater = async () => {
|
||||||
|
await tools.host.sleepy({ id: 1, ms: 20 })
|
||||||
|
throw new Error("late")
|
||||||
|
}
|
||||||
|
return await Promise.race([rejectLater(), "winner"])
|
||||||
|
`),
|
||||||
|
).toBe("winner")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("an empty race is a clear error instead of hanging", async () => {
|
test("an empty race is a clear error instead of hanging", async () => {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue