@zio.dev/zio-blocks 0.0.33 → 0.0.55

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 (215) hide show
  1. package/adr/2026-07-18-data-migration.md +123 -0
  2. package/guides/async-getting-started.md +687 -0
  3. package/guides/compile-time-resource-safety-with-scope.md +21 -16
  4. package/guides/getting-started-with-mux.md +1395 -0
  5. package/guides/query-dsl-extending.md +161 -102
  6. package/guides/query-dsl-fluent-builder.md +217 -157
  7. package/guides/query-dsl-reified-optics.md +12 -10
  8. package/guides/query-dsl-sql.md +640 -165
  9. package/guides/sql-checked-interpolation.md +173 -0
  10. package/guides/sql-transactions.md +286 -0
  11. package/guides/telemetry-guide.md +1130 -0
  12. package/guides/zio-schema-migration.md +29 -22
  13. package/index.md +248 -389
  14. package/package.json +1 -1
  15. package/plans/config-follow-up-prs.md +188 -0
  16. package/plans/config-pr-assessment-roadmap.md +310 -0
  17. package/reference/MuxDataFlow.jsx +250 -0
  18. package/reference/async.md +1499 -0
  19. package/reference/chunk.md +3533 -308
  20. package/reference/codegen/case-class.md +436 -0
  21. package/reference/codegen/emitter-config.md +383 -0
  22. package/reference/codegen/examples.md +664 -0
  23. package/reference/codegen/field.md +316 -0
  24. package/reference/codegen/index.md +317 -0
  25. package/reference/codegen/scala-emitter.md +392 -0
  26. package/reference/codegen/scala-file.md +276 -0
  27. package/reference/codegen/sealed-trait.md +408 -0
  28. package/reference/codegen/type-definition.md +340 -0
  29. package/reference/codegen/type-ref.md +201 -0
  30. package/reference/combinators.md +347 -117
  31. package/reference/config/config-decoder.md +460 -0
  32. package/reference/config/config-source.md +489 -0
  33. package/reference/config/errors.md +278 -0
  34. package/reference/config/flags.md +369 -0
  35. package/reference/config/formats.md +314 -0
  36. package/reference/config/index.md +304 -0
  37. package/reference/config/rollout.md +336 -0
  38. package/reference/context.md +9 -52
  39. package/reference/data-migration.md +269 -0
  40. package/reference/datastar/attributes.md +302 -0
  41. package/reference/datastar/events.md +234 -0
  42. package/reference/datastar/index.md +256 -0
  43. package/reference/datastar/signals.md +230 -0
  44. package/reference/datastar/sse.md +295 -0
  45. package/reference/datastar.md +346 -0
  46. package/reference/docs.md +1461 -345
  47. package/reference/endpoint/auth-type.md +146 -0
  48. package/reference/endpoint/bulk-creation.md +96 -0
  49. package/reference/endpoint/endpoint.md +297 -0
  50. package/reference/endpoint/http-codec.md +249 -0
  51. package/reference/endpoint/index.md +745 -0
  52. package/reference/endpoint/path-codec.md +225 -0
  53. package/reference/endpoint/route-pattern.md +194 -0
  54. package/reference/endpoint/route-tree.md +111 -0
  55. package/reference/endpoint/segment-codec.md +199 -0
  56. package/reference/html.md +1424 -0
  57. package/reference/htmx/attribute-values.md +359 -0
  58. package/reference/htmx/hx-encoding.md +111 -0
  59. package/reference/htmx/hx-params.md +204 -0
  60. package/reference/htmx/hx-swap.md +276 -0
  61. package/reference/htmx/hx-sync.md +251 -0
  62. package/reference/htmx/hx-target.md +314 -0
  63. package/reference/htmx/hx-trigger.md +457 -0
  64. package/reference/htmx/hx-url-update.md +239 -0
  65. package/reference/htmx/index.md +807 -0
  66. package/reference/htmx/response-headers.md +240 -0
  67. package/reference/http-model/headers.md +735 -0
  68. package/reference/http-model/index.md +49 -0
  69. package/reference/http-model/model.md +1517 -0
  70. package/reference/http-model/schema-codecs.md +522 -0
  71. package/reference/http-model/schema.md +750 -0
  72. package/reference/http-model/server-sent-event.md +341 -0
  73. package/reference/jwt.md +195 -0
  74. package/reference/maybe.md +943 -0
  75. package/reference/media-type.md +2 -2
  76. package/reference/mux.md +254 -0
  77. package/reference/mux.mdx +828 -0
  78. package/reference/openapi.md +1351 -0
  79. package/reference/projection.md +654 -0
  80. package/reference/resource-management/defer-handle.md +1 -1
  81. package/reference/resource-management/resource.md +31 -98
  82. package/reference/resource-management/scope.md +28 -220
  83. package/reference/resource-management/wire.md +5 -55
  84. package/reference/ringbuffer/MpmcDiagram.jsx +717 -0
  85. package/reference/ringbuffer/MpscDiagram.jsx +618 -0
  86. package/reference/ringbuffer/SpmcDiagram.jsx +680 -0
  87. package/reference/ringbuffer/SpscDiagram.jsx +677 -0
  88. package/reference/ringbuffer/advanced.mdx +109 -0
  89. package/reference/ringbuffer/index.mdx +145 -0
  90. package/reference/ringbuffer/mpmc.mdx +185 -0
  91. package/reference/ringbuffer/mpsc.mdx +164 -0
  92. package/reference/ringbuffer/spmc.mdx +108 -0
  93. package/reference/ringbuffer/spsc.mdx +416 -0
  94. package/reference/{allows.md → schema/allows.md} +4 -100
  95. package/reference/{binding-resolver.md → schema/binding-resolver.md} +1 -1
  96. package/reference/{binding.md → schema/binding.md} +3 -4
  97. package/reference/schema/built-in-codecs/avro.md +451 -0
  98. package/reference/schema/built-in-codecs/bson.md +510 -0
  99. package/reference/schema/built-in-codecs/csv.md +564 -0
  100. package/reference/schema/built-in-codecs/index.md +77 -0
  101. package/reference/schema/built-in-codecs/json/index.md +295 -0
  102. package/reference/schema/built-in-codecs/json/json-config.md +217 -0
  103. package/reference/{json-patch.md → schema/built-in-codecs/json/json-patch.md} +5 -5
  104. package/reference/{json-schema.md → schema/built-in-codecs/json/json-schema.md} +14 -47
  105. package/reference/schema/built-in-codecs/json/json-selection.md +322 -0
  106. package/reference/{json.md → schema/built-in-codecs/json/json.md} +32 -64
  107. package/reference/schema/built-in-codecs/messagepack.md +508 -0
  108. package/reference/schema/built-in-codecs/thrift.md +433 -0
  109. package/reference/schema/built-in-codecs/toon.md +1078 -0
  110. package/reference/{xml.md → schema/built-in-codecs/xml.md} +13 -9
  111. package/reference/schema/built-in-codecs/yaml.md +552 -0
  112. package/reference/{codec.md → schema/codec.md} +11 -11
  113. package/reference/{dynamic-optic.md → schema/dynamic-optic.md} +196 -5
  114. package/reference/{dynamic-schema.md → schema/dynamic-schema.md} +8 -8
  115. package/reference/schema/format.md +92 -0
  116. package/reference/schema/index.md +52 -0
  117. package/reference/schema/migration.md +297 -0
  118. package/reference/{modifier.md → schema/modifier.md} +58 -7
  119. package/reference/{optics.md → schema/optics.md} +2 -2
  120. package/reference/{patch.md → schema/patch.md} +1 -1
  121. package/{path-interpolator.md → reference/schema/path-interpolator.md} +167 -72
  122. package/reference/schema/reflect-transformer.md +140 -0
  123. package/reference/{schema-evolution → schema/schema-evolution}/as.md +8 -8
  124. package/reference/{schema-evolution → schema/schema-evolution}/index.md +2 -2
  125. package/reference/{schema-evolution → schema/schema-evolution}/into.md +8 -8
  126. package/reference/{schema-expr.md → schema/schema-expr.md} +110 -175
  127. package/reference/schema/schema-search.md +263 -0
  128. package/reference/{schema.md → schema/schema.md} +22 -2
  129. package/reference/{structural-types.md → schema/structural-types.md} +1 -1
  130. package/reference/{type-class-derivation.md → schema/type-class-derivation.md} +63 -1
  131. package/reference/smithy.md +1032 -0
  132. package/reference/sql/db-codec-deriver.md +71 -0
  133. package/reference/sql/db-codec.md +687 -0
  134. package/reference/sql/db-con.md +271 -0
  135. package/reference/sql/db-connection.md +153 -0
  136. package/reference/sql/db-param-writer.md +77 -0
  137. package/reference/sql/db-param.md +66 -0
  138. package/reference/sql/db-result-reader.md +148 -0
  139. package/reference/sql/db-tx.md +114 -0
  140. package/reference/sql/db-value.md +41 -0
  141. package/reference/sql/ddl.md +85 -0
  142. package/reference/sql/frag.md +288 -0
  143. package/reference/sql/index.md +341 -0
  144. package/reference/sql/repo.md +600 -0
  145. package/reference/sql/sql-dialect.md +73 -0
  146. package/reference/sql/sql-logger.md +62 -0
  147. package/reference/sql/sql-name-mapper.md +70 -0
  148. package/reference/sql/table-metadata.md +134 -0
  149. package/reference/sql/table.md +448 -0
  150. package/reference/sql/transactor-zio.md +399 -0
  151. package/reference/sql/transactor.md +363 -0
  152. package/reference/sql-zio.md +112 -0
  153. package/reference/streams/core/index.md +32 -0
  154. package/reference/streams/core/pipeline.md +854 -0
  155. package/reference/streams/core/sink.md +1404 -0
  156. package/reference/streams/core/stream.md +3236 -0
  157. package/reference/streams/execution-and-compatibility/async-execution.md +822 -0
  158. package/reference/streams/execution-and-compatibility/index.md +35 -0
  159. package/reference/streams/execution-and-compatibility/platform-differences.md +297 -0
  160. package/reference/streams/execution-and-compatibility/scala-2-compatibility.md +88 -0
  161. package/reference/streams/execution-and-compatibility/zero-boxing.md +393 -0
  162. package/reference/streams/index.md +726 -0
  163. package/reference/streams/primitives/index.md +30 -0
  164. package/reference/streams/primitives/reader.md +1992 -0
  165. package/reference/streams/primitives/writer.md +1201 -0
  166. package/reference/telemetry/common/any-value.md +90 -0
  167. package/reference/telemetry/common/attribute-key.md +87 -0
  168. package/reference/telemetry/common/attributes.md +118 -0
  169. package/reference/telemetry/common/index.md +39 -0
  170. package/reference/telemetry/common/instrumentation-scope.md +24 -0
  171. package/reference/telemetry/common/resource.md +34 -0
  172. package/reference/telemetry/index.md +311 -0
  173. package/reference/telemetry/logging/index.md +197 -0
  174. package/reference/telemetry/logging/log-enrichment.md +72 -0
  175. package/reference/telemetry/logging/log-formatter.md +100 -0
  176. package/reference/telemetry/logging/log-record-processor.md +56 -0
  177. package/reference/telemetry/logging/log-record.md +44 -0
  178. package/reference/telemetry/logging/log-writer.md +64 -0
  179. package/reference/telemetry/logging/logger-provider.md +142 -0
  180. package/reference/telemetry/logging/logger.md +83 -0
  181. package/reference/telemetry/logging/severity.md +62 -0
  182. package/reference/telemetry/metrics/index.md +150 -0
  183. package/reference/telemetry/metrics/instruments.md +183 -0
  184. package/reference/telemetry/metrics/labeled-instruments.md +74 -0
  185. package/reference/telemetry/metrics/meter-provider.md +76 -0
  186. package/reference/telemetry/metrics/meter.md +98 -0
  187. package/reference/telemetry/metrics/metric-data.md +57 -0
  188. package/reference/telemetry/otel/custom-exporter.md +216 -0
  189. package/reference/telemetry/otel/index.md +212 -0
  190. package/reference/telemetry/tracing/index.md +155 -0
  191. package/reference/telemetry/tracing/sampler.md +89 -0
  192. package/reference/telemetry/tracing/span-builder.md +57 -0
  193. package/reference/telemetry/tracing/span-context.md +39 -0
  194. package/reference/telemetry/tracing/span-data.md +32 -0
  195. package/reference/telemetry/tracing/span-kind.md +55 -0
  196. package/reference/telemetry/tracing/span-processor.md +53 -0
  197. package/reference/telemetry/tracing/span-status.md +47 -0
  198. package/reference/telemetry/tracing/span.md +117 -0
  199. package/reference/telemetry/tracing/tracer-provider.md +91 -0
  200. package/reference/telemetry/tracing/tracer.md +52 -0
  201. package/reference/typeid.md +5 -83
  202. package/sidebars.js +376 -43
  203. package/undocumented-report.md +528 -270
  204. package/reference/formats.md +0 -694
  205. package/reference/http-model.md +0 -1716
  206. package/reference/streams.md +0 -989
  207. package/ringbuffer.md +0 -249
  208. /package/reference/{json-differ.md → schema/built-in-codecs/json/json-differ.md} +0 -0
  209. /package/reference/{dynamic-value.md → schema/dynamic-value.md} +0 -0
  210. /package/reference/{lazy.md → schema/lazy.md} +0 -0
  211. /package/reference/{reflect.md → schema/reflect.md} +0 -0
  212. /package/reference/{registers.md → schema/registers.md} +0 -0
  213. /package/reference/{schema-error.md → schema/schema-error.md} +0 -0
  214. /package/reference/{syntax.md → schema/syntax.md} +0 -0
  215. /package/reference/{validation.md → schema/validation.md} +0 -0
@@ -0,0 +1,854 @@
1
+ ---
2
+ id: pipeline
3
+ title: "Pipeline"
4
+ sidebar_label: "Pipeline"
5
+ description: "The reusable stream transformation: its synchronous and asynchronous factories, andThen composition, and the two routes for applying one."
6
+ keywords:
7
+ - "Stream Transformation"
8
+ - "Pipeline Composition"
9
+ - "Async Pipeline Stages"
10
+ - "Primitive Specialization"
11
+ - "Pipeline"
12
+ ---
13
+
14
+ `Pipeline[-In, +Out]` is a **reusable, composable stream transformation** that converts elements of type `In` into elements of type `Out`. Pipelines are first-class values: you can define them once, compose them with `andThen`, and apply them to any [Stream](./stream.md) via `stream.via(pipe)` or to any `Sink` via `pipe.andThenSink(sink)`.
15
+
16
+ `Pipeline`:
17
+ - Is contravariant in `In` and covariant in `Out` (like a function `In => Out`)
18
+ - Can be applied to a **Stream** (transforming the output) or a **Sink** (pre-processing the input)
19
+ - Participates in JVM primitive specialization to avoid boxing
20
+
21
+ Here is the structural shape of the `Pipeline` type:
22
+
23
+ ```scala
24
+ abstract class Pipeline[-In, +Out] {
25
+ def andThen[C](that: Pipeline[Out, C]): Pipeline[In, C]
26
+ def applyToStream[E](stream: Stream[E, In]): Stream[E, Out]
27
+ def applyToSink[E, Z](sink: Sink[E, Out, Z]): Sink[E, In, Z]
28
+ }
29
+ ```
30
+
31
+ ## Overview
32
+
33
+ Pipelines solve the problem of reusing stream transformations across different streams and sinks. Without pipelines, you repeat filtering and mapping logic for every stream. With pipelines, you define transformations once as first-class values, compose them freely, and apply them anywhere.
34
+
35
+ ### The Problem
36
+
37
+ When you build stream processing logic, you often write chains like:
38
+
39
+ ```scala
40
+ stream
41
+ .filter(_ > 0)
42
+ .map(_ * 2)
43
+ .take(100)
44
+ ```
45
+
46
+ This works, but the transformation is tied to a specific stream. If you want to apply the same logic to a different stream, or to a sink instead, you have to repeat yourself. You cannot pass the chain around as a value, store it in a variable, or compose it with other transformations.
47
+
48
+ ### The Solution
49
+
50
+ `Pipeline[-In, +Out]` lifts stream transformations into first-class values. You define a pipeline once, compose it with other pipelines using `andThen`, and apply it wherever you need:
51
+
52
+ ```scala
53
+ import zio.blocks.streams.*
54
+
55
+ // Define once
56
+ val normalize: Pipeline[Int, Int] =
57
+ Pipeline.filter[Int](_ > 0)
58
+ .andThen(Pipeline.map[Int, Int](_ * 2))
59
+ .andThen(Pipeline.take(100))
60
+
61
+ // Apply to any stream
62
+ val stream1 = Stream(1, 2, 3, 4, 5)
63
+ val stream2 = Stream(10, 20, 30, 40, 50)
64
+ val stream3 = Stream(-5, 3, 7, 2, 8, 1, 9)
65
+
66
+ val result1 = stream1.via(normalize).runCollect
67
+ val result2 = stream2.via(normalize).runCollect
68
+
69
+ // Apply to a sink (pre-process input before the sink sees it)
70
+ val normalizedSink = normalize.andThenSink(Sink.collectAll[Int])
71
+ val result3 = stream3.run(normalizedSink)
72
+ ```
73
+
74
+ `Pipeline` forms a **category** in the mathematical sense:
75
+
76
+ | Law | Statement |
77
+ |----------------|------------------------------------------------------|
78
+ | Left identity | `Pipeline.identity andThen p == p` |
79
+ | Right identity | `p andThen Pipeline.identity == p` |
80
+ | Associativity | `(p andThen q) andThen r == p andThen (q andThen r)` |
81
+
82
+ These laws guarantee that pipelines compose predictably, regardless of how you parenthesize.
83
+
84
+ ### Architecture
85
+
86
+ `Pipeline` sits between `Stream` and `Sink`, mediating how elements flow:
87
+
88
+ ```
89
+ Applying to a Stream (via):
90
+ ┌──────────────┐ ┌──────────────────┐ ┌──────────────┐
91
+ │ Stream[E, In]│ ──→ │ Pipeline[In, Out]│ ──→ │Stream[E, Out]│
92
+ └──────────────┘ └──────────────────┘ └──────────────┘
93
+
94
+ Applying to a Sink (andThenSink):
95
+ ┌──────────────────┐ ┌────────────────┐ ┌──────────────┐
96
+ │ Pipeline[In, Out]│ ──→ │ Sink[E, Out, Z]│ ──→ │Sink[E, In, Z]│
97
+ └──────────────────┘ └────────────────┘ └──────────────┘
98
+
99
+ Composing two Pipelines (andThen):
100
+ ┌──────────────────┐ ┌──────────────────┐ ┌─────────────────┐
101
+ │ Pipeline[A, B] │ ──→ │ Pipeline[B, C] │ ──→ │ Pipeline[A, C] │
102
+ └──────────────────┘ └──────────────────┘ └─────────────────┘
103
+ ```
104
+
105
+ ## Construction
106
+
107
+ Pipelines are built using factory methods on the `Pipeline` companion object. Each factory creates a pipeline that performs a specific transformation: mapping elements, filtering, collecting, or controlling flow. A factory that applies a user-supplied function to produce a new element type asks for `JvmType.Infer` evidence for the **result** type; the one exception is the `map` overload for a function returning `Nothing`, which takes a `DummyImplicit` and supplies the boxed lane itself. The input representation already belongs to the stream or sink at application time; callers do not supply redundant input evidence. Type-preserving factories, and the structural factories `buffer` and `chunked`, retain or rebuild that representation without call-site evidence.
108
+
109
+ Three of those factories have asynchronous twins, and exactly three: `mapAsync`, `filterAsync`, and `collectAsync`. They are cross-platform, their callbacks return [`Async`](../../async.md), they are evaluated sequentially as the downstream pulls, and they work through both application routes. Other `*Async` operators such as `tapEachAsync` and `distinctByAsync` exist on `Stream` rather than on `Pipeline`; see [Asynchronous Stream Execution](../execution-and-compatibility/async-execution.md#async-operators) for the full stream-level list. `Writer`'s similarly named deferred methods are something else again — adaptation wrappers around its synchronous operations, described in [Writer — Asynchronous Writes](../primitives/writer.md#asynchronous-writes).
110
+
111
+ Factory callbacks are protected as user callbacks: synchronous throws and failed callback effects are defects, not typed stream errors. When a pipeline is applied to a sink, both drain paths close the derived reader. Cancellation closes upstream and downstream state, and a cleanup failure is attached to an existing failure rather than hiding it.
112
+
113
+ ### `Pipeline.map[A, B]` — Transform Each Element
114
+
115
+ Applies a function to every element, producing a new element type. Here is the signature:
116
+
117
+ ```scala
118
+ object Pipeline {
119
+ def map[A, B](f: A => B)(implicit jtB: JvmType.Infer[B]): Pipeline[A, B]
120
+ }
121
+ ```
122
+
123
+ This is the most common pipeline constructor:
124
+
125
+ ```scala
126
+ import zio.blocks.streams.*
127
+
128
+ val doubler = Pipeline.map[Int, Int](_ * 2)
129
+ // doubler: Pipeline[Int, Int] = zio.blocks.streams.Pipeline$MapPipeline@2bf42ef7
130
+ val toStr = Pipeline.map[Int, String](_.toString)
131
+ // toStr: Pipeline[Int, String] = zio.blocks.streams.Pipeline$MapPipeline@29332a5b
132
+
133
+ val result = Stream(1, 2, 3).via(doubler).runCollect
134
+ // result: Either[Nothing, Chunk[Int]] = Right(IndexedSeq(2, 4, 6))
135
+ ```
136
+
137
+ ### `Pipeline.mapAsync[A, B]` — Transform Each Element Asynchronously
138
+
139
+ The asynchronous twin of `map`. The callback returns an `Async`, and each one is awaited before the next element is pulled, so at most one invocation is in flight and input order is preserved. Here is the signature:
140
+
141
+ ```scala
142
+ object Pipeline {
143
+ def mapAsync[A, B](f: A => Async[B])(implicit jtB: JvmType.Infer[B]): Pipeline[A, B]
144
+ }
145
+ ```
146
+
147
+ Like `map`, it asks for `JvmType.Infer` evidence for its result type only. A failed or defective `Async` fails the stream as a defect rather than through its typed error channel, and cancellation of a suspended callback is propagated:
148
+
149
+ ```scala
150
+ import zio.blocks.async.*
151
+ import zio.blocks.streams.*
152
+
153
+ val lookup = Pipeline.mapAsync[Int, String](id => Async.succeed(s"user-$id"))
154
+ // lookup: Pipeline[Int, String] = zio.blocks.streams.Pipeline$$anon$4@117b2316
155
+
156
+ val result = Stream(1, 2, 3).via(lookup).runCollectAsync
157
+ // result: Async[Either[Nothing, Chunk[String]]] = zio.blocks.async.Async$slowPath$FoldCausePollable@2def46ea
158
+ ```
159
+
160
+ ### `Pipeline.filter[A]` — Keep Matching Elements
161
+
162
+ Keeps only elements that satisfy a predicate. Here is the signature:
163
+
164
+ ```scala
165
+ object Pipeline {
166
+ def filter[A](pred: A => Boolean): Pipeline[A, A]
167
+ }
168
+ ```
169
+
170
+ Note that the output type is the same as the input type — filtering does not change the element type:
171
+
172
+ ```scala
173
+ import zio.blocks.streams.*
174
+
175
+ val positives = Pipeline.filter[Int](_ > 0)
176
+ // positives: Pipeline[Int, Int] = zio.blocks.streams.Pipeline$FilterPipeline@39c3da53
177
+
178
+ val result = Stream(-2, -1, 0, 1, 2).via(positives).runCollect
179
+ // result: Either[Nothing, Chunk[Int]] = Right(IndexedSeq(1, 2))
180
+ ```
181
+
182
+ ### `Pipeline.filterAsync[A]` — Keep Matching Elements Asynchronously
183
+
184
+ The asynchronous twin of `filter`. Elements are tested sequentially and in input order, and an element is emitted only when its `Async` yields `true`. Here is the signature:
185
+
186
+ ```scala
187
+ object Pipeline {
188
+ def filterAsync[A](f: A => Async[Boolean]): Pipeline[A, A]
189
+ }
190
+ ```
191
+
192
+ Like `filter`, it is type-preserving and therefore takes no `JvmType.Infer` evidence — it keeps whatever representation the stream or sink supplies. A failed predicate effect is a defect, and cancellation of a suspended predicate is propagated:
193
+
194
+ ```scala
195
+ import zio.blocks.async.*
196
+ import zio.blocks.streams.*
197
+
198
+ val allowed = Pipeline.filterAsync[Int](n => Async.succeed(n % 3 == 0))
199
+ // allowed: Pipeline[Int, Int] = zio.blocks.streams.Pipeline$$anon$2@7313125c
200
+
201
+ val result = Stream(1, 2, 3, 4, 5, 6).via(allowed).runCollectAsync
202
+ // result: Async[Either[Nothing, Chunk[Int]]] = zio.blocks.async.Async$slowPath$FoldCausePollable@76cf0fc6
203
+ ```
204
+
205
+ ### `Pipeline.collect[A, B]` — Partial Function Transformation
206
+
207
+ Applies a partial function: only elements for which the function is defined pass through, and they are transformed to the output type. This combines filtering and mapping in one step. Here is the signature:
208
+
209
+ ```scala
210
+ object Pipeline {
211
+ def collect[A, B](pf: PartialFunction[A, B])(implicit jtB: JvmType.Infer[B]): Pipeline[A, B]
212
+ }
213
+ ```
214
+
215
+ Use `collect` when you need to filter and transform simultaneously:
216
+
217
+ ```scala
218
+ import zio.blocks.streams.*
219
+
220
+ val extractInts = Pipeline.collect[Any, Int] { case n: Int => n }
221
+ // extractInts: Pipeline[Any, Int] = zio.blocks.streams.Pipeline$CollectPipeline@5fa160de
222
+
223
+ val result = Stream(1, "a", 2, "b", 3).via(extractInts).runCollect
224
+ // result: Either[Nothing, Chunk[Int]] = Right(IndexedSeq(1, 2, 3))
225
+ ```
226
+
227
+ ### `Pipeline.collectAsync[A, B]` — Asynchronous Filtering Transformation
228
+
229
+ The asynchronous twin of `collect`, with one shape difference worth noticing: where `collect` takes a `PartialFunction[A, B]`, `collectAsync` takes a total function returning `Async[Option[B]]`. A `None` drops the element and a `Some` emits its value. Here is the signature:
230
+
231
+ ```scala
232
+ object Pipeline {
233
+ def collectAsync[A, B](f: A => Async[Option[B]])(implicit jtB: JvmType.Infer[B]): Pipeline[A, B]
234
+ }
235
+ ```
236
+
237
+ Elements are evaluated sequentially and in input order, and like `collect` the factory asks for `JvmType.Infer` evidence for its result type:
238
+
239
+ ```scala
240
+ import zio.blocks.async.*
241
+ import zio.blocks.streams.*
242
+
243
+ val parsePort = Pipeline.collectAsync[String, Int] { raw =>
244
+ Async.succeed(raw.toIntOption.filter(p => p > 0 && p <= 65535))
245
+ }
246
+ // parsePort: Pipeline[String, Int] = zio.blocks.streams.Pipeline$$anon$1@53159f19
247
+
248
+ val result = Stream("80", "not-a-port", "8080", "99999").via(parsePort).runCollectAsync
249
+ // result: Async[Either[Nothing, Chunk[Int]]] = zio.blocks.async.Async$slowPath$FoldCausePollable@7376af26
250
+ ```
251
+
252
+ ### `Pipeline.take[A]` — First N Elements
253
+
254
+ Passes through at most the first `n` elements, then stops. Here is the signature:
255
+
256
+ ```scala
257
+ object Pipeline {
258
+ def take[A](n: Long): Pipeline[A, A]
259
+ }
260
+ ```
261
+
262
+ This naturally short-circuits — upstream stops producing once `n` elements have passed:
263
+
264
+ ```scala
265
+ import zio.blocks.streams.*
266
+
267
+ val firstFive = Pipeline.take[Int](5)
268
+ // firstFive: Pipeline[Int, Int] = zio.blocks.streams.Pipeline$TakePipeline@6dfe0469
269
+
270
+ val result = Stream.range(0, 1000).via(firstFive).runCollect
271
+ // result: Either[Nothing, Chunk[Int]] = Right(IndexedSeq(0, 1, 2, 3, 4))
272
+ ```
273
+
274
+ ### `Pipeline.drop[A]` — Skip First N Elements
275
+
276
+ Skips the first `n` elements, then passes through the rest. Here is the signature:
277
+
278
+ ```scala
279
+ object Pipeline {
280
+ def drop[A](n: Long): Pipeline[A, A]
281
+ }
282
+ ```
283
+
284
+ Use `drop` to skip headers, metadata, or warm-up elements:
285
+
286
+ ```scala
287
+ import zio.blocks.streams.*
288
+
289
+ val skipHeader = Pipeline.drop[String](1)
290
+ // skipHeader: Pipeline[String, String] = zio.blocks.streams.Pipeline$DropPipeline@586ae719
291
+
292
+ val result = Stream("header", "row1", "row2").via(skipHeader).runCollect
293
+ // result: Either[Nothing, Chunk[String]] = Right(IndexedSeq("row1", "row2"))
294
+ ```
295
+
296
+ ### `Pipeline.identity[A]` — Pass-Through
297
+
298
+ The identity pipeline that passes all elements through unchanged. This is the neutral element for `andThen` composition. Here is the signature:
299
+
300
+ ```scala
301
+ object Pipeline {
302
+ def identity[A]: Pipeline[A, A]
303
+ }
304
+ ```
305
+
306
+ You rarely construct `identity` explicitly, but it is important as a base case in generic pipeline-building code:
307
+
308
+ ```scala
309
+ import zio.blocks.streams.*
310
+
311
+ val noOp = Pipeline.identity[Int]
312
+ // noOp: Pipeline[Int, Int] = zio.blocks.streams.Pipeline$$anon$3@43ed8717
313
+
314
+ // These are equivalent:
315
+ // stream.via(noOp) == stream
316
+ // noOp.andThen(p) == p
317
+ // p.andThen(noOp) == p
318
+ ```
319
+
320
+ ## Composing Pipelines
321
+
322
+ Pipelines compose into larger, more complex transformations using `andThen`. Because `Pipeline` forms a mathematical category, composition is associative and respects identity, so you can build pipelines incrementally or conditionally without worrying about how you parenthesize or combine them.
323
+
324
+ ### `Pipeline#andThen[C]` — Sequential Composition
325
+
326
+ Composes two pipelines into one, applying `this` first and `that` second. Here is the signature:
327
+
328
+ ```scala
329
+ abstract class Pipeline[-In, +Out] {
330
+ def andThen[C](that: Pipeline[Out, C]): Pipeline[In, C]
331
+ }
332
+ ```
333
+
334
+ `andThen` is the key operation that makes pipelines composable. Because `Pipeline` forms a category, composition is associative — you can group `andThen` calls however you like and get the same result:
335
+
336
+ ```scala
337
+ import zio.blocks.streams.*
338
+
339
+ // Individual steps
340
+ val filterPositive = Pipeline.filter[Int](_ > 0)
341
+ val double = Pipeline.map[Int, Int](_ * 2)
342
+ val takeFirst10 = Pipeline.take[Int](10)
343
+
344
+ // Compose into a single reusable pipeline
345
+ val normalize = filterPositive
346
+ .andThen(double)
347
+ .andThen(takeFirst10)
348
+
349
+ // Apply to any stream
350
+ val result = Stream(-5, 3, -1, 7, 2, 0, 9, 4, 8, 6, 1, 10)
351
+ .via(normalize)
352
+ .runCollect
353
+ ```
354
+
355
+ ### Building Pipelines Conditionally
356
+
357
+ Because pipelines are values, you can build them dynamically:
358
+
359
+ ```scala
360
+ import zio.blocks.streams.*
361
+
362
+ def buildPipeline(limit: Option[Int], onlyPositive: Boolean): Pipeline[Int, Int] = {
363
+ val base = if (onlyPositive) Pipeline.identity[Int].andThen(Pipeline.filter(_ > 0)) else Pipeline.identity[Int]
364
+ limit.fold(base)(n => base.andThen(Pipeline.take(n.toLong)))
365
+ }
366
+ ```
367
+
368
+ ## Applying to a Stream
369
+
370
+ Apply a pipeline to a stream using `via` to transform its output elements. This is the most direct way to use a pipeline: define it once and apply it to multiple streams without repeating the transformation logic.
371
+
372
+ Applying an asynchronous pipeline stage to a synchronous stream makes the resulting stream asynchronous. There is nothing to annotate and no type that changes: `Stream[E, A]` carries no marker for which way it will materialize, so `syncStream.via(Pipeline.mapAsync(...))` has exactly the type `syncStream.via(Pipeline.map(...))` would have. What changes is how the description compiles — one asynchronous node makes the whole graph compile to an asynchronous reader, with the synchronous source lifted in place. On the JVM a blocking terminal still accepts the result; on Scala.js use an `*Async` terminal. See [Mixing synchronous and asynchronous stages](../execution-and-compatibility/async-execution.md#mixing-synchronous-and-asynchronous-stages) for how that widening works.
373
+
374
+ ### `Stream#via[B]` — Apply a Pipeline to a Stream
375
+
376
+ The primary way to use a pipeline is through `Stream.via`. Here is the signature:
377
+
378
+ ```scala
379
+ abstract class Stream[+E, +A] {
380
+ def via[B](pipe: Pipeline[A, B]): Stream[E, B]
381
+ }
382
+ ```
383
+
384
+ Under the hood, `via` calls `pipe.applyToStream(this)`. Each pipeline type delegates to a specific `Stream` node — for example, `Pipeline.map` creates a `Stream.Mapped`, and `Pipeline.filter` creates a `Stream.Filtered`. Type-preserving stages propagate the incoming stream representation; transforming stages attach the `JvmType.Infer[B]` evidence for their result. Composition preserves this metadata stage by stage rather than rediscovering the original input type at the call site.
385
+
386
+ The key advantage of `via` over inline methods is **reuse**: define the pipeline once and apply it to multiple streams:
387
+
388
+ ```scala
389
+ import zio.blocks.streams.*
390
+
391
+ // A reusable cleaning pipeline for sensor data
392
+ val cleanSensorData: Pipeline[Double, Double] =
393
+ Pipeline.filter[Double](d => !d.isNaN && !d.isInfinite)
394
+ .andThen(Pipeline.filter(d => d >= -100.0 && d <= 100.0))
395
+ // cleanSensorData: Pipeline[Double, Double] = zio.blocks.streams.Pipeline$Composed@1cfa8a20
396
+
397
+ // Apply to different sensor streams
398
+ val sensorStream1 = Stream(45.5, 67.2, Double.NaN, 23.1)
399
+ // sensorStream1: Stream[Nothing, Double] = Stream(45.5, 67.2, NaN, 23.1)
400
+ val sensorStream2 = Stream(89.9, -200.0, 12.5, 55.0)
401
+ // sensorStream2: Stream[Nothing, Double] = Stream(89.9, -200.0, 12.5, 55.0)
402
+
403
+ val sensor1Result = sensorStream1.via(cleanSensorData).runCollect
404
+ // sensor1Result: Either[Nothing, Chunk[Double]] = Right(
405
+ // IndexedSeq(45.5, 67.2, 23.1)
406
+ // )
407
+ val sensor2Result = sensorStream2.via(cleanSensorData).runCollect
408
+ // sensor2Result: Either[Nothing, Chunk[Double]] = Right(
409
+ // IndexedSeq(89.9, 12.5, 55.0)
410
+ // )
411
+ ```
412
+
413
+ ### `Pipeline#applyToStream[E]` — Direct Application
414
+
415
+ You can also call `applyToStream` directly. This is equivalent to `via` but reads left-to-right from the pipeline's perspective. Here is the signature:
416
+
417
+ ```scala
418
+ abstract class Pipeline[-In, +Out] {
419
+ def applyToStream[E](stream: Stream[E, In]): Stream[E, Out]
420
+ }
421
+ ```
422
+
423
+ `stream.via(pipe)` and `pipe.applyToStream(stream)` are identical in behavior. Prefer `via` for readability in stream chains.
424
+
425
+ ## Applying to a Sink
426
+
427
+ Apply a pipeline to a sink using `andThenSink` to pre-process the sink's input elements. This is the dual of `via`: instead of transforming a stream's output, you transform what the sink receives before it processes it.
428
+
429
+ ### `Pipeline#andThenSink[E, Z]` — Pre-Process Sink Input
430
+
431
+ The dual of `via`: instead of transforming a stream's output, you pre-process a sink's input. Here is the signature:
432
+
433
+ ```scala
434
+ abstract class Pipeline[-In, +Out] {
435
+ def andThenSink[E, Z](sink: Sink[E, Out, Z]): Sink[E, In, Z]
436
+ }
437
+ ```
438
+
439
+ This is an alias for `applyToSink`. After calling `andThenSink`, the resulting sink accepts `In` elements, transforms them through the pipeline, and feeds the `Out` elements to the original sink:
440
+
441
+ ```scala
442
+ import zio.blocks.streams.*
443
+
444
+ // A pipeline that normalizes strings
445
+ val normalize = Pipeline.map[String, String](_.trim.toLowerCase)
446
+ // normalize: Pipeline[String, String] = zio.blocks.streams.Pipeline$MapPipeline@68b9a3a0
447
+
448
+ // Apply to different sinks
449
+ val collectNormalized = normalize.andThenSink(Sink.collectAll[String])
450
+ // collectNormalized: Sink[Nothing, String, Chunk[String]] = zio.blocks.streams.Sink$Contramapped@793ea2aa
451
+ val countNormalized = normalize.andThenSink(Sink.count)
452
+ // countNormalized: Sink[Nothing, String, Long] = zio.blocks.streams.Sink$Contramapped@1bae43d7
453
+
454
+ val result = Stream(" Hello ", " WORLD ").run(collectNormalized)
455
+ // result: Either[Nothing, Chunk[String]] = Right(IndexedSeq("hello", "world"))
456
+ ```
457
+
458
+ ### When to Use `andThenSink` Vs `via`
459
+
460
+ Both achieve the same result. Choose based on which side you want to reuse:
461
+
462
+ | Approach | Use when… |
463
+ |--------------------------------------|---------------------------------------------|
464
+ | `stream.via(pipe).run(sink)` | You have a fixed pipeline and varying sinks |
465
+ | `stream.run(pipe.andThenSink(sink))` | You want a reusable "pre-processing sink" |
466
+
467
+ The laws guarantee equivalence: `stream.via(pipe).run(sink) == stream.run(pipe.andThenSink(sink))`.
468
+
469
+ ### `Pipeline#applyToSink[E, Z]` — Direct Application
470
+
471
+ `andThenSink` is an alias for `applyToSink`. Here is the signature:
472
+
473
+ ```scala
474
+ abstract class Pipeline[-In, +Out] {
475
+ def applyToSink[E, Z](sink: Sink[E, Out, Z]): Sink[E, In, Z]
476
+ }
477
+ ```
478
+
479
+ Prefer `andThenSink` for readability.
480
+
481
+ ## JVM Primitive Specialization
482
+
483
+ `Pipeline.map`, `Pipeline.collect`, and their asynchronous counterparts `mapAsync` and `collectAsync` require `JvmType.Infer` for the transformed result type, except for the `map` overload whose function returns `Nothing`, which fixes the boxed lane itself and takes a `DummyImplicit` instead. It is resolved automatically and records which of the nine logical lanes that result belongs to: the eight JVM primitives, or the reference fallback. Those nine logical lanes compact into five interpreter storage lanes — int-like (`Boolean`, `Byte`, `Short`, `Char`, `Int`), `Long`, `Float`, `Double`, and reference — with operator tags selecting the identity-specific reads over them. Nine lanes therefore does not mean nine interpreter arrays; see [Zero-Boxing Streams](../execution-and-compatibility/zero-boxing.md) for how one is chosen. None of these factories request call-site evidence for the input type.
484
+
485
+ Type-preserving factories such as `filter`, `filterAsync`, `identity`, `take`, `drop`, and `buffer` do not require `JvmType.Infer`: they preserve the representation supplied when the pipeline is applied.
486
+
487
+ The rules hold identically through both application routes. On the stream route, result evidence is stored on the transformed stream node. On the sink route, mapping passes the same result evidence to the sink's contramap machinery, and structural pipelines use the generic run-via-sink route. So `stream.via(pipe)`, `pipe.andThenSink(sink)`, and `pipe.applyToSink(sink)` preserve the same specialization information as well as the same semantics.
488
+
489
+ ## Integration
490
+
491
+ `Pipeline` integrates seamlessly with the other core streaming primitives: `Stream` and `Sink`. Understanding these integrations shows how pipelines fit into the broader streaming architecture.
492
+
493
+ ### With Stream
494
+
495
+ `Pipeline` is the mechanism behind `Stream.via`. Every call to `stream.via(pipe)` delegates to `pipe.applyToStream(stream)`, which constructs the appropriate `Stream` subtype node. See [Stream — Integration with Pipeline and Sink](./stream.md#integration-with-pipeline-and-sink) for the stream-side perspective.
496
+
497
+ ### With Sink
498
+
499
+ `Pipeline.andThenSink` creates a new `Sink` that pre-processes its input through the pipeline before the original sink consumes it. The `Sink` type provides its own transformation methods (`contramap`, `map`, `mapError`) — `andThenSink` extends these with the full power of pipeline composition (filtering, taking, dropping, collecting).
500
+
501
+ ## Running the Examples
502
+
503
+ All code from this guide is available as runnable examples in the `streams-examples` module.
504
+
505
+ **1. Clone the repository and navigate to the project:**
506
+
507
+ ```bash
508
+ git clone https://github.com/zio/zio-blocks.git
509
+ cd zio-blocks
510
+ ```
511
+
512
+ **2. Run individual examples with sbt:**
513
+
514
+ ### Basic Usage
515
+
516
+ This example demonstrates six of the Pipeline factory methods: `map`, `filter`, `collect`, `take`, `drop`, and `identity`. Here is the source code:
517
+
518
+ ```scala title="streams-examples/src/main/scala/pipeline/PipelineBasicUsageExample.scala"
519
+ package pipeline
520
+
521
+ import zio.blocks.streams.*
522
+ import zio.sbt.ExprEval.show
523
+
524
+ object PipelineBasicUsageExample extends App {
525
+ println("=== Pipeline Basic Usage ===\n")
526
+
527
+ // 1. Pipeline.map — transform each element
528
+ println("1. Pipeline.map — transform each element:")
529
+ val doubler = Pipeline.map[Int, Int](_ * 2)
530
+ show(Stream(1, 2, 3).via(doubler).runCollect)
531
+
532
+ // 2. Pipeline.map — cross-type transformation
533
+ println("\n2. Pipeline.map — type-changing transformation:")
534
+ val intToString = Pipeline.map[Int, String](n => s"item-$n")
535
+ show(Stream(1, 2, 3).via(intToString).runCollect)
536
+
537
+ // 3. Pipeline.filter — keep elements matching a predicate
538
+ println("\n3. Pipeline.filter — keep matching elements:")
539
+ val positives = Pipeline.filter[Int](_ > 0)
540
+ show(Stream(-2, -1, 0, 1, 2).via(positives).runCollect)
541
+
542
+ // 4. Pipeline.collect — partial function (filter + map)
543
+ println("\n4. Pipeline.collect — partial function transformation:")
544
+ val extractInts = Pipeline.collect[Any, Int] { case n: Int => n * 10 }
545
+ show(Stream(1, "a", 2, "b").via(extractInts).runCollect)
546
+
547
+ // 5. Pipeline.take — first n elements
548
+ println("\n5. Pipeline.take — first n elements (short-circuits):")
549
+ val firstThree = Pipeline.take[Int](3)
550
+ show(Stream.range(0, 1000).via(firstThree).runCollect)
551
+
552
+ // 6. Pipeline.drop — skip first n elements
553
+ println("\n6. Pipeline.drop — skip first n elements:")
554
+ val skipTwo = Pipeline.drop[String](2)
555
+ show(Stream("header", "subheader", "data1", "data2").via(skipTwo).runCollect)
556
+
557
+ // 7. Pipeline.identity — pass-through (no-op)
558
+ println("\n7. Pipeline.identity — pass-through (neutral element):")
559
+ val noOp = Pipeline.identity[Int]
560
+ show(Stream(1, 2, 3).via(noOp).runCollect)
561
+
562
+ // 8. Combining drop and take to get a range
563
+ println("\n8. Combining drop and take for pagination:")
564
+ val page2 = Pipeline.drop[Int](3).andThen(Pipeline.take(3))
565
+ show(Stream.range(0, 10).via(page2).runCollect)
566
+ }
567
+ ```
568
+
569
+ ([source](https://github.com/zio/zio-blocks/blob/main/streams-examples/src/main/scala/pipeline/PipelineBasicUsageExample.scala))
570
+
571
+ Run it with:
572
+
573
+ ```bash
574
+ sbt "streams-examples/runMain pipeline.PipelineBasicUsageExample"
575
+ ```
576
+
577
+ ### Pipeline Composition
578
+
579
+ This example shows how to compose pipelines with `andThen`, apply them to multiple streams, and build pipelines conditionally. Here is the source code:
580
+
581
+ ```scala title="streams-examples/src/main/scala/pipeline/PipelineCompositionExample.scala"
582
+ package pipeline
583
+
584
+ import zio.blocks.streams.*
585
+ import zio.sbt.ExprEval.show
586
+
587
+ object PipelineCompositionExample extends App {
588
+ println("=== Pipeline Composition ===\n")
589
+
590
+ // 1. Basic andThen composition
591
+ println("1. Composing filter + map with andThen:")
592
+ val filterPositive = Pipeline.filter[Int](_ > 0)
593
+ val double = Pipeline.map[Int, Int](_ * 2)
594
+ val composed = filterPositive.andThen(double)
595
+
596
+ show(Stream(-3, -1, 0, 2, 5).via(composed).runCollect)
597
+
598
+ // 2. Multi-stage pipeline
599
+ println("\n2. Multi-stage pipeline (filter → map → take):")
600
+ val multiStage = Pipeline
601
+ .filter[Int](_ % 2 == 0)
602
+ .andThen(Pipeline.map[Int, Int](_ * 10))
603
+ .andThen(Pipeline.take(3))
604
+
605
+ show(Stream.range(1, 11).via(multiStage).runCollect)
606
+
607
+ // 3. Reusing the same pipeline across different streams
608
+ println("\n3. Reusing the same pipeline across different streams:")
609
+ val normalize = Pipeline.filter[Int](_ >= 0).andThen(Pipeline.map[Int, Double](_.toDouble / 100.0))
610
+
611
+ val dataset1 = Stream(150, -20, 75, 200, -10)
612
+ val dataset2 = Stream(50, 100, -5, 300)
613
+
614
+ show(dataset1.via(normalize).runCollect)
615
+ show(dataset2.via(normalize).runCollect)
616
+
617
+ // 4. Category law: left identity
618
+ println("\n4. Category law — left identity (identity andThen p == p):")
619
+ val pipe = Pipeline.map[Int, Int](_ + 1)
620
+ val data = Stream(1, 2, 3)
621
+
622
+ val withIdentityL = data.via(Pipeline.identity[Int].andThen(pipe)).runCollect
623
+ val withoutIdentity = data.via(pipe).runCollect
624
+ show(withIdentityL)
625
+ show(withoutIdentity)
626
+
627
+ // 5. Category law: right identity
628
+ println("\n5. Category law — right identity (p andThen identity == p):")
629
+ val withIdentityR = data.via(pipe.andThen(Pipeline.identity[Int])).runCollect
630
+ show(withIdentityR)
631
+ show(withoutIdentity)
632
+
633
+ // 6. Category law: associativity
634
+ println("\n6. Category law — associativity ((p andThen q) andThen r == p andThen (q andThen r)):")
635
+ val p = Pipeline.filter[Int](_ > 0)
636
+ val q = Pipeline.map[Int, Int](_ * 3)
637
+ val r = Pipeline.take[Int](2)
638
+
639
+ val leftGrouped = (p.andThen(q)).andThen(r)
640
+ val rightGrouped = p.andThen(q.andThen(r))
641
+
642
+ val source = Stream(-1, 2, -3, 4, 5, 6)
643
+ show(source.via(leftGrouped).runCollect)
644
+ show(source.via(rightGrouped).runCollect)
645
+
646
+ // 7. Building pipelines conditionally
647
+ println("\n7. Building pipelines conditionally:")
648
+ def buildPipeline(limit: Option[Int], onlyPositive: Boolean): Pipeline[Int, Int] = {
649
+ var pipe: Pipeline[Int, Int] = Pipeline.identity[Int]
650
+ if (onlyPositive) pipe = pipe.andThen(Pipeline.filter(_ > 0))
651
+ limit.foreach(n => pipe = pipe.andThen(Pipeline.take(n.toLong)))
652
+ pipe
653
+ }
654
+
655
+ val conditionalPipe = buildPipeline(limit = Some(3), onlyPositive = true)
656
+ show(Stream(-1, 2, -3, 4, 5, 6).via(conditionalPipe).runCollect)
657
+
658
+ val noPipe = buildPipeline(limit = None, onlyPositive = false)
659
+ show(Stream(-1, 2, -3).via(noPipe).runCollect)
660
+ }
661
+ ```
662
+
663
+ ([source](https://github.com/zio/zio-blocks/blob/main/streams-examples/src/main/scala/pipeline/PipelineCompositionExample.scala))
664
+
665
+ Run it with:
666
+
667
+ ```bash
668
+ sbt "streams-examples/runMain pipeline.PipelineCompositionExample"
669
+ ```
670
+
671
+ ### Sink Integration
672
+
673
+ This example demonstrates applying pipelines to sinks with `andThenSink`, showing the equivalence between `stream.via(pipe).run(sink)` and `stream.run(pipe.andThenSink(sink))`. Here is the source code:
674
+
675
+ ```scala title="streams-examples/src/main/scala/pipeline/PipelineSinkIntegrationExample.scala"
676
+ package pipeline
677
+
678
+ import zio.blocks.streams.*
679
+ import zio.sbt.ExprEval.show
680
+
681
+ object PipelineSinkIntegrationExample extends App {
682
+ println("=== Pipeline ↔ Sink Integration ===\n")
683
+
684
+ // 1. Basic andThenSink usage
685
+ println("1. Basic andThenSink — pre-process before collecting:")
686
+ val doubler = Pipeline.map[Int, Int](_ * 2)
687
+ val collectDoubled = doubler.andThenSink(Sink.collectAll[Int])
688
+
689
+ show(Stream(1, 2, 3).run(collectDoubled))
690
+
691
+ // 2. Equivalence law: via + run == run + andThenSink
692
+ println("\n2. Equivalence law: stream.via(p).run(sink) == stream.run(p.andThenSink(sink)):")
693
+ val pipe = Pipeline.filter[Int](_ > 2).andThen(Pipeline.map[Int, Int](_ * 10))
694
+ val source = Stream(1, 2, 3, 4, 5)
695
+ val sink = Sink.collectAll[Int]
696
+
697
+ val viaResult = source.via(pipe).run(sink)
698
+ val andThenSinkResult = source.run(pipe.andThenSink(sink))
699
+ show(viaResult)
700
+ show(andThenSinkResult)
701
+
702
+ // 3. Reusable pre-processing sink
703
+ println("\n3. Reusable pre-processing sink:")
704
+ val cleanString = Pipeline.map[String, String](_.trim.toLowerCase)
705
+ val collectCleaned = cleanString.andThenSink(Sink.collectAll[String])
706
+ val countCleaned = cleanString.andThenSink(Sink.count)
707
+
708
+ val rawData = Stream(" Hello ", " WORLD ", " Scala ")
709
+
710
+ show(rawData.run(collectCleaned))
711
+ show(rawData.run(countCleaned))
712
+
713
+ // 4. andThenSink with foldLeft
714
+ println("\n4. Pipeline + foldLeft sink:")
715
+ val sumPositives = Pipeline
716
+ .filter[Int](_ > 0)
717
+ .andThenSink(Sink.foldLeft(0)((acc, x) => acc + x))
718
+
719
+ show(Stream(-5, 3, -2, 7, 1).run(sumPositives))
720
+
721
+ // 5. andThenSink with head/find
722
+ println("\n5. Pipeline + head/find sinks:")
723
+ val firstEven = Pipeline.filter[Int](_ % 2 == 0).andThenSink(Sink.head[Int])
724
+
725
+ show(Stream(1, 3, 4, 6).run(firstEven))
726
+
727
+ // 6. Multiple pipelines, same sink
728
+ println("\n6. Multiple pipelines applied to the same sink:")
729
+ val baseSink = Sink.collectAll[Int]
730
+
731
+ val evens = Pipeline.filter[Int](_ % 2 == 0).andThenSink(baseSink)
732
+ val odds = Pipeline.filter[Int](_ % 2 != 0).andThenSink(baseSink)
733
+
734
+ val nums = Stream(1, 2, 3, 4, 5, 6)
735
+ show(nums.run(evens))
736
+ show(nums.run(odds))
737
+
738
+ // 7. Complex pipeline applied to sink
739
+ println("\n7. Multi-stage pipeline applied to sink:")
740
+ val processingPipe = Pipeline
741
+ .filter[Int](_ > 0)
742
+ .andThen(Pipeline.map[Int, Int](_ * 2))
743
+ .andThen(Pipeline.take(3))
744
+
745
+ val processedSum = processingPipe.andThenSink(Sink.foldLeft(0)(_ + _))
746
+
747
+ show(Stream(-1, 5, 3, 8, 2, 9).run(processedSum))
748
+ }
749
+ ```
750
+
751
+ ([source](https://github.com/zio/zio-blocks/blob/main/streams-examples/src/main/scala/pipeline/PipelineSinkIntegrationExample.scala))
752
+
753
+ Run it with:
754
+
755
+ ```bash
756
+ sbt "streams-examples/runMain pipeline.PipelineSinkIntegrationExample"
757
+ ```
758
+
759
+ ### Asynchronous Stages Through Both Routes
760
+
761
+ This example builds `mapAsync`, `filterAsync`, and `collectAsync` into one composed pipeline value, then applies that same value through `stream.via(pipe)`, `pipe.andThenSink(sink)`, and `pipe.applyToSink(sink)` — showing that all three produce the same elements. Here is the source code:
762
+
763
+ ```scala title="streams-examples/src/main/scala/pipeline/PipelineAsyncExample.scala"
764
+ package pipeline
765
+
766
+ import zio.blocks.async.*
767
+ import zio.blocks.chunk.Chunk
768
+ import zio.blocks.streams.*
769
+
770
+ /**
771
+ * The three asynchronous `Pipeline` factories — `mapAsync`, `filterAsync` and
772
+ * `collectAsync` — built once as values and then applied through both routes:
773
+ * `stream.via(pipe)` and `pipe.andThenSink(sink)` (with `pipe.applyToSink` as
774
+ * the third spelling of the second route).
775
+ *
776
+ * The point of running both is that they agree. A pipeline is a description,
777
+ * not a stage bound to one side of the graph, so the same value produces the
778
+ * same elements whichever end it is attached to.
779
+ *
780
+ * JVM only, because it ends in `.block` to turn an `Async` into a value for
781
+ * `main`.
782
+ */
783
+ object PipelineAsyncExample {
784
+
785
+ /** Each field of a raw record, as a trimmed reading. */
786
+ private val readings: Stream[Nothing, String] =
787
+ Stream(" 17 ", " 4", "31 ", " 8 ", " ", "150")
788
+
789
+ /**
790
+ * `mapAsync` — one asynchronous callback per element, awaited before the next
791
+ * element is pulled. It asks for `JvmType.Infer` evidence for `Int`, its
792
+ * result type, and nothing for its input.
793
+ */
794
+ private val parse: Pipeline[String, Int] =
795
+ Pipeline
796
+ .mapAsync[String, String](raw => Async.succeed(raw.trim))
797
+ .andThen(Pipeline.mapAsync[String, Int](t => Async.succeed(if (t.isEmpty) 0 else t.toInt)))
798
+
799
+ /** `filterAsync` — type-preserving, so it needs no result evidence. */
800
+ private val inRange: Pipeline[Int, Int] =
801
+ Pipeline.filterAsync[Int](n => Async.succeed(n > 0 && n <= 100))
802
+
803
+ /**
804
+ * `collectAsync` — filter and transform in one callback. It returns
805
+ * `Async[Option[B]]` rather than a `PartialFunction`: a `None` drops the
806
+ * element, a `Some` emits its value.
807
+ */
808
+ private val tensDigit: Pipeline[Int, Int] =
809
+ Pipeline.collectAsync[Int, Int](n => Async.succeed(if (n >= 10) Some(n / 10) else None))
810
+
811
+ /** One composed value, applied unchanged through both routes below. */
812
+ private val pipe: Pipeline[String, Int] =
813
+ parse.andThen(inRange).andThen(tensDigit)
814
+
815
+ def main(args: Array[String]): Unit = {
816
+ val sink: Sink[Nothing, Int, Chunk[Int]] = Sink.collectAll[Int]
817
+
818
+ // Route 1 — attach the pipeline to the stream, then run a plain sink.
819
+ val viaStream = readings.via(pipe).runAsync(sink).block
820
+
821
+ // Route 2 — attach the pipeline to the sink, then run the plain stream.
822
+ val viaSink = readings.runAsync(pipe.andThenSink(sink)).block
823
+
824
+ // The third spelling: `andThenSink` is an alias for `applyToSink`.
825
+ val viaApplyToSink = readings.runAsync(pipe.applyToSink(sink)).block
826
+
827
+ report("stream.via(pipe)", viaStream)
828
+ report("pipe.andThenSink(sink)", viaSink)
829
+ report("pipe.applyToSink(sink)", viaApplyToSink)
830
+ println(s"routes agree: ${viaStream == viaSink && viaSink == viaApplyToSink}")
831
+ }
832
+
833
+ private def report[E, Z](label: String, result: Either[E, Z]): Unit =
834
+ result match {
835
+ case Right(value) => println(s"$label -> $value")
836
+ case Left(error) => println(s"$label -> typed error: $error")
837
+ }
838
+ }
839
+ ```
840
+
841
+ ([source](https://github.com/zio/zio-blocks/blob/main/streams-examples/src/main/scala/pipeline/PipelineAsyncExample.scala))
842
+
843
+ Run it with:
844
+
845
+ ```bash
846
+ sbt "streams-examples/runMain pipeline.PipelineAsyncExample"
847
+ ```
848
+
849
+ ## See Also
850
+
851
+ - [Asynchronous Stream Execution](../execution-and-compatibility/async-execution.md#async-operators) — the stream-level `*Async` operators the three asynchronous factories delegate to, and how a mixed graph compiles
852
+ - [Stream](./stream.md#integration-with-pipeline-and-sink) — the producer side of `via`
853
+ - [Sink](./sink.md) — the consumer a pipeline can be attached to instead
854
+ - [Zero-Boxing Streams](../execution-and-compatibility/zero-boxing.md) — the lanes `JvmType.Infer` selects between, and why transforming factories need the evidence