@zio.dev/zio-blocks 0.0.51 → 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 +6 -0
- package/guides/getting-started-with-mux.md +0 -112
- package/guides/query-dsl-extending.md +1 -1
- package/guides/query-dsl-fluent-builder.md +1 -1
- package/guides/query-dsl-reified-optics.md +1 -1
- package/guides/query-dsl-sql.md +395 -1
- package/guides/sql-checked-interpolation.md +173 -0
- package/guides/sql-transactions.md +286 -0
- package/guides/telemetry-guide.md +131 -70
- package/guides/zio-schema-migration.md +6 -6
- package/index.md +200 -583
- package/package.json +1 -1
- package/reference/async.md +1379 -531
- package/reference/chunk.md +3 -3
- package/reference/codegen/index.md +1 -1
- package/reference/combinators.md +4 -4
- 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 +6 -49
- 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 +2 -2
- package/reference/docs.md +2 -2
- package/reference/endpoint/bulk-creation.md +96 -0
- package/reference/endpoint/index.md +9 -89
- package/reference/endpoint/path-codec.md +12 -24
- package/reference/endpoint/route-pattern.md +4 -6
- package/reference/endpoint/segment-codec.md +19 -32
- package/reference/html.md +313 -9
- package/reference/htmx/index.md +4 -52
- package/reference/htmx/response-headers.md +240 -0
- package/reference/http-model/headers.md +735 -0
- package/reference/http-model/index.md +3 -1
- package/reference/http-model/model.md +107 -71
- package/reference/http-model/schema-codecs.md +522 -0
- package/reference/http-model/schema.md +6 -3
- package/reference/http-model/server-sent-event.md +341 -0
- package/reference/jwt.md +195 -0
- package/reference/maybe.md +128 -11
- package/reference/media-type.md +2 -2
- package/reference/mux.md +254 -0
- package/reference/mux.mdx +7 -2
- package/reference/openapi.md +3 -3
- package/reference/projection.md +654 -0
- package/reference/resource-management/resource.md +2 -98
- package/reference/resource-management/scope.md +1 -209
- package/reference/resource-management/wire.md +4 -50
- package/reference/ringbuffer/advanced.mdx +1 -1
- package/reference/ringbuffer/index.mdx +3 -3
- package/reference/ringbuffer/mpmc.mdx +38 -4
- package/reference/ringbuffer/mpsc.mdx +36 -4
- package/reference/ringbuffer/spmc.mdx +1 -1
- package/reference/ringbuffer/spsc.mdx +87 -15
- package/reference/schema/allows.md +0 -96
- package/reference/schema/binding.md +2 -2
- package/reference/schema/built-in-codecs/avro.md +2 -2
- package/reference/schema/built-in-codecs/bson.md +50 -20
- package/reference/schema/built-in-codecs/csv.md +2 -2
- package/reference/schema/built-in-codecs/index.md +3 -3
- package/reference/schema/built-in-codecs/json/index.md +2 -2
- package/reference/schema/built-in-codecs/messagepack.md +3 -3
- package/reference/schema/built-in-codecs/thrift.md +2 -2
- package/reference/schema/built-in-codecs/toon.md +3 -3
- package/reference/schema/built-in-codecs/yaml.md +2 -2
- package/reference/schema/codec.md +11 -11
- package/reference/schema/dynamic-optic.md +48 -3
- package/reference/schema/dynamic-schema.md +3 -3
- package/reference/schema/index.md +2 -0
- package/reference/schema/path-interpolator.md +2 -0
- package/reference/schema/reflect-transformer.md +140 -0
- package/reference/schema/schema-evolution/as.md +4 -4
- package/reference/schema/schema-evolution/into.md +2 -2
- package/reference/schema/schema-expr.md +2 -2
- package/reference/schema/schema-search.md +263 -0
- package/reference/schema/schema.md +10 -2
- package/reference/schema/type-class-derivation.md +1 -1
- package/reference/smithy.md +502 -3
- package/reference/sql/db-codec-deriver.md +3 -3
- package/reference/sql/db-codec.md +22 -22
- package/reference/sql/db-con.md +4 -4
- package/reference/sql/db-connection.md +1 -1
- package/reference/sql/db-param.md +1 -1
- package/reference/sql/db-result-reader.md +4 -2
- package/reference/sql/db-tx.md +46 -14
- package/reference/sql/ddl.md +1 -1
- package/reference/sql/frag.md +44 -10
- package/reference/sql/index.md +7 -7
- package/reference/sql/repo.md +15 -15
- package/reference/sql/sql-dialect.md +1 -1
- package/reference/sql/sql-logger.md +1 -1
- package/reference/sql/sql-name-mapper.md +3 -3
- package/reference/sql/table-metadata.md +3 -3
- package/reference/sql/table.md +10 -10
- package/reference/sql/transactor-zio.md +1 -1
- package/reference/sql/transactor.md +21 -11
- package/reference/sql-zio.md +1 -1
- package/reference/streams/core/index.md +32 -0
- package/reference/streams/{pipeline.md → core/pipeline.md} +210 -74
- package/reference/streams/{sink.md → core/sink.md} +331 -353
- package/reference/streams/{stream.md → core/stream.md} +919 -209
- 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 +140 -67
- package/reference/streams/primitives/index.md +30 -0
- package/reference/streams/primitives/reader.md +1992 -0
- package/reference/streams/{writer.md → primitives/writer.md} +254 -98
- 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 +0 -64
- package/sidebars.js +150 -12
- package/undocumented-report.md +528 -270
- package/reference/config.md +0 -158
- package/reference/streams/concurrent-operators.md +0 -106
- package/reference/streams/reader.md +0 -1284
- package/reference/streams/scala-2-compatibility.md +0 -55
- package/reference/streams/zero-boxing.md +0 -275
- package/reference/telemetry.md +0 -693
|
@@ -1,23 +1,36 @@
|
|
|
1
1
|
---
|
|
2
2
|
id: sink
|
|
3
3
|
title: "Sink"
|
|
4
|
+
sidebar_label: "Sink"
|
|
5
|
+
description: "The stream consumer: its dual synchronous and asynchronous drains, the three construction factories, and the async aggregation and transformation combinators."
|
|
6
|
+
keywords:
|
|
7
|
+
- "Stream Consumer"
|
|
8
|
+
- "Dual Drain"
|
|
9
|
+
- "Async Sinks"
|
|
10
|
+
- "Custom Sink Factories"
|
|
11
|
+
- "Sink"
|
|
4
12
|
---
|
|
5
13
|
|
|
6
|
-
`Sink[+E, -A, +Z]` is a **stream consumer** that reads elements of type `A` and produces a result of type `Z`, potentially failing with an error of type `E`.
|
|
14
|
+
`Sink[+E, -A, +Z]` is a **stream consumer** that reads elements of type `A` and produces a result of type `Z`, potentially failing with an error of type `E`. Every sink has synchronous and asynchronous drain paths: use cross-platform `Stream.runAsync`, or the JVM-only blocking `Stream.run`.
|
|
7
15
|
|
|
8
16
|
`Sink`:
|
|
9
17
|
- Is covariant in `E` (error) and `Z` (result) — these are outputs
|
|
10
18
|
- Is contravariant in `A` (input) — a `Sink[_, Any, _]` accepts any element type
|
|
11
|
-
-
|
|
19
|
+
- Dispatches on the reader's physical primitive lane, so element pulls avoid boxing
|
|
12
20
|
- Provides `Sink#contramap`, `Sink#map`, and `Sink#mapError` for composable transformations
|
|
13
21
|
|
|
22
|
+
Built-in sinks implement both drain paths. Asynchronous constructors include `existsAsync`, `findAsync`, `forallAsync`, `foreachAsync`, and `foldLeftAsync`; callbacks are sequential and back-pressured. The result/error combinators `contramapAsync`, `mapAsync`, and `mapErrorAsync` likewise select the native async drain when used by `runAsync`. On the JVM, driving such a sink through a plain blocking terminal blocks while awaiting its async drain; Scala.js has no blocking terminal.
|
|
23
|
+
|
|
14
24
|
Here is the structural shape of the `Sink` type:
|
|
15
25
|
|
|
16
26
|
```scala
|
|
17
27
|
abstract class Sink[+E, -A, +Z] {
|
|
18
|
-
def contramap[A2](g: A2 =>
|
|
28
|
+
def contramap[A0 <: A, A2](g: A2 => A0)(implicit jtA0: JvmType.Infer[A0]): Sink[E, A2, Z]
|
|
29
|
+
def contramapAsync[A0 <: A, A2](g: A2 => Async[A0])(implicit jtA0: JvmType.Infer[A0]): Sink[E, A2, Z]
|
|
19
30
|
def map[Z2](f: Z => Z2): Sink[E, A, Z2]
|
|
20
|
-
def
|
|
31
|
+
def mapAsync[Z2](f: Z => Async[Z2]): Sink[E, A, Z2]
|
|
32
|
+
def mapError[E2](f: E => E2)(implicit isNothing: Sink.IsNothing[E]): Sink[E2, A, Z]
|
|
33
|
+
def mapErrorAsync[E2](f: E => Async[E2])(implicit isNothing: Sink.IsNothing[E]): Sink[E2, A, Z]
|
|
21
34
|
}
|
|
22
35
|
```
|
|
23
36
|
|
|
@@ -40,7 +53,83 @@ When you call `stream.run(sink)`:
|
|
|
40
53
|
2. The sink's internal `Sink#drain` method pulls elements in a tight loop until end-of-stream
|
|
41
54
|
3. On success, the result wraps in `Right(z)`
|
|
42
55
|
4. Typed errors (`E`) surface as `Left(e)`, while untyped defects propagate as exceptions
|
|
43
|
-
5. The reader's `close()` runs
|
|
56
|
+
5. The reader's `close()` runs after the drain whether it succeeded or threw, and a failure from `close()` is attached to the drain's own error rather than replacing it, ensuring resource safety
|
|
57
|
+
|
|
58
|
+
### Physical Input Lanes and Ownership
|
|
59
|
+
|
|
60
|
+
A sink consumes either kind of reader. `Reader` is not one type with a mode flag: it splits into `Reader.SyncReader[A]`, whose pulls return values, and `Reader.AsyncReader[A]`, whose pulls return [`Async`](../../async.md) values. [The reader union](../primitives/reader.md#the-reader-union) describes the split from the reader's side. From the sink's side, the consequence is that every sink is drained through one of two entry points, and the terminal decides which — the next section covers that choice.
|
|
61
|
+
|
|
62
|
+
Whichever kind arrives, a sink discovers its input representation from the materialized reader's `jvmType`, not from the sink's contravariant static input type. Generic sinks dispatch once per drain and then pull `Boolean`, `Byte`, `Char`, `Short`, `Int`, `Long`, `Float`, and `Double` through library-internal physical pulls. The synchronous drain uses `readBooleanPhysical`, `readBytePhysical`, `readCharPhysical`, `readShortPhysical`, `readIntPhysical`, `readLongsPhysical`, `readFloatPhysical`, and `readDoublesPhysical`, respectively; the asynchronous drain differs on two lanes, pulling `Byte` through `readBytesPhysical` and `Float` through `readFloatsPhysical`. These are the internal counterparts of the public `readBoolean`, `readByte`, `readChar`, `readShort`, `readInt`, `readLongs`, `readFloat`, and `readDoubles`, which a generic sink cannot call because all but `readByte` demand `Elem <:< X` evidence it has no way to supply. Reference inputs use generic `read`. In particular, the four small primitive lanes do not share the `Int` pull.
|
|
63
|
+
|
|
64
|
+
`Long` and `Double` use one-element primitive arrays with `readLongsPhysical(scratch, 0, 1)` and `readDoublesPhysical(scratch, 0, 1)`, and on the asynchronous drain `Byte` and `Float` use one-element arrays in the same way. The returned count carries end-of-stream status out of band, so every `Long` value and every raw `Double` bit pattern—including NaN payloads—remains valid data. The scratch storage is allocated once per drain, not once per element.
|
|
65
|
+
|
|
66
|
+
The reader owns this physical representation. Consequently, widening a specialized stream (for example, from `Stream[Nothing, Int]` to `Stream[Nothing, AnyVal]`) does not erase its `Int` lane before it reaches a sink. A sink adapter preserves the wrapped reader's lane and forwards every exact pull method. It also preserves ownership: the run terminal owns and closes the materialized reader; a sink or sink adapter does not independently close it. External destinations supplied to I/O sinks remain caller-owned unless a constructor explicitly says otherwise.
|
|
67
|
+
|
|
68
|
+
## Consuming Asynchronous Readers
|
|
69
|
+
|
|
70
|
+
Every sink carries two drains, one per reader kind:
|
|
71
|
+
|
|
72
|
+
```scala
|
|
73
|
+
abstract class Sink[+E, -A, +Z] {
|
|
74
|
+
private[streams] def drain(reader: Reader.SyncReader[_]): Z
|
|
75
|
+
private[streams] def drain(reader: Reader.AsyncReader[_]): Async[Z]
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
They are an overload on the argument type, not a runtime branch inside one method, so the selection is a static one made where the reader is known: at the terminal. Both are `private[streams]`, which means you never call either yourself. The terminal you already chose called one of them for you:
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
┌────────────────────────────────────────────────────────────────────┐
|
|
83
|
+
│ Stream#run (JVM only) Stream#runAsync (JVM and JS) │
|
|
84
|
+
│ │ materializes │ materializes │
|
|
85
|
+
│ ▼ ▼ │
|
|
86
|
+
│ Reader.SyncReader[A] Reader.AsyncReader[A] │
|
|
87
|
+
│ │ is handed to │ is handed to │
|
|
88
|
+
│ ▼ ▼ │
|
|
89
|
+
│ drain(SyncReader): Z drain(AsyncReader): Async[Z] │
|
|
90
|
+
│ │ returns │ returns │
|
|
91
|
+
│ ▼ ▼ │
|
|
92
|
+
│ Either[E, Z] Async[Either[E, Z]] │
|
|
93
|
+
└────────────────────────────────────────────────────────────────────┘
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
The asynchronous drain is a native one, not an adapter: `Stream#runAsync` hands the sink a `Reader.AsyncReader` and never converts it to a blocking reader first, so no thread is parked waiting for a pull. [Async Terminals](../execution-and-compatibility/async-execution.md#async-terminals) covers the terminal family that takes this path, and [The `Async[Either[E, Z]]` convention](../execution-and-compatibility/async-execution.md#the-asynceithere-z-convention) explains why the typed error stays inside the `Either`.
|
|
97
|
+
|
|
98
|
+
Every built-in sink implements both drains, so nothing on this page is available on only one path. A third member, `drainAsync(reader: Reader.SyncReader[_]): Async[Z]`, is likewise `private[streams]`; it presents a synchronous reader through the asynchronous contract by calling `reader.toAsync`, and exists for internal stages that must expose an asynchronous face over synchronous input.
|
|
99
|
+
|
|
100
|
+
### Choosing a Factory
|
|
101
|
+
|
|
102
|
+
Because a `Sink` has two abstract drains and both are `private[streams]`, code outside `zio.blocks.streams` cannot implement them — subclassing `Sink` directly is not a supported route, and the compiler will not let you complete the class. Build custom sinks from one of the three factories instead:
|
|
103
|
+
|
|
104
|
+
```scala
|
|
105
|
+
object Sink {
|
|
106
|
+
// JVM and Scala.js
|
|
107
|
+
def createAsync[E, A, Z](f: Reader.AsyncReader[A] => Async[Z]): Sink[E, A, Z]
|
|
108
|
+
def createBoth[E, A, Z](
|
|
109
|
+
sync: Reader.SyncReader[A] => Z,
|
|
110
|
+
async: Reader.AsyncReader[A] => Async[Z]
|
|
111
|
+
): Sink[E, A, Z]
|
|
112
|
+
|
|
113
|
+
// JVM only — absent from the Scala.js companion entirely
|
|
114
|
+
def create[E, A, Z](f: Reader.SyncReader[A] => Z): Sink[E, A, Z]
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Each factory fills in the drain you did not write:
|
|
119
|
+
|
|
120
|
+
| Factory | Callbacks you write | Platforms | Reach for it when |
|
|
121
|
+
|--------------------|-----------------------------------|------------------|----------------------------------------------------------------------|
|
|
122
|
+
| `Sink.createAsync` | one asynchronous | JVM and Scala.js | one implementation is enough, and awaiting per element is acceptable |
|
|
123
|
+
| `Sink.createBoth` | one synchronous, one asynchronous | JVM and Scala.js | the synchronous path can be cheaper than the asynchronous one |
|
|
124
|
+
| `Sink.create` | one synchronous | JVM only | the sink is JVM-only anyway and its callback wants a blocking reader |
|
|
125
|
+
|
|
126
|
+
`Sink.createAsync` writes the synchronous drain for you by awaiting the asynchronous one at a JVM blocking terminal. `Sink.create` writes the asynchronous drain for you by deferring the blocking callback into a cancellable effect, where cancellation closes the reader — the callback still runs inline on whichever thread drives that effect, so an asynchronous terminal driving a `Sink.create` sink blocks that thread for the duration of the callback. `Sink.createBoth` writes neither: it takes two independent callbacks and the terminal runs exactly one, so the two must agree on how much input they consume and what they produce — nothing compares their results.
|
|
127
|
+
|
|
128
|
+
:::warning[`Sink.create` does not exist on Scala.js]
|
|
129
|
+
It is declared in the JVM copy of `SinkCompanionPlatformSpecific` and simply absent from the Scala.js copy, so cross-built code that calls it fails to compile for the JS target rather than failing at runtime. [`Sink.create` and Custom Sinks](../execution-and-compatibility/platform-differences.md#sinkcreate-and-custom-sinks) states the platform rule.
|
|
130
|
+
:::
|
|
131
|
+
|
|
132
|
+
[Custom sink factories](#custom-sink-factories) shows a worked callback against the reader protocol.
|
|
44
133
|
|
|
45
134
|
## Predefined Sinks
|
|
46
135
|
|
|
@@ -128,9 +217,9 @@ Sinks are created using factory methods on the companion object. These methods f
|
|
|
128
217
|
|
|
129
218
|
Gather elements into collections:
|
|
130
219
|
|
|
131
|
-
#### `Sink.collectAll[A]` — Collect
|
|
220
|
+
#### `Sink.collectAll[A]` — Collect Into a Chunk
|
|
132
221
|
|
|
133
|
-
Collects all elements into a `Chunk[A]
|
|
222
|
+
Collects all elements into a [`Chunk[A]`](../../chunk.md):
|
|
134
223
|
|
|
135
224
|
```scala
|
|
136
225
|
object Sink {
|
|
@@ -176,7 +265,7 @@ Folds all elements using an accumulator function, starting from initial value `z
|
|
|
176
265
|
|
|
177
266
|
```scala
|
|
178
267
|
object Sink {
|
|
179
|
-
def foldLeft[A, Z](z: Z)(f: (Z, A) => Z): Sink[Nothing, A, Z]
|
|
268
|
+
def foldLeft[A, Z](z: Z)(f: (Z, A) => Z)(implicit jtZ: JvmType.Infer[Z]): Sink[Nothing, A, Z]
|
|
180
269
|
}
|
|
181
270
|
```
|
|
182
271
|
|
|
@@ -319,6 +408,49 @@ val result = Stream(1, 2, 3).run(Sink.foreach[Int](x => println(s"Got: $x")))
|
|
|
319
408
|
// result: Either[Nothing, Unit] = Right(())
|
|
320
409
|
```
|
|
321
410
|
|
|
411
|
+
### Async Aggregation and Search
|
|
412
|
+
|
|
413
|
+
Five of the aggregation, search, and effectful sinks have an `*Async` twin whose callback returns `Async` instead of a value. The twin consumes the same input and returns the same result type; only the callback's shape differs:
|
|
414
|
+
|
|
415
|
+
```scala
|
|
416
|
+
object Sink {
|
|
417
|
+
def existsAsync[A](pred: A => Async[Boolean]): Sink[Nothing, A, Boolean]
|
|
418
|
+
def findAsync[A](pred: A => Async[Boolean]): Sink[Nothing, A, Option[A]]
|
|
419
|
+
def foldLeftAsync[A, Z](z: Z)(f: (Z, A) => Async[Z])(implicit jtZ: JvmType.Infer[Z]): Sink[Nothing, A, Z]
|
|
420
|
+
def forallAsync[A](pred: A => Async[Boolean]): Sink[Nothing, A, Boolean]
|
|
421
|
+
def foreachAsync[A](f: A => Async[Unit]): Sink[Nothing, A, Unit]
|
|
422
|
+
}
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
Each stops where its synchronous twin stops:
|
|
426
|
+
|
|
427
|
+
| Async sink | Sync twin | Result | Stops pulling at |
|
|
428
|
+
|----------------------|-----------------|-------------|--------------------------------------------------------------|
|
|
429
|
+
| `Sink.existsAsync` | `Sink.exists` | `Boolean` | the first `true`; otherwise end-of-stream, returning `false` |
|
|
430
|
+
| `Sink.findAsync` | `Sink.find` | `Option[A]` | the first match; otherwise end-of-stream, returning `None` |
|
|
431
|
+
| `Sink.foldLeftAsync` | `Sink.foldLeft` | `Z` | end-of-stream |
|
|
432
|
+
| `Sink.forallAsync` | `Sink.forall` | `Boolean` | the first `false`; otherwise end-of-stream, returning `true` |
|
|
433
|
+
| `Sink.foreachAsync` | `Sink.foreach` | `Unit` | end-of-stream |
|
|
434
|
+
|
|
435
|
+
Three properties hold across all five. At most one callback effect runs at a time, in encounter order, so these are sequential and back-pressured rather than concurrent — for concurrency, put it in the stream with [`Stream#mapParAsync`](./stream.md#streammapparasync) before the sink. No effect is started for input the sink never pulls, so short-circuiting still costs nothing after the decisive element. And a callback that fails or is cancelled fails or cancels the run as a *defect*, not as a value in the sink's typed error channel `E` — all five have `E = Nothing`.
|
|
436
|
+
|
|
437
|
+
`Sink.foldLeftAsync` carries the same `JvmType.Infer[Z]` evidence as `Sink.foldLeft`. It keeps the asynchronous fold aligned with the specialized synchronous one; the semantics are identical.
|
|
438
|
+
|
|
439
|
+
```scala
|
|
440
|
+
import zio.blocks.async._
|
|
441
|
+
import zio.blocks.streams._
|
|
442
|
+
|
|
443
|
+
val readings = Stream(12, 7, 30, 21, 5)
|
|
444
|
+
|
|
445
|
+
// Sequential: one predicate effect at a time, stopping at 30.
|
|
446
|
+
val tooHot: Async[Either[Nothing, Boolean]] =
|
|
447
|
+
readings.runAsync(Sink.existsAsync[Int](c => Async.succeed(c > 25)))
|
|
448
|
+
|
|
449
|
+
// The fold visits every element; the accumulator is threaded through Async.
|
|
450
|
+
val total: Async[Either[Nothing, Long]] =
|
|
451
|
+
readings.runAsync(Sink.foldLeftAsync(0L)((sum: Long, c: Int) => Async.succeed(sum + c)))
|
|
452
|
+
```
|
|
453
|
+
|
|
322
454
|
### Failing
|
|
323
455
|
|
|
324
456
|
These sinks can be used to produce typed errors or fail under specific conditions:
|
|
@@ -341,7 +473,7 @@ import zio.blocks.streams._
|
|
|
341
473
|
val sink: Sink[String, Int, Long] =
|
|
342
474
|
if (false) Sink.count
|
|
343
475
|
else Sink.fail("not ready")
|
|
344
|
-
// sink: Sink[String, Int, Long] = zio.blocks.streams.Sink$$anon$
|
|
476
|
+
// sink: Sink[String, Int, Long] = zio.blocks.streams.Sink$$anon$7@7fdf129
|
|
345
477
|
|
|
346
478
|
val result = Stream(1, 2, 3).run(sink)
|
|
347
479
|
// result: Either[String, Long] = Left("not ready")
|
|
@@ -371,11 +503,11 @@ val bos = new ByteArrayOutputStream()
|
|
|
371
503
|
|
|
372
504
|
// Write first batch of bytes
|
|
373
505
|
Stream.fromChunk(zio.blocks.chunk.Chunk[Byte](72, 105)).run(Sink.fromOutputStream(bos))
|
|
374
|
-
//
|
|
506
|
+
// res15: Either[Nothing, Unit] = Right(())
|
|
375
507
|
|
|
376
508
|
// Write second batch to the same stream (reuse it)
|
|
377
509
|
Stream.fromChunk(zio.blocks.chunk.Chunk[Byte](33)).run(Sink.fromOutputStream(bos))
|
|
378
|
-
//
|
|
510
|
+
// res16: Either[Nothing, Unit] = Right(())
|
|
379
511
|
|
|
380
512
|
// When done writing all batches, YOU close the stream
|
|
381
513
|
bos.close()
|
|
@@ -408,7 +540,7 @@ val writer = new StringWriter()
|
|
|
408
540
|
// Write a stream of individual characters
|
|
409
541
|
Stream('H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd')
|
|
410
542
|
.run(Sink.fromJavaWriter(writer))
|
|
411
|
-
//
|
|
543
|
+
// res19: Either[Nothing, Unit] = Right(())
|
|
412
544
|
|
|
413
545
|
// Get the final string
|
|
414
546
|
val result = writer.toString()
|
|
@@ -421,18 +553,22 @@ Like `Sink.fromOutputStream`, this sink intentionally does not close the writer.
|
|
|
421
553
|
|
|
422
554
|
Advanced low-level use cases with direct reader protocol access:
|
|
423
555
|
|
|
424
|
-
####
|
|
556
|
+
#### Custom Sink Factories
|
|
425
557
|
|
|
426
|
-
|
|
558
|
+
`createAsync` is the cross-platform escape hatch for a native asynchronous drain. `createBoth` supplies independent native synchronous and asynchronous drains. The plain `create` factory is JVM-only because its callback consumes a blocking `SyncReader`:
|
|
427
559
|
|
|
428
560
|
```scala
|
|
429
561
|
object Sink {
|
|
430
|
-
def
|
|
562
|
+
def createAsync[E, A, Z](f: Reader.AsyncReader[A] => Async[Z]): Sink[E, A, Z]
|
|
563
|
+
def createBoth[E, A, Z](sync: Reader.SyncReader[A] => Z,
|
|
564
|
+
async: Reader.AsyncReader[A] => Async[Z]): Sink[E, A, Z]
|
|
565
|
+
// JVM only
|
|
566
|
+
def create[E, A, Z](f: Reader.SyncReader[A] => Z): Sink[E, A, Z]
|
|
431
567
|
}
|
|
432
568
|
```
|
|
433
569
|
|
|
434
570
|
:::note
|
|
435
|
-
|
|
571
|
+
These factories give you direct access to the reader protocol. Await asynchronous pulls sequentially and prefer built-in sinks when possible.
|
|
436
572
|
:::
|
|
437
573
|
|
|
438
574
|
Here's a custom sink that computes the average of all integers in a stream:
|
|
@@ -445,7 +581,7 @@ import zio.blocks.streams.io.Reader
|
|
|
445
581
|
val average = Sink.create[Nothing, Int, Double] { reader =>
|
|
446
582
|
def loop(sum: Long, count: Long): (Long, Long) = {
|
|
447
583
|
val v = reader.readInt(Long.MinValue)
|
|
448
|
-
if (v
|
|
584
|
+
if (v == Long.MinValue) (sum, count)
|
|
449
585
|
else {
|
|
450
586
|
val newSum = sum + v
|
|
451
587
|
loop(newSum, count + 1)
|
|
@@ -456,7 +592,7 @@ val average = Sink.create[Nothing, Int, Double] { reader =>
|
|
|
456
592
|
}
|
|
457
593
|
```
|
|
458
594
|
|
|
459
|
-
This example shows how `Sink.create` works.
|
|
595
|
+
This example shows how `Sink.create` works. `readInt` widens an `Int` to `Long`, leaving `Long.MinValue` available as an out-of-domain end marker. For full-domain `Long` and `Double` inputs, do not choose a data sentinel: allocate a reusable one-element primitive array and use `readLongs` or `readDoubles`, whose returned count reports data or end-of-stream without collisions. You'd use `Sink.create` when no built-in sink provides the exact aggregation or transformation logic you need — it is powerful but requires understanding the low-level [Reader protocol](../primitives/reader.md).
|
|
460
596
|
|
|
461
597
|
## Transforming Sinks
|
|
462
598
|
|
|
@@ -467,11 +603,13 @@ Every sink can be transformed using these instance methods:
|
|
|
467
603
|
Transforms the input elements before they reach the sink. The sink's result and error types are unchanged:
|
|
468
604
|
|
|
469
605
|
```scala
|
|
470
|
-
|
|
471
|
-
def contramap[A2](g: A2 =>
|
|
606
|
+
abstract class Sink[+E, -A, +Z] {
|
|
607
|
+
def contramap[A0 <: A, A2](g: A2 => A0)(implicit jtA0: JvmType.Infer[A0]): Sink[E, A2, Z]
|
|
472
608
|
}
|
|
473
609
|
```
|
|
474
610
|
|
|
611
|
+
The evidence describes the callback's **result** type `A0`, not the new sink input `A2`. The bound `A0 <: A` is variance-sound for contravariant `A`, while invariant `JvmType.Infer[A0]` lets the mapped reader advertise the callback's exact physical result lane. Primitive results therefore retain their exact lane; the `AnyRef` fallback deliberately uses the boxed reference lane. `contramapAsync` has the same evidence and representation rules for `A2 => Async[A0]`. Both adapters transform only elements requested by the wrapped sink, preserve short-circuiting, and leave reader closure to the run terminal.
|
|
612
|
+
|
|
475
613
|
`Sink#contramap` is the dual of `Sink#map`: it transforms what goes *in*, not what comes *out*:
|
|
476
614
|
|
|
477
615
|
```scala
|
|
@@ -479,8 +617,8 @@ import zio.blocks.streams._
|
|
|
479
617
|
|
|
480
618
|
// A sink that counts the length of strings
|
|
481
619
|
val totalLength: Sink[Nothing, String, Long] =
|
|
482
|
-
Sink.sumInt.contramap[String](_.length)
|
|
483
|
-
// totalLength: Sink[Nothing, String, Long] = zio.blocks.streams.Sink$Contramapped@
|
|
620
|
+
Sink.sumInt.contramap[Int, String](_.length)
|
|
621
|
+
// totalLength: Sink[Nothing, String, Long] = zio.blocks.streams.Sink$Contramapped@288bf077
|
|
484
622
|
|
|
485
623
|
val result = Stream("hello", "world").run(totalLength)
|
|
486
624
|
// result: Either[Nothing, Long] = Right(10L)
|
|
@@ -491,7 +629,7 @@ val result = Stream("hello", "world").run(totalLength)
|
|
|
491
629
|
Transforms the result after the sink finishes draining:
|
|
492
630
|
|
|
493
631
|
```scala
|
|
494
|
-
|
|
632
|
+
abstract class Sink[+E, -A, +Z] {
|
|
495
633
|
def map[Z2](f: Z => Z2): Sink[E, A, Z2]
|
|
496
634
|
}
|
|
497
635
|
```
|
|
@@ -503,7 +641,7 @@ import zio.blocks.streams._
|
|
|
503
641
|
|
|
504
642
|
val countAsString: Sink[Nothing, Any, String] =
|
|
505
643
|
Sink.count.map(n => s"Total: $n elements")
|
|
506
|
-
// countAsString: Sink[Nothing, Any, String] = zio.blocks.streams.Sink$Mapped@
|
|
644
|
+
// countAsString: Sink[Nothing, Any, String] = zio.blocks.streams.Sink$Mapped@36040911
|
|
507
645
|
|
|
508
646
|
val result = Stream(1, 2, 3).run(countAsString)
|
|
509
647
|
// result: Either[Nothing, String] = Right("Total: 3 elements")
|
|
@@ -514,12 +652,12 @@ val result = Stream(1, 2, 3).run(countAsString)
|
|
|
514
652
|
Transforms the error channel of a sink:
|
|
515
653
|
|
|
516
654
|
```scala
|
|
517
|
-
|
|
518
|
-
|
|
655
|
+
abstract class Sink[+E, -A, +Z] {
|
|
656
|
+
def mapError[E2](f: E => E2)(implicit isNothing: Sink.IsNothing[E]): Sink[E2, A, Z]
|
|
519
657
|
}
|
|
520
658
|
```
|
|
521
659
|
|
|
522
|
-
|
|
660
|
+
The `IsNothing` evidence records whether `E` is `Nothing`. For an infallible sink the method returns the same sink without allocating a wrapper; otherwise it maps typed errors:
|
|
523
661
|
|
|
524
662
|
```scala
|
|
525
663
|
import zio.blocks.streams._
|
|
@@ -532,6 +670,76 @@ val mapped = Sink.drain.mapError[String](_.toString)
|
|
|
532
670
|
val failing = Sink.fail("oops").mapError[RuntimeException](new RuntimeException(_))
|
|
533
671
|
```
|
|
534
672
|
|
|
673
|
+
### `Sink#contramapAsync[A2]` — Pre-Process Input Asynchronously
|
|
674
|
+
|
|
675
|
+
Applies an asynchronous function to each element before it reaches the sink:
|
|
676
|
+
|
|
677
|
+
```scala
|
|
678
|
+
abstract class Sink[+E, -A, +Z] {
|
|
679
|
+
def contramapAsync[A0 <: A, A2](g: A2 => Async[A0])(implicit jtA0: JvmType.Infer[A0]): Sink[E, A2, Z]
|
|
680
|
+
}
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
The evidence is the same as `Sink#contramap`'s and targets the callback's **result** type `A0`, not the new sink input `A2`. `g` runs sequentially, one effect at a time, as the wrapped sink requests elements, so short-circuiting is unchanged and `g` is never started for input the sink does not pull. A failed or cancelled `g` fails or cancels the run as a defect and stops pulling input; it does not appear in the sink's typed error channel.
|
|
684
|
+
|
|
685
|
+
```scala
|
|
686
|
+
import zio.blocks.async._
|
|
687
|
+
import zio.blocks.streams._
|
|
688
|
+
|
|
689
|
+
// Resolve each identifier to a length before counting it
|
|
690
|
+
val totalLength: Sink[Nothing, String, Long] =
|
|
691
|
+
Sink.sumInt.contramapAsync[Int, String](id => Async.succeed(id.length))
|
|
692
|
+
|
|
693
|
+
val result: Async[Either[Nothing, Long]] =
|
|
694
|
+
Stream("hello", "world").runAsync(totalLength)
|
|
695
|
+
```
|
|
696
|
+
|
|
697
|
+
### `Sink#mapAsync[Z2]` — Transform Result Asynchronously
|
|
698
|
+
|
|
699
|
+
Transforms the result asynchronously after the sink finishes draining:
|
|
700
|
+
|
|
701
|
+
```scala
|
|
702
|
+
abstract class Sink[+E, -A, +Z] {
|
|
703
|
+
def mapAsync[Z2](f: Z => Async[Z2]): Sink[E, A, Z2]
|
|
704
|
+
}
|
|
705
|
+
```
|
|
706
|
+
|
|
707
|
+
`f` runs once, after the drain completes, so it does not change input consumption. When the sink fails, `f` is never started. Failure or cancellation of `f` fails or cancels the run as a defect.
|
|
708
|
+
|
|
709
|
+
```scala
|
|
710
|
+
import zio.blocks.async._
|
|
711
|
+
import zio.blocks.streams._
|
|
712
|
+
|
|
713
|
+
val persistedCount: Sink[Nothing, Any, String] =
|
|
714
|
+
Sink.count.mapAsync(n => Async.succeed(s"Total: $n elements"))
|
|
715
|
+
|
|
716
|
+
val result: Async[Either[Nothing, String]] =
|
|
717
|
+
Stream(1, 2, 3).runAsync(persistedCount)
|
|
718
|
+
```
|
|
719
|
+
|
|
720
|
+
### `Sink#mapErrorAsync[E2]` — Transform Error Asynchronously
|
|
721
|
+
|
|
722
|
+
Transforms the typed error channel asynchronously:
|
|
723
|
+
|
|
724
|
+
```scala
|
|
725
|
+
abstract class Sink[+E, -A, +Z] {
|
|
726
|
+
def mapErrorAsync[E2](f: E => Async[E2])(implicit isNothing: Sink.IsNothing[E]): Sink[E2, A, Z]
|
|
727
|
+
}
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
It carries the same `IsNothing` evidence as `Sink#mapError`: for an infallible sink (`E = Nothing`) the method returns the same sink without allocating a wrapper, and `f` can never be evaluated. Successful results, upstream stream errors, and defects pass through untransformed; only the sink's own typed error reaches `f`. Failure or cancellation of `f` is a defect.
|
|
731
|
+
|
|
732
|
+
```scala
|
|
733
|
+
import zio.blocks.async._
|
|
734
|
+
import zio.blocks.streams._
|
|
735
|
+
|
|
736
|
+
// Infallible: no wrapper is allocated and `f` can never run
|
|
737
|
+
val mapped = Sink.drain.mapErrorAsync[String](e => Async.succeed(e.toString))
|
|
738
|
+
|
|
739
|
+
// Fallible: the typed error is transformed before it reaches the `Left`
|
|
740
|
+
val failing = Sink.fail("oops").mapErrorAsync[RuntimeException](e => Async.succeed(new RuntimeException(e)))
|
|
741
|
+
```
|
|
742
|
+
|
|
535
743
|
## Integration with Stream
|
|
536
744
|
|
|
537
745
|
`Stream.run(sink)` is the primary entry point. ZIO Blocks also provides convenience methods on `Stream` that delegate to built-in sinks:
|
|
@@ -564,7 +772,7 @@ import zio.blocks.chunk.Chunk
|
|
|
564
772
|
val cleanAndCollect: Sink[Nothing, String, Chunk[String]] =
|
|
565
773
|
Pipeline.map[String, String](_.trim.toLowerCase)
|
|
566
774
|
.andThenSink(Sink.collectAll[String])
|
|
567
|
-
// cleanAndCollect: Sink[Nothing, String, Chunk[String]] = zio.blocks.streams.Sink$Contramapped@
|
|
775
|
+
// cleanAndCollect: Sink[Nothing, String, Chunk[String]] = zio.blocks.streams.Sink$Contramapped@5d54c379
|
|
568
776
|
|
|
569
777
|
val result = Stream(" Hello ", " WORLD ").run(cleanAndCollect)
|
|
570
778
|
// result: Either[Nothing, Chunk[String]] = Right(IndexedSeq("hello", "world"))
|
|
@@ -576,11 +784,13 @@ See [Pipeline — Applying to a Sink](./pipeline.md#applying-to-a-sink) for more
|
|
|
576
784
|
|
|
577
785
|
## JVM NIO Sinks
|
|
578
786
|
|
|
579
|
-
The `NioSinks` object (JVM-only) provides sinks for Java NIO (`java.nio`) buffers and channels.
|
|
787
|
+
The `NioSinks` object (JVM-only) provides sinks for Java NIO (`java.nio`) buffers and channels. NIO offers efficient buffers and both blocking and selector-based channel APIs, but these sinks use blocking channel writes; they do not expose selector-based non-blocking output. When you're writing to network sockets, files, or other NIO-based resources, these sinks give you a convenient way to drain streams directly into NIO data structures.
|
|
580
788
|
|
|
581
|
-
Traditional Java I/O (`OutputStream`, `Writer`) blocks threads and requires manual buffering for efficiency.
|
|
789
|
+
Traditional Java I/O (`OutputStream`, `Writer`) blocks threads and requires manual buffering for efficiency. `NioSinks.fromChannel` also blocks, but handles buffer allocation, position management, and flushing automatically (default 8KB), while typed variants like `NioSinks.fromByteBufferInt` and `NioSinks.fromByteBufferLong` eliminate boxing overhead by writing primitives directly to buffers you provide.
|
|
582
790
|
|
|
583
|
-
Choose `NioSinks.fromChannel` when
|
|
791
|
+
Choose `NioSinks.fromChannel` when blocking channel output is acceptable and you want automatic buffering for network sockets or files. Choose typed variants when you control buffer allocation and are streaming millions of primitives where boxing would degrade performance.
|
|
792
|
+
|
|
793
|
+
Each of these sinks implements both drains, so NIO output works under the blocking `Stream#run` and under `Stream#runAsync` alike. What the asynchronous drain removes is the thread parked waiting for stream *input*; the destination writes are the same blocking `java.nio` calls on either path. `NioSinks.fromChannel` does budget its asynchronous flush loop: when a channel write makes no progress it yields instead of spinning, so a stalled channel does not monopolise the caller. [Reader](../primitives/reader.md#from-native-asynchronous-sources) covers the NIO adapters on the source side in full, and this page does not duplicate them.
|
|
584
794
|
|
|
585
795
|
Here are the available NIO sinks:
|
|
586
796
|
|
|
@@ -598,8 +808,8 @@ object NioSinks {
|
|
|
598
808
|
### From ByteBuffer Sinks
|
|
599
809
|
|
|
600
810
|
**`NioSinks.fromByteBuffer` and typed variants** — Write primitive streams directly into a pre-allocated NIO ByteBuffer:
|
|
601
|
-
- `NioSinks.fromByteBuffer` — writes individual `Byte` elements
|
|
602
|
-
- `NioSinks.fromByteBufferInt`, `NioSinks.fromByteBufferLong`, `NioSinks.fromByteBufferFloat`, `NioSinks.fromByteBufferDouble` — write primitives directly using the buffer's native methods (`putInt`, `putLong`, etc.)
|
|
811
|
+
- `NioSinks.fromByteBuffer` — writes individual `Byte` elements.
|
|
812
|
+
- `NioSinks.fromByteBufferInt`, `NioSinks.fromByteBufferLong`, `NioSinks.fromByteBufferFloat`, `NioSinks.fromByteBufferDouble` — write primitives directly using the buffer's native methods (`putInt`, `putLong`, etc.), one buffer write per element rather than one per byte.
|
|
603
813
|
|
|
604
814
|
Here's an example using ByteBuffer with typed primitive writes:
|
|
605
815
|
|
|
@@ -614,11 +824,11 @@ val buffer = ByteBuffer.allocate(32).order(ByteOrder.BIG_ENDIAN)
|
|
|
614
824
|
|
|
615
825
|
// Write a stream of Longs to the buffer
|
|
616
826
|
Stream(1L, 2L, 3L, 4L).run(NioSinks.fromByteBufferLong(buffer))
|
|
617
|
-
//
|
|
827
|
+
// res29: Either[Nothing, Unit] = Right(())
|
|
618
828
|
|
|
619
829
|
// After writing, rewind to read
|
|
620
830
|
buffer.rewind()
|
|
621
|
-
//
|
|
831
|
+
// res30: ByteBuffer = java.nio.HeapByteBuffer[pos=32 lim=32 cap=32]
|
|
622
832
|
|
|
623
833
|
val readBack = List(
|
|
624
834
|
buffer.getLong(),
|
|
@@ -629,29 +839,13 @@ val readBack = List(
|
|
|
629
839
|
// readBack: List[Long] = List(1L, 2L, 3L, 4L)
|
|
630
840
|
```
|
|
631
841
|
|
|
632
|
-
This example allocates a 32-byte buffer (4 Longs × 8 bytes each), writes four `Long` values using `NioSinks.fromByteBufferLong` (which
|
|
842
|
+
This example allocates a 32-byte buffer (4 Longs × 8 bytes each), writes four `Long` values using `NioSinks.fromByteBufferLong` (which calls `putLong` on each element), then rewinds and reads them back to verify. The typed variant writes one 8-byte buffer operation per element, where the byte variant would require the stream to carry each byte separately.
|
|
633
843
|
|
|
634
|
-
The following example shows streaming voltage sensor readings through a calibration curve and buffering them for downstream computation. When processing sensor arrays or scientific measurements, pre-allocated buffers with typed sinks
|
|
844
|
+
The following example shows streaming voltage sensor readings through a calibration curve and buffering them for downstream computation. When processing sensor arrays or scientific measurements, pre-allocated buffers with typed sinks let each element be written through its exact primitive lane without boxing.
|
|
635
845
|
|
|
636
846
|
Here is the complete example:
|
|
637
847
|
|
|
638
848
|
```scala title="streams-examples/src/main/scala/sink/SinkScientificComputingExample.scala"
|
|
639
|
-
/*
|
|
640
|
-
* Copyright 2024-2026 John A. De Goes and the ZIO Contributors
|
|
641
|
-
*
|
|
642
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
643
|
-
* you may not use this file except in compliance with the License.
|
|
644
|
-
* You may obtain a copy of the License at
|
|
645
|
-
*
|
|
646
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
647
|
-
*
|
|
648
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
649
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
650
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
651
|
-
* See the License for the specific language governing permissions and
|
|
652
|
-
* limitations under the License.
|
|
653
|
-
*/
|
|
654
|
-
|
|
655
849
|
package sink
|
|
656
850
|
|
|
657
851
|
import zio.blocks.streams.*
|
|
@@ -737,165 +931,11 @@ sbt "streams-examples/runMain sink.SinkScientificComputingExample"
|
|
|
737
931
|
|
|
738
932
|
This use case is typical in scientific instrumentation, machine learning data preprocessing, and signal processing pipelines where you need to efficiently batch-process numerical streams into memory-efficient structures for downstream computation.
|
|
739
933
|
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
These typed sinks achieve **zero-boxing performance** by using a special "sentinel" value to signal end-of-stream, rather than allocating wrapper objects or checking for `null`. This design eliminates allocations entirely, keeping the read loop a **single primitive comparison per element**. This loop shape is a deliberate, protected performance choice (see the repository's `AGENTS.md`, "Sentinel performance policy"): no per-element flag checks, rawbits conversions, boxing, or extra branches are permitted in it.
|
|
743
|
-
|
|
744
|
-
A natural question: what happens if the stream *contains* the sentinel value (e.g. a `Long.MaxValue` element streamed into `NioSinks.fromByteBufferLong`)? The sink **throws `IllegalArgumentException`** — your data is never silently dropped. Detection costs nothing on the hot path: every read records an out-of-band `lastReadWasEOF` flag on the reader, and the sink consults it **once, after the drain loop exits**, to distinguish genuine end-of-stream from a real sentinel-valued element:
|
|
745
|
-
|
|
746
|
-
```scala
|
|
747
|
-
// fromByteBufferLong - tight loop with primitives only
|
|
748
|
-
val s = Long.MaxValue
|
|
749
|
-
var v = reader.readLong(s)(using unsafeEvidence)
|
|
750
|
-
while (v != s) { // single primitive comparison per element
|
|
751
|
-
buf.putLong(v)
|
|
752
|
-
v = reader.readLong(s)(using unsafeEvidence)
|
|
753
|
-
}
|
|
754
|
-
if (!reader.lastReadWasEOF) // consulted once, post-loop: zero hot-path cost
|
|
755
|
-
throw new IllegalArgumentException("stream contains Long.MaxValue ...")
|
|
756
|
-
```
|
|
757
|
-
|
|
758
|
-
**Sentinels per typed sink:**
|
|
759
|
-
| Method | Input Type | Sentinel Value | Collision behavior |
|
|
760
|
-
|--------|-----------|---|---|
|
|
761
|
-
| `NioSinks.fromByteBuffer` | `Byte` | `-1` (as `Int`) | No collision possible — bytes are widened to [0, 255] |
|
|
762
|
-
| `NioSinks.fromByteBufferInt` | `Int` | `Long.MinValue` | No collision possible — outside Int range |
|
|
763
|
-
| `NioSinks.fromByteBufferLong` | `Long` | `Long.MaxValue` | Throws `IllegalArgumentException` |
|
|
764
|
-
| `NioSinks.fromByteBufferFloat` | `Float` | `Double.MaxValue` | No collision possible — outside Float range |
|
|
765
|
-
| `NioSinks.fromByteBufferDouble` | `Double` | `Double.MaxValue` | Throws `IllegalArgumentException` |
|
|
766
|
-
|
|
767
|
-
**If your data may contain the sentinel value**, use a generic sink instead — these use an out-of-band object sentinel and handle every value:
|
|
768
|
-
- `Sink.collectAll[A]` — collects into a Chunk
|
|
769
|
-
- `Sink.foreach[A](f: A => Unit)` — processes each element individually
|
|
770
|
-
- `Sink.foldLeft[A, Z](z: Z)(f: (Z, A) => Z)` — accumulates
|
|
771
|
-
- `Sink.create[E, A, Z](f: Reader[A] => Z)` — manual control
|
|
772
|
-
|
|
773
|
-
For a runnable demonstration of the guard, see the example below:
|
|
774
|
-
|
|
775
|
-
```scala title="streams-examples/src/main/scala/sink/SinkSentinelGuardExample.scala"
|
|
776
|
-
/*
|
|
777
|
-
* Copyright 2024-2026 John A. De Goes and the ZIO Contributors
|
|
778
|
-
*
|
|
779
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
780
|
-
* you may not use this file except in compliance with the License.
|
|
781
|
-
* You may obtain a copy of the License at
|
|
782
|
-
*
|
|
783
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
784
|
-
*
|
|
785
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
786
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
787
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
788
|
-
* See the License for the specific language governing permissions and
|
|
789
|
-
* limitations under the License.
|
|
790
|
-
*/
|
|
791
|
-
|
|
792
|
-
package sink
|
|
793
|
-
|
|
794
|
-
import zio.blocks.streams.*
|
|
795
|
-
import zio.blocks.streams.NioSinks
|
|
796
|
-
import java.nio.ByteBuffer
|
|
797
|
-
|
|
798
|
-
object SinkSentinelGuardExample extends App {
|
|
799
|
-
println("=== Sentinel Collisions Are Rejected Loudly (Never Silently) ===\n")
|
|
800
|
-
|
|
801
|
-
println("Context: the typed NIO sinks use a primitive sentinel (e.g. Long.MaxValue for")
|
|
802
|
-
println("fromByteBufferLong) to detect end-of-stream, keeping the drain loop a single")
|
|
803
|
-
println("primitive comparison per element — zero boxing, zero allocation. This is a")
|
|
804
|
-
println("deliberate performance choice (see AGENTS.md, Sentinel performance policy).")
|
|
805
|
-
println("If your stream contains the sentinel value itself, the sink does NOT silently")
|
|
806
|
-
println("truncate: it detects the collision at zero hot-path cost (one out-of-band EOF")
|
|
807
|
-
println("flag check after the loop exits) and throws IllegalArgumentException.\n")
|
|
808
|
-
|
|
809
|
-
// Example 1: normal data drains at full speed
|
|
810
|
-
println("Test 1: Stream without sentinel values drains completely")
|
|
811
|
-
println("-" * 60)
|
|
812
|
-
|
|
813
|
-
val safeData = List(100L, 200L, 300L, 400L, 500L)
|
|
814
|
-
val buffer1 = ByteBuffer.allocate(safeData.length * 8)
|
|
815
|
-
Stream.fromIterable(safeData).run(NioSinks.fromByteBufferLong(buffer1))
|
|
816
|
-
buffer1.flip()
|
|
817
|
-
|
|
818
|
-
var count1 = 0
|
|
819
|
-
while (buffer1.hasRemaining) {
|
|
820
|
-
println(f" [$count1] ${buffer1.getLong()}")
|
|
821
|
-
count1 += 1
|
|
822
|
-
}
|
|
823
|
-
println(f"\n✓ All ${count1} values written\n")
|
|
824
|
-
|
|
825
|
-
// Example 2: a sentinel-valued element is rejected with a clear error
|
|
826
|
-
println("Test 2: Stream containing Long.MaxValue is rejected, not truncated")
|
|
827
|
-
println("-" * 60)
|
|
828
|
-
|
|
829
|
-
val riskyData = List(100L, 200L, Long.MaxValue, 300L, 400L)
|
|
830
|
-
println(
|
|
831
|
-
f"Stream data: ${riskyData.map(v => if (v == Long.MaxValue) "Long.MaxValue" else v.toString).mkString(", ")}\n"
|
|
832
|
-
)
|
|
833
|
-
|
|
834
|
-
val buffer2 = ByteBuffer.allocate(riskyData.length * 8)
|
|
835
|
-
try {
|
|
836
|
-
Stream.fromIterable(riskyData).run(NioSinks.fromByteBufferLong(buffer2))
|
|
837
|
-
println("✗ UNEXPECTED: drain completed without error")
|
|
838
|
-
} catch {
|
|
839
|
-
case e: IllegalArgumentException =>
|
|
840
|
-
println(s"✓ Rejected loudly: ${e.getMessage}")
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
// Recommendations
|
|
844
|
-
println("\n=== Recommendations ===")
|
|
845
|
-
println("1. If your data might contain the sentinel value (Long.MaxValue for the Long")
|
|
846
|
-
println(" sink, Double.MaxValue for the Double sink):")
|
|
847
|
-
println(" → Use a generic sink (Sink.collectAll, Sink.foreach, Sink.foldLeft) — these")
|
|
848
|
-
println(" use an out-of-band object sentinel and handle every value")
|
|
849
|
-
println("2. Otherwise the typed sinks are maximally fast: a single primitive comparison")
|
|
850
|
-
println(" per element, zero boxing, zero allocation")
|
|
851
|
-
println("3. Either way, data is never silently dropped — a collision throws")
|
|
852
|
-
println()
|
|
853
|
-
println("Sentinels per typed sink:")
|
|
854
|
-
println(" → fromByteBufferInt: sentinel = Long.MinValue (outside Int range — no collision possible)")
|
|
855
|
-
println(" → fromByteBufferLong: sentinel = Long.MaxValue (collision throws)")
|
|
856
|
-
println(" → fromByteBufferFloat: sentinel = Double.MaxValue (outside Float range — no collision possible)")
|
|
857
|
-
println(" → fromByteBufferDouble: sentinel = Double.MaxValue (collision throws)")
|
|
858
|
-
}
|
|
859
|
-
```
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
Run it with:
|
|
863
|
-
|
|
864
|
-
```bash
|
|
865
|
-
sbt "streams-examples/runMain sink.SinkSentinelGuardExample"
|
|
866
|
-
```
|
|
867
|
-
:::
|
|
868
|
-
|
|
869
|
-
You might ask: **Why not use a sentinel object like generic sinks do, instead of primitive values?** The answer reveals a fundamental performance trade-off.
|
|
870
|
-
|
|
871
|
-
Generic sinks use object sentinels to signal end-of-stream:
|
|
872
|
-
|
|
873
|
-
```scala
|
|
874
|
-
// Sink.collectAll - uses object reference for end-of-stream
|
|
875
|
-
def loop(v: Any): Unit =
|
|
876
|
-
if (v.asInstanceOf[AnyRef] ne EndOfStream) {
|
|
877
|
-
b += v.asInstanceOf[A]
|
|
878
|
-
loop(reader.read(EndOfStream))
|
|
879
|
-
}
|
|
880
|
-
val firstValue = reader.read(EndOfStream) // EndOfStream is an object
|
|
881
|
-
loop(firstValue)
|
|
882
|
-
```
|
|
883
|
-
|
|
884
|
-
**Performance Impact:**
|
|
885
|
-
- **Typed sinks:** Direct primitive comparison, zero allocations, tight loop optimizable by JVM
|
|
886
|
-
- **Generic sinks:** Object casting, reference equality check, one `EndOfStream` object per stream
|
|
887
|
-
|
|
888
|
-
For a stream processing **millions of elements**, the typed sink approach has measurably better performance because:
|
|
889
|
-
1. No casting overhead per iteration
|
|
890
|
-
2. Primitive values are faster than object references
|
|
891
|
-
3. JIT compiler can better optimize tight primitive loops
|
|
892
|
-
4. Zero per-element allocation pressure
|
|
893
|
-
|
|
894
|
-
Neither approach silently drops data: the generic sinks use a reference-unique object that no stream element can equal, and the typed sinks detect a value/sentinel collision via the out-of-band EOF flag (consulted once, post-loop) and throw rather than truncate.
|
|
934
|
+
The typed sinks dispatch through their exact primitive lanes. `Long` and `Double` use collision-free bulk-count status, so `Long.MinValue`, `Long.MaxValue`, every finite or infinite `Double`, signed zero, and every raw NaN payload are written as ordinary data. There is no sentinel-value truncation restriction. The supplied buffer remains caller-owned and is not flipped, rewound, or closed by the sink. [Zero-Boxing Streams](../execution-and-compatibility/zero-boxing.md) carries the per-lane end-of-stream table this rests on.
|
|
895
935
|
|
|
896
936
|
### From Channel Sink
|
|
897
937
|
|
|
898
|
-
The **`
|
|
938
|
+
The **`NioSinks.fromChannel`** constructor performs buffered writes to a `WritableByteChannel` (e.g., a network socket or file channel). This is the general-purpose NIO sink: it accumulates bytes in an internal buffer of size `bufSize` (default 8192), flushes when the buffer is full, and flushes again at end-of-stream. It does not close the caller-owned channel.
|
|
899
939
|
|
|
900
940
|
It handles `IOException` as a typed error, so failures surface as `Left(IOException)` from `Stream.run`. Use this for network I/O or when you can't pre-allocate a buffer. The channel I/O is blocking—NIO's non-blocking advantage comes when using selectors across many channels, which this sink does not expose.
|
|
901
941
|
|
|
@@ -904,22 +944,6 @@ Suppose you're collecting metrics from thousands of sensors (temperature, pressu
|
|
|
904
944
|
Here is the complete example:
|
|
905
945
|
|
|
906
946
|
```scala title="streams-examples/src/main/scala/sink/SinkTelemetryExample.scala"
|
|
907
|
-
/*
|
|
908
|
-
* Copyright 2024-2026 John A. De Goes and the ZIO Contributors
|
|
909
|
-
*
|
|
910
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
911
|
-
* you may not use this file except in compliance with the License.
|
|
912
|
-
* You may obtain a copy of the License at
|
|
913
|
-
*
|
|
914
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
915
|
-
*
|
|
916
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
917
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
918
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
919
|
-
* See the License for the specific language governing permissions and
|
|
920
|
-
* limitations under the License.
|
|
921
|
-
*/
|
|
922
|
-
|
|
923
947
|
package sink
|
|
924
948
|
|
|
925
949
|
import zio.blocks.streams.*
|
|
@@ -1032,22 +1056,6 @@ Run individual examples with sbt:
|
|
|
1032
1056
|
This example demonstrates the most commonly used built-in sinks: `Sink.drain`, `Sink.count`, `Sink.collectAll`, `Sink.head`, `Sink.last`, and `Sink.take`:
|
|
1033
1057
|
|
|
1034
1058
|
```scala title="streams-examples/src/main/scala/sink/SinkBasicUsageExample.scala"
|
|
1035
|
-
/*
|
|
1036
|
-
* Copyright 2024-2026 John A. De Goes and the ZIO Contributors
|
|
1037
|
-
*
|
|
1038
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
1039
|
-
* you may not use this file except in compliance with the License.
|
|
1040
|
-
* You may obtain a copy of the License at
|
|
1041
|
-
*
|
|
1042
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
1043
|
-
*
|
|
1044
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
1045
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
1046
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
1047
|
-
* See the License for the specific language governing permissions and
|
|
1048
|
-
* limitations under the License.
|
|
1049
|
-
*/
|
|
1050
|
-
|
|
1051
1059
|
package sink
|
|
1052
1060
|
|
|
1053
1061
|
import zio.blocks.streams.*
|
|
@@ -1114,22 +1122,6 @@ sbt "streams-examples/runMain sink.SinkBasicUsageExample"
|
|
|
1114
1122
|
This example shows aggregation sinks (`Sink.foldLeft`, `Sink.sumInt`, `Sink.sumDouble`) and search sinks (`Sink.exists`, `Sink.forall`, `Sink.find`, `Sink.foreach`):
|
|
1115
1123
|
|
|
1116
1124
|
```scala title="streams-examples/src/main/scala/sink/SinkAggregationExample.scala"
|
|
1117
|
-
/*
|
|
1118
|
-
* Copyright 2024-2026 John A. De Goes and the ZIO Contributors
|
|
1119
|
-
*
|
|
1120
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
1121
|
-
* you may not use this file except in compliance with the License.
|
|
1122
|
-
* You may obtain a copy of the License at
|
|
1123
|
-
*
|
|
1124
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
1125
|
-
*
|
|
1126
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
1127
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
1128
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
1129
|
-
* See the License for the specific language governing permissions and
|
|
1130
|
-
* limitations under the License.
|
|
1131
|
-
*/
|
|
1132
|
-
|
|
1133
1125
|
package sink
|
|
1134
1126
|
|
|
1135
1127
|
import zio.blocks.streams.*
|
|
@@ -1213,22 +1205,6 @@ sbt "streams-examples/runMain sink.SinkAggregationExample"
|
|
|
1213
1205
|
This example demonstrates `Sink#contramap`, `Sink#map`, `Sink#mapError`, `Sink.fail`, `Sink.create`, and `Pipeline#andThenSink`:
|
|
1214
1206
|
|
|
1215
1207
|
```scala title="streams-examples/src/main/scala/sink/SinkTransformationExample.scala"
|
|
1216
|
-
/*
|
|
1217
|
-
* Copyright 2024-2026 John A. De Goes and the ZIO Contributors
|
|
1218
|
-
*
|
|
1219
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
1220
|
-
* you may not use this file except in compliance with the License.
|
|
1221
|
-
* You may obtain a copy of the License at
|
|
1222
|
-
*
|
|
1223
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
1224
|
-
*
|
|
1225
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
1226
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
1227
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
1228
|
-
* See the License for the specific language governing permissions and
|
|
1229
|
-
* limitations under the License.
|
|
1230
|
-
*/
|
|
1231
|
-
|
|
1232
1208
|
package sink
|
|
1233
1209
|
|
|
1234
1210
|
import zio.blocks.streams.*
|
|
@@ -1240,14 +1216,14 @@ object SinkTransformationExample extends App {
|
|
|
1240
1216
|
// 1. contramap — pre-process input
|
|
1241
1217
|
println("1. Sink.contramap — pre-process input elements:")
|
|
1242
1218
|
val stringLengthSum: Sink[Nothing, String, Long] =
|
|
1243
|
-
Sink.sumInt.contramap[String](_.length)
|
|
1219
|
+
Sink.sumInt.contramap[Int, String](_.length)
|
|
1244
1220
|
|
|
1245
1221
|
show(Stream("hello", "world").run(stringLengthSum))
|
|
1246
1222
|
|
|
1247
1223
|
// 2. contramap — change element type
|
|
1248
1224
|
println("\n2. contramap to convert types:")
|
|
1249
1225
|
val parseInts: Sink[Nothing, String, Long] =
|
|
1250
|
-
Sink.sumInt.contramap[String](_.toInt)
|
|
1226
|
+
Sink.sumInt.contramap[Int, String](_.toInt)
|
|
1251
1227
|
|
|
1252
1228
|
show(Stream("10", "20", "30").run(parseInts))
|
|
1253
1229
|
|
|
@@ -1261,7 +1237,7 @@ object SinkTransformationExample extends App {
|
|
|
1261
1237
|
// 4. Chaining contramap + map
|
|
1262
1238
|
println("\n4. Chaining contramap + map:")
|
|
1263
1239
|
val pipeline = Sink.sumInt
|
|
1264
|
-
.contramap[String](_.length)
|
|
1240
|
+
.contramap[Int, String](_.length)
|
|
1265
1241
|
.map(total => s"Total chars: $total")
|
|
1266
1242
|
|
|
1267
1243
|
show(Stream("hi", "hello").run(pipeline))
|
|
@@ -1329,98 +1305,100 @@ Run this example with:
|
|
|
1329
1305
|
sbt "streams-examples/runMain sink.SinkTransformationExample"
|
|
1330
1306
|
```
|
|
1331
1307
|
|
|
1332
|
-
###
|
|
1308
|
+
### Async Sinks End to End
|
|
1333
1309
|
|
|
1334
|
-
This example
|
|
1310
|
+
This example builds a custom mean sink with `Sink.createBoth` — a `while` loop for the synchronous drain, an `Async#flatMap` recursion for the asynchronous one — then drives it and `Sink.existsAsync` through `Stream#runAsync`, unwrapping each `Either` at the end. It is JVM-only, because it finishes with `.block` to turn the `Async` into a value for `main`:
|
|
1335
1311
|
|
|
1336
|
-
```scala title="streams-examples/src/main/scala/sink/
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1312
|
+
```scala title="streams-examples/src/main/scala/sink/SinkAsyncExample.scala"
|
|
1313
|
+
package sink
|
|
1314
|
+
|
|
1315
|
+
import zio.blocks.async._
|
|
1316
|
+
import zio.blocks.streams._
|
|
1317
|
+
import zio.blocks.streams.io.Reader
|
|
1318
|
+
|
|
1319
|
+
/**
|
|
1320
|
+
* A custom sink built with `Sink.createBoth`, plus one of the `*Async`
|
|
1321
|
+
* combinators, both driven from the cross-platform asynchronous terminal
|
|
1322
|
+
* `Stream#runAsync`.
|
|
1343
1323
|
*
|
|
1344
|
-
*
|
|
1324
|
+
* `createBoth` supplies two independent drains. The terminal picks exactly one:
|
|
1325
|
+
* `runAsync` takes the asynchronous callback, and the JVM-only blocking `run`
|
|
1326
|
+
* takes the synchronous one. Both must agree on what they consume and what they
|
|
1327
|
+
* return.
|
|
1345
1328
|
*
|
|
1346
|
-
*
|
|
1347
|
-
*
|
|
1348
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
1349
|
-
* See the License for the specific language governing permissions and
|
|
1350
|
-
* limitations under the License.
|
|
1329
|
+
* JVM only, because it ends in `.block` to turn the `Async` into a value for
|
|
1330
|
+
* `main`.
|
|
1351
1331
|
*/
|
|
1332
|
+
object SinkAsyncExample {
|
|
1333
|
+
|
|
1334
|
+
/**
|
|
1335
|
+
* Mean of an `Int` lane. The synchronous callback is a plain `while` loop
|
|
1336
|
+
* over `readInt`, which allocates no `Async` per element; the asynchronous
|
|
1337
|
+
* callback threads the same accumulator through `flatMap`.
|
|
1338
|
+
*/
|
|
1339
|
+
val meanCelsius: Sink[Nothing, Int, Double] =
|
|
1340
|
+
Sink.createBoth[Nothing, Int, Double](
|
|
1341
|
+
sync = { (reader: Reader.SyncReader[Int]) =>
|
|
1342
|
+
var sum = 0L
|
|
1343
|
+
var count = 0L
|
|
1344
|
+
var value = reader.readInt(Long.MinValue)
|
|
1345
|
+
while (value != Long.MinValue) {
|
|
1346
|
+
sum += value
|
|
1347
|
+
count += 1L
|
|
1348
|
+
value = reader.readInt(Long.MinValue)
|
|
1349
|
+
}
|
|
1350
|
+
mean(sum, count)
|
|
1351
|
+
},
|
|
1352
|
+
async = { (reader: Reader.AsyncReader[Int]) =>
|
|
1353
|
+
def loop(sum: Long, count: Long): Async[Double] =
|
|
1354
|
+
reader.readInt(Long.MinValue).flatMap { value =>
|
|
1355
|
+
if (value == Long.MinValue) Async.succeed(mean(sum, count))
|
|
1356
|
+
else loop(sum + value, count + 1L)
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
loop(0L, 0L)
|
|
1360
|
+
}
|
|
1361
|
+
)
|
|
1352
1362
|
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
import zio.blocks.streams.*
|
|
1356
|
-
import zio.blocks.streams.NioSinks
|
|
1357
|
-
import java.nio.ByteBuffer
|
|
1363
|
+
private def mean(sum: Long, count: Long): Double =
|
|
1364
|
+
if (count == 0L) 0.0 else sum.toDouble / count.toDouble
|
|
1358
1365
|
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
println("Context: the typed NIO sinks use a primitive sentinel (e.g. Long.MaxValue for")
|
|
1363
|
-
println("fromByteBufferLong) to detect end-of-stream, keeping the drain loop a single")
|
|
1364
|
-
println("primitive comparison per element — zero boxing, zero allocation. This is a")
|
|
1365
|
-
println("deliberate performance choice (see AGENTS.md, Sentinel performance policy).")
|
|
1366
|
-
println("If your stream contains the sentinel value itself, the sink does NOT silently")
|
|
1367
|
-
println("truncate: it detects the collision at zero hot-path cost (one out-of-band EOF")
|
|
1368
|
-
println("flag check after the loop exits) and throws IllegalArgumentException.\n")
|
|
1369
|
-
|
|
1370
|
-
// Example 1: normal data drains at full speed
|
|
1371
|
-
println("Test 1: Stream without sentinel values drains completely")
|
|
1372
|
-
println("-" * 60)
|
|
1373
|
-
|
|
1374
|
-
val safeData = List(100L, 200L, 300L, 400L, 500L)
|
|
1375
|
-
val buffer1 = ByteBuffer.allocate(safeData.length * 8)
|
|
1376
|
-
Stream.fromIterable(safeData).run(NioSinks.fromByteBufferLong(buffer1))
|
|
1377
|
-
buffer1.flip()
|
|
1378
|
-
|
|
1379
|
-
var count1 = 0
|
|
1380
|
-
while (buffer1.hasRemaining) {
|
|
1381
|
-
println(f" [$count1] ${buffer1.getLong()}")
|
|
1382
|
-
count1 += 1
|
|
1383
|
-
}
|
|
1384
|
-
println(f"\n✓ All ${count1} values written\n")
|
|
1366
|
+
def main(args: Array[String]): Unit = {
|
|
1367
|
+
val celsius: Stream[Nothing, Int] = Stream(12, 7, 30, 21, 5)
|
|
1385
1368
|
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
println("-" * 60)
|
|
1369
|
+
// Nothing has run yet: both terminals are descriptions.
|
|
1370
|
+
val average: Async[Either[Nothing, Double]] = celsius.runAsync(meanCelsius)
|
|
1389
1371
|
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1372
|
+
// `existsAsync` runs at most one predicate effect at a time and stops at
|
|
1373
|
+
// the first `true`; 30 matches, so 21 and 5 are never pulled.
|
|
1374
|
+
val tooHot: Async[Either[Nothing, Boolean]] =
|
|
1375
|
+
celsius.runAsync(Sink.existsAsync[Int](c => Async.succeed(c > 25)))
|
|
1394
1376
|
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
} catch {
|
|
1400
|
-
case e: IllegalArgumentException =>
|
|
1401
|
-
println(s"✓ Rejected loudly: ${e.getMessage}")
|
|
1377
|
+
// `.block` parks the calling thread until the value arrives. It belongs
|
|
1378
|
+
// here, at the edge of a JVM `main`, and nowhere else.
|
|
1379
|
+
report("createBoth mean", average.block)
|
|
1380
|
+
report("existsAsync (> 25)", tooHot.block)
|
|
1402
1381
|
}
|
|
1403
1382
|
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
println("
|
|
1408
|
-
|
|
1409
|
-
println(" use an out-of-band object sentinel and handle every value")
|
|
1410
|
-
println("2. Otherwise the typed sinks are maximally fast: a single primitive comparison")
|
|
1411
|
-
println(" per element, zero boxing, zero allocation")
|
|
1412
|
-
println("3. Either way, data is never silently dropped — a collision throws")
|
|
1413
|
-
println()
|
|
1414
|
-
println("Sentinels per typed sink:")
|
|
1415
|
-
println(" → fromByteBufferInt: sentinel = Long.MinValue (outside Int range — no collision possible)")
|
|
1416
|
-
println(" → fromByteBufferLong: sentinel = Long.MaxValue (collision throws)")
|
|
1417
|
-
println(" → fromByteBufferFloat: sentinel = Double.MaxValue (outside Float range — no collision possible)")
|
|
1418
|
-
println(" → fromByteBufferDouble: sentinel = Double.MaxValue (collision throws)")
|
|
1383
|
+
private def report[E, Z](label: String, result: Either[E, Z]): Unit =
|
|
1384
|
+
result match {
|
|
1385
|
+
case Right(value) => println(s"$label -> $value")
|
|
1386
|
+
case Left(error) => println(s"$label -> typed error: $error")
|
|
1387
|
+
}
|
|
1419
1388
|
}
|
|
1420
1389
|
```
|
|
1421
1390
|
|
|
1422
|
-
Run
|
|
1391
|
+
Run this example with:
|
|
1423
1392
|
|
|
1424
1393
|
```bash
|
|
1425
|
-
sbt "streams-examples/runMain sink.
|
|
1394
|
+
sbt "streams-examples/runMain sink.SinkAsyncExample"
|
|
1426
1395
|
```
|
|
1396
|
+
|
|
1397
|
+
## See Also
|
|
1398
|
+
|
|
1399
|
+
- [Asynchronous Stream Execution](../execution-and-compatibility/async-execution.md#async-terminals) — the terminals that select a sink's asynchronous drain, and the `Async[Either[E, Z]]` convention
|
|
1400
|
+
- [Platform Differences](../execution-and-compatibility/platform-differences.md#sinkcreate-and-custom-sinks) — why `Sink.create` is JVM-only and what replaces it in shared source
|
|
1401
|
+
- [Reader](../primitives/reader.md#the-reader-union) — the two reader kinds a sink drains, from the reader's side
|
|
1402
|
+
- [Zero-Boxing Streams](../execution-and-compatibility/zero-boxing.md) — how a primitive lane is chosen, and the per-lane end-of-stream table
|
|
1403
|
+
- [Stream](./stream.md) — the producer a sink consumes
|
|
1404
|
+
- [Pipeline](./pipeline.md) — transformations that can be attached to a sink's input
|