@zio.dev/zio-blocks 0.0.51 → 0.0.56

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 (166) 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 +6 -0
  4. package/guides/getting-started-with-mux.md +0 -112
  5. package/guides/query-dsl-extending.md +1 -1
  6. package/guides/query-dsl-fluent-builder.md +1 -1
  7. package/guides/query-dsl-reified-optics.md +1 -1
  8. package/guides/query-dsl-sql.md +395 -1
  9. package/guides/sql-checked-interpolation.md +173 -0
  10. package/guides/sql-transactions.md +286 -0
  11. package/guides/telemetry-guide.md +131 -70
  12. package/guides/zio-schema-migration.md +6 -6
  13. package/index.md +200 -559
  14. package/package.json +1 -1
  15. package/reference/async.md +1379 -531
  16. package/reference/chunk.md +3 -3
  17. package/reference/codegen/index.md +1 -1
  18. package/reference/combinators.md +4 -4
  19. package/reference/config/config-decoder.md +460 -0
  20. package/reference/config/config-source.md +489 -0
  21. package/reference/config/errors.md +278 -0
  22. package/reference/config/flags.md +369 -0
  23. package/reference/config/formats.md +314 -0
  24. package/reference/config/index.md +304 -0
  25. package/reference/config/rollout.md +336 -0
  26. package/reference/context.md +6 -49
  27. package/reference/data-migration.md +269 -0
  28. package/reference/datastar/attributes.md +302 -0
  29. package/reference/datastar/events.md +234 -0
  30. package/reference/datastar/index.md +256 -0
  31. package/reference/datastar/signals.md +230 -0
  32. package/reference/datastar/sse.md +295 -0
  33. package/reference/datastar.md +2 -2
  34. package/reference/docs.md +2 -2
  35. package/reference/endpoint/bulk-creation.md +96 -0
  36. package/reference/endpoint/endpoint.md +1 -0
  37. package/reference/endpoint/index.md +9 -89
  38. package/reference/endpoint/path-codec.md +12 -24
  39. package/reference/endpoint/route-pattern.md +4 -6
  40. package/reference/endpoint/segment-codec.md +19 -32
  41. package/reference/html.md +313 -9
  42. package/reference/htmx/index.md +4 -52
  43. package/reference/htmx/response-headers.md +240 -0
  44. package/reference/http-model/headers.md +735 -0
  45. package/reference/http-model/index.md +3 -1
  46. package/reference/http-model/model.md +107 -71
  47. package/reference/http-model/schema-codecs.md +522 -0
  48. package/reference/http-model/schema.md +6 -3
  49. package/reference/http-model/server-sent-event.md +341 -0
  50. package/reference/jwt.md +195 -0
  51. package/reference/maybe.md +128 -11
  52. package/reference/media-type.md +2 -2
  53. package/reference/mux.mdx +7 -2
  54. package/reference/openapi.md +3 -3
  55. package/reference/projection.md +654 -0
  56. package/reference/resource-management/index.md +1 -1
  57. package/reference/resource-management/resource.md +2 -98
  58. package/reference/resource-management/scope.md +1 -209
  59. package/reference/resource-management/wire.md +4 -50
  60. package/reference/ringbuffer/advanced.mdx +1 -1
  61. package/reference/ringbuffer/index.mdx +3 -3
  62. package/reference/ringbuffer/mpmc.mdx +38 -4
  63. package/reference/ringbuffer/mpsc.mdx +36 -4
  64. package/reference/ringbuffer/spmc.mdx +1 -1
  65. package/reference/ringbuffer/spsc.mdx +87 -15
  66. package/reference/schema/allows.md +0 -96
  67. package/reference/schema/binding.md +2 -2
  68. package/reference/schema/built-in-codecs/avro.md +2 -2
  69. package/reference/schema/built-in-codecs/bson.md +50 -20
  70. package/reference/schema/built-in-codecs/csv.md +2 -2
  71. package/reference/schema/built-in-codecs/index.md +3 -3
  72. package/reference/schema/built-in-codecs/json/index.md +2 -2
  73. package/reference/schema/built-in-codecs/json/json.md +1 -0
  74. package/reference/schema/built-in-codecs/messagepack.md +3 -3
  75. package/reference/schema/built-in-codecs/thrift.md +2 -2
  76. package/reference/schema/built-in-codecs/toon.md +3 -3
  77. package/reference/schema/built-in-codecs/yaml.md +2 -2
  78. package/reference/schema/codec.md +11 -11
  79. package/reference/schema/dynamic-optic.md +48 -3
  80. package/reference/schema/dynamic-schema.md +3 -3
  81. package/reference/schema/index.md +2 -0
  82. package/reference/schema/path-interpolator.md +2 -0
  83. package/reference/schema/reflect-transformer.md +140 -0
  84. package/reference/schema/schema-evolution/as.md +4 -4
  85. package/reference/schema/schema-evolution/into.md +2 -2
  86. package/reference/schema/schema-expr.md +2 -2
  87. package/reference/schema/schema-search.md +263 -0
  88. package/reference/schema/schema.md +10 -2
  89. package/reference/schema/type-class-derivation.md +1 -1
  90. package/reference/smithy.md +502 -3
  91. package/reference/sql/db-codec-deriver.md +3 -3
  92. package/reference/sql/db-codec.md +22 -22
  93. package/reference/sql/db-con.md +4 -4
  94. package/reference/sql/db-connection.md +1 -1
  95. package/reference/sql/db-param.md +1 -1
  96. package/reference/sql/db-result-reader.md +4 -2
  97. package/reference/sql/db-tx.md +46 -14
  98. package/reference/sql/ddl.md +1 -1
  99. package/reference/sql/frag.md +44 -10
  100. package/reference/sql/index.md +7 -7
  101. package/reference/sql/repo.md +15 -15
  102. package/reference/sql/sql-dialect.md +1 -1
  103. package/reference/sql/sql-logger.md +1 -1
  104. package/reference/sql/sql-name-mapper.md +3 -3
  105. package/reference/sql/table-metadata.md +3 -3
  106. package/reference/sql/table.md +10 -10
  107. package/reference/sql/transactor-zio.md +1 -1
  108. package/reference/sql/transactor.md +21 -11
  109. package/reference/sql-zio.md +2 -2
  110. package/reference/streams/core/index.md +32 -0
  111. package/reference/streams/{pipeline.md → core/pipeline.md} +210 -74
  112. package/reference/streams/{sink.md → core/sink.md} +331 -353
  113. package/reference/streams/{stream.md → core/stream.md} +919 -209
  114. package/reference/streams/execution-and-compatibility/async-execution.md +822 -0
  115. package/reference/streams/execution-and-compatibility/index.md +35 -0
  116. package/reference/streams/execution-and-compatibility/platform-differences.md +297 -0
  117. package/reference/streams/execution-and-compatibility/scala-2-compatibility.md +88 -0
  118. package/reference/streams/execution-and-compatibility/zero-boxing.md +393 -0
  119. package/reference/streams/index.md +140 -67
  120. package/reference/streams/primitives/index.md +30 -0
  121. package/reference/streams/primitives/reader.md +1992 -0
  122. package/reference/streams/{writer.md → primitives/writer.md} +254 -98
  123. package/reference/telemetry/common/any-value.md +90 -0
  124. package/reference/telemetry/common/attribute-key.md +87 -0
  125. package/reference/telemetry/common/attributes.md +118 -0
  126. package/reference/telemetry/common/index.md +39 -0
  127. package/reference/telemetry/common/instrumentation-scope.md +24 -0
  128. package/reference/telemetry/common/resource.md +34 -0
  129. package/reference/telemetry/index.md +311 -0
  130. package/reference/telemetry/logging/index.md +197 -0
  131. package/reference/telemetry/logging/log-enrichment.md +72 -0
  132. package/reference/telemetry/logging/log-formatter.md +100 -0
  133. package/reference/telemetry/logging/log-record-processor.md +56 -0
  134. package/reference/telemetry/logging/log-record.md +44 -0
  135. package/reference/telemetry/logging/log-writer.md +64 -0
  136. package/reference/telemetry/logging/logger-provider.md +142 -0
  137. package/reference/telemetry/logging/logger.md +83 -0
  138. package/reference/telemetry/logging/severity.md +62 -0
  139. package/reference/telemetry/metrics/index.md +150 -0
  140. package/reference/telemetry/metrics/instruments.md +183 -0
  141. package/reference/telemetry/metrics/labeled-instruments.md +74 -0
  142. package/reference/telemetry/metrics/meter-provider.md +76 -0
  143. package/reference/telemetry/metrics/meter.md +98 -0
  144. package/reference/telemetry/metrics/metric-data.md +57 -0
  145. package/reference/telemetry/otel/custom-exporter.md +216 -0
  146. package/reference/telemetry/otel/index.md +212 -0
  147. package/reference/telemetry/tracing/index.md +155 -0
  148. package/reference/telemetry/tracing/sampler.md +89 -0
  149. package/reference/telemetry/tracing/span-builder.md +57 -0
  150. package/reference/telemetry/tracing/span-context.md +39 -0
  151. package/reference/telemetry/tracing/span-data.md +32 -0
  152. package/reference/telemetry/tracing/span-kind.md +55 -0
  153. package/reference/telemetry/tracing/span-processor.md +53 -0
  154. package/reference/telemetry/tracing/span-status.md +47 -0
  155. package/reference/telemetry/tracing/span.md +117 -0
  156. package/reference/telemetry/tracing/tracer-provider.md +91 -0
  157. package/reference/telemetry/tracing/tracer.md +52 -0
  158. package/reference/typeid.md +0 -64
  159. package/sidebars.js +365 -185
  160. package/undocumented-report.md +528 -270
  161. package/reference/config.md +0 -158
  162. package/reference/streams/concurrent-operators.md +0 -106
  163. package/reference/streams/reader.md +0 -1284
  164. package/reference/streams/scala-2-compatibility.md +0 -55
  165. package/reference/streams/zero-boxing.md +0 -275
  166. package/reference/telemetry.md +0 -693
@@ -0,0 +1,822 @@
1
+ ---
2
+ id: async-execution
3
+ title: "Asynchronous Stream Execution"
4
+ sidebar_label: "Async Execution"
5
+ description: "How one Stream type describes synchronous and asynchronous pipelines, and the full async constructor, operator, and terminal API."
6
+ keywords:
7
+ - "Asynchronous Streams"
8
+ - "Stream Compilation"
9
+ - "Async Terminals"
10
+ - "Close Ownership"
11
+ - "Stream"
12
+ ---
13
+
14
+ One `Stream[E, A]` describes both synchronous and asynchronous pipelines. There is no asynchronous stream type to convert to, no mode parameter to thread through your signatures, and no annotation that marks a description as one or the other. The API supports mixed synchronous and asynchronous stream composition without a second stream type or mode parameter, including dynamic inner streams and platform-specific materialization.
15
+
16
+ The terminals ending in `Async` are the cross-platform ones. They compile and run on the JVM and on Scala.js, and they are the family shared code should be written against. The blocking terminals (`run`, `runCollect`, `head`, `start`, and their siblings) still exist, but only on the JVM.
17
+
18
+ ## Overview
19
+
20
+ The asynchronous surface is five things: constructors that produce a stream from an `Async`, element-level operators that take an `Async` callback, the `*Async` terminal family, one bounded-concurrency operator, and the platform adapters that turn native asynchronous I/O into a stream.
21
+
22
+ | Addition | Size | Documented in |
23
+ |----------------------------|-------------------------|---------------------------------------------------------|
24
+ | Async source constructors | 10 names / 14 overloads | [Async Source Constructors](#async-source-constructors) |
25
+ | Sequential async operators | 10 names | [Async Operators](#async-operators) |
26
+ | Async terminals | 12 names / 16 overloads | [Async Terminals](#async-terminals) |
27
+ | Manual-pull terminals | 2 names | [Manual Pull and Ownership](#manual-pull-and-ownership) |
28
+ | Bounded concurrency | 1 name (`mapParAsync`) | [Bounded Concurrency](../core/stream.md#bounded-concurrency) |
29
+ | The `Reader` union | 2 subtypes | [Reader](../primitives/reader.md) |
30
+ | Platform I/O adapters | JVM NIO and JS streams | [Reader](../primitives/reader.md#from-native-asynchronous-sources) |
31
+
32
+ Every asynchronous addition follows one naming convention: the synchronous name with `Async` appended. There is no `fromAsync` and no `asyncPush`.
33
+
34
+ ## Dependency and Imports
35
+
36
+ The streams module carries the asynchronous effect type with it — `zio-blocks-streams` depends on `zio-blocks-async`, so one coordinate is all you add:
37
+
38
+ ```scala
39
+ libraryDependencies += "dev.zio" %%% "zio-blocks-streams" % "0.0.56"
40
+ ```
41
+
42
+ Use `%%%` in a cross-built project so the same line resolves for both JVM and Scala.js; `%%` is enough for a JVM-only build.
43
+
44
+ Every snippet on this page assumes these imports:
45
+
46
+ ```scala
47
+ import zio.blocks.streams._ // Stream, Sink, Pipeline, JvmType
48
+ import zio.blocks.streams.io.Reader // Reader, Reader.SyncReader, Reader.AsyncReader
49
+ import zio.blocks.async._ // Async, Pollable, Completer, and the Async extension methods
50
+ import zio.blocks.chunk.Chunk
51
+ ```
52
+
53
+ Importing `zio.blocks.async._` rather than `zio.blocks.async.Async` matters: `map`, `flatMap`, `block`, `either`, and the rest of the `Async` combinators are extension methods brought into scope by the package import.
54
+
55
+ ## One Stream Type, Two Execution Modes
56
+
57
+ The central claim of this page is short: the type that decides between synchronous and asynchronous execution is `Reader`, not `Stream`.
58
+
59
+ `Stream[E, A]` is a description. Nothing in it runs until a terminal is driven, and at that moment the description is compiled into a `Reader[A]`. `Reader` is the union of a synchronous and an asynchronous kind, and that compilation is the only place the two modes part ways.
60
+
61
+ ```
62
+ ┌───────────────────────────────────────────────────────────────┐
63
+ │ Stream[E, A] - a description; nothing has run yet │
64
+ └───────────────────────────────────────────────────────────────┘
65
+ │ a terminal is driven
66
+ ▼
67
+ ┌───────────────────────────────────────────────────────────────┐
68
+ │ Stream.compile - compile the graph structurally │
69
+ │ once, at materialization; never per element │
70
+ └───────────────────────────────────────────────────────────────┘
71
+ │ │
72
+ │ every node compiles │ any asynchronous node
73
+ │ synchronously │ is present
74
+ │ │
75
+ ▼ ▼
76
+ ┌────────────────────────┐ ┌──────────────────────────────────┐
77
+ │ Reader.SyncReader[A] │ │ Reader.AsyncReader[A] │
78
+ │ read and close │ │ read and close return Async; │
79
+ │ return directly │ │ sync stages lifted in place │
80
+ └────────────────────────┘ └──────────────────────────────────┘
81
+ ```
82
+
83
+ ### Classification Happens at Compile Time
84
+
85
+ Compilation is structural and single-pass. `Stream#compile` is an abstract per-node method: each node compiles its upstream and returns a `Reader` directly, so a graph whose every node compiles synchronously yields a `SyncReader`, and a graph containing any asynchronous node yields an `AsyncReader`. Asynchronous operator nodes accept either upstream kind — an already-asynchronous upstream is extended through `AsyncInterpreter.transform`, and a synchronous one is lifted through `AsyncInterpreter.transformSync` — so a synchronous source needs no annotation to sit beneath an asynchronous stage.
86
+
87
+ Separately, and only on the JVM, a blocking terminal first tries to fuse the whole graph into the flat-array `SyncInterpreter`. Nine node types cannot be represented in that form and throw `AsyncBoundaryRequired`; the fallback then compiles the graph the ordinary way and converts the result back to a `SyncReader` at the terminal. That fusion is a performance path for blocking terminals, not the mechanism that decides between the two execution modes.
88
+
89
+ This decision is made **once, at materialization**. It is never made per element, and it is never made per pull. A stream that turns out to be entirely synchronous runs through the synchronous engine with no asynchronous machinery in the loop at all.
90
+
91
+ The same description can be materialized more than once, and each materialization classifies independently. Classification is a property of the graph, not of the value's type.
92
+
93
+ ### The Reader Union
94
+
95
+ `Reader[+Elem]` is the root over two kinds:
96
+
97
+ ```scala
98
+ abstract class Reader[+Elem] {
99
+ def ++[Elem2 >: Elem](next: => Reader[Elem2]): Reader[Elem2]
100
+ def concat[Elem2 >: Elem](next: () => Reader[Elem2]): Reader[Elem2]
101
+ def concatAsync[Elem2 >: Elem](next: () => Async[Reader[Elem2]]): Reader.AsyncReader[Elem2]
102
+ def withReleaseAsync(release: () => Async[Unit]): Reader.AsyncReader[Elem]
103
+ def jvmType: JvmType
104
+ }
105
+ ```
106
+
107
+ The root carries only kind-independent composition and one piece of metadata. Everything that actually pulls or closes lives on one of the two subtypes: `Reader.SyncReader[Elem]`, whose `read` and `close` return directly, and `Reader.AsyncReader[Elem]`, whose pull and lifecycle operations return `Async`.
108
+
109
+ A synchronous graph materializes as the former; a graph containing any asynchronous node materializes as the latter. See [Reader](../primitives/reader.md) for the full member list of both kinds, for how to implement a custom reader, and for `SyncReader#toAsync` and the JVM-only `AsyncReader#toSync`.
110
+
111
+ ### Mixing Synchronous and Asynchronous Stages
112
+
113
+ When a synchronous source meets an asynchronous operator, the asynchronous node compiles to an `AsyncReader` and lifts its synchronous upstream through `AsyncInterpreter.transformSync`. Nothing in user code needs annotating, and no static type changes.
114
+
115
+ Composition widens. Two synchronous participants stay synchronous; a single asynchronous participant makes the result asynchronous:
116
+
117
+ ```scala
118
+ import zio.blocks.streams.io.Reader
119
+ import zio.blocks.async._
120
+
121
+ val syncReader = Reader.singleInt(1)
122
+ val asyncReader = Reader.singleInt(2).toAsync
123
+
124
+ val ss: Reader.SyncReader[Int] = syncReader ++ Reader.singleInt(2)
125
+ val sa: Reader.AsyncReader[Int] = Reader.singleInt(1) ++ asyncReader
126
+ val as: Reader.AsyncReader[Int] = asyncReader ++ Reader.singleInt(3)
127
+ val aa: Reader.AsyncReader[Int] = asyncReader ++ Reader.singleInt(4).toAsync
128
+ ```
129
+
130
+ At the stream level the same widening happens with no visible type at all. Adding one asynchronous stage to a synchronous pipeline leaves the annotation exactly as it was:
131
+
132
+ ```scala
133
+ import zio.blocks.streams._
134
+ import zio.blocks.streams.io.Reader
135
+ import zio.blocks.async._
136
+
137
+ val syncOnly: Stream[Nothing, Int] =
138
+ Stream.fromReader[Nothing, Int](Reader.fromIterable(List(1, 2, 3, 4, 5))).map(_ * 10)
139
+
140
+ val mixed: Stream[Nothing, Int] =
141
+ syncOnly.filterAsync(i => Async.succeed(i > 20))
142
+ ```
143
+
144
+ On the JVM a blocking terminal still accepts `mixed`: the asynchronous reader is converted back at the final boundary. On Scala.js, use an `*Async` terminal.
145
+
146
+ ### Why Two Engines
147
+
148
+ The synchronous engine keeps its lane registers as stack locals inside a single loop. Stack locals cannot survive a suspension — the moment a callback returns a value that is not yet ready, the loop's frame has to unwind and there is nowhere for those registers to live. The asynchronous path is therefore a separate, heap-allocated engine that keeps the equivalent state in an object it can park and resume.
149
+
150
+ That is the whole reason classification exists. It is also the reason a purely synchronous stream pays nothing for the library's asynchronous support: a graph with no asynchronous node never touches the heap-allocated engine.
151
+
152
+ ### There Is No Mode Annotation, and No Lane Diagnostic
153
+
154
+ Two things readers look for here, and will not find:
155
+
156
+ - **No type-level marker.** `Stream[E, A]` carries no phantom parameter, no `Sync`/`Async` tag, and no evidence that says which way a description will compile. You cannot write a signature that only accepts asynchronous streams, and you cannot ask a `Stream` value whether it will materialize asynchronously.
157
+ - **No public lane diagnostic.** A `Stream` exposes no lane of its own; `Reader#jvmType` reports the lane of a reader you already hold, which answers a narrower question than whether every fused stage preserved it. `JvmType.Infer` reports the static type, which is exactly the thing the representation machinery stopped trusting. See [Zero-Boxing Optimization](./zero-boxing.md) for what the lanes are and how one is chosen.
158
+
159
+ If you need to control the kind rather than observe it, use the union-preserving `Stream.fromReader` overloads below: they let you hand a specific reader kind to the stream.
160
+
161
+ ## Async Source Constructors
162
+
163
+ These are the companion constructors that turn an `Async` into a stream. Each of them defers its thunk until the first reader operation is driven.
164
+
165
+ ### `Stream.attemptAsync`
166
+
167
+ ```scala
168
+ def attemptAsync[A](f: => Async[A])(implicit jtA: JvmType.Infer[A]): Stream[Throwable, A]
169
+ ```
170
+
171
+ Lazily evaluates an asynchronous thunk once per materialization and emits its result. Non-fatal synchronous throws and asynchronous failures become typed errors; fatal throwables remain defects.
172
+
173
+ ### `Stream.attemptEvalAsync`
174
+
175
+ ```scala
176
+ def attemptEvalAsync(f: => Async[Any]): Stream[Throwable, Nothing]
177
+ ```
178
+
179
+ Lazily executes an asynchronous effect once per materialization and emits nothing. Non-fatal synchronous throws and asynchronous failures become typed errors; fatal throwables remain defects. Use it for an effect whose result you do not want in the stream.
180
+
181
+ ### `Stream.deferAsync`
182
+
183
+ ```scala
184
+ def deferAsync(finalizer: => Async[Unit]): Stream[Nothing, Nothing]
185
+ ```
186
+
187
+ Creates an empty stream that lazily registers an asynchronous release action. It is awaited exactly once when each materialization closes, including after failure, early termination, or cancellation; failure is a defect.
188
+
189
+ ### `Stream.evalAsync`
190
+
191
+ ```scala
192
+ def evalAsync(f: => Async[Any]): Stream[Nothing, Nothing]
193
+ ```
194
+
195
+ Lazily executes an asynchronous effect once per materialization and emits nothing; synchronous throws and asynchronous failures are defects. This is `attemptEvalAsync` without the typed error channel.
196
+
197
+ ### `Stream.fromAcquireReleaseAsync`
198
+
199
+ ```scala
200
+ def fromAcquireReleaseAsync[R, E, A](
201
+ acquire: => Async[R],
202
+ release: R => Async[Unit]
203
+ )(use: R => Stream[E, A])(implicit jtA: JvmType.Infer[A]): Stream[E, A]
204
+ ```
205
+
206
+ Lazily acquires one resource per materialization, constructs the stream with `use`, and awaits release exactly once after completion, failure, early termination, or cancellation — including cancellation during acquisition, once the resource is obtained. Acquisition, `use`, and release failures are defects.
207
+
208
+ ### `Stream.fromIteratorAsync`
209
+
210
+ ```scala
211
+ def fromIteratorAsync[A](it: => Async[Iterator[A]])(implicit jtA: JvmType.Infer[A]): Stream[Nothing, A]
212
+ ```
213
+
214
+ Asynchronously obtains one iterator per materialization and consumes it in order. Acquisition and iterator failures are defects.
215
+
216
+ ### `Stream.fromReaderAsync`
217
+
218
+ ```scala
219
+ def fromReaderAsync[E, A](mkReader: => Async[Reader[A]])(implicit jtA: JvmType.Infer[A]): Stream[E, A]
220
+ ```
221
+
222
+ Lazily runs `mkReader` once per materialization and closes the resulting reader when the stream closes. Effect and thunk failures are defects.
223
+
224
+ ### `Stream.unfoldAsync`
225
+
226
+ ```scala
227
+ def unfoldAsync[S, A](s: S)(f: S => Async[Option[(A, S)]])(implicit jtA: JvmType.Infer[A]): Stream[Nothing, A]
228
+ ```
229
+
230
+ Lazily unfolds state sequentially, emitting each `A` and continuing with the paired state until `f` returns `None`. At most one callback is active at a time; callback failure is a defect.
231
+
232
+ ### `Stream.unwrap`
233
+
234
+ ```scala
235
+ def unwrap[E](stream: => Async[Stream[E, Nothing]])(implicit dummy: DummyImplicit): Stream[E, Nothing]
236
+ def unwrap[E, A](stream: => Async[Stream[E, A]])(implicit jtA: JvmType.Infer[A]): Stream[E, A]
237
+ ```
238
+
239
+ Flattens an asynchronously produced stream. The effect is evaluated lazily once per materialization; effect failures are defects, while the produced stream retains its typed error channel. The `DummyImplicit` overload exists to preserve inference when the element type is `Nothing`.
240
+
241
+ `unwrap` is the idiom for feeding an asynchronously produced stream into an operator that has no asynchronous twin. `flatMap`, `catchAll`, `catchDefect`, `orElse`, and `flatMapPar` all compose with it unchanged:
242
+
243
+ ```scala
244
+ import zio.blocks.streams._
245
+ import zio.blocks.async._
246
+
247
+ val stream: Stream[Nothing, Int] = Stream(1, 2, 3)
248
+
249
+ val flatMapped: Stream[Nothing, Long] =
250
+ stream.flatMap(i => Stream.unwrap(Async.succeed(Stream(i.toLong))))
251
+
252
+ val recovered: Stream[String, Int] =
253
+ Stream.fail[String]("failure").catchAll(_ => Stream.unwrap(Async.succeed(stream)))
254
+ ```
255
+
256
+ ### `Stream.fromReader`
257
+
258
+ Four overloads dispatch on the kind of reader you hand them, so the kind you chose is the kind the stream materializes:
259
+
260
+ ```scala
261
+ def fromReader[E, A](mkReader: => Reader[A]): Stream[E, A]
262
+
263
+ def fromReader[E, A](mkReader: => Reader.SyncReader[A])(implicit
264
+ dummy1: DummyImplicit,
265
+ dummy2: DummyImplicit
266
+ ): Stream[E, A]
267
+
268
+ def fromReader[E, A](mkReader: => Nothing)(implicit
269
+ dummy1: DummyImplicit,
270
+ dummy2: DummyImplicit,
271
+ dummy3: DummyImplicit
272
+ ): Stream[E, A]
273
+
274
+ def fromReader[E, A](mkReader: => Reader.AsyncReader[A])(implicit dummy: DummyImplicit): Stream[E, A]
275
+ ```
276
+
277
+ Each overload lazily obtains one reader per materialization and closes it when the stream closes; reader-thunk failures are defects. The `Reader[A]` overload is documented as advanced: it is the union-preserving escape hatch, and it preserves whether the returned reader is synchronous or asynchronous. The `AsyncReader` overload awaits the reader's close.
278
+
279
+ ### Laziness and Error Conventions
280
+
281
+ Two rules govern this whole family, and they are worth stating on their own because they are the two things most often assumed backwards.
282
+
283
+ **Laziness.** Compilation and materialization remain synchronous. Closing a stream before initialization neither invokes the thunk nor acquires a resource — an asynchronous constructor's effect starts only when the stream is first pulled. Do not compensate by eagerly opening a resource before constructing the stream.
284
+
285
+ **Errors.** Only the two `attempt*` constructors — `attemptAsync` and `attemptEvalAsync` — convert non-fatal callback failures into typed `Throwable` errors. Every other constructor's callback failure remains a defect, which fails the outer `Async` rather than appearing as a `Left`.
286
+
287
+ :::warning[A defect is not a typed error]
288
+ A defect does not surface in the `Either` that a terminal returns. It fails the surrounding `Async`, so a `match` on `Left`/`Right` will never see it. Reach for `attemptAsync` when you want a callback's failure in the `Left` channel.
289
+ :::
290
+
291
+ ## Async Operators
292
+
293
+ These are the sequential, element-level twins of the synchronous operators. Each applies its callback to one element at a time, in order, with at most one invocation active.
294
+
295
+ ### `Stream#mapAsync`
296
+
297
+ ```scala
298
+ def mapAsync[B](f: A => Async[B])(implicit jtB: JvmType.Infer[B]): Stream[E, B]
299
+ ```
300
+
301
+ Asynchronously transforms each element. Synchronous twin: [`map`](../core/stream.md#streammapb).
302
+
303
+ ### `Stream#mapErrorAsync`
304
+
305
+ ```scala
306
+ def mapErrorAsync[E2](f: E => Async[E2]): Stream[E2, A]
307
+ ```
308
+
309
+ Asynchronously transforms the typed error channel. It runs only when the source fails with a typed error; callback failure is a defect. It genuinely changes the error type, so a `Stream[String, A]` can become a `Stream[Long, A]`. Synchronous twin: [`mapError`](../core/stream.md#streammaperrore2).
310
+
311
+ ### `Stream#filterAsync`
312
+
313
+ ```scala
314
+ def filterAsync(pred: A => Async[Boolean]): Stream[E, A]
315
+ ```
316
+
317
+ Tests elements sequentially and emits those satisfying the asynchronous predicate, preserving order. Predicate failure is a defect. Synchronous twin: [`filter`](../core/stream.md#streamfilter).
318
+
319
+ ### `Stream#collectAsync`
320
+
321
+ ```scala
322
+ def collectAsync[B](f: A => Async[Option[B]])(implicit jtB: JvmType.Infer[B]): Stream[E, B]
323
+ ```
324
+
325
+ Asynchronously transforms defined elements, dropping `None` results. Synchronous twin: [`collect`](../core/stream.md#streamcollectb).
326
+
327
+ ### `Stream#mapAccumAsync`
328
+
329
+ ```scala
330
+ def mapAccumAsync[S, B](init: S)(f: (S, A) => Async[(S, B)])(implicit jtB: JvmType.Infer[B]): Stream[E, B]
331
+ ```
332
+
333
+ Asynchronously transforms elements while threading state sequentially. At most one invocation of `f` is active at a time. Synchronous twin: [`mapAccum`](../core/stream.md#stateful-transformations).
334
+
335
+ ### `Stream#scanAsync`
336
+
337
+ ```scala
338
+ def scanAsync[S](init: S)(f: (S, A) => Async[S])(implicit jtS: JvmType.Infer[S]): Stream[E, S]
339
+ ```
340
+
341
+ Asynchronously emits the accumulator at each step, starting with `init`. The output stream has one more element than the input. Synchronous twin: [`scan`](../core/stream.md#stateful-transformations).
342
+
343
+ ### `Stream#takeWhileAsync`
344
+
345
+ ```scala
346
+ def takeWhileAsync(pred: A => Async[Boolean]): Stream[E, A]
347
+ ```
348
+
349
+ Tests elements sequentially and emits them while the asynchronous predicate holds, then closes upstream at the first `false`. Predicate failure is a defect. Synchronous twin: [`takeWhile`](../core/stream.md#skipping-and-taking).
350
+
351
+ ### `Stream#distinctByAsync`
352
+
353
+ ```scala
354
+ def distinctByAsync[K](f: A => Async[K]): Stream[E, A]
355
+ ```
356
+
357
+ Sequentially computes keys and emits the first element for each key, preserving order. Key state is per materialization and may grow without bound; asynchronous failures are defects. Synchronous twin: [`distinctBy`](../core/stream.md#streamdistinctbyk).
358
+
359
+ ### `Stream#tapEachAsync`
360
+
361
+ ```scala
362
+ def tapEachAsync(f: A => Async[Unit]): Stream[E, A]
363
+ ```
364
+
365
+ Runs an asynchronous effect for each element and passes it through. Synchronous twin: [`tapEach`](../core/stream.md#other-operations).
366
+
367
+ ### `Stream#ensuringAsync`
368
+
369
+ ```scala
370
+ def ensuringAsync(finalizer: => Async[Unit]): Stream[E, A]
371
+ ```
372
+
373
+ Registers an asynchronous finalizer lazily and awaits it exactly once when the materialized stream closes, including normal completion, failure, early termination, and cancellation. Finalizer failure is a defect. Synchronous twin: [`ensuring`](../core/stream.md#streamensuring).
374
+
375
+ For the one *concurrent* asynchronous operator, `mapParAsync`, see [Bounded Concurrency](../core/stream.md#bounded-concurrency).
376
+
377
+ ## Async Terminals
378
+
379
+ A terminal is what drives a stream. The cross-platform family all ends in `Async` and all shares one return shape.
380
+
381
+ ### The `Async[Either[E, Z]]` Convention
382
+
383
+ Every cross-platform terminal returns `Async[Either[E, Z]]`, never `Async[Z]`. The two channels are kept apart deliberately:
384
+
385
+ - The **typed error channel** `E` stays inside the `Either`. A stream that fails with a typed error still succeeds at the `Async` level: the `Async` completes normally, carrying `Left(e)`.
386
+ - `Async`'s own **untyped `Throwable` channel** is reserved for defects — a callback that threw, a finalizer that failed, a cleanup failure. These fail the outer `Async` and never appear as a `Left`.
387
+
388
+ This one rule explains the shape of every signature in this section:
389
+
390
+ ```scala
391
+ import zio.blocks.streams._
392
+ import zio.blocks.chunk.Chunk
393
+ import zio.blocks.async._
394
+
395
+ val readings: Stream[String, Int] = Stream(12, 7, 30)
396
+
397
+ val collected: Async[Either[String, Chunk[Int]]] = readings.runCollectAsync
398
+
399
+ val described: Async[String] = collected.map(result =>
400
+ result match {
401
+ case Right(values) => s"collected ${values.length} readings"
402
+ case Left(error) => s"typed error: $error"
403
+ }
404
+ )
405
+ ```
406
+
407
+ ### Collecting and Running
408
+
409
+ ```scala
410
+ def runAsync[ES, E3, Z](sink: Sink[ES, A, Z])(implicit
411
+ errorConcat: Concat.WithOut[E @uncheckedVariance, ES, E3]
412
+ ): Async[Either[E3, Z]]
413
+
414
+ def runCollectAsync: Async[Either[E, Chunk[A]]]
415
+
416
+ def runDrainAsync: Async[Either[E, Unit]]
417
+
418
+ def runForeachAsync(f: A => Async[Unit]): Async[Either[E, Unit]]
419
+ ```
420
+
421
+ `runAsync` is the general form: it runs the stream through the asynchronous drain of a `Sink`, and materialization and cleanup are lazy, cancellation-safe, and performed exactly once. Its error type is the concatenation of the stream's error type and the sink's, which is why the implicit `Concat` evidence appears.
422
+
423
+ `runCollectAsync` collects all elements in order. It requires memory proportional to the entire output and does not terminate for an infinite stream. `runDrainAsync` discards them. `runForeachAsync` applies an asynchronous callback to each element sequentially.
424
+
425
+ ### Folding
426
+
427
+ `runFoldAsync` is a five-member family: four primitive accumulator lanes and one generic.
428
+
429
+ | Accumulator | Signature | Blocking `runFold` twin |
430
+ |-------------|------------------------------------------------------------|-------------------------|
431
+ | `Double` | `runFoldAsync(z: Double)(f: (Double, A) => Async[Double])` | yes |
432
+ | `Float` | `runFoldAsync(z: Float)(f: (Float, A) => Async[Float])` | none |
433
+ | `Int` | `runFoldAsync(z: Int)(f: (Int, A) => Async[Int])` | yes |
434
+ | `Long` | `runFoldAsync(z: Long)(f: (Long, A) => Async[Long])` | yes |
435
+ | generic `Z` | `runFoldAsync[Z](z: Z)(f: (Z, A) => Async[Z])` | yes |
436
+
437
+ The generic overload takes an implicit `JvmType.Infer[Z]`; the four primitive ones do not, and the overload is selected by the static type of `z`. Write `0L` rather than `0` when you want the `Long` lane.
438
+
439
+ The `Float` lane has no counterpart in the blocking `runFold` family, which offers only `Double`, `Int`, `Long`, and generic. It is new with the asynchronous terminals.
440
+
441
+ Each fold callback is applied sequentially, one element at a time.
442
+
443
+ ### Queries
444
+
445
+ The query terminals are one-liners over `runAsync`. Knowing which `Sink` each delegates to tells you its semantics exactly:
446
+
447
+ | Terminal | Returns | Delegates to |
448
+ |---------------------|-------------------------------|--------------------------|
449
+ | `countAsync` | `Async[Either[E, Long]]` | `Sink.count` |
450
+ | `existsAsync(pred)` | `Async[Either[E, Boolean]]` | `Sink.existsAsync(pred)` |
451
+ | `findAsync(pred)` | `Async[Either[E, Option[A]]]` | `Sink.findAsync(pred)` |
452
+ | `forallAsync(pred)` | `Async[Either[E, Boolean]]` | `Sink.forallAsync(pred)` |
453
+ | `foreachAsync(f)` | `Async[Either[E, Unit]]` | `runForeachAsync(f)` |
454
+ | `headAsync` | `Async[Either[E, Option[A]]]` | `Sink.head` |
455
+ | `lastAsync` | `Async[Either[E, Option[A]]]` | `Sink.last` |
456
+
457
+ `existsAsync`, `findAsync`, and `forallAsync` take an `A => Async[Boolean]` predicate; `foreachAsync` is an alias for `runForeachAsync`. `countAsync`, `headAsync`, and `lastAsync` take no callback and therefore reuse the ordinary callback-free sinks, which drain a synchronous or an asynchronous reader alike.
458
+
459
+ ### Blocking Twins Are JVM-only
460
+
461
+ Thirteen blocking members — `count`, `exists`, `find`, `forall`, `foreach`, `head`, `last`, `run`, `runCollect`, `runDrain`, `runFold`, `runForeach`, and `start` — live on the JVM only. Shared, cross-compiled sources cannot call them; they must use the `*Async` family, `startAsync`, and `useReaderAsync` instead.
462
+
463
+ For the full platform matrix, including which reader conversions and sink constructors exist on which platform, see [Platform Differences](./platform-differences.md).
464
+
465
+ ### Driving an `Async` From a JVM `main`
466
+
467
+ An `Async[Either[E, Z]]` is a value. Something has to drive it, and on the JVM that something is `.block`:
468
+
469
+ ```scala
470
+ import zio.blocks.streams._
471
+ import zio.blocks.chunk.Chunk
472
+ import zio.blocks.async._
473
+
474
+ val stream: Stream[String, Int] = Stream(1, 2, 3)
475
+
476
+ // At the edge of the world, and on the JVM only:
477
+ val result: Either[String, Chunk[Int]] = stream.runCollectAsync.block
478
+ ```
479
+
480
+ `.block` drives the effect to its value, parking the calling thread until it is ready; a failure is re-thrown as its cause. A ready value returns immediately without parking.
481
+
482
+ This is the edge-of-the-world idiom, and it is JVM-only. Two rules keep it honest:
483
+
484
+ 1. **`.block` belongs in `main`, or in a test, and nowhere else.** Never call it inside a stream callback or inside a `poll` — blocking the driver from within the loop it is driving deadlocks it.
485
+ 2. **Scala.js code must not use it at all.** JavaScript cannot block, so unless the effect has already completed synchronously, `.block` throws an `IllegalStateException` there. Cross-platform code should keep the `Async` and hand it to the host: convert it at the boundary (for example with `toFuture`) and let the runtime drive it.
486
+
487
+ Inside an `Async.async { ... }` block, use the direct-style `.await` instead, which extracts the value without blocking. See [Async](../../async.md) for both.
488
+
489
+ ### A Downstream Adopter: `Body`
490
+
491
+ `Body` in `http-model` is the clearest in-repo illustration of what the `*Async` convention looks like for a cross-platform consumer, because a body is exactly a `Stream[Nothing, Byte]` that someone eventually wants as bytes or as text.
492
+
493
+ Each blocking accessor has an asynchronous twin under the library-wide naming convention, five in all: `Body#toChunkAsync`, `Body#toArrayAsync`, `Body#asStringAsync`, `Body#asStringFromContentTypeAsync`, and `Body#textAsync`. The twins are the cross-platform API. The original accessors block, so they compile on Scala.js but throw `IllegalStateException` the moment the stream actually has to suspend — see [Why Blocking Terminals Are JVM-Only](./platform-differences.md#why-blocking-terminals-are-jvm-only). `Body#toChunk` is implemented in terms of the asynchronous one, taking a known-chunk fast path first and otherwise running `runCollectAsync` and blocking on the result.
494
+
495
+ Writing a cross-platform call site is the rename plus a change of result type:
496
+
497
+ ```scala
498
+ import zio.blocks.async._
499
+ import zio.blocks.chunk.Chunk
500
+ import zio.http.Body
501
+
502
+ // Blocking: works on the JVM; on Scala.js this throws once the stream suspends
503
+ def bytesBlocking(body: Body): Chunk[Byte] = body.toChunk
504
+
505
+ // JVM and Scala.js
506
+ def bytes(body: Body): Async[Chunk[Byte]] = body.toChunkAsync
507
+ def text(body: Body): Async[String] = body.textAsync
508
+ ```
509
+
510
+ [Body](../../http-model/model.md#body) documents the type itself, its constructors, and the rest of its accessors.
511
+
512
+ ## Manual Pull and Ownership
513
+
514
+ Sometimes you want the reader rather than a result — to interleave pulls with other work, or to hand the source to a protocol loop. Two terminals give you one, and they differ in exactly one respect: who is responsible for closing it.
515
+
516
+ ### `Stream#startAsync`
517
+
518
+ ```scala
519
+ def startAsync: Async[Reader.AsyncReader[A]]
520
+ ```
521
+
522
+ Materializes this stream as a caller-owned asynchronous reader. **Ownership transfers to the caller**, who must drive the reader and await `close()`. This is the one place in the API where the library does not close what it opened — if you forget the `close()`, finalizers registered by `ensuringAsync`, `deferAsync`, and `fromAcquireReleaseAsync` never run.
523
+
524
+ ### `Stream#useReaderAsync`
525
+
526
+ ```scala
527
+ def useReaderAsync[Z](f: Reader.AsyncReader[A] => Async[Z]): Async[Z]
528
+ ```
529
+
530
+ The scoped alternative. Ownership is **retained** by the library: the reader is passed to `f`, and its close is awaited on every outcome — success, typed failure, defect, and cancellation alike. Prefer it whenever the reader's lifetime is bounded by a single block of code.
531
+
532
+ Note the return type: `Async[Z]`, not `Async[Either[E, Z]]`. `useReaderAsync` hands you the reader, so whatever `f` produces is what you get back; stream errors surface through the reader's own pulls.
533
+
534
+ ### One Active Operation Per Reader
535
+
536
+ An `AsyncReader` is a single-consumer cursor, not a concurrent work queue. **At most one operation may be in flight at a time**: await the `Async` returned by a `read`, `readAll`, `skip`, or `close` before beginning the next one.
537
+
538
+ Readers are not thread-safe either. Overlapping pulls, or driving one reader from two threads without external synchronization, is outside the contract — the reader's internal position and lifecycle state are not defended against it, and the result is not specified.
539
+
540
+ ### Cleanup Failures
541
+
542
+ When cleanup fails on a path that has already failed, the cleanup failure is **attached to** the primary failure rather than replacing it. The original cause is what propagates; the cleanup cause is recorded as a suppressed exception on it.
543
+
544
+ This means a `Throwable` that reaches you from a failed `Async` may carry more than one story. Inspect `getSuppressed` before concluding that a close error was the only thing that went wrong.
545
+
546
+ ## Cancellation
547
+
548
+ Cancellation in this library is **cooperative, never preemptive**. Cancelling signals the in-flight operation; it does not interrupt a thread, and it never waits for an in-flight `poll` to return.
549
+
550
+ Two pieces of the API matter here:
551
+
552
+ - **`Pollable#cancel()`** signals cancellation of the currently pending operation, and reaches the active leaf operation rather than stopping at the driver. Implementations that own cancellable work override `cancel()` with an idempotent, non-blocking signal; implementations without such work inherit the no-op. A running driver invokes it only when cancellation wins the race against completion.
553
+ - **`Async.Running#cancel(onCleanupFailure: Throwable => Unit)`** cancels a run and reports a failure from its asynchronous cleanup. The reporter is retained only when this cancellation wins completion, and it is invoked at most once. Use it when a cleanup failure during cancellation must not be lost.
554
+
555
+ Cancellation closes an acquired reader and awaits its finalizer. `startAsync` is the deliberate exception, because it has already transferred that responsibility to its caller.
556
+
557
+ See [Async](../../async.md) for `Pollable`, `Cancelable`, and `Async.Running` themselves.
558
+
559
+ ## Resource Management
560
+
561
+ Two members carry resources through an asynchronous stream, and they compose with the ownership rules above.
562
+
563
+ `Stream.fromAcquireReleaseAsync` brackets a resource around a stream: one acquisition per materialization, and release awaited exactly once after completion, failure, early termination, or cancellation — including cancellation that arrives during acquisition, once the resource is obtained.
564
+
565
+ `Stream#ensuringAsync` registers a finalizer without a resource: awaited exactly once when the materialized stream closes, on every outcome.
566
+
567
+ Both are driven by the *close* of the materialized reader, which is what ties them to ownership:
568
+
569
+ - Under `runCollectAsync` and every other terminal, the library closes the reader, so both run without your involvement.
570
+ - Under `useReaderAsync`, the library still closes the reader, so both still run — on success and on failure alike.
571
+ - Under `startAsync`, **you** close the reader. Until you await `close()`, neither the release action nor the finalizer has run.
572
+
573
+ A failure inside a finalizer is a defect, and if the stream had already failed, that defect is attached to the primary failure rather than replacing it.
574
+
575
+ ## How This Is Verified
576
+
577
+ The asynchronous execution path is covered by three-way differential equivalence: for each generated program, the *ready* execution, the *suspended* execution, and an independent reference model must agree on the result and on the materialized reader kind. The generated campaign is frozen at a fixed seed, and a separate sweep runs every physical lane against every logical terminal. A production run records only demand and callback counts; failure provenance, throwable order, ownership transitions, outstanding resources, epoch, and suppressed exceptions are computed by the reference model, and a hand-written scenario asserts them against it — that scenario is also the only one carrying a non-empty fault script, injecting a throw and a late close.
578
+
579
+ :::note[On allocation figures]
580
+ Near-zero allocation numbers observed for this path are profiler noise, not a promise. An allocation profiler is required before claiming that any particular stream program allocates nothing.
581
+ :::
582
+
583
+ ## Running the Examples
584
+
585
+ Every example below is a runnable file in the `streams-examples` module. Clone the repository and run them with sbt:
586
+
587
+ ```bash
588
+ git clone https://github.com/zio/zio-blocks.git
589
+ cd zio-blocks
590
+ ```
591
+
592
+ ### Async Terminals and `.block`
593
+
594
+ Three terminals on one description, a fourth on a stream that fails with a typed error, the `Either` unwrapped on both branches, and `.block` confined to the edge of `main`:
595
+
596
+ ```scala title="streams-examples/src/main/scala/stream/StreamAsyncTerminalsExample.scala"
597
+ package stream
598
+
599
+ import zio.blocks.async._
600
+ import zio.blocks.chunk.Chunk
601
+ import zio.blocks.streams.Stream
602
+
603
+ /**
604
+ * The cross-platform asynchronous terminal family.
605
+ *
606
+ * Every `*Async` terminal returns `Async[Either[E, Z]]`: the typed error
607
+ * channel stays in the `Either`, and `Async`'s own `Throwable` channel is
608
+ * reserved for defects. `.block` drives the `Async` to its value and belongs
609
+ * only here, at the edge of a JVM `main`.
610
+ */
611
+ object StreamAsyncTerminalsExample {
612
+ final case class Reading(sensor: String, celsius: Int)
613
+
614
+ def main(args: Array[String]): Unit = {
615
+ val readings: Stream[String, Reading] =
616
+ Stream(Reading("north", 12), Reading("south", 7), Reading("east", 30))
617
+
618
+ // One description, three terminals. Nothing has run yet.
619
+ val collected: Async[Either[String, Chunk[Reading]]] = readings.runCollectAsync
620
+ val total: Async[Either[String, Long]] =
621
+ readings.runFoldAsync(0L)((sum, reading) => Async.succeed(sum + reading.celsius))
622
+ val first: Async[Either[String, Option[Reading]]] = readings.headAsync
623
+
624
+ // A stream that fails with a typed error surfaces it as `Left`, not as a
625
+ // failure of the outer `Async`.
626
+ val offline: Stream[String, Reading] = Stream.fail("west sensor is offline")
627
+ val failed: Async[Either[String, Chunk[Reading]]] = offline.runCollectAsync
628
+
629
+ // `.block` parks the calling thread until the value is ready. It is JVM
630
+ // only: on Scala.js it throws, so keep the `Async` and let the host drive it.
631
+ report("runCollectAsync", collected.block.map(_.length))
632
+ report("runFoldAsync", total.block)
633
+ report("headAsync", first.block.map(_.map(_.sensor)))
634
+ report("runCollectAsync (failing)", failed.block.map(_.length))
635
+ }
636
+
637
+ private def report[Z](label: String, result: Either[String, Z]): Unit =
638
+ result match {
639
+ case Right(value) => println(s"$label -> $value")
640
+ case Left(error) => println(s"$label -> typed error: $error")
641
+ }
642
+ }
643
+ ```
644
+
645
+ Run it with:
646
+
647
+ ```bash
648
+ sbt "streams-examples/runMain stream.StreamAsyncTerminalsExample"
649
+ ```
650
+
651
+ ### Ownership: `startAsync` Versus `useReaderAsync`
652
+
653
+ A finalizer that counts its own runs, proving that `useReaderAsync` closes on success *and* on failure, while `startAsync` closes only because the caller does it:
654
+
655
+ ```scala title="streams-examples/src/main/scala/stream/StreamAsyncOwnershipExample.scala"
656
+ package stream
657
+
658
+ import java.util.concurrent.atomic.AtomicInteger
659
+
660
+ import zio.blocks.async._
661
+ import zio.blocks.chunk.Chunk
662
+ import zio.blocks.streams.Stream
663
+ import zio.blocks.streams.io.Reader
664
+
665
+ /**
666
+ * Manual pull and close ownership.
667
+ *
668
+ * `startAsync` hands the reader to the caller, who must drive it and await
669
+ * `close()`. `useReaderAsync` keeps ownership and awaits `close()` on every
670
+ * outcome. An observable finalizer makes the difference visible.
671
+ */
672
+ object StreamAsyncOwnershipExample {
673
+ def main(args: Array[String]): Unit = {
674
+ // startAsync: ownership transfers. Forget the `close()` and the finalizer
675
+ // never runs.
676
+ val startFinalized = new AtomicInteger
677
+ val reader: Reader.AsyncReader[Int] = source(startFinalized).startAsync.block
678
+ val started: Chunk[Int] =
679
+ try reader.readAll[Int]().block
680
+ finally reader.close().block
681
+ println(s"startAsync -> $started, finalizer ran ${startFinalized.get()} time(s)")
682
+
683
+ // useReaderAsync on success: ownership is retained, close is awaited.
684
+ val useFinalized = new AtomicInteger
685
+ val used: Chunk[Int] = source(useFinalized).useReaderAsync(r => r.readAll[Int]()).block
686
+ println(s"useReaderAsync -> $used, finalizer ran ${useFinalized.get()} time(s)")
687
+
688
+ // useReaderAsync on failure: close is awaited just the same.
689
+ val failFinalized = new AtomicInteger
690
+ val failed: Either[Throwable, Chunk[Int]] =
691
+ source(failFinalized)
692
+ .useReaderAsync[Chunk[Int]](_ => Async.fail(new RuntimeException("consumer gave up")))
693
+ .either
694
+ .block
695
+ val message = failed.left.map(_.getMessage)
696
+ println(s"useReaderAsync (failing) -> $message, finalizer ran ${failFinalized.get()} time(s)")
697
+ }
698
+
699
+ /** A stream carrying an asynchronous finalizer that counts its own runs. */
700
+ private def source(finalized: AtomicInteger): Stream[Nothing, Int] =
701
+ Stream(1, 2, 3).ensuringAsync(Async.succeed {
702
+ finalized.incrementAndGet()
703
+ ()
704
+ })
705
+ }
706
+ ```
707
+
708
+ Run it with:
709
+
710
+ ```bash
711
+ sbt "streams-examples/runMain stream.StreamAsyncOwnershipExample"
712
+ ```
713
+
714
+ ### A Mixed Synchronous and Asynchronous Pipeline
715
+
716
+ A synchronous source, two asynchronous operators, and no change to any annotation:
717
+
718
+ ```scala title="streams-examples/src/main/scala/stream/StreamMixedKindExample.scala"
719
+ package stream
720
+
721
+ import zio.blocks.async._
722
+ import zio.blocks.streams.Stream
723
+ import zio.blocks.streams.io.Reader
724
+
725
+ /**
726
+ * Mixing a synchronous source with an asynchronous operator.
727
+ *
728
+ * Adding an asynchronous stage changes no static type and requires no
729
+ * annotation. The description is still `Stream[Nothing, Int]`; what changes is
730
+ * the reader it compiles to, and that happens once, at materialization.
731
+ */
732
+ object StreamMixedKindExample {
733
+ def main(args: Array[String]): Unit = {
734
+ // Entirely synchronous: a synchronous reader plus a synchronous callback.
735
+ val syncOnly: Stream[Nothing, Int] =
736
+ Stream.fromReader[Nothing, Int](Reader.fromIterable(List(1, 2, 3, 4, 5))).map(_ * 10)
737
+
738
+ // The same source with one asynchronous stage appended. Note that the
739
+ // annotation on the left is identical.
740
+ val mixed: Stream[Nothing, Int] =
741
+ syncOnly.filterAsync(i => Async.succeed(i > 20)).mapAsync(i => Async.succeed(i + 1))
742
+
743
+ println(s"syncOnly -> ${syncOnly.runCollectAsync.block}")
744
+ println(s"mixed -> ${mixed.runCollectAsync.block}")
745
+
746
+ // The JVM blocking terminal accepts the mixed graph too: the asynchronous
747
+ // reader is converted back at the final boundary.
748
+ println(s"mixed (blocking terminal) -> ${mixed.runCollect}")
749
+ }
750
+ }
751
+ ```
752
+
753
+ Run it with:
754
+
755
+ ```bash
756
+ sbt "streams-examples/runMain stream.StreamMixedKindExample"
757
+ ```
758
+
759
+ ### A Composed Asynchronous Pipeline
760
+
761
+ Real suspension through a `Completer`, `Stream.unwrap` feeding `filterAsync`, `mapAsync`, and `ensuringAsync`, 33,000 nested stages to demonstrate that the asynchronous path is stack-safe, and an assertion that the finalizer runs exactly once:
762
+
763
+ ```scala title="streams-examples/src/main/scala/stream/StreamAsyncOrderPipelineExample.scala"
764
+ /*
765
+ * Copyright 2024-2026 John A. De Goes and the ZIO Contributors
766
+ *
767
+ * Licensed under the Apache License, Version 2.0 (the "License");
768
+ * you may not use this file except in compliance with the License.
769
+ */
770
+ package stream
771
+
772
+ import java.util.concurrent.atomic.AtomicInteger
773
+
774
+ import zio.blocks.async._
775
+ import zio.blocks.chunk.Chunk
776
+ import zio.blocks.streams.Stream
777
+
778
+ /** A composed asynchronous order-validation and audit pipeline. */
779
+ object StreamAsyncOrderPipelineExample {
780
+ final case class Order(id: Int, amount: Int)
781
+
782
+ def deferred[A](value: => A): Async[A] = {
783
+ val result = new Completer[A]
784
+ val thread = new Thread(() => result.succeed(value))
785
+ thread.start()
786
+ result
787
+ }
788
+
789
+ def main(args: Array[String]): Unit = {
790
+ val finalized = new AtomicInteger
791
+ val validated: Stream[Nothing, Order] = Stream
792
+ .unwrap(deferred(Stream(Order(1, 20), Order(2, -1), Order(3, 40))))
793
+ .filterAsync(order => Async.succeed(order.amount > 0))
794
+ .mapAsync(order => deferred(order.copy(amount = order.amount + 5)))
795
+ .ensuringAsync(Async.succeed { finalized.incrementAndGet(); () })
796
+
797
+ // Real applications commonly assemble reusable generated middleware. Its
798
+ // finite depth must not change the meaning of an identity transformation.
799
+ val withMiddleware =
800
+ (0 until 33000).foldLeft(validated)((orders, _) => orders.map(identity))
801
+
802
+ val result = withMiddleware.runCollectAsync.block
803
+ require(result == Right(Chunk(Order(1, 25), Order(3, 45))), s"unexpected result: $result")
804
+ require(finalized.get() == 1, s"finalizer ran ${finalized.get()} times")
805
+ println(result)
806
+ }
807
+ }
808
+ ```
809
+
810
+ Run it with:
811
+
812
+ ```bash
813
+ sbt "streams-examples/runMain stream.StreamAsyncOrderPipelineExample"
814
+ ```
815
+
816
+ ## See Also
817
+
818
+ - [Reader](../primitives/reader.md) — the `SyncReader` / `AsyncReader` union, custom reader implementations, mixed-kind composition, and the JVM NIO and Scala.js `ReadableStream` adapters
819
+ - [Bounded Concurrency](../core/stream.md#bounded-concurrency) — `mapPar`, `mapParAsync`, `mergeAll`, and `flatMapPar`
820
+ - [Platform Differences](./platform-differences.md) — what exists on the JVM, what exists on Scala.js, and what throws
821
+ - [Async](../../async.md) — `Async[A]`, `Pollable`, `Completer`, `Async.Running`, and cancellation
822
+ - [Zero-Boxing Optimization](./zero-boxing.md) — primitive lanes, and why async is lane-aware rather than end-to-end allocation-free