Fiber Collections
Effect.forkChild is perfect for a fixed handful of fibers, but real services
spawn fibers dynamically — one per WebSocket connection, one per scheduled job,
one per in-flight subscription. You need somewhere to keep those handles so they
can be interrupted when the owning scope closes, and so
a fiber is forgotten once it finishes. That’s what the fiber collections do:
FiberHandle— holds at most one fiber. Starting a new one interrupts the previous. Great for “latest wins” work like a debounced refresh.FiberMap— holds fibers keyed by some value. Ideal for one fiber per entity (connection id, user id, job id).FiberSet— holds an unkeyed set of fibers. Use it for a pool of workers you start and forget.
All three are created within a Scope. When that scope closes, every fiber in
the collection is interrupted automatically — so they fit naturally inside a
service’s layer, and you never leak a background fiber.
import { Context, Effect, FiberMap, Layer } from "effect"
// A service that runs one background subscription fiber per topic.export class Subscriptions extends Context.Service<Subscriptions, { subscribe(topic: string): Effect.Effect<void> unsubscribe(topic: string): Effect.Effect<void>}>()("app/Subscriptions") { static readonly layer = Layer.effect( Subscriptions, Effect.gen(function*() { // The FiberMap is tied to this layer's Scope: when the layer is // released, every subscription fiber is interrupted for us. const fibers = yield* FiberMap.make<string>()
const subscribe = Effect.fn("Subscriptions.subscribe")( function*(topic: string) { // `run` forks the effect and stores it under `topic`. If a fiber // already exists for that key it is interrupted and replaced. yield* FiberMap.run( fibers, topic, Effect.forever( Effect.log(`polling ${topic}`).pipe(Effect.delay("1 second")) ) ) } )
const unsubscribe = Effect.fn("Subscriptions.unsubscribe")( function*(topic: string) { // Interrupt and drop just this key's fiber. yield* FiberMap.remove(fibers, topic) } )
return Subscriptions.of({ subscribe, unsubscribe }) }) )}Why not a plain array of fibers?
Section titled “Why not a plain array of fibers?”You could keep fibers in a Set yourself, but you’d have to remove each one
when it completes, interrupt every survivor on shutdown, and handle the race
between a fiber finishing and you removing it. The fiber collections do all of
this correctly:
- a fiber is removed automatically when it terminates, so the collection never holds dead handles;
- closing the owning scope interrupts all remaining fibers;
- failures can be propagated to the collection so one bad fiber can surface an error instead of failing silently.
FiberHandle — at most one fiber
Section titled “FiberHandle — at most one fiber”A FiberHandle stores a single fiber. FiberHandle.run forks an effect into it;
if it already holds a running fiber, that one is interrupted first. This is the
“only the latest matters” pattern — re-running a search as the user types, or
restarting a watcher when config changes.
import { Effect, FiberHandle } from "effect"
const program = Effect.scoped( Effect.gen(function*() { const handle = yield* FiberHandle.make<string>()
// Start a task... yield* FiberHandle.run(handle, Effect.succeed("v1").pipe(Effect.delay("1 second")))
// ...then start a newer one. The previous fiber is interrupted. yield* FiberHandle.run(handle, Effect.succeed("v2"))
// `join` waits for the current fiber and re-raises its failure, if any. yield* FiberHandle.join(handle) }))Pass { onlyIfMissing: true } to run if you’d rather keep the existing fiber
and skip starting a new one when one is already present.
FiberMap — one fiber per key
Section titled “FiberMap — one fiber per key”A FiberMap<K> indexes fibers by a key. FiberMap.run(map, key, effect) forks
under that key (replacing any existing fiber for it), FiberMap.remove(map, key)
interrupts and drops one, and FiberMap.has / FiberMap.size let you inspect
the collection. This is the workhorse for “one background fiber per entity” — see
the Subscriptions service above.
FiberSet — an unkeyed pool
Section titled “FiberSet — an unkeyed pool”A FiberSet is the simplest of the three: an unkeyed bag of fibers. Use it to
fork a fluctuating number of tasks and have them all cleaned up together.
import { Effect, FiberSet } from "effect"
const program = Effect.scoped( Effect.gen(function*() { const set = yield* FiberSet.make<void>()
// Fork some work into the set; each fiber removes itself when it finishes. yield* FiberSet.run(set, Effect.log("job a").pipe(Effect.delay("100 millis"))) yield* FiberSet.run(set, Effect.log("job b").pipe(Effect.delay("200 millis")))
// Wait until every fiber in the set has completed. yield* FiberSet.awaitEmpty(set) }))// Leaving the scope would interrupt any fibers still running.Choosing between them
Section titled “Choosing between them”| Need | Use |
| --- | --- |
| Only the most recent task should run | FiberHandle |
| One fiber per id / entity, addressable later | FiberMap |
| A pool of interchangeable background tasks | FiberSet |
| A fixed, known number of fibers in a gen block | plain forkChild + Fiber.join |
| Map a collection with a parallelism cap | the concurrency option |