···4242export const sync = <A>(f: () => A): Eff<A, never, unknown> =>
4343 ({ _tag: "Sync", f })
44444545+/**
4646+ * ESCAPE HATCH: Bridges callback-based async APIs into the effect system.
4747+ *
4848+ * JavaScript's world is full of callback-based APIs (setTimeout, event listeners,
4949+ * Node.js callbacks). This function lets you wrap them into pure Eff values that
5050+ * can be composed, sequenced, raced, and cancelled like any other effect.
5151+ *
5252+ * The register function receives a `resume` callback. Call it with an Exit when
5353+ * the async operation completes. Return a cleanup function for cancellation.
5454+ *
5555+ * Use this to integrate external async APIs. For Promises, prefer `fromPromise`.
5656+ *
5757+ * @example
5858+ * // Wrap setTimeout
5959+ * const delay = (ms: number): Eff<void, never, unknown> =>
6060+ * async(resume => {
6161+ * const id = setTimeout(() => resume({ _tag: "Success", value: undefined }), ms)
6262+ * return () => clearTimeout(id) // cleanup on cancellation
6363+ * })
6464+ *
6565+ * // Wrap event listener
6666+ * const onClick = (el: HTMLElement): Eff<MouseEvent, never, unknown> =>
6767+ * async(resume => {
6868+ * const handler = (e: MouseEvent) => resume({ _tag: "Success", value: e })
6969+ * el.addEventListener("click", handler)
7070+ * return () => el.removeEventListener("click", handler)
7171+ * })
7272+ */
4573export const async = <A, E>(
4674 register: (resume: (exit: Exit<A, E>) => void) => Cleanup
4775): Eff<A, E, unknown> =>
+20-8
src/effect/trampoline.ts
···99import { interpret } from "./interpret"
10101111/**
1212- * ESCAPE HATCH: Trampoline - the boundary between pure interpretation and JS execution.
1212+ * ESCAPE HATCH: The boundary between pure effect interpretation and JS execution.
1313 *
1414- * This is one of three documented escape hatches in purus. The trampoline:
1515- * 1. Takes pure Step values from the interpreter
1616- * 2. Executes them using JavaScript's runtime (Promises, event loop)
1717- * 3. Returns a Promise with the final Exit
1414+ * Eff values are pure data - they describe WHAT to do, not HOW. The trampoline
1515+ * is where we actually DO it, using JavaScript's runtime (Promises, event loop).
1816 *
1919- * EDUCATIONAL NOTE: In a pure language, this would be handled by the runtime.
2020- * In JS, we must have this impure boundary somewhere. By isolating it here,
2121- * all other code remains pure and testable.
1717+ * This is the heart of "effects as data": the interpreter (interpret.ts) produces
1818+ * pure Step values, and the trampoline executes them. By isolating all impurity
1919+ * here, the rest of purus remains pure, composable, and testable.
2020+ *
2121+ * In a language like Haskell, the runtime handles this. In JS, we must build it.
2222+ *
2323+ * The trampoline handles three cases:
2424+ * - Done: Return the final Exit value
2525+ * - Suspended: Yield to event loop (prevents stack overflow), then continue
2626+ * - Blocked: Wait for async operation, then continue
2727+ *
2828+ * @example
2929+ * // You rarely call this directly - use runPromise, runFiber, etc.
3030+ * // But understanding it helps you understand the effect model:
3131+ * const step = interpret(myEffect, ctx, makeFiber)
3232+ * const exit = await trampoline(step)
3333+ * // exit: Exit<A, E> - Success, Failure, or Interrupted
2234 */
2335export const trampoline = <A, E>(step: Step<A, E>): Promise<Exit<A, E>> =>
2436 match(step)({
+20-3
src/prelude/result.ts
···3737 result._tag === "Ok" ? onOk(result.value) : onErr(result.error)
38383939/**
4040- * ESCAPE HATCH: Catches JavaScript exceptions at the boundary.
4141- * JS uses exceptions for errors, but FP uses Result. This bridges that gap.
4242- * Inside purus, we use Result - this is only for external JS interop.
4040+ * ESCAPE HATCH: Bridges thrown exceptions to Result values.
4141+ *
4242+ * JavaScript throws exceptions for errors, but pure FP treats errors as values.
4343+ * This function catches any thrown exception and converts it to Err, letting
4444+ * you safely call impure JS code from your pure functional core.
4545+ *
4646+ * Use this at the boundary between impure JS libraries and your pure code.
4747+ * Inside purus, all error handling uses Result - no exceptions thrown.
4848+ *
4949+ * @example
5050+ * // Safely parse untrusted JSON
5151+ * const result = tryCatch(() => JSON.parse(userInput))
5252+ * // Result<unknown, unknown> - exception becomes Err, no throws!
5353+ *
5454+ * // Chain with Result operations
5555+ * pipe(
5656+ * tryCatch(() => riskyOperation()),
5757+ * mapResult(transform),
5858+ * unwrapOr(defaultValue)
5959+ * )
4360 */
4461export const tryCatch = <T>(f: () => T): Result<T, unknown> => {
4562 try {