opencode-effect-enforcer 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +278 -0
  3. package/guidance/effect-first-development.md +1247 -0
  4. package/guidance/post__effect-and-the-near-inexpressible-majesty-of-layers.md +490 -0
  5. package/guidance/post__parse-dont-validate.md +109 -0
  6. package/guidance/progressive-disclosure-guidance.md +38 -0
  7. package/package.json +63 -0
  8. package/patterns/avoid-any.md +37 -0
  9. package/patterns/avoid-data-tagged-error.md +34 -0
  10. package/patterns/avoid-direct-json.md +51 -0
  11. package/patterns/avoid-direct-tag-checks.md +54 -0
  12. package/patterns/avoid-expect-in-if.md +52 -0
  13. package/patterns/avoid-mutable-state.md +70 -0
  14. package/patterns/avoid-native-fetch.md +61 -0
  15. package/patterns/avoid-node-imports.md +86 -0
  16. package/patterns/avoid-non-null-assertion.md +44 -0
  17. package/patterns/avoid-object-type.md +46 -0
  18. package/patterns/avoid-option-getorthrow.md +39 -0
  19. package/patterns/avoid-platform-coupling.md +43 -0
  20. package/patterns/avoid-process-env.md +43 -0
  21. package/patterns/avoid-react-hooks.md +73 -0
  22. package/patterns/avoid-schema-suffix.md +45 -0
  23. package/patterns/avoid-sync-fs.md +68 -0
  24. package/patterns/avoid-try-catch.md +47 -0
  25. package/patterns/avoid-ts-ignore.md +38 -0
  26. package/patterns/avoid-untagged-errors.md +67 -0
  27. package/patterns/avoid-yield-ref.md +46 -0
  28. package/patterns/casting-awareness.md +46 -0
  29. package/patterns/context-tag-extends.md +84 -0
  30. package/patterns/effect-catchall-default.md +61 -0
  31. package/patterns/effect-promise-vs-trypromise.md +47 -0
  32. package/patterns/effect-run-in-body.md +58 -0
  33. package/patterns/imperative-loops.md +76 -0
  34. package/patterns/prefer-arr-sort.md +52 -0
  35. package/patterns/prefer-duration-values.md +56 -0
  36. package/patterns/prefer-effect-fn.md +161 -0
  37. package/patterns/prefer-match-over-switch.md +48 -0
  38. package/patterns/prefer-option-over-null.md +56 -0
  39. package/patterns/prefer-redacted-config.md +70 -0
  40. package/patterns/prefer-schema-class.md +54 -0
  41. package/patterns/require-effect-concurrency.md +83 -0
  42. package/patterns/stream-large-files.md +63 -0
  43. package/patterns/throw-in-effect-gen.md +62 -0
  44. package/patterns/use-clock-service.md +45 -0
  45. package/patterns/use-command-executor-service.md +54 -0
  46. package/patterns/use-console-service.md +54 -0
  47. package/patterns/use-filesystem-service.md +59 -0
  48. package/patterns/use-http-client-service.md +77 -0
  49. package/patterns/use-path-service.md +53 -0
  50. package/patterns/use-random-service.md +45 -0
  51. package/patterns/use-temp-file-scoped.md +66 -0
  52. package/patterns/vm-in-wrong-file.md +51 -0
  53. package/patterns/yield-in-for-loop.md +61 -0
  54. package/skills/effect-ai-chat/SKILL.md +472 -0
  55. package/skills/effect-ai-language-model/SKILL.md +652 -0
  56. package/skills/effect-ai-prompt/SKILL.md +752 -0
  57. package/skills/effect-ai-provider/SKILL.md +668 -0
  58. package/skills/effect-ai-streaming/SKILL.md +418 -0
  59. package/skills/effect-ai-tool/SKILL.md +1132 -0
  60. package/skills/effect-atom-rpc/SKILL.md +488 -0
  61. package/skills/effect-atom-state/SKILL.md +640 -0
  62. package/skills/effect-batching/SKILL.md +614 -0
  63. package/skills/effect-cache/SKILL.md +570 -0
  64. package/skills/effect-cli/SKILL.md +523 -0
  65. package/skills/effect-command-executor/SKILL.md +675 -0
  66. package/skills/effect-concurrency-testing/SKILL.md +612 -0
  67. package/skills/effect-config/SKILL.md +580 -0
  68. package/skills/effect-context-witness/SKILL.md +274 -0
  69. package/skills/effect-domain-modeling/SKILL.md +1212 -0
  70. package/skills/effect-domain-predicates/SKILL.md +867 -0
  71. package/skills/effect-error-handling/SKILL.md +1581 -0
  72. package/skills/effect-fiber/SKILL.md +731 -0
  73. package/skills/effect-filesystem/SKILL.md +624 -0
  74. package/skills/effect-graph/SKILL.md +571 -0
  75. package/skills/effect-http-api/SKILL.md +1760 -0
  76. package/skills/effect-http-client/SKILL.md +989 -0
  77. package/skills/effect-http-server/SKILL.md +920 -0
  78. package/skills/effect-incremental-migration/SKILL.md +362 -0
  79. package/skills/effect-layer-design/SKILL.md +642 -0
  80. package/skills/effect-managed-runtime/SKILL.md +395 -0
  81. package/skills/effect-mcp-server/SKILL.md +608 -0
  82. package/skills/effect-observability/SKILL.md +719 -0
  83. package/skills/effect-optics/SKILL.md +554 -0
  84. package/skills/effect-parallelization/SKILL.md +668 -0
  85. package/skills/effect-path/SKILL.md +296 -0
  86. package/skills/effect-pattern-matching/SKILL.md +914 -0
  87. package/skills/effect-platform-abstraction/SKILL.md +1175 -0
  88. package/skills/effect-platform-layers/SKILL.md +514 -0
  89. package/skills/effect-pubsub-event-bus/SKILL.md +384 -0
  90. package/skills/effect-react-composition/SKILL.md +986 -0
  91. package/skills/effect-react-vm/SKILL.md +675 -0
  92. package/skills/effect-rpc-api/SKILL.md +624 -0
  93. package/skills/effect-rpc-client/SKILL.md +666 -0
  94. package/skills/effect-rpc-cluster/SKILL.md +1623 -0
  95. package/skills/effect-rpc-server/SKILL.md +767 -0
  96. package/skills/effect-scheduling/SKILL.md +124 -0
  97. package/skills/effect-schema-composition/SKILL.md +975 -0
  98. package/skills/effect-schema-v4/SKILL.md +691 -0
  99. package/skills/effect-scope/SKILL.md +682 -0
  100. package/skills/effect-service-implementation/SKILL.md +656 -0
  101. package/skills/effect-socket/SKILL.md +703 -0
  102. package/skills/effect-sql/SKILL.md +781 -0
  103. package/skills/effect-stream/SKILL.md +765 -0
  104. package/skills/effect-testing/SKILL.md +1331 -0
  105. package/skills/effect-typeclass-design/SKILL.md +161 -0
  106. package/skills/effect-wide-events/Article.md +66 -0
  107. package/skills/effect-wide-events/SKILL.md +95 -0
  108. package/skills/effect-workflow/SKILL.md +810 -0
  109. package/src/agent-policy.ts +22 -0
  110. package/src/enforcer.ts +104 -0
  111. package/src/frontmatter.ts +34 -0
  112. package/src/guidance.ts +66 -0
  113. package/src/index.ts +38 -0
  114. package/src/pattern-catalog.ts +115 -0
  115. package/src/pattern-matcher.ts +178 -0
  116. package/src/pattern.ts +97 -0
  117. package/src/skills.ts +29 -0
  118. package/src/write-projection.ts +66 -0
@@ -0,0 +1,731 @@
1
+ ---
2
+ name: effect-fiber
3
+ description: Fork, supervise, and interrupt Effect fibers with Effect.forkChild/forkScoped/forkIn/forkDetach, Fiber join/await/interrupt, uninterruptible regions, and the FiberHandle/FiberMap/FiberSet supervision collections. Use when running background work, cancelling or restarting tasks, implementing latest-wins or keyed workers, bridging Effect into callback APIs, or debugging interruption and fiber lifetime issues.
4
+ ---
5
+
6
+ You are an Effect TypeScript expert specializing in fiber lifecycle, interruption, and supervision with `Fiber`, `FiberHandle`, `FiberMap`, and `FiberSet`.
7
+
8
+ ## Effect Source Reference
9
+
10
+ The Effect v4 source is available at `~/.local/share/opencode/repos/github.com/Effect-TS/effect@main/`.
11
+ Browse and read files there directly to look up APIs, types, and implementations.
12
+
13
+ Reference this for:
14
+
15
+ - `Fiber` interface and await/join/interrupt operations (`packages/effect/src/Fiber.ts`)
16
+ - Fork variants, interruption combinators, run\* APIs (`packages/effect/src/Effect.ts`)
17
+ - Single-slot supervision (`packages/effect/src/FiberHandle.ts`)
18
+ - Keyed fiber collections (`packages/effect/src/FiberMap.ts`)
19
+ - Grow-only fiber collections (`packages/effect/src/FiberSet.ts`)
20
+ - Runtime internals — `FiberImpl`, fork/interrupt mechanics (`packages/effect/src/internal/effect.ts`)
21
+ - v3 → v4 fork renames (`migration/forking.md`), keep-alive changes (`migration/fiber-keep-alive.md` — partially stale, see section 9)
22
+ - Real usage and edge cases (`packages/effect/test/FiberHandle.test.ts`, `FiberMap.test.ts`, `FiberSet.test.ts`)
23
+
24
+ ## Core Model
25
+
26
+ A `Fiber<A, E = never>` is a handle to a lightweight, cooperatively scheduled execution of an `Effect` that may still be running or may have completed. It is the unit of concurrency in Effect. A fiber's outcome is an `Exit<A, E>` — success with `A`, or failure with a `Cause<E>` that can contain typed errors, defects, and interruptions.
27
+
28
+ ```ts
29
+ import {
30
+ Cause,
31
+ Deferred,
32
+ Effect,
33
+ Exit,
34
+ Fiber,
35
+ FiberHandle,
36
+ FiberMap,
37
+ FiberSet,
38
+ Schedule,
39
+ Scope,
40
+ Semaphore
41
+ } from 'effect';
42
+ ```
43
+
44
+ Key facts to internalize:
45
+
46
+ - **Structured concurrency.** A fiber forked with `Effect.forkChild` is attached to its parent: when the parent fiber completes (success, failure, or interruption), all still-running children are interrupted before the parent's exit settles. `forkScoped`/`forkIn` tie the fiber's lifetime to a `Scope` instead; `forkDetach` produces a global fiber with no automatic lifetime.
47
+ - **Interruption is cooperative.** It is observed at effect boundaries; uninterruptible regions and finalizers run to completion first. Interrupting is itself an effect that waits for the target to fully settle.
48
+ - **Fiber ids are plain `number`s** in v4 (there is no composite `FiberId` type). Interruptors are recorded in the `Cause` as a `ReadonlySet<number>`.
49
+ - **`Exit` is an `Effect`.** You can `yield*` an `Exit` directly to propagate its result into the current fiber.
50
+ - Useful synchronous members on the fiber object: `fiber.id`, `fiber.pollUnsafe(): Exit<A, E> | undefined`, `fiber.addObserver(cb): () => void` (returns an unsubscribe function), `fiber.interruptUnsafe(fiberId?)`.
51
+
52
+ High-level concurrency combinators (`Effect.all`, `Effect.forEach`, `Effect.race*`) are covered by the effect-parallelization skill — prefer those when you just need concurrent results; reach for explicit fibers when you need lifecycle control.
53
+
54
+ ---
55
+
56
+ ## 1. Forking Fibers
57
+
58
+ The v3 names are gone: `Effect.fork` → `Effect.forkChild`, `Effect.forkDaemon` → `Effect.forkDetach` (`Effect.forkAll` and `Effect.forkWithErrorHandler` were removed). Four variants exist in v4, differing only in lifetime:
59
+
60
+ | Variant | Interrupted when… | Requirements |
61
+ | ------------------------------ | -------------------------------------------- | --------------- |
62
+ | `Effect.forkChild` | the parent fiber completes | `R` |
63
+ | `Effect.forkScoped` | the current `Scope` closes | `R \| Scope` |
64
+ | `Effect.forkIn(effect, scope)` | the supplied `Scope` closes | `R` |
65
+ | `Effect.forkDetach` | never automatically (explicit interrupt only)| `R` |
66
+
67
+ All four return `Effect<Fiber<A, E>, never, R>` — forking never fails — and all accept the same options object:
68
+
69
+ ```ts
70
+ {
71
+ readonly startImmediately?: boolean | undefined;
72
+ readonly uninterruptible?: boolean | 'inherit' | undefined;
73
+ }
74
+ ```
75
+
76
+ - `startImmediately: true` evaluates the fiber synchronously up to its first suspension. The default is a deferred start: the fiber is scheduled and begins on the next scheduler tick, after the parent yields.
77
+ - `uninterruptible: true` starts the fiber uninterruptible; `'inherit'` copies the parent's current interruptibility; default is interruptible.
78
+
79
+ ```ts
80
+ const task = Effect.gen(function* () {
81
+ yield* Effect.sleep('2 seconds');
82
+ return 'result';
83
+ });
84
+
85
+ const program = Effect.gen(function* () {
86
+ // data-first and data-last forms both work
87
+ const fiber1 = yield* Effect.forkChild(task);
88
+ const fiber2 = yield* task.pipe(Effect.forkChild);
89
+ const fiber3 = yield* Effect.forkChild(task, { startImmediately: true });
90
+ const fiber4 = yield* task.pipe(Effect.forkChild({ startImmediately: true }));
91
+
92
+ const result = yield* Fiber.join(fiber1);
93
+ return result;
94
+ });
95
+ ```
96
+
97
+ Forking into a scope:
98
+
99
+ ```ts
100
+ const scopedFork = Effect.scoped(
101
+ Effect.gen(function* () {
102
+ // tied to the enclosing scope
103
+ const fiber = yield* Effect.forkScoped(task);
104
+
105
+ // or fork into an explicit scope you manage
106
+ const scope = yield* Effect.scope;
107
+ const fiber2 = yield* Effect.forkIn(task, scope);
108
+
109
+ yield* Effect.sleep('1 second');
110
+ // both fibers are interrupted when the scope closes
111
+ })
112
+ );
113
+ ```
114
+
115
+ Detached fibers survive the parent — pair them with explicit interruption or `Fiber.runIn`:
116
+
117
+ ```ts
118
+ const daemon = Effect.gen(function* () {
119
+ const fiber = yield* Effect.forkDetach(pollForever);
120
+ // attach a manually managed fiber to a scope after the fact:
121
+ // the scope's close interrupts it (does not wait for it before registering)
122
+ const scope = yield* Effect.scope;
123
+ Fiber.runIn(fiber, scope);
124
+ });
125
+ ```
126
+
127
+ ### Lazy start gotcha
128
+
129
+ Because forked fibers start on the next tick by default, side effects have not happened immediately after the fork:
130
+
131
+ ```ts
132
+ const program = Effect.gen(function* () {
133
+ let started = false;
134
+ const fiber = yield* Effect.forkChild(Effect.sync(() => (started = true)));
135
+ // started === false here!
136
+ yield* Effect.yieldNow; // let scheduled fibers run
137
+ // started === true
138
+ });
139
+ ```
140
+
141
+ Use `{ startImmediately: true }` when registration must happen before the parent proceeds (e.g. installing an `Effect.onInterrupt` handler or subscribing to a queue).
142
+
143
+ ---
144
+
145
+ ## 2. Joining, Awaiting, and Reading Exits
146
+
147
+ ```ts
148
+ const program = Effect.gen(function* () {
149
+ const fiber = yield* Effect.forkChild(compute);
150
+
151
+ // join: flatten the fiber's result into the current fiber.
152
+ // Failure (or interruption) of the fiber fails the current effect.
153
+ const value = yield* Fiber.join(fiber);
154
+
155
+ // await: NEVER fails — always succeeds with the Exit for inspection.
156
+ const exit = yield* Fiber.await(fiber);
157
+
158
+ if (Exit.isSuccess(exit)) {
159
+ console.log(exit.value);
160
+ } else if (Cause.hasInterrupts(exit.cause)) {
161
+ // exit is already narrowed to Failure here, so .cause is accessible
162
+ console.log('interrupted by', Cause.interruptors(exit.cause));
163
+ } else {
164
+ console.log('failed:', Cause.squash(exit.cause));
165
+ }
166
+ });
167
+ ```
168
+
169
+ Many fibers at once:
170
+
171
+ ```ts
172
+ // every outcome as data, ordered like the input
173
+ const exits = yield* Fiber.awaitAll([fiberA, fiberB]);
174
+ // Array<Exit<A, E>>
175
+
176
+ // all success values; fails fast with the first failed fiber's Cause.
177
+ // NOTE: does NOT interrupt the remaining fibers — do that yourself.
178
+ const values = yield* Fiber.joinAll([fiberA, fiberB]);
179
+ ```
180
+
181
+ Working with `Exit` values:
182
+
183
+ ```ts
184
+ const summary = Exit.match(exit, {
185
+ onSuccess: (value) => `ok: ${value}`,
186
+ onFailure: (cause) => `failed: ${Cause.squash(cause)}`
187
+ });
188
+
189
+ Exit.getSuccess(exit); // Option<A>
190
+ Exit.getCause(exit); // Option<Cause<E>>
191
+ Exit.hasFails(exit); // has typed errors
192
+ Exit.hasDies(exit); // has defects
193
+ Exit.hasInterrupts(exit); // has interruptions
194
+ Cause.hasInterruptsOnly(cause); // ONLY interruptions (clean cancellation)
195
+ // the Exit.has* checks are TYPE GUARDS narrowing to Failure<A, E>; in an
196
+ // else-if chain test the cause instead (Cause.hasFails/hasDies/hasInterrupts
197
+ // return plain booleans) or the final else narrows `exit` to never
198
+
199
+ // Exit is itself an Effect: yielding it propagates success/failure
200
+ const value = yield* exit;
201
+ ```
202
+
203
+ To capture the exit of an inline effect (no fiber needed) use `Effect.exit(effect)`, which returns `Effect<Exit<A, E>, never, R>`.
204
+
205
+ Synchronous inspection (outside or inside effects):
206
+
207
+ ```ts
208
+ const exit = fiber.pollUnsafe(); // Exit<A, E> | undefined — undefined while running
209
+ const cancel = fiber.addObserver((exit) => console.log('done', exit));
210
+ cancel(); // unsubscribe
211
+ ```
212
+
213
+ There is no `Fiber.poll` effect in v4 — `pollUnsafe` and `addObserver` are the low-level hooks. In tests, `fiber.pollUnsafe()` combined with `TestClock` from `effect/testing` is the standard way to assert "still running" vs "completed" (see the effect-concurrency-testing skill for the full idiom; effect-testing covers TestClock setup).
214
+
215
+ ---
216
+
217
+ ## 3. Interruption
218
+
219
+ ```ts
220
+ // interrupt one fiber and WAIT for it to fully settle
221
+ // (finalizers + uninterruptible regions run to completion first)
222
+ yield* Fiber.interrupt(fiber); // Effect<void>
223
+
224
+ // interrupt many, wait for all of them to settle
225
+ yield* Fiber.interruptAll([fiber1, fiber2]);
226
+
227
+ // record a specific fiber id as the interruptor (diagnostics/tracing)
228
+ yield* Fiber.interruptAs(fiber, controllerFiber.id);
229
+ yield* Fiber.interruptAllAs([fiber1, fiber2], controllerFiber.id);
230
+
231
+ // interrupt the CURRENT fiber
232
+ yield* Effect.interrupt; // Effect<never>
233
+ ```
234
+
235
+ Notes:
236
+
237
+ - `Fiber.interrupt` uses the current fiber's id as the interruptor. The id set ends up in the `Cause` and is what `Effect.onInterrupt` finalizers and `Cause.interruptors` see.
238
+ - Fire-and-forget cancellation from synchronous code: `fiber.interruptUnsafe()` — sends the signal without waiting.
239
+ - Self-interruption via `Effect.interrupt` produces an exit where `Cause.hasInterruptsOnly` is `true`; platform `runMain` runners map that to exit code 130 rather than an error.
240
+ - A constructed interrupted exit is available as `Exit.interrupt(fiberId?)`.
241
+
242
+ Current-fiber accessors:
243
+
244
+ ```ts
245
+ const self = yield* Effect.fiber; // Effect<Fiber<unknown, unknown>>
246
+ const id = yield* Effect.fiberId; // Effect<number>
247
+ const eff = Effect.withFiber((fiber) => Effect.succeed(fiber.id)); // low-level constructor
248
+ const maybe = Fiber.getCurrent(); // sync: Fiber | undefined (undefined outside a fiber)
249
+ ```
250
+
251
+ ---
252
+
253
+ ## 4. Uninterruptible Regions and Cleanup
254
+
255
+ ```ts
256
+ Effect.uninterruptible(critical); // cannot be interrupted inside
257
+ Effect.interruptible(effect); // re-enable inside an uninterruptible region
258
+ ```
259
+
260
+ Interruption inside an uninterruptible region is **deferred, not dropped**: the pending interruption fires the moment the region ends. The canonical acquire/use/release shape protects acquisition and cleanup while keeping the work cancellable:
261
+
262
+ ```ts
263
+ const withConnection = Effect.uninterruptibleMask((restore) =>
264
+ Effect.gen(function* () {
265
+ const conn = yield* acquireConnection; // protected
266
+ return yield* restore(useConnection(conn)).pipe(
267
+ // cleanup runs on success, failure, AND interruption
268
+ Effect.onExit(() => closeConnection(conn))
269
+ );
270
+ })
271
+ );
272
+ ```
273
+
274
+ `Effect.interruptibleMask((restore) => ...)` is the inverse: the body is interruptible and `restore` re-applies the outer interruptibility.
275
+
276
+ Interruption-specific and general finalizers:
277
+
278
+ ```ts
279
+ // runs ONLY when the effect is interrupted; receives interruptor fiber ids
280
+ const guarded = Effect.onInterrupt(longRunning, (interruptors) =>
281
+ Effect.log(`interrupted by: ${[...interruptors].join(', ')}`)
282
+ );
283
+
284
+ // always runs (success / failure / interruption); finalizer cannot fail
285
+ const cleaned = Effect.ensuring(task, Effect.log('cleanup'));
286
+
287
+ // runs with the Exit — inspect how the effect ended
288
+ const observed = Effect.onExit(task, (exit) =>
289
+ Effect.log(Exit.isSuccess(exit) ? 'ok' : 'failed/interrupted')
290
+ );
291
+
292
+ // runs only on failure, with the Cause
293
+ const onFail = Effect.onError(task, (cause) => Effect.log(Cause.squash(cause)));
294
+ ```
295
+
296
+ Bridging interruption to `AbortSignal`-aware APIs:
297
+
298
+ ```ts
299
+ const download = Effect.scoped(
300
+ Effect.gen(function* () {
301
+ // fresh AbortController per acquisition; aborted when the scope closes
302
+ const signal = yield* Effect.abortSignal; // Effect<AbortSignal, never, Scope>
303
+ // pass `signal` to fetch(), child processes, etc.
304
+ })
305
+ );
306
+ ```
307
+
308
+ ---
309
+
310
+ ## 5. Structured Concurrency Lifetimes
311
+
312
+ The most important v4 behavior: **when a fiber finishes its work, any children forked with `forkChild` that are still running are interrupted** before the parent's exit is reported. This makes fire-and-forget with `forkChild` a bug:
313
+
314
+ ```ts
315
+ // WRONG — the child is interrupted as soon as the parent returns,
316
+ // likely before it even starts (lazy start!)
317
+ const wrong = Effect.gen(function* () {
318
+ yield* Effect.forkChild(sendAuditLog(event));
319
+ return 'done';
320
+ });
321
+ ```
322
+
323
+ Correct options, by intent:
324
+
325
+ ```ts
326
+ // 1. The work belongs to a longer-lived scope (service, request, app)
327
+ const scoped = Effect.gen(function* () {
328
+ yield* Effect.forkScoped(sendAuditLog(event));
329
+ return 'done';
330
+ }); // requires Scope — provided by Effect.scoped / a Layer scope
331
+
332
+ // 2. The parent should wait for children forked during the effect
333
+ const awaited = Effect.awaitAllChildren(
334
+ Effect.gen(function* () {
335
+ yield* Effect.forkChild(sendAuditLog(event));
336
+ return 'done';
337
+ })
338
+ ); // completes only after the audit-log child completes
339
+
340
+ // 3. Truly detached background work (you own its shutdown)
341
+ const detached = Effect.gen(function* () {
342
+ const fiber = yield* Effect.forkDetach(sendAuditLog(event));
343
+ return 'done';
344
+ });
345
+ ```
346
+
347
+ Whichever lifetime you pick, **a forked fiber's failure is observed by nobody unless you arrange it**: `join`/`await` the fiber, supervise it via `FiberHandle`/`FiberMap`/`FiberSet` `join`, or attach `Effect.catchCause`/`Effect.onError` plus logging inside the forked effect. v4 removed `Effect.forkWithErrorHandler`, and the runtime does not log unhandled fiber failures.
348
+
349
+ `Effect.awaitAllChildren` only waits for children forked while the wrapped effect runs — children that existed beforehand are not awaited.
350
+
351
+ `forkIn`/`forkScoped` register an interruption finalizer on the scope and remove it when the fiber completes on its own. Forking into an already-closed scope interrupts the new fiber immediately. The same applies to `Fiber.runIn(fiber, scope)`, which only registers the finalizer — it does not wait for the fiber.
352
+
353
+ Scope mechanics themselves — `Effect.scoped`, manual `Scope.make`/`close`, finalizer ordering — are covered by the effect-scope skill.
354
+
355
+ ---
356
+
357
+ ## 6. FiberHandle — Single-Slot Supervision
358
+
359
+ `FiberHandle<A, E>` manages **at most one fiber**. Running a new effect into it interrupts the previous fiber (latest wins); closing the owning scope interrupts the current fiber; completed fibers remove themselves.
360
+
361
+ ```ts
362
+ const program = Effect.gen(function* () {
363
+ // always pass type parameters — defaults are <unknown, unknown>
364
+ const handle = yield* FiberHandle.make<string, MyError>();
365
+ // Effect<FiberHandle<string, MyError>, never, Scope>
366
+
367
+ // fork into the handle; interrupts whatever was there
368
+ const fiber = yield* FiberHandle.run(handle, task);
369
+
370
+ // keep the existing fiber; the NEW effect gets an already-interrupted fiber
371
+ yield* FiberHandle.run(handle, otherTask, { onlyIfMissing: true });
372
+
373
+ const current = FiberHandle.getUnsafe(handle); // Option<Fiber<string, MyError>>
374
+ const current2 = yield* FiberHandle.get(handle); // Effect-wrapped version
375
+
376
+ yield* FiberHandle.clear(handle); // interrupt current fiber, leave handle empty
377
+
378
+ yield* FiberHandle.awaitEmpty(handle); // wait for the current fiber to complete
379
+ yield* FiberHandle.join(handle); // see below
380
+ }).pipe(Effect.scoped);
381
+ ```
382
+
383
+ `run` options: `{ onlyIfMissing?: boolean; propagateInterruption?: boolean }`. The type also accepts `startImmediately`, but it is a no-op — collection fibers are forked via `Effect.runForkWith` and always start synchronously (see "How collection fibers relate to the caller" in section 8).
384
+
385
+ ### join vs awaitEmpty
386
+
387
+ - `FiberHandle.join(handle): Effect<void, E>` — fails with the **first managed-fiber failure**; succeeds (void) only when the handle's scope closes. It never resolves just because a fiber finished successfully. Use it as a supervision watchdog.
388
+ - `FiberHandle.awaitEmpty(handle): Effect<void, E>` — waits for the currently held fiber to complete.
389
+
390
+ ### propagateInterruption
391
+
392
+ By default (`false`), interruption of a managed fiber — including by an external `Fiber.interrupt` — does **not** fail `join`. With `propagateInterruption: true`, external interruptions do fail `join`; interruptions performed internally by the collection itself (replacement, `clear`, scope close — recorded with internal fiber id `-1`) never do.
393
+
394
+ ### Installing existing fibers
395
+
396
+ ```ts
397
+ const fiber = Effect.runFork(task);
398
+ yield* FiberHandle.set(handle, fiber, { onlyIfMissing: true });
399
+ FiberHandle.setUnsafe(handle, fiber); // synchronous variant
400
+ ```
401
+
402
+ ### Runtime helpers — synchronous runners for callback code
403
+
404
+ `runtime` captures the current services and returns a **synchronous** function that forks effects into the handle:
405
+
406
+ ```ts
407
+ const handle = yield* FiberHandle.make<void, never>();
408
+ const run = yield* FiberHandle.runtime(handle)<MyService>(); // note the curried <R>() call
409
+
410
+ // plain function — safe to call from event handlers
411
+ const fiber = run(taskNeedingMyService, { onlyIfMissing: false });
412
+
413
+ // Promise-returning runner for an existing handle (rejects with Cause.squash)
414
+ const runPromise = yield* FiberHandle.runtimePromise(handle)<MyService>();
415
+ ```
416
+
417
+ Runner options: `{ signal?: AbortSignal; scheduler?: Scheduler; onlyIfMissing?: boolean; propagateInterruption?: boolean }`.
418
+
419
+ Shortcuts that create the handle and runner together (scoped):
420
+
421
+ ```ts
422
+ const run = yield* FiberHandle.makeRuntime<never>(); // <R, E, A>
423
+ const runPromise = yield* FiberHandle.makeRuntimePromise(); // Promise-returning runner
424
+ const promise = runPromise(Effect.succeed('hello')); // rejects with Cause.squash on failure
425
+ ```
426
+
427
+ ### Closed-handle behavior
428
+
429
+ After the scope closes: `FiberHandle.run` **interrupts the calling fiber**; the captured `runtime`/`makeRuntime` runners instead return a shared, already-interrupted fiber. Fibers passed to `setUnsafe` on a closed handle are interrupted immediately.
430
+
431
+ ---
432
+
433
+ ## 7. FiberMap — Keyed Fibers
434
+
435
+ `FiberMap<K, A, E>` is a map of running fibers indexed by key. Running a new effect under an existing key interrupts the previous fiber for that key; entries remove themselves on completion; scope close interrupts everything.
436
+
437
+ ```ts
438
+ const program = Effect.gen(function* () {
439
+ const map = yield* FiberMap.make<string, void, JobError>();
440
+ // Effect<FiberMap<string, void, JobError>, never, Scope>
441
+
442
+ // fork under a key — replaces (interrupts) any previous "job-1" fiber
443
+ const fiber = yield* FiberMap.run(map, 'job-1', runJob(1));
444
+
445
+ // dedupe: keep the running fiber, new effect gets an interrupted fiber
446
+ yield* FiberMap.run(map, 'job-1', runJob(1), { onlyIfMissing: true });
447
+
448
+ yield* FiberMap.has(map, 'job-1'); // Effect<boolean> (hasUnsafe: sync)
449
+ yield* FiberMap.get(map, 'job-1'); // Effect<Option<Fiber<void, JobError>>>
450
+ yield* FiberMap.size(map); // Effect<number>
451
+
452
+ yield* FiberMap.remove(map, 'job-1'); // interrupt + remove that key
453
+ yield* FiberMap.clear(map); // interrupt every fiber
454
+
455
+ // FiberMap is Iterable<[K, Fiber<A, E>]>
456
+ for (const [key, fiber] of map) {
457
+ console.log(key, fiber.id);
458
+ }
459
+
460
+ yield* FiberMap.awaitEmpty(map); // Effect<void, E> — wait until no fibers remain
461
+ yield* FiberMap.join(map); // Effect<void, E> — fail on first fiber failure
462
+ }).pipe(Effect.scoped);
463
+ ```
464
+
465
+ `run` options are the same as FiberHandle's: `{ onlyIfMissing?, propagateInterruption? }` (`startImmediately` is in the type but is a no-op here too). `set`/`setUnsafe` install existing fibers under a key with `{ onlyIfMissing?, propagateInterruption? }`.
466
+
467
+ Runtime helpers take the key as the first runner argument:
468
+
469
+ ```ts
470
+ const run = yield* FiberMap.runtime(map)<MyService>();
471
+ run('job-2', taskNeedingMyService, { onlyIfMissing: true });
472
+ const runPromise = yield* FiberMap.runtimePromise(map)<MyService>(); // Promise runner, key-first
473
+
474
+ // or create map + runner at once (note generic order: <R, K>)
475
+ const run2 = yield* FiberMap.makeRuntime<never, string>();
476
+ const runPromise2 = yield* FiberMap.makeRuntimePromise<never, string>();
477
+ ```
478
+
479
+ Closed-map behavior matches FiberHandle: `FiberMap.run` interrupts the caller; runtime runners return a pre-interrupted fiber.
480
+
481
+ ---
482
+
483
+ ## 8. FiberSet — Grow-Only Fiber Collections
484
+
485
+ `FiberSet<A, E>` tracks an unkeyed set of fibers. No replacement semantics — every `run`/`add` grows the set; completed fibers remove themselves; scope close interrupts all. Nothing limits admission: unbounded `run` calls on a production ingest path are a hazard — gate them with a `Semaphore` (see the bounded pattern under Key Patterns).
486
+
487
+ ```ts
488
+ const program = Effect.gen(function* () {
489
+ const set = yield* FiberSet.make<void, TaskError>();
490
+ // Effect<FiberSet<void, TaskError>, never, Scope>
491
+
492
+ const fiber = yield* FiberSet.run(set, handleRequest(req));
493
+ yield* FiberSet.add(set, existingFiber); // track an already-forked fiber
494
+ FiberSet.addUnsafe(set, anotherFiber); // synchronous variant
495
+
496
+ yield* FiberSet.size(set); // Effect<number>
497
+ yield* FiberSet.clear(set); // interrupt all members
498
+
499
+ // FiberSet is Iterable<Fiber<A, E>>
500
+ const exits = yield* Fiber.awaitAll(set);
501
+
502
+ yield* FiberSet.awaitEmpty(set); // Effect<void> — graceful drain
503
+ yield* FiberSet.join(set); // Effect<void, E> — fail on first fiber failure
504
+ }).pipe(Effect.scoped);
505
+ ```
506
+
507
+ `run` options: `{ propagateInterruption? }` (no `onlyIfMissing` — there is no key; `startImmediately` is in the type but is a no-op). On a closed set, `FiberSet.run` returns an already-interrupted fiber (it does **not** interrupt the caller, unlike FiberHandle/FiberMap).
508
+
509
+ Runtime helpers mirror the others in shape. `FiberSet.runtime` and `runtimePromise` forward `propagateInterruption` when registering the managed fiber:
510
+
511
+ ```ts
512
+ const run = yield* FiberSet.runtime(set)<MyService>();
513
+ run(taskNeedingMyService, { propagateInterruption: true });
514
+ const runPromise = yield* FiberSet.runtimePromise(set)<MyService>(); // Promise runner for an existing set
515
+
516
+ const run2 = yield* FiberSet.makeRuntime(); // scoped set + runner
517
+ const runPromise2 = yield* FiberSet.makeRuntimePromise();
518
+ ```
519
+
520
+ ### How collection fibers relate to the caller
521
+
522
+ Fibers forked via `FiberHandle/FiberMap/FiberSet.run` (and the runtime runners) are created with `Effect.runForkWith(parent.context)` — they are **root fibers carrying the caller's services, not children of the calling fiber**. Consequences:
523
+
524
+ - They start executing **immediately and synchronously** up to their first suspension (no lazy start, unlike `Effect.forkChild`).
525
+ - The calling fiber's completion does not interrupt them — only key replacement, `remove`/`clear`, or the collection's scope close does.
526
+
527
+ ---
528
+
529
+ ## 9. Running Fibers at the Program Edge
530
+
531
+ `Effect.runFork` starts a root fiber synchronously (it evaluates until the first suspension before returning):
532
+
533
+ ```ts
534
+ const fiber = Effect.runFork(program, {
535
+ signal: abortController.signal, // interrupt the fiber when aborted
536
+ scheduler: customScheduler, // optional Scheduler service
537
+ uninterruptible: true, // start the fiber uninterruptible
538
+ onFiberStart: (fiber) => console.log('started', fiber.id)
539
+ });
540
+
541
+ fiber.addObserver((exit) => console.log('done', exit));
542
+ Effect.runFork(Fiber.interrupt(fiber)); // interruption is itself an effect
543
+ ```
544
+
545
+ All keys of `Effect.RunOptions` are optional: `{ signal?, scheduler?, uninterruptible?, onFiberStart? }`.
546
+
547
+ When the effect still needs services, pre-apply a `Context`:
548
+
549
+ ```ts
550
+ const runWith = Effect.runForkWith(servicesContext); // <R> pre-applied
551
+ const fiber = runWith(effectNeedingServices, { signal });
552
+ ```
553
+
554
+ This is exactly what the FiberHandle/Map/Set runtime helpers wrap for you — prefer those when the forked fibers need supervision.
555
+
556
+ ### Keep-alive and runMain
557
+
558
+ In the current v4 runtime there is **no per-fiber keep-alive in the core runtime** (it existed earlier in v4 but was removed in beta.80; the `migration/fiber-keep-alive.md` doc predates the removal). A bare `Effect.runFork`/`Effect.runPromise` whose fiber is suspended on a pure Effect primitive (e.g. `Deferred.await`, `Effect.never`) does not by itself hold the Node.js process open.
559
+
560
+ `Runtime.makeRunMain`-based runners — `NodeRuntime.runMain` from `@effect/platform-node`, `BunRuntime.runMain`, etc. — install a long-interval timer that keeps the process alive until the main fiber completes, and additionally provide SIGINT/SIGTERM handling (interrupting the root fiber gracefully), exit-code mapping (interruption-only causes → 130), and error reporting. Always use `runMain` for long-lived program entry points:
561
+
562
+ ```ts
563
+ import { NodeRuntime } from '@effect/platform-node';
564
+
565
+ NodeRuntime.runMain(program);
566
+ ```
567
+
568
+ See the effect-managed-runtime skill for embedding Effect in existing applications.
569
+
570
+ ---
571
+
572
+ ## Key Patterns
573
+
574
+ ### Latest-wins cancellation (FiberHandle + runtime)
575
+
576
+ Each new request interrupts the in-flight one — autocomplete, file watching, "restart preview on change":
577
+
578
+ ```ts
579
+ const searchBox = Effect.gen(function* () {
580
+ const handle = yield* FiberHandle.make<void, never>();
581
+ const run = yield* FiberHandle.runtime(handle)<SearchApi>();
582
+
583
+ input.addEventListener('input', () => {
584
+ // synchronous call; interrupts the previous search automatically
585
+ run(performSearch(input.value));
586
+ });
587
+
588
+ yield* Effect.never; // keep the scope open while the UI lives
589
+ }).pipe(Effect.scoped);
590
+ ```
591
+
592
+ ### Restart-on-crash worker with a supervision watchdog
593
+
594
+ `Effect.retry` restarts the worker with backoff; `FiberHandle.join` propagates a final, unrecovered failure to the supervisor:
595
+
596
+ ```ts
597
+ const worker = consumeJobs.pipe(
598
+ Effect.retry(
599
+ Schedule.exponential('250 millis').pipe(Schedule.upTo({ times: 10 }))
600
+ )
601
+ );
602
+
603
+ const supervisor = Effect.gen(function* () {
604
+ const handle = yield* FiberHandle.make<never, JobError>();
605
+ yield* FiberHandle.run(handle, worker);
606
+
607
+ // blocks until the worker exhausts retries (fails) or the scope closes
608
+ yield* FiberHandle.join(handle);
609
+ }).pipe(Effect.scoped);
610
+ ```
611
+
612
+ ### Keyed jobs with dedupe and per-key cancellation (FiberMap)
613
+
614
+ ```ts
615
+ const downloads = Effect.gen(function* () {
616
+ const map = yield* FiberMap.make<string, void, DownloadError>();
617
+
618
+ const start = (url: string) =>
619
+ // onlyIfMissing dedupes: a second start() for the same url is a no-op
620
+ FiberMap.run(map, url, download(url), { onlyIfMissing: true });
621
+
622
+ const cancel = (url: string) => FiberMap.remove(map, url);
623
+
624
+ yield* start('https://example.com/a.bin');
625
+ yield* start('https://example.com/a.bin'); // already running — skipped
626
+ yield* cancel('https://example.com/a.bin');
627
+
628
+ yield* FiberMap.awaitEmpty(map); // drain remaining downloads
629
+ }).pipe(Effect.scoped);
630
+ ```
631
+
632
+ ### Background task set with graceful drain (FiberSet)
633
+
634
+ ```ts
635
+ const webhookProcessor = Effect.gen(function* () {
636
+ const tasks = yield* FiberSet.make<void, WebhookError>();
637
+
638
+ for (const event of events) {
639
+ yield* FiberSet.run(tasks, handleWebhook(event));
640
+ }
641
+
642
+ // graceful shutdown: wait for in-flight tasks instead of interrupting them.
643
+ // Drop this line to let the closing scope interrupt whatever is left.
644
+ yield* FiberSet.awaitEmpty(tasks);
645
+ }).pipe(Effect.scoped);
646
+ ```
647
+
648
+ To fail fast when any background task crashes, run the service's main loop against `FiberSet.join(tasks)` (it fails with the first non-interruption failure).
649
+
650
+ ### Bounded background task set (FiberSet + Semaphore)
651
+
652
+ `FiberSet` never applies backpressure on its own. Bound admission by taking a semaphore permit before `run` and releasing it when the task fiber settles:
653
+
654
+ ```ts
655
+ const boundedProcessor = Effect.gen(function* () {
656
+ const tasks = yield* FiberSet.make<void, WebhookError>();
657
+ const permits = yield* Semaphore.make(16); // at most 16 in flight
658
+
659
+ for (const event of events) {
660
+ yield* Semaphore.take(permits, 1); // waits while 16 tasks are in flight
661
+ yield* FiberSet.run(
662
+ tasks,
663
+ handleWebhook(event).pipe(
664
+ // runs on success, failure, AND interruption — permits never leak
665
+ Effect.ensuring(Semaphore.release(permits, 1))
666
+ )
667
+ );
668
+ }
669
+
670
+ yield* FiberSet.awaitEmpty(tasks);
671
+ }).pipe(Effect.scoped);
672
+ ```
673
+
674
+ See the effect-parallelization skill for `Semaphore` details (`withPermits`, `withPermitsIfAvailable`, `PartitionedSemaphore`).
675
+
676
+ ### Bridging callback APIs into supervised fibers
677
+
678
+ ```ts
679
+ const socketHandler = Effect.gen(function* () {
680
+ const set = yield* FiberSet.make<void, never>();
681
+ const run = yield* FiberSet.runtime(set)<MessageStore>();
682
+
683
+ socket.on('message', (data) => {
684
+ // every message handled on a tracked fiber;
685
+ // all in-flight handlers are interrupted when the scope closes
686
+ run(storeMessage(data));
687
+ });
688
+
689
+ yield* Effect.never;
690
+ }).pipe(Effect.scoped);
691
+ ```
692
+
693
+ ### Protected handoff between fibers (Deferred + interruption-safe cleanup)
694
+
695
+ ```ts
696
+ const handoff = Effect.gen(function* () {
697
+ const deferred = yield* Deferred.make<Result, WorkerError>();
698
+
699
+ const producer = yield* Effect.forkScoped(
700
+ produceResult.pipe(
701
+ Effect.onExit((exit) => Deferred.done(deferred, exit)),
702
+ Effect.onInterrupt(() => Effect.log('producer cancelled'))
703
+ )
704
+ );
705
+
706
+ // consumer waits without caring which fiber produces
707
+ return yield* Deferred.await(deferred);
708
+ });
709
+ ```
710
+
711
+ ---
712
+
713
+ ## Common Mistakes
714
+
715
+ 1. **`Effect.fork` / `Effect.forkDaemon` don't exist in v4** — they are `Effect.forkChild` and `Effect.forkDetach`. `Effect.forkAll` and `Effect.forkWithErrorHandler` were removed entirely.
716
+ 2. **Fire-and-forget with `forkChild` silently dies** — children are interrupted when the parent fiber completes. Use `forkScoped`/`forkIn` for scope-tied work, `forkDetach` for detached work, or `Effect.awaitAllChildren` to wait.
717
+ 3. **Asserting right after a fork** — forked fibers start on the next scheduler tick by default. `yield* Effect.yieldNow` first, or fork with `{ startImmediately: true }`.
718
+ 4. **Expecting `Fiber.await` to fail** — it always succeeds with an `Exit`. Use `Fiber.join` to propagate the fiber's failure; use `Fiber.await` to inspect.
719
+ 5. **Looking for `Fiber.poll`** — there is no effectful poll in v4. Use the synchronous `fiber.pollUnsafe()` (`Exit | undefined`) or `fiber.addObserver`.
720
+ 6. **Assuming `Fiber.joinAll` cancels the rest on failure** — it fails fast but leaves the other fibers running. Follow up with `Fiber.interruptAll` if they should stop.
721
+ 7. **Treating `Fiber.interrupt` as instant** — it waits for the target to fully settle, including finalizers and uninterruptible regions. For a non-blocking signal use `fiber.interruptUnsafe()`.
722
+ 8. **Using `join` to wait for completion on FiberHandle/FiberMap/FiberSet** — `join` only resolves on a fiber failure or when the scope closes; successful fibers just leave the collection. Use `awaitEmpty` to wait for work to finish.
723
+ 9. **Expecting external interruption to fail `join`** — by default it doesn't. Pass `{ propagateInterruption: true }` to `run`/`set`/`add`; the collection's own internal interruptions (replacement, `clear`, scope close) never fail `join` either way.
724
+ 10. **Expecting `onlyIfMissing: true` to error when occupied** — it succeeds, returning a shared already-interrupted fiber while keeping the existing one. Check `Exit.hasInterrupts(yield* Fiber.await(fiber))` to detect the rejected start.
725
+ 11. **Calling `run` on a closed collection** — `FiberHandle.run`/`FiberMap.run` interrupt the *calling* fiber; `FiberSet.run` and all `runtime()` runners return a pre-interrupted fiber instead. Neither throws.
726
+ 12. **Assuming collection fibers are children of the caller** — they are root fibers created via `Effect.runForkWith` with the caller's context: they start immediately and survive the calling fiber; only the collection (scope close, replacement, remove/clear) interrupts them.
727
+ 13. **Relying on `Effect.runFork`/`runPromise` to keep Node alive** — beta.80 removed the core fiber keep-alive; a fiber suspended on `Deferred.await`/`Effect.never` won't hold the process open. Use `NodeRuntime.runMain` (built on `Runtime.makeRunMain`).
728
+ 14. **Letting interruption leak through acquire/release** — wrap the whole sequence in `Effect.uninterruptibleMask` and `restore` only the use phase; pending interruption is delivered as soon as the region ends, so cleanup still runs exactly once.
729
+ 15. **Leaving collection type parameters off** — `FiberHandle.make()` defaults to `<unknown, unknown>`, making `join` surface `unknown` errors. Always pass them: `FiberHandle.make<A, E>()`, `FiberMap.make<K, A, E>()`, `FiberSet.make<A, E>()`.
730
+ 16. **Using v3 `FiberId` types** — v4 fiber ids are plain `number`s; `Cause.interruptors(cause)` and `Effect.onInterrupt` finalizers give you `ReadonlySet<number>`.
731
+ 17. **Assuming someone logs a background fiber's failure** — the v4 runtime has no unhandled-fiber-failure reporting and `Effect.forkWithErrorHandler` is gone. Unobserved failures vanish: `join`/`await` the fiber, watch the collection with `FiberHandle/FiberMap/FiberSet.join`, or build `Effect.catchCause`/`Effect.onError` + logging into the forked effect.