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,765 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: effect-stream
|
|
3
|
+
description: Build effectful pull-based streaming pipelines with Effect Stream — creation, transformation, consumption, NDJSON/Msgpack encoding, concurrency, resource safety. Use when working with values produced over time, paginated APIs, event listeners, or streaming I/O.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
You are an Effect TypeScript expert specializing in pull-based streaming with `Stream`, `Sink`, and `Channel`.
|
|
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
|
+
- Stream constructors and combinators (`packages/effect/src/Stream.ts`)
|
|
16
|
+
- Creating streams from various sources (`ai-docs/src/02_stream/10_creating-streams.ts`)
|
|
17
|
+
- Consuming and transforming streams (`ai-docs/src/02_stream/20_consuming-streams.ts`)
|
|
18
|
+
- Encoding/decoding with NDJSON and Msgpack (`ai-docs/src/02_stream/30_encoding.ts`)
|
|
19
|
+
|
|
20
|
+
## Core Model
|
|
21
|
+
|
|
22
|
+
A `Stream<A, E, R>` is a program that can emit many `A` values, fail with `E`, and require `R`. Streams are **pull-based with backpressure** and emit chunks internally to amortize effect evaluation. They support monadic composition and error handling similar to `Effect`, adapted for multiple values.
|
|
23
|
+
|
|
24
|
+
Use a stream when values are naturally many-valued and ordered over time. For one effect repeated only for its side effects, prefer `Effect.repeat` with `Schedule`; see the effect-scheduling skill.
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { Effect, Schedule, Schema, Sink, Stream } from 'effect';
|
|
28
|
+
import { Ndjson, Msgpack } from 'effect/unstable/encoding';
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
For Node.js readable streams:
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import { NodeStream } from '@effect/platform-node';
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## 1. Creating Streams
|
|
40
|
+
|
|
41
|
+
Source chooser:
|
|
42
|
+
|
|
43
|
+
- Values/tests: `Stream.make` or `Stream.fromIterable`.
|
|
44
|
+
- Callback boundary consumed by one worker: private `Queue` + `Stream.fromQueue`.
|
|
45
|
+
- Broadcast events: private `PubSub` + `Stream.fromPubSub`.
|
|
46
|
+
- Current value plus changes: `SubscriptionRef`.
|
|
47
|
+
- Schedule outputs/ticks: `Stream.fromSchedule`.
|
|
48
|
+
- Paginated pull API: `Stream.paginate`.
|
|
49
|
+
- Effect that first reads services/config: `Stream.unwrap`.
|
|
50
|
+
- Async iterable/platform source: `Stream.fromAsyncIterable` when no native Effect source exists.
|
|
51
|
+
|
|
52
|
+
### From values and iterables
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
// Fixed values
|
|
56
|
+
const s1 = Stream.make(1, 2, 3);
|
|
57
|
+
|
|
58
|
+
// From any iterable
|
|
59
|
+
const s2 = Stream.fromIterable([1, 2, 3, 4, 5]);
|
|
60
|
+
|
|
61
|
+
// Integer range (inclusive on both ends)
|
|
62
|
+
const s3 = Stream.range(1, 100);
|
|
63
|
+
|
|
64
|
+
// Infinite stream via pure iteration
|
|
65
|
+
const s4 = Stream.iterate(1, (n) => n * 2); // 1, 2, 4, 8, ...
|
|
66
|
+
|
|
67
|
+
// Single value from an effect
|
|
68
|
+
const s5 = Stream.fromEffect(Effect.succeed(42));
|
|
69
|
+
|
|
70
|
+
// Empty stream
|
|
71
|
+
const s6 = Stream.empty;
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### From effects (polling / repeating)
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
// Poll an effect on a schedule — useful for metrics, health checks, cache refresh
|
|
78
|
+
const samples = Stream.fromEffectSchedule(
|
|
79
|
+
Effect.succeed(3),
|
|
80
|
+
Schedule.spaced('30 seconds')
|
|
81
|
+
).pipe(Stream.take(10));
|
|
82
|
+
|
|
83
|
+
// Repeat an effect forever (no schedule delay)
|
|
84
|
+
const forever = Stream.fromEffectRepeat(Effect.succeed('tick'));
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Paginated APIs
|
|
88
|
+
|
|
89
|
+
`Stream.paginate` drives cursor-based pagination. Return the current page and `Option.some(nextCursor)` or `Option.none()` to stop.
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
import * as Option from 'effect/Option';
|
|
93
|
+
|
|
94
|
+
const fetchAllPages = Stream.paginate(
|
|
95
|
+
0, // initial cursor
|
|
96
|
+
Effect.fn(function* (page) {
|
|
97
|
+
yield* Effect.sleep('50 millis'); // simulate network
|
|
98
|
+
const results = Array.from(
|
|
99
|
+
{ length: 100 },
|
|
100
|
+
(_, i) => `Job ${i + 1 + page * 100}`
|
|
101
|
+
);
|
|
102
|
+
const nextPage = page < 10 ? Option.some(page + 1) : Option.none();
|
|
103
|
+
return [results, nextPage] as const;
|
|
104
|
+
})
|
|
105
|
+
);
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### From async iterables
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
class IterError extends Schema.TaggedError<IterError>()('IterError', {
|
|
112
|
+
cause: Schema.Defect()
|
|
113
|
+
}) {}
|
|
114
|
+
|
|
115
|
+
async function* generate() {
|
|
116
|
+
yield 'a';
|
|
117
|
+
yield 'b';
|
|
118
|
+
yield 'c';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const letters = Stream.fromAsyncIterable(
|
|
122
|
+
generate(),
|
|
123
|
+
(cause) => new IterError({ cause })
|
|
124
|
+
);
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### From DOM events
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
// Direct event listener binding
|
|
131
|
+
const clicks = Stream.fromEventListener<PointerEvent>(button, 'click');
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### From callback-based APIs
|
|
135
|
+
|
|
136
|
+
`Stream.callback` gives you a `Queue` to push values into. Use `Effect.acquireRelease` inside to register/unregister listeners with guaranteed cleanup.
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
const callbackStream = Stream.callback<PointerEvent>(
|
|
140
|
+
Effect.fn(function* (queue) {
|
|
141
|
+
function onEvent(event: PointerEvent) {
|
|
142
|
+
Queue.offerUnsafe(queue, event);
|
|
143
|
+
}
|
|
144
|
+
yield* Effect.acquireRelease(
|
|
145
|
+
Effect.sync(() => button.addEventListener('click', onEvent)),
|
|
146
|
+
() =>
|
|
147
|
+
Effect.sync(() => button.removeEventListener('click', onEvent))
|
|
148
|
+
);
|
|
149
|
+
})
|
|
150
|
+
);
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Options: `{ bufferSize?: number, strategy?: "sliding" | "dropping" | "suspend" }`
|
|
154
|
+
|
|
155
|
+
### From ReadableStream (DOM/Web)
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
const webStream = Stream.fromReadableStream({
|
|
159
|
+
evaluate: () => response.body!,
|
|
160
|
+
onError: (cause) => new MyError({ cause }),
|
|
161
|
+
releaseLockOnEnd: false // default: cancels reader; true releases lock instead
|
|
162
|
+
});
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### From Node.js readable streams
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
import { NodeStream } from '@effect/platform-node';
|
|
169
|
+
import { Readable } from 'node:stream';
|
|
170
|
+
|
|
171
|
+
class NodeErr extends Schema.TaggedError<NodeErr>()('NodeErr', {
|
|
172
|
+
cause: Schema.Defect()
|
|
173
|
+
}) {}
|
|
174
|
+
|
|
175
|
+
const nodeStream = NodeStream.fromReadable({
|
|
176
|
+
evaluate: () => Readable.from(['Hello', ' ', 'world']),
|
|
177
|
+
onError: (cause) => new NodeErr({ cause }),
|
|
178
|
+
closeOnDone: true // true by default
|
|
179
|
+
});
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
For bounded collection of a Node readable, use `NodeStream.toString`, `NodeStream.toArrayBuffer`, or `NodeStream.toUint8Array` with `maxBytes`. The limit is inclusive: `maxBytes: 0` permits an empty stream but fails through `onError` as soon as any byte is received, and the consumer destroys the readable on interruption or failure.
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
const text = NodeStream.toString(() => readable, {
|
|
186
|
+
maxBytes: 0,
|
|
187
|
+
onError: (cause) => new NodeErr({ cause })
|
|
188
|
+
});
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Advanced constructors
|
|
192
|
+
|
|
193
|
+
```ts
|
|
194
|
+
// Unwrap: create a stream from an effect that returns a stream
|
|
195
|
+
const unwrapped = Stream.unwrap(Effect.succeed(Stream.make(1, 2, 3)));
|
|
196
|
+
|
|
197
|
+
// From a Channel directly
|
|
198
|
+
const fromChan = Stream.fromChannel(myChannel);
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## 2. Transforming Streams
|
|
204
|
+
|
|
205
|
+
Choose `map` for pure work, `mapEffect` for effectful work, and bounded `mapEffect(..., { concurrency })` for parallel work. Add `unordered: true` only when output order is irrelevant. Use `flatMap` for zero/many outputs, `filter`/`filterEffect` for selection, and `mapAccum`/`mapAccumEffect` for stateful transforms.
|
|
206
|
+
|
|
207
|
+
### Pure transforms
|
|
208
|
+
|
|
209
|
+
```ts
|
|
210
|
+
// Per-element mapping (receives element and index)
|
|
211
|
+
stream.pipe(Stream.map((value, index) => value * 2));
|
|
212
|
+
|
|
213
|
+
// Filter elements
|
|
214
|
+
stream.pipe(Stream.filter((x) => x > 10));
|
|
215
|
+
|
|
216
|
+
// Windowing
|
|
217
|
+
stream.pipe(Stream.take(5)); // first 5 elements
|
|
218
|
+
stream.pipe(Stream.drop(3)); // skip first 3
|
|
219
|
+
stream.pipe(Stream.takeWhile((x) => x < 100));
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Effectful transforms
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
// mapEffect with concurrency control
|
|
226
|
+
stream.pipe(
|
|
227
|
+
Stream.mapEffect((order) => enrichOrder(order), { concurrency: 4 })
|
|
228
|
+
);
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### FlatMap
|
|
232
|
+
|
|
233
|
+
Transform each element into a stream and flatten. Supports concurrency.
|
|
234
|
+
|
|
235
|
+
```ts
|
|
236
|
+
Stream.make('US', 'CA', 'NZ').pipe(
|
|
237
|
+
Stream.flatMap(
|
|
238
|
+
(country) =>
|
|
239
|
+
Stream.range(1, 50).pipe(
|
|
240
|
+
Stream.map((i) => ({ id: `${country}_${i}`, country }))
|
|
241
|
+
),
|
|
242
|
+
{ concurrency: 2 }
|
|
243
|
+
)
|
|
244
|
+
);
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### Accumulation
|
|
248
|
+
|
|
249
|
+
```ts
|
|
250
|
+
// Running accumulator — emits initial state plus each accumulated state
|
|
251
|
+
// Output: [0, 1, 3, 6]
|
|
252
|
+
Stream.make(1, 2, 3).pipe(Stream.scan(0, (acc, n) => acc + n));
|
|
253
|
+
|
|
254
|
+
// Effectful variant
|
|
255
|
+
Stream.make(1, 2, 3).pipe(
|
|
256
|
+
Stream.scanEffect(0, (acc, n) => Effect.succeed(acc + n))
|
|
257
|
+
);
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### Grouping and batching
|
|
261
|
+
|
|
262
|
+
```ts
|
|
263
|
+
// Group into fixed-size chunks
|
|
264
|
+
stream.pipe(Stream.grouped(100));
|
|
265
|
+
|
|
266
|
+
// Group by size OR time window (whichever comes first)
|
|
267
|
+
stream.pipe(Stream.groupedWithin(100, '1 second'));
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### Rate control
|
|
271
|
+
|
|
272
|
+
```ts
|
|
273
|
+
// Debounce — emit only the latest element after a pause
|
|
274
|
+
stream.pipe(Stream.debounce('300 millis'));
|
|
275
|
+
|
|
276
|
+
// Throttle — control throughput
|
|
277
|
+
stream.pipe(
|
|
278
|
+
Stream.throttle({
|
|
279
|
+
cost: () => 1,
|
|
280
|
+
units: 10,
|
|
281
|
+
duration: '1 second',
|
|
282
|
+
strategy: 'shape' // "shape" delays, "enforce" drops
|
|
283
|
+
})
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
// Timeout — end stream if no element produced within duration
|
|
287
|
+
stream.pipe(Stream.timeout('5 seconds'));
|
|
288
|
+
|
|
289
|
+
// Timeout with fallback — switch to another stream on timeout
|
|
290
|
+
stream.pipe(
|
|
291
|
+
Stream.timeoutOrElse({
|
|
292
|
+
duration: '5 seconds',
|
|
293
|
+
orElse: () => Stream.make(fallbackValue)
|
|
294
|
+
})
|
|
295
|
+
);
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
Both `timeout` and `timeoutOrElse` are dual functions. `timeout` is implemented as `timeoutOrElse` with `Stream.empty` as the fallback. Non-finite durations return the stream unchanged; zero duration immediately switches to `orElse`.
|
|
299
|
+
|
|
300
|
+
### Indexing and neighbors
|
|
301
|
+
|
|
302
|
+
```ts
|
|
303
|
+
stream.pipe(Stream.zipWithIndex); // [A, number]
|
|
304
|
+
stream.pipe(Stream.zipWithNext); // [A, Option<A>]
|
|
305
|
+
stream.pipe(Stream.zipWithPrevious); // [Option<A>, A]
|
|
306
|
+
stream.pipe(Stream.zipWithPreviousAndNext); // [Option<A>, A, Option<A>]
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
---
|
|
310
|
+
|
|
311
|
+
## 3. Consuming Streams
|
|
312
|
+
|
|
313
|
+
All `run*` methods return `Effect` values — the stream is only pulled when the effect is executed.
|
|
314
|
+
|
|
315
|
+
Use `runForEach` for side-effecting consumers, `runDrain` when values are irrelevant, and `runFold` for bounded aggregation. Reserve `runCollect` for tests and known-finite, memory-bounded streams; never collect an unbounded production event stream. In tests, prefer `take(n)` + `runCollect`.
|
|
316
|
+
|
|
317
|
+
For stream tests, use `fromIterable` for finite fixtures, `empty` for no events, and a test-owned `Queue` plus `fromQueue` when the test must drive events interactively. Coordinate with `Deferred`, `Queue`, `Latch`, or `TestClock`, never real sleeps.
|
|
318
|
+
|
|
319
|
+
```ts
|
|
320
|
+
// Collect all elements into an array
|
|
321
|
+
const all = Stream.runCollect(stream);
|
|
322
|
+
// Effect<Array<A>, E, R>
|
|
323
|
+
|
|
324
|
+
// Run for side effects, ignore output
|
|
325
|
+
const drained = Stream.runDrain(stream);
|
|
326
|
+
// Effect<void, E, R>
|
|
327
|
+
|
|
328
|
+
// Execute effectful consumer per element
|
|
329
|
+
stream.pipe(Stream.runForEach((item) => Effect.log(`Got: ${item}`)));
|
|
330
|
+
// Effect<void, E, R>
|
|
331
|
+
|
|
332
|
+
// Fold to a single value (initial is a LazyArg — a thunk)
|
|
333
|
+
stream.pipe(
|
|
334
|
+
Stream.runFold(
|
|
335
|
+
() => 0,
|
|
336
|
+
(acc, n) => acc + n
|
|
337
|
+
)
|
|
338
|
+
);
|
|
339
|
+
// Effect<number, E, R>
|
|
340
|
+
|
|
341
|
+
// First / last element as Option
|
|
342
|
+
Stream.runHead(stream); // Effect<Option<A>, E, R>
|
|
343
|
+
Stream.runLast(stream); // Effect<Option<A>, E, R>
|
|
344
|
+
|
|
345
|
+
// Count / Sum helpers
|
|
346
|
+
Stream.runCount(stream); // Effect<number, E, R>
|
|
347
|
+
Stream.runSum(stream); // Effect<number, E, R> (stream must be Stream<number>)
|
|
348
|
+
|
|
349
|
+
// Consume with a Sink
|
|
350
|
+
stream.pipe(
|
|
351
|
+
Stream.map((order) => order.totalCents),
|
|
352
|
+
Stream.run(Sink.sum)
|
|
353
|
+
);
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
---
|
|
357
|
+
|
|
358
|
+
## 4. Encoding & Decoding (NDJSON / Msgpack)
|
|
359
|
+
|
|
360
|
+
Use `Stream.pipeThroughChannel` with codec channels from `effect/unstable/encoding`.
|
|
361
|
+
|
|
362
|
+
```ts
|
|
363
|
+
import { Ndjson, Msgpack } from 'effect/unstable/encoding';
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
### Text decoding (split multi-byte characters)
|
|
367
|
+
|
|
368
|
+
To turn a byte stream into text, use `Stream.decodeText` (or `Channel.decodeText`) rather than hand-rolling `new TextDecoder().decode(chunk)` per chunk. These helpers decode with streaming enabled, so multi-byte UTF-8 characters split across `Uint8Array` chunk boundaries are reassembled correctly; per-chunk `TextDecoder` calls would corrupt characters that straddle a boundary.
|
|
369
|
+
|
|
370
|
+
```ts
|
|
371
|
+
byteStream.pipe(Stream.decodeText, Stream.runForEach(handleText));
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
### NDJSON — string variants
|
|
375
|
+
|
|
376
|
+
```ts
|
|
377
|
+
// Decode: raw NDJSON string → parsed JSON objects
|
|
378
|
+
rawStream.pipe(
|
|
379
|
+
Stream.pipeThroughChannel(Ndjson.decodeString()),
|
|
380
|
+
Stream.runCollect
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
// Decode with schema validation
|
|
384
|
+
rawStream.pipe(
|
|
385
|
+
Stream.pipeThroughChannel(Ndjson.decodeSchemaString(MySchema)()),
|
|
386
|
+
Stream.runCollect
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
// Encode: objects → NDJSON strings
|
|
390
|
+
objectStream.pipe(
|
|
391
|
+
Stream.pipeThroughChannel(Ndjson.encodeString()),
|
|
392
|
+
Stream.runCollect
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
// Encode through schema (applies transforms like date formatting)
|
|
396
|
+
typedStream.pipe(
|
|
397
|
+
Stream.pipeThroughChannel(Ndjson.encodeSchemaString(MySchema)()),
|
|
398
|
+
Stream.runCollect
|
|
399
|
+
);
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
### NDJSON — binary variants (Uint8Array)
|
|
403
|
+
|
|
404
|
+
For TCP sockets, file descriptors, etc.
|
|
405
|
+
|
|
406
|
+
```ts
|
|
407
|
+
binaryStream.pipe(Stream.pipeThroughChannel(Ndjson.decode())); // Uint8Array → objects
|
|
408
|
+
objectStream.pipe(Stream.pipeThroughChannel(Ndjson.encode())); // objects → Uint8Array
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
### NDJSON options
|
|
412
|
+
|
|
413
|
+
```ts
|
|
414
|
+
// Ignore blank lines instead of raising NdjsonError
|
|
415
|
+
Stream.pipeThroughChannel(Ndjson.decodeString({ ignoreEmptyLines: true }));
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
### Msgpack
|
|
419
|
+
|
|
420
|
+
Same API shape — replace `Ndjson` with `Msgpack`. Note that `Msgpack.decodeSchema(schema)` is curried: it returns a factory you must invoke (`()`) to get the `Channel` value passed to `Stream.pipeThroughChannel`, exactly like the NDJSON schema helpers.
|
|
421
|
+
|
|
422
|
+
```ts
|
|
423
|
+
const decoder = Msgpack.decodeSchema(
|
|
424
|
+
Schema.Struct({
|
|
425
|
+
id: Schema.Number,
|
|
426
|
+
name: Schema.String
|
|
427
|
+
})
|
|
428
|
+
)();
|
|
429
|
+
|
|
430
|
+
binaryStream.pipe(Stream.pipeThroughChannel(decoder), Stream.runCollect);
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
### Realistic pipeline: decode → transform → re-encode
|
|
434
|
+
|
|
435
|
+
```ts
|
|
436
|
+
const pipeline = rawNdjsonStream.pipe(
|
|
437
|
+
Stream.pipeThroughChannel(Ndjson.decodeSchemaString(LogEntry)()),
|
|
438
|
+
Stream.filter((entry) => entry.level === 'error'),
|
|
439
|
+
Stream.pipeThroughChannel(Ndjson.encodeSchemaString(LogEntry)()),
|
|
440
|
+
Stream.runCollect
|
|
441
|
+
);
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
### Handling encoding errors
|
|
445
|
+
|
|
446
|
+
`Ndjson.NdjsonError` has a `kind` field: `"Pack"` (encoding) or `"Unpack"` (decoding).
|
|
447
|
+
|
|
448
|
+
```ts
|
|
449
|
+
rawStream.pipe(
|
|
450
|
+
Stream.pipeThroughChannel(Ndjson.decodeString()),
|
|
451
|
+
Stream.catchTag('NdjsonError', (err) =>
|
|
452
|
+
Stream.succeed({ recovered: true, kind: err.kind })
|
|
453
|
+
),
|
|
454
|
+
Stream.runCollect
|
|
455
|
+
);
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
---
|
|
459
|
+
|
|
460
|
+
## 5. Error Handling
|
|
461
|
+
|
|
462
|
+
### catchTag / catchTags
|
|
463
|
+
|
|
464
|
+
Recover from specific tagged errors, producing a fallback stream.
|
|
465
|
+
|
|
466
|
+
```ts
|
|
467
|
+
stream.pipe(
|
|
468
|
+
Stream.catchTag('NetworkError', (err) => Stream.succeed(fallbackValue))
|
|
469
|
+
);
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
### retry
|
|
473
|
+
|
|
474
|
+
Retry a failing stream with a schedule. The stream restarts from the beginning on each retry.
|
|
475
|
+
|
|
476
|
+
```ts
|
|
477
|
+
stream.pipe(Stream.retry(Schedule.recurs(3)));
|
|
478
|
+
|
|
479
|
+
// With exponential backoff
|
|
480
|
+
stream.pipe(Stream.retry(Schedule.exponential('100 millis')));
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
Schedules can be effectful and fail. `Stream.retry` includes the schedule's error in the resulting stream error channel; `Effect.schedule` and `Effect.scheduleFrom` likewise union schedule errors into their error channels. Recover or map that error explicitly rather than assuming only the repeated operation can fail. For sequential schedule composition, use `Schedule.concat` / `Schedule.concatResult`; the former `andThen` names were removed.
|
|
484
|
+
|
|
485
|
+
### Execution-plan attempt events
|
|
486
|
+
|
|
487
|
+
`Stream.withExecutionPlan` accepts an `onEvent` observer for attempt-level logs and metrics:
|
|
488
|
+
|
|
489
|
+
```ts
|
|
490
|
+
const planned = stream.pipe(
|
|
491
|
+
Stream.withExecutionPlan(plan, {
|
|
492
|
+
onEvent: (event) => Effect.log('execution plan event', event)
|
|
493
|
+
})
|
|
494
|
+
);
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
Events are `AttemptStart`, `AttemptSuccess`, or `AttemptFailure`. Every start has one terminal event, failures carry the full `Cause`, and `attempt` is cumulative while `stepAttempt` is 1-based within a step. The observer must have a `never` error channel; an observer defect is isolated from the attempt outcome and does not leave events unpaired. If a downstream consumer intentionally stops pulling early, the truncated attempt is reported as successful.
|
|
498
|
+
|
|
499
|
+
### orElseIfEmpty / orElseSucceed
|
|
500
|
+
|
|
501
|
+
```ts
|
|
502
|
+
// Provide a default stream if the source emits nothing
|
|
503
|
+
stream.pipe(Stream.orElseIfEmpty(() => Stream.make(defaultValue)));
|
|
504
|
+
|
|
505
|
+
// Provide a single fallback value when the source fails
|
|
506
|
+
stream.pipe(Stream.orElseSucceed((error) => defaultValue));
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
---
|
|
510
|
+
|
|
511
|
+
## 6. Concurrency & Merging
|
|
512
|
+
|
|
513
|
+
### Buffer policy
|
|
514
|
+
|
|
515
|
+
Prefer natural backpressure. Add `Stream.buffer` only to deliberately decouple producer and consumer: `"suspend"` backpressures when full, `"dropping"` drops new values, and `"sliding"` drops old values to retain the latest. Avoid `capacity: "unbounded"` unless growth is bounded elsewhere and documented.
|
|
516
|
+
|
|
517
|
+
### merge
|
|
518
|
+
|
|
519
|
+
Interleave elements from two streams concurrently in arrival order.
|
|
520
|
+
|
|
521
|
+
```ts
|
|
522
|
+
Stream.merge(streamA, streamB);
|
|
523
|
+
Stream.merge(streamA, streamB, { haltStrategy: 'left' }); // stop when left ends
|
|
524
|
+
// HaltStrategy: "left" | "right" | "both" | "either"
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
### mergeAll
|
|
528
|
+
|
|
529
|
+
Merge many streams concurrently. The streams are passed as a single **iterable**, followed by the options.
|
|
530
|
+
|
|
531
|
+
```ts
|
|
532
|
+
Stream.mergeAll([streamA, streamB, streamC], {
|
|
533
|
+
concurrency: 4
|
|
534
|
+
});
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
### interleave
|
|
538
|
+
|
|
539
|
+
Deterministically alternate elements from two streams (round-robin).
|
|
540
|
+
|
|
541
|
+
```ts
|
|
542
|
+
Stream.interleave(left, right);
|
|
543
|
+
|
|
544
|
+
// Custom interleave pattern via boolean decider stream
|
|
545
|
+
Stream.interleaveWith(left, right, Stream.make(true, false, false, true));
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
### mergeResult
|
|
549
|
+
|
|
550
|
+
Tag values from two streams: left as `Result.succeed`, right as `Result.fail`.
|
|
551
|
+
|
|
552
|
+
```ts
|
|
553
|
+
Stream.mergeResult(left, right); // Stream<Result<LeftA, RightA>>
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
### mergeEffect
|
|
557
|
+
|
|
558
|
+
Run a background effect concurrently with a stream; keep the stream's elements.
|
|
559
|
+
|
|
560
|
+
```ts
|
|
561
|
+
stream.pipe(Stream.mergeEffect(Effect.log('background task')));
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
### zipWith
|
|
565
|
+
|
|
566
|
+
Pair elements from two streams positionally.
|
|
567
|
+
|
|
568
|
+
```ts
|
|
569
|
+
Stream.zipWith(numbersStream, labelsStream, (n, label) => `${label}: ${n}`);
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
### broadcast
|
|
573
|
+
|
|
574
|
+
PubSub-backed multicast: the source is consumed once and fanned out to every subscriber. Returns a scoped effect, and the **producer starts immediately** — it does not wait for subscribers to attach.
|
|
575
|
+
|
|
576
|
+
```ts
|
|
577
|
+
Effect.scoped(
|
|
578
|
+
Effect.gen(function* () {
|
|
579
|
+
const shared = yield* stream.pipe(
|
|
580
|
+
Stream.broadcast({ capacity: 16, replay: 3 })
|
|
581
|
+
);
|
|
582
|
+
// Each consumer subscribes independently. Because the producer starts
|
|
583
|
+
// immediately, a late subscriber only sees values still held in `replay`.
|
|
584
|
+
const fiberA = yield* Stream.runCollect(shared).pipe(Effect.forkChild);
|
|
585
|
+
const fiberB = yield* Stream.runCollect(shared).pipe(Effect.forkChild);
|
|
586
|
+
// ...
|
|
587
|
+
})
|
|
588
|
+
);
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
Options: `{ capacity: number | "unbounded", strategy?: "sliding" | "dropping" | "suspend", replay?: number }`
|
|
592
|
+
|
|
593
|
+
Because the producer starts immediately, subscribers that attach after the source has already emitted will miss earlier values unless `replay` is configured (and `replay` only retains the most recent N values — it is not a full log). For a **fixed, known set of consumers**, prefer `broadcastN`: it subscribes all downstream streams before starting the source, so none of them miss values.
|
|
594
|
+
|
|
595
|
+
### broadcastN
|
|
596
|
+
|
|
597
|
+
Fixed-fanout multicast (added in beta.68). Produces a tuple of `n` streams; the source starts only **after all `n` downstream streams have been subscribed**, so every consumer sees the full sequence without needing `replay`. If a downstream stream is interrupted, it unsubscribes and no longer contributes backpressure.
|
|
598
|
+
|
|
599
|
+
```ts
|
|
600
|
+
Effect.scoped(
|
|
601
|
+
Effect.gen(function* () {
|
|
602
|
+
const [left, right] = yield* Stream.make(1, 2, 3).pipe(
|
|
603
|
+
Stream.broadcastN({ n: 2, capacity: 8 })
|
|
604
|
+
);
|
|
605
|
+
|
|
606
|
+
const [leftValues, rightValues] = yield* Effect.all(
|
|
607
|
+
[Stream.runCollect(left), Stream.runCollect(right)],
|
|
608
|
+
{ concurrency: 'unbounded' }
|
|
609
|
+
);
|
|
610
|
+
// leftValues and rightValues each === [1, 2, 3]
|
|
611
|
+
})
|
|
612
|
+
);
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
Options: `{ n: number, capacity: number | "unbounded", strategy?: "sliding" | "dropping" | "suspend", replay?: number }`
|
|
616
|
+
|
|
617
|
+
### share
|
|
618
|
+
|
|
619
|
+
Like broadcast but subscribes lazily when the first consumer starts, keeps upstream alive while consumers exist.
|
|
620
|
+
|
|
621
|
+
```ts
|
|
622
|
+
const shared = yield* stream.pipe(Stream.share({ capacity: 16 }));
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
---
|
|
626
|
+
|
|
627
|
+
## 7. Resource Safety
|
|
628
|
+
|
|
629
|
+
### Long-lived service consumers
|
|
630
|
+
|
|
631
|
+
Expose `Stream` values from service interfaces while keeping producer `Queue`, `PubSub`, and mutable state private. Own long-lived consumers in a layer and ordinarily run them with `stream.pipe(Stream.runForEach(handle), Effect.forkScoped)` so layer shutdown interrupts the consumer.
|
|
632
|
+
|
|
633
|
+
`forkScoped` provides lifetime supervision, not failure recovery or restart supervision. A failed child fiber does not automatically fail its parent or restart itself. If consumer failure must stop the application, restart with policy, or be reported, explicitly join/monitor the fiber or install a supervisor at the owning runtime boundary. Preserve interruption as shutdown; do not blanket-catch causes and turn interruption into a retry loop.
|
|
634
|
+
|
|
635
|
+
### scoped
|
|
636
|
+
|
|
637
|
+
Run a stream that requires `Scope` in a managed scope, ensuring finalizers run when the stream completes.
|
|
638
|
+
|
|
639
|
+
```ts
|
|
640
|
+
const safeStream = Stream.scoped(
|
|
641
|
+
Stream.fromEffect(
|
|
642
|
+
Effect.acquireRelease(
|
|
643
|
+
Effect.log('acquire').pipe(Effect.as('resource')),
|
|
644
|
+
() => Effect.log('release')
|
|
645
|
+
)
|
|
646
|
+
)
|
|
647
|
+
);
|
|
648
|
+
// Stream<string, never, never> — Scope is eliminated
|
|
649
|
+
```
|
|
650
|
+
|
|
651
|
+
As of beta.69, `Stream.scoped` provides its managed scope to the **pull effects** as well — including effects created by `Stream.fromEffect` and by sequential `Stream.mapEffect`. So `Effect.acquireRelease` finalizers used inside those pulls run when the stream completes, not leaked until the outer program ends.
|
|
652
|
+
|
|
653
|
+
### unwrap
|
|
654
|
+
|
|
655
|
+
Create a stream from an effect that produces a stream. The outer effect runs once; the inner stream is then consumed.
|
|
656
|
+
|
|
657
|
+
```ts
|
|
658
|
+
const stream = Stream.unwrap(
|
|
659
|
+
Effect.gen(function* () {
|
|
660
|
+
const config = yield* loadConfig;
|
|
661
|
+
return Stream.fromIterable(config.items);
|
|
662
|
+
})
|
|
663
|
+
);
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
### callback with acquireRelease
|
|
667
|
+
|
|
668
|
+
The `Stream.callback` constructor accepts a scoped effect, so you can register and unregister resources:
|
|
669
|
+
|
|
670
|
+
```ts
|
|
671
|
+
Stream.callback<Event>(
|
|
672
|
+
Effect.fn(function* (queue) {
|
|
673
|
+
yield* Effect.acquireRelease(
|
|
674
|
+
Effect.sync(() =>
|
|
675
|
+
emitter.on('data', (e) => Queue.offerUnsafe(queue, e))
|
|
676
|
+
),
|
|
677
|
+
() => Effect.sync(() => emitter.removeAllListeners('data'))
|
|
678
|
+
);
|
|
679
|
+
})
|
|
680
|
+
);
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
---
|
|
684
|
+
|
|
685
|
+
## 8. Piping Through Channels
|
|
686
|
+
|
|
687
|
+
`Stream.pipeThroughChannel` connects a stream to a `Channel` for encode/decode, compression, framing, etc.
|
|
688
|
+
|
|
689
|
+
```ts
|
|
690
|
+
// pipeThroughChannel: upstream errors flow into the channel
|
|
691
|
+
stream.pipe(Stream.pipeThroughChannel(myChannel));
|
|
692
|
+
|
|
693
|
+
// pipeThroughChannelOrFail: upstream errors preserved alongside channel errors
|
|
694
|
+
stream.pipe(Stream.pipeThroughChannelOrFail(myChannel));
|
|
695
|
+
```
|
|
696
|
+
|
|
697
|
+
---
|
|
698
|
+
|
|
699
|
+
## Key Patterns
|
|
700
|
+
|
|
701
|
+
### Pagination → transform → consume
|
|
702
|
+
|
|
703
|
+
```ts
|
|
704
|
+
const pipeline = Stream.paginate(0, fetchPage).pipe(
|
|
705
|
+
Stream.mapEffect(enrichItem, { concurrency: 8 }),
|
|
706
|
+
Stream.filter((item) => item.isValid),
|
|
707
|
+
Stream.grouped(50),
|
|
708
|
+
Stream.runForEach((batch) => writeBatch(batch))
|
|
709
|
+
);
|
|
710
|
+
```
|
|
711
|
+
|
|
712
|
+
### Event stream → debounce → side effect
|
|
713
|
+
|
|
714
|
+
```ts
|
|
715
|
+
const autosave = Stream.fromEventListener(input, 'input').pipe(
|
|
716
|
+
Stream.debounce('500 millis'),
|
|
717
|
+
Stream.mapEffect((e) => saveDocument(e.target.value)),
|
|
718
|
+
Stream.runDrain
|
|
719
|
+
);
|
|
720
|
+
```
|
|
721
|
+
|
|
722
|
+
### Decode NDJSON file → filter → re-encode
|
|
723
|
+
|
|
724
|
+
```ts
|
|
725
|
+
const filterErrors = fileStream.pipe(
|
|
726
|
+
Stream.pipeThroughChannel(Ndjson.decodeSchemaString(LogEntry)()),
|
|
727
|
+
Stream.filter((entry) => entry.level === 'error'),
|
|
728
|
+
Stream.pipeThroughChannel(Ndjson.encodeSchemaString(LogEntry)()),
|
|
729
|
+
Stream.runCollect
|
|
730
|
+
);
|
|
731
|
+
```
|
|
732
|
+
|
|
733
|
+
### Retry with backoff
|
|
734
|
+
|
|
735
|
+
```ts
|
|
736
|
+
const resilient = unreliableStream.pipe(
|
|
737
|
+
Stream.retry(
|
|
738
|
+
Schedule.exponential('100 millis').pipe(Schedule.upTo({ times: 5 }))
|
|
739
|
+
),
|
|
740
|
+
Stream.runCollect
|
|
741
|
+
);
|
|
742
|
+
```
|
|
743
|
+
|
|
744
|
+
`Schedule.upTo({ times: n })` bounds an unbounded schedule to `n` recurrences. To log full retry metadata without changing the schedule's behavior, add `Schedule.tap`, whose callback receives `{ attempt, input, output, duration, elapsed }`:
|
|
745
|
+
|
|
746
|
+
```ts
|
|
747
|
+
const monitored = Schedule.exponential('100 millis').pipe(
|
|
748
|
+
Schedule.upTo({ times: 5 }),
|
|
749
|
+
Schedule.tap((meta) =>
|
|
750
|
+
Effect.log(
|
|
751
|
+
`attempt ${meta.attempt}, next delay ${meta.duration}, elapsed ${meta.elapsed}`
|
|
752
|
+
)
|
|
753
|
+
)
|
|
754
|
+
);
|
|
755
|
+
```
|
|
756
|
+
|
|
757
|
+
## Common Mistakes
|
|
758
|
+
|
|
759
|
+
1. **Forgetting `runFold` initial is a thunk** — `Stream.runFold(() => 0, f)` not `Stream.runFold(0, f)`
|
|
760
|
+
2. **Using `Stream.acquireRelease` when it doesn't exist** — use `Stream.scoped` + `Effect.acquireRelease` or `Stream.callback` with `Effect.acquireRelease` instead
|
|
761
|
+
3. **Not specifying `onError` for `fromAsyncIterable` / `fromReadableStream`** — these require an error mapper
|
|
762
|
+
4. **Assuming `retry` resumes** — `Stream.retry` restarts the entire stream from the beginning on each retry
|
|
763
|
+
5. **Ignoring `haltStrategy` on `merge`** — default is `"both"` (wait for both to end); use `"either"` to stop as soon as one ends
|
|
764
|
+
6. **Assuming `forkScoped` supervises failures** — it scopes lifetime only. Explicitly monitor/restart/report long-lived consumers according to the owning service policy.
|
|
765
|
+
7. **Collecting open streams** — use `runForEach`/`runDrain` in production and `take(n)` + `runCollect` for finite tests.
|