···47474848| Article | Description |
4949|---------------------------------------------------------|--------------------------------------------------------|
5050-| [Why Errors as Values?](./concepts/errors-as-values.md) | The exception problem, railway oriented programming |
5151-| [Branded Types In Depth](./concepts/branded-types.md) | Phantom types, smart constructors, production patterns |
5252-| Typestate Pattern | State machines in the type system |
5353-| Effect Composition | Building complex effects from simple ones |
5454-| Fiber Internals | How the runtime works under the hood |
5555-| Dependency Injection Patterns | Testing, layered environments |
5656-| Testing Strategies | Unit testing effectful code |
5050+| [Why Errors as Values?](./concepts/01-errors-as-values.md) | The exception problem, railway oriented programming |
5151+| [Branded Types In Depth](./concepts/02-branded-types.md) | Phantom types, smart constructors, production patterns |
5252+| [Typestate Pattern](./concepts/05-typestate-pattern.md) | State machines in the type system |
5353+| [Effect Composition](./concepts/06-effect-composition.md) | Building complex effects from simple ones |
5454+| [Fiber Internals](./concepts/07-fiber-internals.md) | How the runtime works under the hood |
5555+| [Dependency Injection Patterns](./concepts/08-dependency-injection-patterns.md) | Testing, layered environments |
5656+| [Testing Strategies](./concepts/09-testing-strategies.md) | Unit testing effectful code |
57575858---
5959
+1-1
docs/guides/concepts/02-branded-types.md
···435435436436- [Tutorial Chapter 6: Branded Types](../tutorial/06-branded-types.md) - Hands-on introduction
437437- [Workflow Engine Example](../../../examples/workflow-engine/) - Branded types in action
438438-- [Typestate Pattern](./typestate-pattern.md) - Encoding state machines with phantom types
438438+- [Typestate Pattern](./05-typestate-pattern.md) - Encoding state machines with phantom types
···684684685685- [Validation and Error Accumulation](./03-validation-and-error-accumulation.md) - Applicative in action
686686- [Why Errors as Values?](./01-errors-as-values.md) - Result fundamentals
687687-- [Effect Composition](./effect-composition.md) - How effects compose internally
687687+- [Effect Composition](./06-effect-composition.md) - How effects compose internally
688688- [Tutorial Chapter 3: Typed Errors with Result](../tutorial/03-typed-errors-with-result.md) - Hands-on Result practice
+323
docs/guides/concepts/05-typestate-pattern.md
···11+# Typestate Pattern
22+33+This article explains how to encode state machines in the type system, making invalid state transitions impossible at compile time.
44+55+---
66+77+## The Problem: Runtime State Checks
88+99+Consider a document workflow where documents move from Draft to Review to Published:
1010+1111+```typescript
1212+interface Document {
1313+ id: string
1414+ content: string
1515+ status: "draft" | "review" | "published"
1616+}
1717+1818+const submitForReview = (doc: Document): Document => {
1919+ if (doc.status !== "draft") {
2020+ throw new Error("Can only submit drafts for review")
2121+ }
2222+ return { ...doc, status: "review" }
2323+}
2424+2525+const publish = (doc: Document): Document => {
2626+ if (doc.status !== "review") {
2727+ throw new Error("Can only publish reviewed documents")
2828+ }
2929+ return { ...doc, status: "published" }
3030+}
3131+```
3232+3333+This works, but has problems:
3434+3535+1. **Errors are runtime** - Invalid transitions crash the program
3636+2. **Checks are manual** - Each function must validate state
3737+3. **Errors are invisible** - TypeScript can't warn you about `publish(draftDoc)`
3838+4. **Tests must cover every path** - You need tests for each invalid transition
3939+4040+What if the type system could enforce valid transitions?
4141+4242+---
4343+4444+## Typestate: States in the Type System
4545+4646+Typestate encodes the current state as a type parameter. Different states are different types, and only valid transitions compile.
4747+4848+```typescript
4949+// Draft documents and Published documents are different types
5050+type DraftDoc = Document & { __state: "draft" }
5151+type PublishedDoc = Document & { __state: "published" }
5252+5353+// This function ONLY accepts drafts
5454+const submitForReview = (doc: DraftDoc): ReviewDoc => ...
5555+5656+// TypeScript error: PublishedDoc is not DraftDoc
5757+submitForReview(publishedDoc)
5858+```
5959+6060+The state lives in the type, not just the data. Invalid transitions fail at compile time.
6161+6262+---
6363+6464+## The Entity<T, S> Type
6565+6666+purus provides `Entity<T, S>` for typestate:
6767+6868+```typescript
6969+import { type Entity, entity, transition } from "purus-ts"
7070+7171+// Entity<T, S> = T plus a phantom state tag
7272+type Entity<T, S extends string> = T & State<S>
7373+```
7474+7575+The `State<S>` is a phantom type - it exists only in the type system and has zero runtime cost. An `Entity<Document, "draft">` is just a `Document` at runtime.
7676+7777+### Creating Entities
7878+7979+Use `entity()` to create an entity in an initial state:
8080+8181+```typescript
8282+interface Order {
8383+ id: string
8484+ items: string[]
8585+ total: number
8686+}
8787+8888+// Create an order in "pending" state
8989+const newOrder = entity<Order, "pending">({
9090+ id: "order-123",
9191+ items: ["item-1"],
9292+ total: 100
9393+})
9494+// Type: Entity<Order, "pending">
9595+```
9696+9797+---
9898+9999+## Defining Transitions
100100+101101+The `transition()` function creates type-safe state transitions:
102102+103103+```typescript
104104+import { transition } from "purus-ts"
105105+106106+// transition<T, From, To> creates a function
107107+// that converts Entity<T, From> to Entity<T, To>
108108+109109+const confirm = transition<Order, "pending", "confirmed">()
110110+const ship = transition<Order, "confirmed", "shipped">()
111111+const deliver = transition<Order, "shipped", "delivered">()
112112+```
113113+114114+Now TypeScript enforces the workflow:
115115+116116+```typescript
117117+const pending = entity<Order, "pending">({ ... })
118118+119119+const confirmed = confirm(pending) // Entity<Order, "confirmed">
120120+const shipped = ship(confirmed) // Entity<Order, "shipped">
121121+const delivered = deliver(shipped) // Entity<Order, "delivered">
122122+123123+// Compile errors:
124124+ship(pending) // Error: "pending" is not "confirmed"
125125+deliver(pending) // Error: "pending" is not "shipped"
126126+confirm(shipped) // Error: "shipped" is not "pending"
127127+```
128128+129129+### Transitions with Transformations
130130+131131+Often state transitions also modify the data. Pass a transform function:
132132+133133+```typescript
134134+interface Order {
135135+ id: string
136136+ items: string[]
137137+ shippedAt?: Date
138138+ deliveredAt?: Date
139139+}
140140+141141+const ship = transition<Order, "confirmed", "shipped">(order => ({
142142+ ...order,
143143+ shippedAt: new Date()
144144+}))
145145+146146+const deliver = transition<Order, "shipped", "delivered">(order => ({
147147+ ...order,
148148+ deliveredAt: new Date()
149149+}))
150150+```
151151+152152+---
153153+154154+## Real Example: Order Workflow
155155+156156+Here's a complete e-commerce order workflow:
157157+158158+```typescript
159159+import { type Entity, entity, transition } from "purus-ts"
160160+161161+// State types for documentation
162162+type Pending = "pending"
163163+type Confirmed = "confirmed"
164164+type Shipped = "shipped"
165165+type Delivered = "delivered"
166166+type Cancelled = "cancelled"
167167+168168+interface Order {
169169+ id: string
170170+ customerId: string
171171+ items: Array<{ productId: string; quantity: number }>
172172+ confirmedAt?: Date
173173+ shippedAt?: Date
174174+ deliveredAt?: Date
175175+ cancelledAt?: Date
176176+}
177177+178178+// Transitions
179179+const confirmOrder = transition<Order, Pending, Confirmed>(order => ({
180180+ ...order,
181181+ confirmedAt: new Date()
182182+}))
183183+184184+const shipOrder = transition<Order, Confirmed, Shipped>(order => ({
185185+ ...order,
186186+ shippedAt: new Date()
187187+}))
188188+189189+const deliverOrder = transition<Order, Shipped, Delivered>(order => ({
190190+ ...order,
191191+ deliveredAt: new Date()
192192+}))
193193+194194+// Cancellation can happen from pending or confirmed
195195+const cancelPending = transition<Order, Pending, Cancelled>(order => ({
196196+ ...order,
197197+ cancelledAt: new Date()
198198+}))
199199+200200+const cancelConfirmed = transition<Order, Confirmed, Cancelled>(order => ({
201201+ ...order,
202202+ cancelledAt: new Date()
203203+}))
204204+205205+// Usage
206206+const newOrder = entity<Order, Pending>({
207207+ id: "order-123",
208208+ customerId: "cust-456",
209209+ items: [{ productId: "prod-789", quantity: 2 }]
210210+})
211211+212212+// Happy path
213213+const confirmed = confirmOrder(newOrder)
214214+const shipped = shipOrder(confirmed)
215215+const delivered = deliverOrder(shipped)
216216+217217+// Or cancel early
218218+const cancelled = cancelPending(newOrder)
219219+// const cancelTooLate = cancelPending(shipped) // Error!
220220+```
221221+222222+---
223223+224224+## Real Example: Document Workflow
225225+226226+A publishing workflow with approval steps:
227227+228228+```typescript
229229+interface Document {
230230+ title: string
231231+ content: string
232232+ author: string
233233+ reviewNotes?: string
234234+ publishedAt?: Date
235235+}
236236+237237+type Draft = "draft"
238238+type Review = "review"
239239+type Approved = "approved"
240240+type Published = "published"
241241+type Rejected = "rejected"
242242+243243+// Transitions
244244+const submitForReview = transition<Document, Draft, Review>()
245245+246246+const approve = transition<Document, Review, Approved>(doc => ({
247247+ ...doc,
248248+ reviewNotes: "Approved for publication"
249249+}))
250250+251251+const reject = transition<Document, Review, Rejected>(doc => ({
252252+ ...doc,
253253+ reviewNotes: "Needs revision"
254254+}))
255255+256256+const publish = transition<Document, Approved, Published>(doc => ({
257257+ ...doc,
258258+ publishedAt: new Date()
259259+}))
260260+261261+// Rejected docs can be revised back to draft
262262+const revise = transition<Document, Rejected, Draft>(doc => ({
263263+ ...doc,
264264+ reviewNotes: undefined
265265+}))
266266+267267+// Type-safe workflow
268268+const draft = entity<Document, Draft>({
269269+ title: "My Article",
270270+ content: "...",
271271+ author: "Alice"
272272+})
273273+274274+const inReview = submitForReview(draft)
275275+const approved = approve(inReview)
276276+const published = publish(approved)
277277+278278+// publish(draft) would not compile!
279279+```
280280+281281+---
282282+283283+## When to Use Typestate
284284+285285+### Good Candidates
286286+287287+- **Order processing** - pending, paid, shipped, delivered
288288+- **Document workflows** - draft, review, approved, published
289289+- **Connection states** - disconnected, connecting, connected
290290+- **Authentication** - anonymous, authenticated, admin
291291+- **Resource lifecycle** - created, initialized, active, disposed
292292+293293+### Signs You Need Typestate
294294+295295+- Runtime "invalid state" errors
296296+- Functions that check status before proceeding
297297+- State machines with strict transition rules
298298+- Bugs from calling methods in wrong order
299299+300300+### When to Skip
301301+302302+- Simple boolean flags (use branded types instead)
303303+- States that can transition freely
304304+- Prototyping (add rigor later)
305305+306306+---
307307+308308+## Key Takeaways
309309+310310+1. **Typestate encodes state in types** - Different states are different types
311311+2. **Invalid transitions don't compile** - No runtime checks needed
312312+3. **Zero runtime cost** - Phantom types exist only in the type system
313313+4. **Use `entity()` for creation** - Creates an entity in initial state
314314+5. **Use `transition()` for state changes** - Type-safe, with optional transform
315315+316316+Typestate turns runtime errors into compile-time errors. The type system enforces your state machine, so invalid workflows are impossible to write.
317317+318318+---
319319+320320+## See Also
321321+322322+- [Branded Types In Depth](./02-branded-types.md) - Foundation for phantom types
323323+- [Workflow Engine Example](../../../examples/workflow-engine/) - Full order processing system
+345
docs/guides/concepts/06-effect-composition.md
···11+# Effect Composition
22+33+This article explains how purus effects compose together, building complex programs from simple building blocks.
44+55+---
66+77+## Effects as Data
88+99+The key insight in purus is that effects are data, not computation.
1010+1111+When you write:
1212+1313+```typescript
1414+const program = pipe(
1515+ succeed(10),
1616+ flatMap(n => succeed(n * 2)),
1717+ mapEff(n => n.toString())
1818+)
1919+```
2020+2121+Nothing runs. You've built a data structure:
2222+2323+```
2424+FlatMap {
2525+ effect: FlatMap {
2626+ effect: Succeed { value: 10 }
2727+ f: n => Succeed { value: n * 2 }
2828+ }
2929+ f: n => Succeed { value: n.toString() }
3030+}
3131+```
3232+3333+The `Eff<A, E, R>` type is a discriminated union describing WHAT to do:
3434+3535+```typescript
3636+type Eff<A, E, R> =
3737+ | { _tag: "Succeed"; value: A } // Return a value
3838+ | { _tag: "Fail"; error: E } // Fail with error
3939+ | { _tag: "Sync"; f: () => A } // Run sync function
4040+ | { _tag: "Async"; register: ... } // Async operation
4141+ | { _tag: "FlatMap"; effect: ...; f: ...} // Sequence two effects
4242+ | { _tag: "Fold"; ... } // Handle success/failure
4343+ | { _tag: "Access"; f: (r: R) => A } // Read environment
4444+ | { _tag: "Provide"; effect: ...; env } // Supply environment
4545+ | { _tag: "Fork"; effect: ... } // Run in background
4646+```
4747+4848+This is the "free monad" pattern simplified: you build a tree of instructions, then an interpreter executes them.
4949+5050+### Why Effects as Data?
5151+5252+1. **Lazy** - Nothing runs until you call `runPromise`
5353+2. **Composable** - Effects combine without executing
5454+3. **Testable** - You can inspect the tree structure
5555+4. **Refactorable** - The same effect can be run, logged, mocked, or transformed
5656+5757+---
5858+5959+## The FlatMap Pattern
6060+6161+`flatMap` is the fundamental composition operator. It sequences two effects where the second depends on the first.
6262+6363+```typescript
6464+import { succeed, flatMap, pipe } from "purus-ts"
6565+6666+// flatMap: (a: A) => Eff<B, E, R> => Eff<A, E, R> => Eff<B, E, R>
6767+6868+const program = pipe(
6969+ succeed(10),
7070+ flatMap(n => succeed(n * 2)),
7171+ flatMap(n => succeed(n + 5))
7272+)
7373+// Type: Eff<number, never, unknown>
7474+// Result when run: 25
7575+```
7676+7777+### flatMap vs map
7878+7979+- `mapEff` transforms the success value with a pure function
8080+- `flatMap` transforms with a function that returns an effect
8181+8282+```typescript
8383+// mapEff: transform value (A => B)
8484+pipe(succeed(10), mapEff(n => n * 2))
8585+8686+// flatMap: transform to new effect (A => Eff<B, E, R>)
8787+pipe(succeed(10), flatMap(n => fetchUser(n)))
8888+```
8989+9090+Use `flatMap` when the next step requires an effect (async, may fail, needs environment).
9191+9292+### Short-Circuit on Error
9393+9494+If any effect in the chain fails, subsequent `flatMap` calls are skipped:
9595+9696+```typescript
9797+const program = pipe(
9898+ succeed(10),
9999+ flatMap(n => fail({ _tag: "Error" })), // Fails here
100100+ flatMap(n => succeed(n * 2)), // Skipped
101101+ flatMap(n => succeed(n.toString())) // Skipped
102102+)
103103+// Type: Eff<string, { _tag: "Error" }, unknown>
104104+// Result: Failure({ _tag: "Error" })
105105+```
106106+107107+This is "railway oriented programming" - the error channel runs parallel to the success channel.
108108+109109+---
110110+111111+## Sequencing with pipe()
112112+113113+`pipe()` enables readable left-to-right composition:
114114+115115+```typescript
116116+// Without pipe (nested, inside-out):
117117+mapEff(toString)(flatMap(n => succeed(n + 5))(mapEff(n => n * 2)(succeed(10))))
118118+119119+// With pipe (sequential, top-to-bottom):
120120+pipe(
121121+ succeed(10),
122122+ mapEff(n => n * 2),
123123+ flatMap(n => succeed(n + 5)),
124124+ mapEff(toString)
125125+)
126126+```
127127+128128+`pipe()` works with any functions, not just effects:
129129+130130+```typescript
131131+const result = pipe(
132132+ " hello world ",
133133+ s => s.trim(),
134134+ s => s.toUpperCase(),
135135+ s => s.split(" ")
136136+)
137137+// ["HELLO", "WORLD"]
138138+```
139139+140140+### flow() for Reusable Pipelines
141141+142142+`flow()` creates a function instead of applying immediately:
143143+144144+```typescript
145145+import { flow, mapEff, flatMap } from "purus-ts"
146146+147147+// Create reusable transformation
148148+const processNumber = flow(
149149+ mapEff((n: number) => n * 2),
150150+ flatMap(n => succeed(n + 5)),
151151+ mapEff(toString)
152152+)
153153+154154+// Use it
155155+const result1 = processNumber(succeed(10)) // "25"
156156+const result2 = processNumber(succeed(20)) // "45"
157157+```
158158+159159+---
160160+161161+## Building Custom Combinators
162162+163163+Compose basic operations into domain-specific combinators:
164164+165165+### Conditional Execution
166166+167167+```typescript
168168+const when = <A, E, R>(
169169+ condition: boolean,
170170+ effect: Eff<A, E, R>
171171+): Eff<A | void, E, R> =>
172172+ condition ? effect : succeed(undefined)
173173+174174+// Usage
175175+pipe(
176176+ when(user.isAdmin, deleteAllData()),
177177+ flatMap(() => succeed("Done"))
178178+)
179179+```
180180+181181+### Fallback Chain
182182+183183+```typescript
184184+const firstSuccess = <A, E, R>(
185185+ effects: Array<Eff<A, E, R>>
186186+): Eff<A, E, R> =>
187187+ effects.reduce((acc, eff) =>
188188+ pipe(acc, catchAll(() => eff))
189189+ )
190190+191191+// Try each in order, return first success
192192+const result = firstSuccess([
193193+ fetchFromCache(key),
194194+ fetchFromApi(key),
195195+ fetchFromBackup(key)
196196+])
197197+```
198198+199199+### Logging Wrapper
200200+201201+```typescript
202202+const withLogging = <A, E, R>(
203203+ label: string,
204204+ effect: Eff<A, E, R>
205205+): Eff<A, E, R> =>
206206+ pipe(
207207+ sync(() => console.log(`Starting: ${label}`)),
208208+ flatMap(() => effect),
209209+ flatMap(a => pipe(
210210+ sync(() => console.log(`Completed: ${label}`)),
211211+ mapEff(() => a)
212212+ ))
213213+ )
214214+215215+// Usage
216216+const program = withLogging("fetch user", fetchUser(id))
217217+```
218218+219219+---
220220+221221+## Error Channel Composition
222222+223223+Effects have two channels: success (`A`) and error (`E`). Both compose:
224224+225225+### Widening Error Types
226226+227227+```typescript
228228+type DbError = { _tag: "DbError"; message: string }
229229+type ApiError = { _tag: "ApiError"; status: number }
230230+231231+// This effect can fail with DbError
232232+const getUser: Eff<User, DbError, unknown> = ...
233233+234234+// This effect can fail with ApiError
235235+const sendEmail: Eff<void, ApiError, unknown> = ...
236236+237237+// Composed effect can fail with either
238238+const program: Eff<void, DbError | ApiError, unknown> = pipe(
239239+ getUser,
240240+ flatMap(user => sendEmail(user.email))
241241+)
242242+```
243243+244244+TypeScript unions the error types automatically.
245245+246246+### Recovering from Errors
247247+248248+Use `catchAll` to handle errors and continue:
249249+250250+```typescript
251251+const withFallback = pipe(
252252+ getFromCache(key),
253253+ catchAll(error => getFromDatabase(key))
254254+)
255255+// Type: Eff<Value, DbError, unknown>
256256+// (CacheError is handled, DbError remains)
257257+```
258258+259259+### Transforming Errors
260260+261261+Use `foldEff` to handle both channels:
262262+263263+```typescript
264264+const normalized = pipe(
265265+ effect,
266266+ foldEff(
267267+ error => fail({ _tag: "AppError", cause: error }),
268268+ value => succeed(value)
269269+ )
270270+)
271271+```
272272+273273+---
274274+275275+## Common Patterns
276276+277277+### Sequential Operations
278278+279279+Use `flatMap` chain or array reduce:
280280+281281+```typescript
282282+// Chain
283283+const sequential = pipe(
284284+ step1,
285285+ flatMap(() => step2),
286286+ flatMap(() => step3)
287287+)
288288+289289+// From array
290290+const steps = [step1, step2, step3]
291291+const sequential = steps.reduce(
292292+ (acc, step) => flatMap(() => step)(acc),
293293+ succeed(undefined)
294294+)
295295+```
296296+297297+### Parallel Operations
298298+299299+Use `all` combinator:
300300+301301+```typescript
302302+import { all } from "purus-ts"
303303+304304+const parallel = all([
305305+ fetchUser(id),
306306+ fetchOrders(id),
307307+ fetchPreferences(id)
308308+])
309309+// Type: Eff<[User, Order[], Preferences], Error, unknown>
310310+```
311311+312312+### Racing
313313+314314+Use `race` for first-to-complete:
315315+316316+```typescript
317317+import { race } from "purus-ts"
318318+319319+const fastest = race([
320320+ fetchFromServer1(key),
321321+ fetchFromServer2(key)
322322+])
323323+// Returns whichever completes first
324324+```
325325+326326+---
327327+328328+## Key Takeaways
329329+330330+1. **Effects are data** - They describe computation, they don't run it
331331+2. **flatMap sequences effects** - The fundamental composition operator
332332+3. **pipe() enables readability** - Top-to-bottom instead of inside-out
333333+4. **Errors short-circuit** - Failed effects skip subsequent flatMaps
334334+5. **Build custom combinators** - Compose basics into domain operations
335335+6. **Both channels compose** - Success types narrow, error types widen
336336+337337+Understanding effect composition is the key to productive purus usage. Once you see effects as composable data, complex programs become pipelines of simple transformations.
338338+339339+---
340340+341341+## See Also
342342+343343+- [Why Errors as Values?](./01-errors-as-values.md) - Railway oriented programming
344344+- [Fiber Internals](./07-fiber-internals.md) - How effects actually execute
345345+- [HTTP Client Example](../../../examples/http-client/) - Real-world effect composition
+341
docs/guides/concepts/07-fiber-internals.md
···11+# Fiber Internals
22+33+This article explains how purus executes effects under the hood: the trampoline pattern, stack safety, and fiber lifecycle.
44+55+---
66+77+## Why a Custom Runtime?
88+99+JavaScript Promises run immediately and can't be cancelled. But purus effects are:
1010+1111+- **Lazy** - Nothing runs until you call `runPromise`
1212+- **Cancellable** - Fibers can be interrupted mid-execution
1313+- **Stack-safe** - Deep recursion doesn't overflow the stack
1414+1515+To achieve this, purus implements its own runtime. The key insight: separate description (Eff) from execution (Trampoline).
1616+1717+```
1818+Eff (Data) Step (Interpreter) Trampoline (Execution)
1919+------------ ----------------- ----------------------
2020+ Succeed --> Done(Success) --> Promise.resolve(value)
2121+ FlatMap --> Suspended(next) --> Promise.resolve().then(...)
2222+ Async --> Blocked(...) --> new Promise(...)
2323+```
2424+2525+---
2626+2727+## The Architecture
2828+2929+purus execution has three layers:
3030+3131+### 1. Eff - The Effect AST
3232+3333+Effects are a tree of nodes describing what to do:
3434+3535+```typescript
3636+type Eff<A, E, R> =
3737+ | { _tag: "Succeed"; value: A }
3838+ | { _tag: "Fail"; error: E }
3939+ | { _tag: "FlatMap"; effect: Eff<...>; f: ... }
4040+ | { _tag: "Async"; register: ... }
4141+ // ... more variants
4242+```
4343+4444+### 2. Step - Interpreter Output
4545+4646+The interpreter transforms Eff into Step, describing what to do next:
4747+4848+```typescript
4949+type Step<A, E> =
5050+ | { _tag: "Done"; exit: Exit<A, E> } // Finished
5151+ | { _tag: "Suspended"; resume: () => Step } // More work (sync)
5252+ | { _tag: "Blocked"; onComplete: ...; next } // Waiting (async)
5353+```
5454+5555+### 3. Trampoline - Actual Execution
5656+5757+The trampoline turns Steps into Promises, using the JavaScript event loop:
5858+5959+```typescript
6060+const trampoline = <A, E>(step: Step<A, E>): Promise<Exit<A, E>> =>
6161+ match(step)({
6262+ Done: ({ exit }) => Promise.resolve(exit),
6363+ Suspended: ({ resume }) => Promise.resolve().then(() => trampoline(resume())),
6464+ Blocked: ({ onComplete, next }) =>
6565+ new Promise(resolve => onComplete(() => resolve(trampoline(next()))))
6666+ })
6767+```
6868+6969+---
7070+7171+## The Trampoline Pattern
7272+7373+### The Stack Overflow Problem
7474+7575+Consider deep recursion:
7676+7777+```typescript
7878+const countdown = (n: number): Eff<void, never, unknown> =>
7979+ n === 0
8080+ ? succeed(undefined)
8181+ : pipe(succeed(n), flatMap(() => countdown(n - 1)))
8282+8383+runPromise(countdown(100000)) // Stack overflow!
8484+```
8585+8686+Each `flatMap` adds a function call. Deep enough, and JavaScript's call stack overflows.
8787+8888+### Trampolining to the Rescue
8989+9090+Instead of calling functions directly, we return a description of what to call:
9191+9292+```typescript
9393+// Instead of: f(g(h(x)))
9494+// We do: { call: h, next: { call: g, next: { call: f } } }
9595+```
9696+9797+The trampoline loop processes one step at a time:
9898+9999+```typescript
100100+Suspended: ({ resume }) =>
101101+ Promise.resolve().then(() => trampoline(resume()))
102102+```
103103+104104+The `Promise.resolve().then(...)` yields to the event loop, unwinding the stack before continuing. No matter how deep the effect tree, the stack stays shallow.
105105+106106+---
107107+108108+## Stack Safety in Detail
109109+110110+### Sync Operations
111111+112112+For sync operations like `Succeed` or `Sync`, the interpreter returns `Suspended`:
113113+114114+```typescript
115115+FlatMap: ({ effect, f }) =>
116116+ suspended(() =>
117117+ interpret(effect, {
118118+ onSuccess: (a) => interpret(f(a), cont, makeFiber),
119119+ onFailure: cont.onFailure
120120+ }, makeFiber)
121121+ )
122122+```
123123+124124+The `suspended(() => ...)` wraps the continuation. The trampoline processes it:
125125+126126+```typescript
127127+Suspended: ({ resume }) =>
128128+ Promise.resolve().then(() => trampoline(resume()))
129129+```
130130+131131+This yields to the event loop, keeping the stack at constant depth.
132132+133133+### Async Operations
134134+135135+For async operations like `Async`, the interpreter returns `Blocked`:
136136+137137+```typescript
138138+Async: ({ register }) => {
139139+ let result: Exit<A, E> | null = null
140140+ let callback: (() => void) | null = null
141141+142142+ register((exit) => {
143143+ result = exit
144144+ callback?.()
145145+ })
146146+147147+ if (result !== null) {
148148+ return handleExit(result) // Already completed
149149+ }
150150+151151+ return blocked(
152152+ cleanup,
153153+ (cb) => { callback = cb },
154154+ () => handleExit(result!)
155155+ )
156156+}
157157+```
158158+159159+The trampoline waits for the async operation:
160160+161161+```typescript
162162+Blocked: ({ onComplete, next }) =>
163163+ new Promise(resolve =>
164164+ onComplete(() => resolve(trampoline(next())))
165165+ )
166166+```
167167+168168+When the async operation completes, it calls `callback`, which resolves the Promise and continues trampolining.
169169+170170+---
171171+172172+## Fiber Lifecycle
173173+174174+A Fiber is a lightweight thread with its own execution context:
175175+176176+```typescript
177177+interface Fiber<A, E> {
178178+ id: string
179179+ await: () => Promise<Exit<A, E>> // Wait for completion
180180+ interrupt: () => void // Request cancellation
181181+ join: () => Eff<A, E, unknown> // Wait as an Effect
182182+}
183183+```
184184+185185+### Creation
186186+187187+`runFiber` creates a fiber and starts execution:
188188+189189+```typescript
190190+const createFiber = <A, E, R>(eff: Eff<A, E, R>, env: R): Fiber<A, E> => {
191191+ const id = `fiber-${++counter}`
192192+ let isInterrupted = false
193193+ let exitResult: Option<Exit<A, E>> = none
194194+ const awaiters: Array<(exit: Exit<A, E>) => void> = []
195195+196196+ // Start interpretation
197197+ const initialStep = interpret(eff, {
198198+ env,
199199+ onSuccess: (a) => done(success(a)),
200200+ onFailure: (e) => done(failure(e))
201201+ }, makeFiber)
202202+203203+ // Run the trampoline
204204+ trampoline(initialStep).then((exit) => {
205205+ const finalExit = isInterrupted ? interrupted(id) : exit
206206+ exitResult = some(finalExit)
207207+ awaiters.forEach(cb => cb(finalExit))
208208+ })
209209+210210+ return { id, await: ..., interrupt: ..., join: ... }
211211+}
212212+```
213213+214214+### Completion States
215215+216216+A fiber completes with an `Exit<A, E>`:
217217+218218+```typescript
219219+type Exit<A, E> =
220220+ | { _tag: "Success"; value: A }
221221+ | { _tag: "Failure"; error: E }
222222+ | { _tag: "Interrupted"; by: string }
223223+```
224224+225225+---
226226+227227+## Async Operation Handling
228228+229229+The `async` constructor bridges callback-based APIs:
230230+231231+```typescript
232232+const delay = (ms: number): Eff<void, never, unknown> =>
233233+ async(resume => {
234234+ const id = setTimeout(
235235+ () => resume({ _tag: "Success", value: undefined }),
236236+ ms
237237+ )
238238+ return () => clearTimeout(id) // Cleanup function
239239+ })
240240+```
241241+242242+### The Flow
243243+244244+1. `async(register)` creates an `Async` node
245245+2. Interpreter calls `register`, passing the `resume` callback
246246+3. Interpreter returns `Blocked` step with the continuation
247247+4. Trampoline creates a Promise that waits for completion
248248+5. When the async operation finishes, it calls `resume`
249249+6. `resume` stores the result and calls the callback
250250+7. Trampoline continues with the next step
251251+252252+---
253253+254254+## Interruption and Cleanup
255255+256256+Fibers can be interrupted, which triggers cleanup:
257257+258258+```typescript
259259+const fiber = runFiber(longRunningEffect, {})
260260+// Later...
261261+fiber.interrupt()
262262+```
263263+264264+### How Interruption Works
265265+266266+1. `interrupt()` sets a flag: `isInterrupted = true`
267267+2. When the fiber completes, the trampoline checks this flag
268268+3. If interrupted, the Exit becomes `Interrupted` instead of Success/Failure
269269+270270+### Cleanup Functions
271271+272272+The cleanup function from `async` is called when:
273273+274274+- The fiber is interrupted
275275+- A timeout expires
276276+- A race is lost
277277+278278+```typescript
279279+const cancellable = async(resume => {
280280+ const controller = new AbortController()
281281+282282+ fetch(url, { signal: controller.signal })
283283+ .then(r => resume({ _tag: "Success", value: r }))
284284+ .catch(e => resume({ _tag: "Failure", error: e }))
285285+286286+ return () => controller.abort() // Cleanup
287287+})
288288+```
289289+290290+---
291291+292292+## The Pure Interpreter
293293+294294+The `interpret` function is pure - it takes an Eff and returns a Step:
295295+296296+```typescript
297297+const interpret = <A, E, R>(
298298+ eff: Eff<A, E, R>,
299299+ cont: Continuation<A, E, R>,
300300+ makeFiber: ...
301301+): Step<unknown, unknown> =>
302302+ match(eff)({
303303+ Succeed: ({ value }) => cont.onSuccess(value),
304304+ Fail: ({ error }) => cont.onFailure(error),
305305+ FlatMap: ({ effect, f }) =>
306306+ suspended(() => interpret(effect, {
307307+ onSuccess: (a) => interpret(f(a), cont, makeFiber),
308308+ onFailure: cont.onFailure
309309+ }, makeFiber)),
310310+ // ... other cases
311311+ })
312312+```
313313+314314+This purity is important:
315315+316316+- **Testable** - You can test interpretation without running effects
317317+- **Predictable** - No hidden side effects
318318+- **Composable** - Interpreters can be stacked or modified
319319+320320+---
321321+322322+## Key Takeaways
323323+324324+1. **Effects are data** - Eff is a tree describing what to do
325325+2. **Interpreter is pure** - Transforms Eff to Step without side effects
326326+3. **Trampoline executes** - The only place that touches JS runtime
327327+4. **Stack safety via yielding** - `Promise.resolve().then()` unwinds stack
328328+5. **Fibers manage lifecycle** - Creation, completion, interruption
329329+6. **Cleanup enables cancellation** - The return value from `async` register
330330+331331+Understanding the runtime helps you:
332332+- Debug unexpected behavior
333333+- Know what's safe to do in callbacks
334334+- Understand performance characteristics
335335+336336+---
337337+338338+## See Also
339339+340340+- [Effect Composition](./06-effect-composition.md) - How effects combine
341341+- [Testing Strategies](./09-testing-strategies.md) - Using Exit for assertions