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,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.