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.
- package/LICENSE +21 -0
- package/README.md +278 -0
- package/guidance/effect-first-development.md +1247 -0
- package/guidance/post__effect-and-the-near-inexpressible-majesty-of-layers.md +490 -0
- package/guidance/post__parse-dont-validate.md +109 -0
- package/guidance/progressive-disclosure-guidance.md +38 -0
- package/package.json +63 -0
- package/patterns/avoid-any.md +37 -0
- package/patterns/avoid-data-tagged-error.md +34 -0
- package/patterns/avoid-direct-json.md +51 -0
- package/patterns/avoid-direct-tag-checks.md +54 -0
- package/patterns/avoid-expect-in-if.md +52 -0
- package/patterns/avoid-mutable-state.md +70 -0
- package/patterns/avoid-native-fetch.md +61 -0
- package/patterns/avoid-node-imports.md +86 -0
- package/patterns/avoid-non-null-assertion.md +44 -0
- package/patterns/avoid-object-type.md +46 -0
- package/patterns/avoid-option-getorthrow.md +39 -0
- package/patterns/avoid-platform-coupling.md +43 -0
- package/patterns/avoid-process-env.md +43 -0
- package/patterns/avoid-react-hooks.md +73 -0
- package/patterns/avoid-schema-suffix.md +45 -0
- package/patterns/avoid-sync-fs.md +68 -0
- package/patterns/avoid-try-catch.md +47 -0
- package/patterns/avoid-ts-ignore.md +38 -0
- package/patterns/avoid-untagged-errors.md +67 -0
- package/patterns/avoid-yield-ref.md +46 -0
- package/patterns/casting-awareness.md +46 -0
- package/patterns/context-tag-extends.md +84 -0
- package/patterns/effect-catchall-default.md +61 -0
- package/patterns/effect-promise-vs-trypromise.md +47 -0
- package/patterns/effect-run-in-body.md +58 -0
- package/patterns/imperative-loops.md +76 -0
- package/patterns/prefer-arr-sort.md +52 -0
- package/patterns/prefer-duration-values.md +56 -0
- package/patterns/prefer-effect-fn.md +161 -0
- package/patterns/prefer-match-over-switch.md +48 -0
- package/patterns/prefer-option-over-null.md +56 -0
- package/patterns/prefer-redacted-config.md +70 -0
- package/patterns/prefer-schema-class.md +54 -0
- package/patterns/require-effect-concurrency.md +83 -0
- package/patterns/stream-large-files.md +63 -0
- package/patterns/throw-in-effect-gen.md +62 -0
- package/patterns/use-clock-service.md +45 -0
- package/patterns/use-command-executor-service.md +54 -0
- package/patterns/use-console-service.md +54 -0
- package/patterns/use-filesystem-service.md +59 -0
- package/patterns/use-http-client-service.md +77 -0
- package/patterns/use-path-service.md +53 -0
- package/patterns/use-random-service.md +45 -0
- package/patterns/use-temp-file-scoped.md +66 -0
- package/patterns/vm-in-wrong-file.md +51 -0
- package/patterns/yield-in-for-loop.md +61 -0
- package/skills/effect-ai-chat/SKILL.md +472 -0
- package/skills/effect-ai-language-model/SKILL.md +652 -0
- package/skills/effect-ai-prompt/SKILL.md +752 -0
- package/skills/effect-ai-provider/SKILL.md +668 -0
- package/skills/effect-ai-streaming/SKILL.md +418 -0
- package/skills/effect-ai-tool/SKILL.md +1132 -0
- package/skills/effect-atom-rpc/SKILL.md +488 -0
- package/skills/effect-atom-state/SKILL.md +640 -0
- package/skills/effect-batching/SKILL.md +614 -0
- package/skills/effect-cache/SKILL.md +570 -0
- package/skills/effect-cli/SKILL.md +523 -0
- package/skills/effect-command-executor/SKILL.md +675 -0
- package/skills/effect-concurrency-testing/SKILL.md +612 -0
- package/skills/effect-config/SKILL.md +580 -0
- package/skills/effect-context-witness/SKILL.md +274 -0
- package/skills/effect-domain-modeling/SKILL.md +1212 -0
- package/skills/effect-domain-predicates/SKILL.md +867 -0
- package/skills/effect-error-handling/SKILL.md +1581 -0
- package/skills/effect-fiber/SKILL.md +731 -0
- package/skills/effect-filesystem/SKILL.md +624 -0
- package/skills/effect-graph/SKILL.md +571 -0
- package/skills/effect-http-api/SKILL.md +1760 -0
- package/skills/effect-http-client/SKILL.md +989 -0
- package/skills/effect-http-server/SKILL.md +920 -0
- package/skills/effect-incremental-migration/SKILL.md +362 -0
- package/skills/effect-layer-design/SKILL.md +642 -0
- package/skills/effect-managed-runtime/SKILL.md +395 -0
- package/skills/effect-mcp-server/SKILL.md +608 -0
- package/skills/effect-observability/SKILL.md +719 -0
- package/skills/effect-optics/SKILL.md +554 -0
- package/skills/effect-parallelization/SKILL.md +668 -0
- package/skills/effect-path/SKILL.md +296 -0
- package/skills/effect-pattern-matching/SKILL.md +914 -0
- package/skills/effect-platform-abstraction/SKILL.md +1175 -0
- package/skills/effect-platform-layers/SKILL.md +514 -0
- package/skills/effect-pubsub-event-bus/SKILL.md +384 -0
- package/skills/effect-react-composition/SKILL.md +986 -0
- package/skills/effect-react-vm/SKILL.md +675 -0
- package/skills/effect-rpc-api/SKILL.md +624 -0
- package/skills/effect-rpc-client/SKILL.md +666 -0
- package/skills/effect-rpc-cluster/SKILL.md +1623 -0
- package/skills/effect-rpc-server/SKILL.md +767 -0
- package/skills/effect-scheduling/SKILL.md +124 -0
- package/skills/effect-schema-composition/SKILL.md +975 -0
- package/skills/effect-schema-v4/SKILL.md +691 -0
- package/skills/effect-scope/SKILL.md +682 -0
- package/skills/effect-service-implementation/SKILL.md +656 -0
- package/skills/effect-socket/SKILL.md +703 -0
- package/skills/effect-sql/SKILL.md +781 -0
- package/skills/effect-stream/SKILL.md +765 -0
- package/skills/effect-testing/SKILL.md +1331 -0
- package/skills/effect-typeclass-design/SKILL.md +161 -0
- package/skills/effect-wide-events/Article.md +66 -0
- package/skills/effect-wide-events/SKILL.md +95 -0
- package/skills/effect-workflow/SKILL.md +810 -0
- package/src/agent-policy.ts +22 -0
- package/src/enforcer.ts +104 -0
- package/src/frontmatter.ts +34 -0
- package/src/guidance.ts +66 -0
- package/src/index.ts +38 -0
- package/src/pattern-catalog.ts +115 -0
- package/src/pattern-matcher.ts +178 -0
- package/src/pattern.ts +97 -0
- package/src/skills.ts +29 -0
- 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.
|