···4949 const handleExit = matchExit<A, E, Step<unknown, unknown>>(
5050 (value) => cont.onSuccess(value),
5151 (error) => cont.onFailure(error),
5252+ // Route interruption through the error channel so Fold handlers
5353+ // (bracket release) still run. The cast is safe: this error value
5454+ // is ephemeral — the fiber-level isInterrupted flag overrides the
5555+ // final exit to Interrupted before it reaches user code.
5256 () => cont.onFailure("interrupted" as E),
5357 )
5458···5761 return handleExit(result)
5862 }
59636464+ // When interrupted, set result to Interrupted exit and fire callback
6565+ // so the continuation chain (including Fold/bracket) runs normally.
6666+ const onInterrupt = () => {
6767+ if (result === null) {
6868+ result = interrupted("interrupted") as Exit<A, E>
6969+ callback?.()
7070+ }
7171+ }
7272+6073 return blocked(
6174 cleanup,
7575+ onInterrupt,
6276 (cb) => {
6377 callback = cb
6478 },
6579 // result is set by the async callback before onComplete fires;
6680 // if it's somehow null (e.g. early cleanup), treat as interrupted.
6767- () => (result !== null ? handleExit(result) : done(interrupted("unknown"))),
8181+ () =>
8282+ result !== null ? handleExit(result) : done(interrupted("unknown")),
6883 )
6984 },
7085
···6363 let isInterrupted = false
6464 let exitResult: Option<Exit<A, E>> = none
6565 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.
6666+ // Track the current async operation's cleanup and interrupt signal.
6767+ // When interrupt() is called, we call cleanup (cancels the async op)
6868+ // then onInterrupt (sets result to Interrupted and fires callback),
6969+ // so the continuation chain (including foldEff/bracket) runs normally.
7070 let currentCleanup: (() => void) | null = null
7171- let forceResolveBlocked: (() => void) | null = null
7171+ let currentOnInterrupt: (() => void) | null = null
72727373 const makeFiberFn = <A2, E2>(e: Eff<A2, E2, R>, r: R) => createFiber(e, r)
7474···7979 match(step)({
8080 Done: ({ exit }) => {
8181 currentCleanup = null
8282- forceResolveBlocked = null
8282+ currentOnInterrupt = null
8383 return Promise.resolve(exit)
8484 },
85858686- Suspended: ({ resume }) =>
8787- Promise.resolve().then(() => run(resume())),
8686+ Suspended: ({ resume }) => Promise.resolve().then(() => run(resume())),
88878989- Blocked: ({ cleanup, onComplete, next }) => {
8888+ Blocked: ({ cleanup, onInterrupt, onComplete, next }) => {
9089 currentCleanup = cleanup
9090+ currentOnInterrupt = onInterrupt
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- }
10192 onComplete(() => {
102102- forceResolveBlocked = null
10393 currentCleanup = null
9494+ currentOnInterrupt = null
10495 resolve(run(next()))
10596 })
10697 })
···137128 )(exitResult),
138129 interrupt: () => {
139130 isInterrupted = true
140140- // Cancel the in-flight async operation and force-resume the
141141- // continuation chain so foldEff/bracket handlers still run.
131131+ // Cancel the in-flight async operation, then signal interruption
132132+ // through the continuation chain so foldEff/bracket handlers run.
142133 currentCleanup?.()
143134 currentCleanup = null
144144- forceResolveBlocked?.()
135135+ currentOnInterrupt?.()
136136+ currentOnInterrupt = null
145137 },
146138 join: (): Eff<A, E, unknown> =>
147139 asyncEff((resume) => {
+6-17
src/optics/bridges.ts
···22 * Pre-built optics for purus-ts data types.
33 *
44 * Bridges connect the optics module to Result, Option, and Validation,
55- * providing ready-to-use Prisms for each variant and a Traversal for arrays.
55+ * providing ready-to-use Prisms for each variant.
66 *
77 * @module optics/bridges
88 */
991010-import type { Result } from "../prelude/result"
1111-import type { Option } from "../prelude/option"
1210import type { Validation } from "../data/validation"
1313-import type { Prism, Traversal } from "./types"
1414-import { ok, err } from "../prelude/result"
1515-import { some, none } from "../prelude/option"
1611import { valid } from "../data/validation"
1212+import type { Option } from "../prelude/option"
1313+import { none, some } from "../prelude/option"
1414+import type { Result } from "../prelude/result"
1515+import { err, ok } from "../prelude/result"
1716import { prism } from "./prism"
1818-import { traversal } from "./traversal"
1717+import type { Prism } from "./types"
19182019/**
2120 * Prism focusing on the Ok variant of a Result.
···5655 (v) => (v._tag === "Valid" ? some(v.value) : none),
5756 (a) => valid(a),
5857 )
5959-6060-/**
6161- * Traversal focusing on every element of a readonly array.
6262- * getAll returns all elements; modify applies a function to each element.
6363- */
6464-export const eachTraversal = <A>(): Traversal<readonly A[], A> =>
6565- traversal(
6666- (s) => s,
6767- (f) => (s) => s.map(f),
6868- )