@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.
- package/adr/2026-07-18-data-migration.md +123 -0
- package/guides/async-getting-started.md +687 -0
- package/guides/compile-time-resource-safety-with-scope.md +21 -16
- package/guides/getting-started-with-mux.md +1395 -0
- package/guides/query-dsl-extending.md +161 -102
- package/guides/query-dsl-fluent-builder.md +217 -157
- package/guides/query-dsl-reified-optics.md +12 -10
- package/guides/query-dsl-sql.md +640 -165
- package/guides/sql-checked-interpolation.md +173 -0
- package/guides/sql-transactions.md +286 -0
- package/guides/telemetry-guide.md +1130 -0
- package/guides/zio-schema-migration.md +29 -22
- package/index.md +248 -389
- package/package.json +1 -1
- package/plans/config-follow-up-prs.md +188 -0
- package/plans/config-pr-assessment-roadmap.md +310 -0
- package/reference/MuxDataFlow.jsx +250 -0
- package/reference/async.md +1499 -0
- package/reference/chunk.md +3533 -308
- package/reference/codegen/case-class.md +436 -0
- package/reference/codegen/emitter-config.md +383 -0
- package/reference/codegen/examples.md +664 -0
- package/reference/codegen/field.md +316 -0
- package/reference/codegen/index.md +317 -0
- package/reference/codegen/scala-emitter.md +392 -0
- package/reference/codegen/scala-file.md +276 -0
- package/reference/codegen/sealed-trait.md +408 -0
- package/reference/codegen/type-definition.md +340 -0
- package/reference/codegen/type-ref.md +201 -0
- package/reference/combinators.md +347 -117
- package/reference/config/config-decoder.md +460 -0
- package/reference/config/config-source.md +489 -0
- package/reference/config/errors.md +278 -0
- package/reference/config/flags.md +369 -0
- package/reference/config/formats.md +314 -0
- package/reference/config/index.md +304 -0
- package/reference/config/rollout.md +336 -0
- package/reference/context.md +9 -52
- package/reference/data-migration.md +269 -0
- package/reference/datastar/attributes.md +302 -0
- package/reference/datastar/events.md +234 -0
- package/reference/datastar/index.md +256 -0
- package/reference/datastar/signals.md +230 -0
- package/reference/datastar/sse.md +295 -0
- package/reference/datastar.md +346 -0
- package/reference/docs.md +1461 -345
- package/reference/endpoint/auth-type.md +146 -0
- package/reference/endpoint/bulk-creation.md +96 -0
- package/reference/endpoint/endpoint.md +297 -0
- package/reference/endpoint/http-codec.md +249 -0
- package/reference/endpoint/index.md +745 -0
- package/reference/endpoint/path-codec.md +225 -0
- package/reference/endpoint/route-pattern.md +194 -0
- package/reference/endpoint/route-tree.md +111 -0
- package/reference/endpoint/segment-codec.md +199 -0
- package/reference/html.md +1424 -0
- package/reference/htmx/attribute-values.md +359 -0
- package/reference/htmx/hx-encoding.md +111 -0
- package/reference/htmx/hx-params.md +204 -0
- package/reference/htmx/hx-swap.md +276 -0
- package/reference/htmx/hx-sync.md +251 -0
- package/reference/htmx/hx-target.md +314 -0
- package/reference/htmx/hx-trigger.md +457 -0
- package/reference/htmx/hx-url-update.md +239 -0
- package/reference/htmx/index.md +807 -0
- package/reference/htmx/response-headers.md +240 -0
- package/reference/http-model/headers.md +735 -0
- package/reference/http-model/index.md +49 -0
- package/reference/http-model/model.md +1517 -0
- package/reference/http-model/schema-codecs.md +522 -0
- package/reference/http-model/schema.md +750 -0
- package/reference/http-model/server-sent-event.md +341 -0
- package/reference/jwt.md +195 -0
- package/reference/maybe.md +943 -0
- package/reference/media-type.md +2 -2
- package/reference/mux.md +254 -0
- package/reference/mux.mdx +828 -0
- package/reference/openapi.md +1351 -0
- package/reference/projection.md +654 -0
- package/reference/resource-management/defer-handle.md +1 -1
- package/reference/resource-management/resource.md +31 -98
- package/reference/resource-management/scope.md +28 -220
- package/reference/resource-management/wire.md +5 -55
- package/reference/ringbuffer/MpmcDiagram.jsx +717 -0
- package/reference/ringbuffer/MpscDiagram.jsx +618 -0
- package/reference/ringbuffer/SpmcDiagram.jsx +680 -0
- package/reference/ringbuffer/SpscDiagram.jsx +677 -0
- package/reference/ringbuffer/advanced.mdx +109 -0
- package/reference/ringbuffer/index.mdx +145 -0
- package/reference/ringbuffer/mpmc.mdx +185 -0
- package/reference/ringbuffer/mpsc.mdx +164 -0
- package/reference/ringbuffer/spmc.mdx +108 -0
- package/reference/ringbuffer/spsc.mdx +416 -0
- package/reference/{allows.md → schema/allows.md} +4 -100
- package/reference/{binding-resolver.md → schema/binding-resolver.md} +1 -1
- package/reference/{binding.md → schema/binding.md} +3 -4
- package/reference/schema/built-in-codecs/avro.md +451 -0
- package/reference/schema/built-in-codecs/bson.md +510 -0
- package/reference/schema/built-in-codecs/csv.md +564 -0
- package/reference/schema/built-in-codecs/index.md +77 -0
- package/reference/schema/built-in-codecs/json/index.md +295 -0
- package/reference/schema/built-in-codecs/json/json-config.md +217 -0
- package/reference/{json-patch.md → schema/built-in-codecs/json/json-patch.md} +5 -5
- package/reference/{json-schema.md → schema/built-in-codecs/json/json-schema.md} +14 -47
- package/reference/schema/built-in-codecs/json/json-selection.md +322 -0
- package/reference/{json.md → schema/built-in-codecs/json/json.md} +32 -64
- package/reference/schema/built-in-codecs/messagepack.md +508 -0
- package/reference/schema/built-in-codecs/thrift.md +433 -0
- package/reference/schema/built-in-codecs/toon.md +1078 -0
- package/reference/{xml.md → schema/built-in-codecs/xml.md} +13 -9
- package/reference/schema/built-in-codecs/yaml.md +552 -0
- package/reference/{codec.md → schema/codec.md} +11 -11
- package/reference/{dynamic-optic.md → schema/dynamic-optic.md} +196 -5
- package/reference/{dynamic-schema.md → schema/dynamic-schema.md} +8 -8
- package/reference/schema/format.md +92 -0
- package/reference/schema/index.md +52 -0
- package/reference/schema/migration.md +297 -0
- package/reference/{modifier.md → schema/modifier.md} +58 -7
- package/reference/{optics.md → schema/optics.md} +2 -2
- package/reference/{patch.md → schema/patch.md} +1 -1
- package/{path-interpolator.md → reference/schema/path-interpolator.md} +167 -72
- package/reference/schema/reflect-transformer.md +140 -0
- package/reference/{schema-evolution → schema/schema-evolution}/as.md +8 -8
- package/reference/{schema-evolution → schema/schema-evolution}/index.md +2 -2
- package/reference/{schema-evolution → schema/schema-evolution}/into.md +8 -8
- package/reference/{schema-expr.md → schema/schema-expr.md} +110 -175
- package/reference/schema/schema-search.md +263 -0
- package/reference/{schema.md → schema/schema.md} +22 -2
- package/reference/{structural-types.md → schema/structural-types.md} +1 -1
- package/reference/{type-class-derivation.md → schema/type-class-derivation.md} +63 -1
- package/reference/smithy.md +1032 -0
- package/reference/sql/db-codec-deriver.md +71 -0
- package/reference/sql/db-codec.md +687 -0
- package/reference/sql/db-con.md +271 -0
- package/reference/sql/db-connection.md +153 -0
- package/reference/sql/db-param-writer.md +77 -0
- package/reference/sql/db-param.md +66 -0
- package/reference/sql/db-result-reader.md +148 -0
- package/reference/sql/db-tx.md +114 -0
- package/reference/sql/db-value.md +41 -0
- package/reference/sql/ddl.md +85 -0
- package/reference/sql/frag.md +288 -0
- package/reference/sql/index.md +341 -0
- package/reference/sql/repo.md +600 -0
- package/reference/sql/sql-dialect.md +73 -0
- package/reference/sql/sql-logger.md +62 -0
- package/reference/sql/sql-name-mapper.md +70 -0
- package/reference/sql/table-metadata.md +134 -0
- package/reference/sql/table.md +448 -0
- package/reference/sql/transactor-zio.md +399 -0
- package/reference/sql/transactor.md +363 -0
- package/reference/sql-zio.md +112 -0
- package/reference/streams/core/index.md +32 -0
- package/reference/streams/core/pipeline.md +854 -0
- package/reference/streams/core/sink.md +1404 -0
- package/reference/streams/core/stream.md +3236 -0
- package/reference/streams/execution-and-compatibility/async-execution.md +822 -0
- package/reference/streams/execution-and-compatibility/index.md +35 -0
- package/reference/streams/execution-and-compatibility/platform-differences.md +297 -0
- package/reference/streams/execution-and-compatibility/scala-2-compatibility.md +88 -0
- package/reference/streams/execution-and-compatibility/zero-boxing.md +393 -0
- package/reference/streams/index.md +726 -0
- package/reference/streams/primitives/index.md +30 -0
- package/reference/streams/primitives/reader.md +1992 -0
- package/reference/streams/primitives/writer.md +1201 -0
- package/reference/telemetry/common/any-value.md +90 -0
- package/reference/telemetry/common/attribute-key.md +87 -0
- package/reference/telemetry/common/attributes.md +118 -0
- package/reference/telemetry/common/index.md +39 -0
- package/reference/telemetry/common/instrumentation-scope.md +24 -0
- package/reference/telemetry/common/resource.md +34 -0
- package/reference/telemetry/index.md +311 -0
- package/reference/telemetry/logging/index.md +197 -0
- package/reference/telemetry/logging/log-enrichment.md +72 -0
- package/reference/telemetry/logging/log-formatter.md +100 -0
- package/reference/telemetry/logging/log-record-processor.md +56 -0
- package/reference/telemetry/logging/log-record.md +44 -0
- package/reference/telemetry/logging/log-writer.md +64 -0
- package/reference/telemetry/logging/logger-provider.md +142 -0
- package/reference/telemetry/logging/logger.md +83 -0
- package/reference/telemetry/logging/severity.md +62 -0
- package/reference/telemetry/metrics/index.md +150 -0
- package/reference/telemetry/metrics/instruments.md +183 -0
- package/reference/telemetry/metrics/labeled-instruments.md +74 -0
- package/reference/telemetry/metrics/meter-provider.md +76 -0
- package/reference/telemetry/metrics/meter.md +98 -0
- package/reference/telemetry/metrics/metric-data.md +57 -0
- package/reference/telemetry/otel/custom-exporter.md +216 -0
- package/reference/telemetry/otel/index.md +212 -0
- package/reference/telemetry/tracing/index.md +155 -0
- package/reference/telemetry/tracing/sampler.md +89 -0
- package/reference/telemetry/tracing/span-builder.md +57 -0
- package/reference/telemetry/tracing/span-context.md +39 -0
- package/reference/telemetry/tracing/span-data.md +32 -0
- package/reference/telemetry/tracing/span-kind.md +55 -0
- package/reference/telemetry/tracing/span-processor.md +53 -0
- package/reference/telemetry/tracing/span-status.md +47 -0
- package/reference/telemetry/tracing/span.md +117 -0
- package/reference/telemetry/tracing/tracer-provider.md +91 -0
- package/reference/telemetry/tracing/tracer.md +52 -0
- package/reference/typeid.md +5 -83
- package/sidebars.js +376 -43
- package/undocumented-report.md +528 -270
- package/reference/formats.md +0 -694
- package/reference/http-model.md +0 -1716
- package/reference/streams.md +0 -989
- package/ringbuffer.md +0 -249
- /package/reference/{json-differ.md → schema/built-in-codecs/json/json-differ.md} +0 -0
- /package/reference/{dynamic-value.md → schema/dynamic-value.md} +0 -0
- /package/reference/{lazy.md → schema/lazy.md} +0 -0
- /package/reference/{reflect.md → schema/reflect.md} +0 -0
- /package/reference/{registers.md → schema/registers.md} +0 -0
- /package/reference/{schema-error.md → schema/schema-error.md} +0 -0
- /package/reference/{syntax.md → schema/syntax.md} +0 -0
- /package/reference/{validation.md → schema/validation.md} +0 -0
|
@@ -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.55"
|
|
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
|