···150150- **Interrupt**: "Stop! We're making pizza instead" (cancel the work)
151151152152```typescript
153153-const fiber = yield* fork(longComputation) // Start in background
154154-// ... do other work ...
155155-const result = yield* join(fiber) // Get the result when needed
153153+const result = pipe(
154154+ fork(longComputation), // Start in background
155155+ flatMap(fiber => join(fiber)) // Get the result when needed
156156+)
156157```
157158158159---
···42424343Background job processing with real cancellation and dependency injection.
44444545-**Key concepts:** `fork`/`join`, `catchAll`, `provide` for DI
4545+**Key concepts:** `allSequential`, `timeout`, `retry`, `catchAll`, `provide` for DI
46464747```bash
4848bun run examples/task-queue/without-purus.ts
···6060// TS 5.5+ infers these — `x !== undefined` is a single-step narrowing.
61616262/** Narrows `T | undefined` to `T`. Use with `.filter(isDefined)` or in pipes. */
6363-export const isDefined = <T>(x: T | undefined) => x !== undefined
6363+export const isDefined = <T>(x: T | undefined): x is T => x !== undefined
64646565/** Narrows `T | null` to `T`. */
6666-export const isNotNull = <T>(x: T | null) => x !== null
6666+export const isNotNull = <T>(x: T | null): x is T => x !== null
67676868/** Narrows `T | null | undefined` to `T`. */
6969-export const isNotNullish = <T>(x: T | null | undefined) =>
6969+export const isNotNullish = <T>(x: T | null | undefined): x is NonNullable<T> =>
7070 x !== null && x !== undefined
71717272// =============================================================================
···11import { match } from "../prelude/match"
22import type { Eff, Fiber, FiberId } from "./eff"
33import { makeFiberId } from "./eff"
44-import { type Exit, matchExit } from "./exit"
55-import { blocked, type Step, suspended } from "./step"
44+import { type Exit, interrupted, matchExit } from "./exit"
55+import { blocked, done, type Step, suspended } from "./step"
6677/**
88 * Continuation - what to do with the result of an effect.
···6262 (cb) => {
6363 callback = cb
6464 },
6565- () => handleExit(result!),
6565+ // result is set by the async callback before onComplete fires;
6666+ // if it's somehow null (e.g. early cleanup), treat as interrupted.
6767+ () => (result !== null ? handleExit(result) : done(interrupted("unknown"))),
6668 )
6769 },
6870
+53-2
src/effect/trampoline.ts
···51515252/**
5353 * Create a fiber from an effect.
5454+ *
5555+ * Uses an interrupt-aware trampoline that:
5656+ * - Checks the interrupt flag at each Suspended/Blocked step
5757+ * - Calls the current async cleanup when interrupted
5858+ * - Short-circuits to an Interrupted exit when the flag is set
5459 */
5560const createFiber = <A, E, R>(eff: Eff<A, E, R>, env: R): Fiber<A, E> => {
5661 const id = `fiber-${++fiberCounter}`
···5863 let isInterrupted = false
5964 let exitResult: Option<Exit<A, E>> = none
6065 const awaiters: Array<(exit: Exit<A, E>) => void> = []
6666+ // Track the current async operation's cleanup for cancellation on interrupt.
6767+ // When interrupt() is called, we call this cleanup and force-resolve the
6868+ // blocked promise so the continuation chain (including foldEff/bracket)
6969+ // can still run with the interruption flowing through the error channel.
7070+ let currentCleanup: (() => void) | null = null
7171+ let forceResolveBlocked: (() => void) | null = null
61726273 const makeFiberFn = <A2, E2>(e: Eff<A2, E2, R>, r: R) => createFiber(e, r)
63747575+ // Interrupt-aware trampoline: when interrupted during a Blocked step,
7676+ // calls the async cleanup and force-resumes through the normal
7777+ // continuation chain. This ensures foldEff/bracket handlers still run.
7878+ const run = (step: Step<A, E>): Promise<Exit<A, E>> =>
7979+ match(step)({
8080+ Done: ({ exit }) => {
8181+ currentCleanup = null
8282+ forceResolveBlocked = null
8383+ return Promise.resolve(exit)
8484+ },
8585+8686+ Suspended: ({ resume }) =>
8787+ Promise.resolve().then(() => run(resume())),
8888+8989+ Blocked: ({ cleanup, onComplete, next }) => {
9090+ currentCleanup = cleanup
9191+ return new Promise((resolve) => {
9292+ // When interrupt() is called during a Blocked step, the async
9393+ // cleanup fires (cancelling the operation), and we force-resolve
9494+ // with an Interrupted exit. The next() thunk cannot be used here
9595+ // because it relies on the async callback's result being set.
9696+ forceResolveBlocked = () => {
9797+ forceResolveBlocked = null
9898+ currentCleanup = null
9999+ resolve(interrupted(id) as Exit<A, E>)
100100+ }
101101+ onComplete(() => {
102102+ forceResolveBlocked = null
103103+ currentCleanup = null
104104+ resolve(run(next()))
105105+ })
106106+ })
107107+ },
108108+ })
109109+64110 const initialStep = interpret(
65111 eff,
66112 {
···72118 makeFiberFn,
73119 ) as Step<A, E>
741207575- // Start execution
7676- trampoline(initialStep).then((exit) => {
121121+ // Start execution using the interrupt-aware trampoline
122122+ run(initialStep).then((exit) => {
77123 const finalExit: Exit<A, E> = isInterrupted
78124 ? (interrupted(id) as Exit<A, E>)
79125 : exit
···91137 )(exitResult),
92138 interrupt: () => {
93139 isInterrupted = true
140140+ // Cancel the in-flight async operation and force-resume the
141141+ // continuation chain so foldEff/bracket handlers still run.
142142+ currentCleanup?.()
143143+ currentCleanup = null
144144+ forceResolveBlocked?.()
94145 },
95146 join: (): Eff<A, E, unknown> =>
96147 asyncEff((resume) => {
+4-4
src/prelude/compose.ts
···9090 * @example
9191 * ```typescript
9292 * const result = ifElse(true)(
9393- * () => "was false",
9494- * () => "was true"
9393+ * () => "was true",
9494+ * () => "was false"
9595 * )
9696 * // Returns: "was true"
9797 * ```
9898 */
9999export const ifElse =
100100 <T, E>(predicate: boolean) =>
101101- (onFalse: () => T | E, onTrue: () => T | E): T | E =>
102102- predicate ? onFalse() : onTrue()
101101+ (onTrue: () => T | E, onFalse: () => T | E): T | E =>
102102+ predicate ? onTrue() : onFalse()
103103104104/**
105105 * Pipe a value through a series of transformations.
+23-7
src/prelude/match.ts
···187187 * Unlike match() which works on objects with _tag, this matches
188188 * primitive literal values directly.
189189 *
190190+ * Exhaustive form requires a handler for every literal in the union.
191191+ * Partial form requires a defaultCase for unhandled values.
192192+ *
190193 * @example
191194 * ```typescript
192195 * type Direction = "north" | "south" | "east" | "west"
···202205 *
203206 * @example
204207 * ```typescript
205205- * // With a default case:
206206- * const dayType = matchLiteral(day)({
208208+ * // With a default case for partial matches:
209209+ * const dayType = matchLiteralOr(day)("weekday")({
207210 * saturday: "weekend",
208211 * sunday: "weekend",
209209- * }, "weekday")
212212+ * })
210213 * ```
211214 */
212215export const matchLiteral =
213216 <T extends string | number | boolean>(value: T) =>
214214- <R>(cases: { [K in T & PropertyKey]: R }, defaultCase?: R): R =>
215215- (value as PropertyKey) in cases
216216- ? cases[value as T & PropertyKey]
217217- : (defaultCase as R)
217217+ <R>(cases: { [K in T & PropertyKey]: R }): R =>
218218+ cases[value as T & PropertyKey]
219219+220220+/**
221221+ * Match on literal values with a default for unhandled cases.
222222+ *
223223+ * Unlike matchLiteral() which requires exhaustive cases,
224224+ * this allows partial matches with a fallback value.
225225+ */
226226+export const matchLiteralOr =
227227+ <T extends string | number | boolean>(value: T) =>
228228+ <R>(defaultCase: R) =>
229229+ (cases: Partial<{ [K in T & PropertyKey]: R }>): R => {
230230+ // Safe: `in` check guarantees key exists; Partial makes type R | undefined but runtime value is R
231231+ const record = cases as Record<PropertyKey, R>
232232+ return (value as PropertyKey) in cases ? record[value as PropertyKey]! : defaultCase
233233+ }
+1-1
src/prelude/result.ts
···256256 const results: B[] = []
257257 for (const a of as) {
258258 const r = f(a)
259259- if (r._tag === "Err") return r as unknown as Result<readonly B[], E>
259259+ if (r._tag === "Err") return r
260260 results.push(r.value)
261261 }
262262 return ok(results)