@zio.dev/zio-blocks 0.0.33 → 0.0.51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/guides/compile-time-resource-safety-with-scope.md +16 -17
  2. package/guides/getting-started-with-mux.md +1507 -0
  3. package/guides/query-dsl-extending.md +161 -102
  4. package/guides/query-dsl-fluent-builder.md +217 -157
  5. package/guides/query-dsl-reified-optics.md +12 -10
  6. package/guides/query-dsl-sql.md +246 -165
  7. package/guides/telemetry-guide.md +1069 -0
  8. package/guides/zio-schema-migration.md +29 -22
  9. package/index.md +292 -50
  10. package/package.json +1 -1
  11. package/plans/config-follow-up-prs.md +188 -0
  12. package/plans/config-pr-assessment-roadmap.md +310 -0
  13. package/reference/MuxDataFlow.jsx +250 -0
  14. package/reference/async.md +651 -0
  15. package/reference/chunk.md +3533 -308
  16. package/reference/codegen/case-class.md +436 -0
  17. package/reference/codegen/emitter-config.md +383 -0
  18. package/reference/codegen/examples.md +664 -0
  19. package/reference/codegen/field.md +316 -0
  20. package/reference/codegen/index.md +317 -0
  21. package/reference/codegen/scala-emitter.md +392 -0
  22. package/reference/codegen/scala-file.md +276 -0
  23. package/reference/codegen/sealed-trait.md +408 -0
  24. package/reference/codegen/type-definition.md +340 -0
  25. package/reference/codegen/type-ref.md +201 -0
  26. package/reference/combinators.md +347 -117
  27. package/reference/config.md +158 -0
  28. package/reference/context.md +4 -4
  29. package/reference/datastar.md +346 -0
  30. package/reference/docs.md +1461 -345
  31. package/reference/endpoint/auth-type.md +146 -0
  32. package/reference/endpoint/endpoint.md +297 -0
  33. package/reference/endpoint/http-codec.md +249 -0
  34. package/reference/endpoint/index.md +825 -0
  35. package/reference/endpoint/path-codec.md +237 -0
  36. package/reference/endpoint/route-pattern.md +196 -0
  37. package/reference/endpoint/route-tree.md +111 -0
  38. package/reference/endpoint/segment-codec.md +212 -0
  39. package/reference/html.md +1120 -0
  40. package/reference/htmx/attribute-values.md +359 -0
  41. package/reference/htmx/hx-encoding.md +111 -0
  42. package/reference/htmx/hx-params.md +204 -0
  43. package/reference/htmx/hx-swap.md +276 -0
  44. package/reference/htmx/hx-sync.md +251 -0
  45. package/reference/htmx/hx-target.md +314 -0
  46. package/reference/htmx/hx-trigger.md +457 -0
  47. package/reference/htmx/hx-url-update.md +239 -0
  48. package/reference/htmx/index.md +855 -0
  49. package/reference/http-model/index.md +47 -0
  50. package/reference/http-model/model.md +1481 -0
  51. package/reference/http-model/schema.md +747 -0
  52. package/reference/maybe.md +826 -0
  53. package/reference/media-type.md +2 -2
  54. package/reference/mux.mdx +823 -0
  55. package/reference/openapi.md +1351 -0
  56. package/reference/resource-management/defer-handle.md +1 -1
  57. package/reference/resource-management/resource.md +31 -2
  58. package/reference/resource-management/scope.md +28 -12
  59. package/reference/resource-management/wire.md +3 -7
  60. package/reference/ringbuffer/MpmcDiagram.jsx +717 -0
  61. package/reference/ringbuffer/MpscDiagram.jsx +618 -0
  62. package/reference/ringbuffer/SpmcDiagram.jsx +680 -0
  63. package/reference/ringbuffer/SpscDiagram.jsx +677 -0
  64. package/reference/ringbuffer/advanced.mdx +109 -0
  65. package/reference/ringbuffer/index.mdx +145 -0
  66. package/reference/ringbuffer/mpmc.mdx +151 -0
  67. package/reference/ringbuffer/mpsc.mdx +132 -0
  68. package/reference/ringbuffer/spmc.mdx +108 -0
  69. package/reference/ringbuffer/spsc.mdx +344 -0
  70. package/reference/{allows.md → schema/allows.md} +4 -4
  71. package/reference/{binding-resolver.md → schema/binding-resolver.md} +1 -1
  72. package/reference/{binding.md → schema/binding.md} +2 -3
  73. package/reference/schema/built-in-codecs/avro.md +451 -0
  74. package/reference/schema/built-in-codecs/bson.md +480 -0
  75. package/reference/schema/built-in-codecs/csv.md +564 -0
  76. package/reference/schema/built-in-codecs/index.md +77 -0
  77. package/reference/schema/built-in-codecs/json/index.md +295 -0
  78. package/reference/schema/built-in-codecs/json/json-config.md +217 -0
  79. package/reference/{json-patch.md → schema/built-in-codecs/json/json-patch.md} +5 -5
  80. package/reference/{json-schema.md → schema/built-in-codecs/json/json-schema.md} +14 -47
  81. package/reference/schema/built-in-codecs/json/json-selection.md +322 -0
  82. package/reference/{json.md → schema/built-in-codecs/json/json.md} +32 -64
  83. package/reference/schema/built-in-codecs/messagepack.md +508 -0
  84. package/reference/schema/built-in-codecs/thrift.md +433 -0
  85. package/reference/schema/built-in-codecs/toon.md +1078 -0
  86. package/reference/{xml.md → schema/built-in-codecs/xml.md} +13 -9
  87. package/reference/schema/built-in-codecs/yaml.md +552 -0
  88. package/reference/{codec.md → schema/codec.md} +10 -10
  89. package/reference/{dynamic-optic.md → schema/dynamic-optic.md} +151 -5
  90. package/reference/{dynamic-schema.md → schema/dynamic-schema.md} +8 -8
  91. package/reference/schema/format.md +92 -0
  92. package/reference/schema/index.md +50 -0
  93. package/reference/schema/migration.md +297 -0
  94. package/reference/{modifier.md → schema/modifier.md} +58 -7
  95. package/reference/{optics.md → schema/optics.md} +2 -2
  96. package/reference/{patch.md → schema/patch.md} +1 -1
  97. package/{path-interpolator.md → reference/schema/path-interpolator.md} +165 -72
  98. package/reference/{schema-evolution → schema/schema-evolution}/as.md +8 -8
  99. package/reference/{schema-evolution → schema/schema-evolution}/index.md +2 -2
  100. package/reference/{schema-evolution → schema/schema-evolution}/into.md +8 -8
  101. package/reference/{schema-expr.md → schema/schema-expr.md} +110 -175
  102. package/reference/{schema.md → schema/schema.md} +12 -0
  103. package/reference/{structural-types.md → schema/structural-types.md} +1 -1
  104. package/reference/{type-class-derivation.md → schema/type-class-derivation.md} +63 -1
  105. package/reference/smithy.md +533 -0
  106. package/reference/sql/db-codec-deriver.md +71 -0
  107. package/reference/sql/db-codec.md +687 -0
  108. package/reference/sql/db-con.md +271 -0
  109. package/reference/sql/db-connection.md +153 -0
  110. package/reference/sql/db-param-writer.md +77 -0
  111. package/reference/sql/db-param.md +66 -0
  112. package/reference/sql/db-result-reader.md +146 -0
  113. package/reference/sql/db-tx.md +82 -0
  114. package/reference/sql/db-value.md +41 -0
  115. package/reference/sql/ddl.md +85 -0
  116. package/reference/sql/frag.md +254 -0
  117. package/reference/sql/index.md +341 -0
  118. package/reference/sql/repo.md +600 -0
  119. package/reference/sql/sql-dialect.md +73 -0
  120. package/reference/sql/sql-logger.md +62 -0
  121. package/reference/sql/sql-name-mapper.md +70 -0
  122. package/reference/sql/table-metadata.md +134 -0
  123. package/reference/sql/table.md +448 -0
  124. package/reference/sql/transactor-zio.md +399 -0
  125. package/reference/sql/transactor.md +353 -0
  126. package/reference/sql-zio.md +112 -0
  127. package/reference/streams/concurrent-operators.md +106 -0
  128. package/reference/streams/index.md +653 -0
  129. package/reference/streams/pipeline.md +718 -0
  130. package/reference/streams/reader.md +1284 -0
  131. package/reference/streams/scala-2-compatibility.md +55 -0
  132. package/reference/streams/sink.md +1426 -0
  133. package/reference/streams/stream.md +2526 -0
  134. package/reference/streams/writer.md +1045 -0
  135. package/reference/streams/zero-boxing.md +275 -0
  136. package/reference/telemetry.md +693 -0
  137. package/reference/typeid.md +5 -19
  138. package/sidebars.js +238 -43
  139. package/reference/formats.md +0 -694
  140. package/reference/http-model.md +0 -1716
  141. package/reference/streams.md +0 -989
  142. package/ringbuffer.md +0 -249
  143. /package/reference/{json-differ.md → schema/built-in-codecs/json/json-differ.md} +0 -0
  144. /package/reference/{dynamic-value.md → schema/dynamic-value.md} +0 -0
  145. /package/reference/{lazy.md → schema/lazy.md} +0 -0
  146. /package/reference/{reflect.md → schema/reflect.md} +0 -0
  147. /package/reference/{registers.md → schema/registers.md} +0 -0
  148. /package/reference/{schema-error.md → schema/schema-error.md} +0 -0
  149. /package/reference/{syntax.md → schema/syntax.md} +0 -0
  150. /package/reference/{validation.md → schema/validation.md} +0 -0
@@ -0,0 +1,1426 @@
1
+ ---
2
+ id: sink
3
+ title: "Sink"
4
+ ---
5
+
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`. You pass a sink to [Stream.run](./stream.md) to execute the stream synchronously and get `Either[E, Z]`.
7
+
8
+ `Sink`:
9
+ - Is covariant in `E` (error) and `Z` (result) — these are outputs
10
+ - Is contravariant in `A` (input) — a `Sink[_, Any, _]` accepts any element type
11
+ - Participates in JVM primitive specialization for zero-boxing overhead
12
+ - Provides `Sink#contramap`, `Sink#map`, and `Sink#mapError` for composable transformations
13
+
14
+ Here is the structural shape of the `Sink` type:
15
+
16
+ ```scala
17
+ abstract class Sink[+E, -A, +Z] {
18
+ def contramap[A2](g: A2 => A): Sink[E, A2, Z]
19
+ def map[Z2](f: Z => Z2): Sink[E, A, Z2]
20
+ def mapError[E2](f: E => E2): Sink[E2, A, Z]
21
+ }
22
+ ```
23
+
24
+ ## Overview
25
+
26
+ Sink is the terminal piece in the streaming architecture. A [Stream](./stream.md) describes *what* to produce, a [Pipeline](./pipeline.md) describes *how* to transform, and a Sink describes *how to consume*:
27
+
28
+ ```
29
+ ┌──────────────┐ ┌──────────────────┐ ┌──────────────┐
30
+ │ Stream[E, A] │ ──→ │ Pipeline[A, B] │ ──→ │ Sink[E, B, Z]│
31
+ └──────────────┘ └──────────────────┘ └──────────────┘
32
+ │
33
+ ┌───────▼──────┐
34
+ │ Either[E, Z] │
35
+ └──────────────┘
36
+ ```
37
+
38
+ When you call `stream.run(sink)`:
39
+ 1. The stream compiles into a `Reader` (a low-level pull-based source)
40
+ 2. The sink's internal `Sink#drain` method pulls elements in a tight loop until end-of-stream
41
+ 3. On success, the result wraps in `Right(z)`
42
+ 4. Typed errors (`E`) surface as `Left(e)`, while untyped defects propagate as exceptions
43
+ 5. The reader's `close()` runs in a `finally` block, ensuring resource safety
44
+
45
+ ## Predefined Sinks
46
+
47
+ These are value sinks (no factory arguments). They work on any element type.
48
+
49
+ ### `Sink.drain` — Discard All Elements
50
+
51
+ Consumes every element and discards them. Returns `Unit`:
52
+
53
+ ```scala
54
+ object Sink {
55
+ val drain: Sink[Nothing, Any, Unit]
56
+ }
57
+ ```
58
+
59
+ Use `Sink.drain` when you only care about side effects (e.g., via `Stream#tapEach`) and not the elements themselves:
60
+
61
+ ```scala
62
+ import zio.blocks.streams._
63
+ import scala.collection.mutable.Buffer
64
+
65
+ val log = Buffer[String]()
66
+ // log: Buffer[String] = ArrayBuffer(
67
+ // "Processing: 1",
68
+ // "Processing: 2",
69
+ // "Processing: 3"
70
+ // )
71
+ val result = Stream(1, 2, 3)
72
+ .tapEach(x => log += s"Processing: $x")
73
+ .run(Sink.drain)
74
+ // result: Either[Nothing, Unit] = Right(())
75
+ // result is Right(())
76
+ // log contains: ["Processing: 1", "Processing: 2", "Processing: 3"]
77
+ ```
78
+
79
+ ### `Sink.count` — Count Elements
80
+
81
+ Counts the total number of elements consumed. Returns `Long`:
82
+
83
+ ```scala
84
+ object Sink {
85
+ val count: Sink[Nothing, Any, Long]
86
+ }
87
+ ```
88
+
89
+ Count all elements in a stream:
90
+
91
+ ```scala
92
+ import zio.blocks.streams._
93
+
94
+ val result = Stream(1, 2, 3, 4, 5).run(Sink.count)
95
+ // result: Either[Nothing, Long] = Right(5L)
96
+ ```
97
+
98
+ ### `Sink.sumInt` / `Sink.sumLong` / `Sink.sumFloat` / `Sink.sumDouble` — Typed Numeric Sums
99
+
100
+ Returns the sum of all elements as a numeric type. Each sink accepts the corresponding primitive type:
101
+
102
+ ```scala
103
+ object Sink {
104
+ val sumInt: Sink[Nothing, Int, Long]
105
+ val sumLong: Sink[Nothing, Long, Long]
106
+ val sumFloat: Sink[Nothing, Float, Double]
107
+ val sumDouble: Sink[Nothing, Double, Double]
108
+ }
109
+ ```
110
+
111
+ Note that `Sink.sumInt` returns `Long` (to avoid overflow) and `Sink.sumFloat` returns `Double` (to reduce rounding loss):
112
+
113
+ ```scala
114
+ import zio.blocks.streams._
115
+
116
+ val intSum = Stream(1, 2, 3, 4, 5).run(Sink.sumInt)
117
+ // intSum: Either[Nothing, Long] = Right(15L)
118
+
119
+ val doubleSum = Stream(1.5, 2.5, 3.0).run(Sink.sumDouble)
120
+ // doubleSum: Either[Nothing, Double] = Right(7.0)
121
+ ```
122
+
123
+ ## Construction
124
+
125
+ Sinks are created using factory methods on the companion object. These methods fall into several categories based on what they do:
126
+
127
+ ### Collecting
128
+
129
+ Gather elements into collections:
130
+
131
+ #### `Sink.collectAll[A]` — Collect into a Chunk
132
+
133
+ Collects all elements into a `Chunk[A]`:
134
+
135
+ ```scala
136
+ object Sink {
137
+ def collectAll[A]: Sink[Nothing, A, Chunk[A]]
138
+ }
139
+ ```
140
+
141
+ This is the sink behind `Stream.runCollect`:
142
+
143
+ ```scala
144
+ import zio.blocks.streams._
145
+
146
+ val result = Stream(1, 2, 3).run(Sink.collectAll[Int])
147
+ // result: Either[Nothing, Chunk[Int]] = Right(IndexedSeq(1, 2, 3))
148
+ ```
149
+
150
+ #### `Sink.take[A]` — Collect First N Elements
151
+
152
+ Collects at most `n` elements into a `Chunk[A]`, then stops (short-circuiting the upstream):
153
+
154
+ ```scala
155
+ object Sink {
156
+ def take[A](n: Int): Sink[Nothing, A, Chunk[A]]
157
+ }
158
+ ```
159
+
160
+ Collect only the first three elements from a large stream:
161
+
162
+ ```scala
163
+ import zio.blocks.streams._
164
+
165
+ val result = Stream.range(0, 1000).run(Sink.take(3))
166
+ // result: Either[Nothing, Chunk[Int]] = Right(IndexedSeq(0, 1, 2))
167
+ ```
168
+
169
+ ### Aggregation and Search
170
+
171
+ These sinks combine elements into a single result or search for specific elements within a stream:
172
+
173
+ #### `Sink.foldLeft[A, Z]` — General Left Fold
174
+
175
+ Folds all elements using an accumulator function, starting from initial value `z`:
176
+
177
+ ```scala
178
+ object Sink {
179
+ def foldLeft[A, Z](z: Z)(f: (Z, A) => Z): Sink[Nothing, A, Z]
180
+ }
181
+ ```
182
+
183
+ This is the most general aggregation sink:
184
+
185
+ ```scala
186
+ import zio.blocks.streams._
187
+
188
+ val sum = Stream(1, 2, 3, 4).run(Sink.foldLeft(0)(_ + _))
189
+ // sum: Either[Nothing, Int] = Right(10)
190
+
191
+ val concat = Stream("a", "b", "c").run(Sink.foldLeft("")(_ + _))
192
+ // concat: Either[Nothing, String] = Right("abc")
193
+ ```
194
+
195
+ #### `Sink.head[A]` — First Element
196
+
197
+ Returns the first element wrapped in `Some`, or `None` for an empty stream:
198
+
199
+ ```scala
200
+ object Sink {
201
+ def head[A]: Sink[Nothing, A, Option[A]]
202
+ }
203
+ ```
204
+
205
+ Get the first element from a stream, or None if empty:
206
+
207
+ ```scala
208
+ import zio.blocks.streams._
209
+
210
+ val first = Stream(10, 20, 30).run(Sink.head[Int])
211
+ // first: Either[Nothing, Option[Int]] = Right(Some(10))
212
+
213
+ val empty = Stream.empty.run(Sink.head[Int])
214
+ // empty: Either[Nothing, Option[Int]] = Right(None)
215
+ ```
216
+
217
+ #### `Sink.last[A]` — Last Element
218
+
219
+ Returns the last element wrapped in `Some`, or `None` for an empty stream. Must consume all elements:
220
+
221
+ ```scala
222
+ object Sink {
223
+ def last[A]: Sink[Nothing, A, Option[A]]
224
+ }
225
+ ```
226
+
227
+ Get the last element from a stream:
228
+
229
+ ```scala
230
+ import zio.blocks.streams._
231
+
232
+ val result = Stream(10, 20, 30).run(Sink.last[Int])
233
+ // result: Either[Nothing, Option[Int]] = Right(Some(30))
234
+ ```
235
+
236
+ #### `Sink.find[A]` — First Matching Element
237
+
238
+ Returns the first element satisfying `pred`, or `None`. Short-circuits on first match:
239
+
240
+ ```scala
241
+ object Sink {
242
+ def find[A](pred: A => Boolean): Sink[Nothing, A, Option[A]]
243
+ }
244
+ ```
245
+
246
+ Find the first even number in the stream:
247
+
248
+ ```scala
249
+ import zio.blocks.streams._
250
+
251
+ val found = Stream(1, 3, 4, 6).run(Sink.find[Int](_ % 2 == 0))
252
+ // found: Either[Nothing, Option[Int]] = Right(Some(4))
253
+ ```
254
+
255
+ #### `Sink.exists[A]` — Any Element Matches
256
+
257
+ Returns `true` if any element satisfies `pred`. Short-circuits on first match:
258
+
259
+ ```scala
260
+ object Sink {
261
+ def exists[A](pred: A => Boolean): Sink[Nothing, A, Boolean]
262
+ }
263
+ ```
264
+
265
+ Check if any element matches a condition:
266
+
267
+ ```scala
268
+ import zio.blocks.streams._
269
+
270
+ val hasNegative = Stream(1, -2, 3).run(Sink.exists[Int](_ < 0))
271
+ // hasNegative: Either[Nothing, Boolean] = Right(true)
272
+ ```
273
+
274
+ #### `Sink.forall[A]` — All Elements Match
275
+
276
+ Returns `true` if all elements satisfy `pred`. Short-circuits to `false` on first failure:
277
+
278
+ ```scala
279
+ object Sink {
280
+ def forall[A](pred: A => Boolean): Sink[Nothing, A, Boolean]
281
+ }
282
+ ```
283
+
284
+ Test whether all elements satisfy a condition:
285
+
286
+ ```scala
287
+ import zio.blocks.streams._
288
+
289
+ val allPositive = Stream(1, 2, 3).run(Sink.forall[Int](_ > 0))
290
+ // allPositive: Either[Nothing, Boolean] = Right(true)
291
+
292
+ val notAll = Stream(1, -2, 3).run(Sink.forall[Int](_ > 0))
293
+ // notAll: Either[Nothing, Boolean] = Right(false)
294
+ ```
295
+
296
+ ### Effectful
297
+
298
+ These sinks perform side effects during stream consumption:
299
+
300
+ #### `Sink.foreach[A]` — Apply Side Effect to Each Element
301
+
302
+ Applies `f` to every element for side effects. Returns `Unit`:
303
+
304
+ ```scala
305
+ object Sink {
306
+ def foreach[A](f: A => Unit): Sink[Nothing, A, Unit]
307
+ }
308
+ ```
309
+
310
+ Print each element as it is processed:
311
+
312
+ ```scala
313
+ import zio.blocks.streams._
314
+
315
+ val result = Stream(1, 2, 3).run(Sink.foreach[Int](x => println(s"Got: $x")))
316
+ // Got: 1
317
+ // Got: 2
318
+ // Got: 3
319
+ // result: Either[Nothing, Unit] = Right(())
320
+ ```
321
+
322
+ ### Failing
323
+
324
+ These sinks can be used to produce typed errors or fail under specific conditions:
325
+
326
+ #### `Sink.fail[E]` — Immediately Fail
327
+
328
+ Creates a sink that fails immediately with a typed error, without consuming any elements:
329
+
330
+ ```scala
331
+ object Sink {
332
+ def fail[E](e: E): Sink[E, Any, Nothing]
333
+ }
334
+ ```
335
+
336
+ Use this in conditional sink construction:
337
+
338
+ ```scala
339
+ import zio.blocks.streams._
340
+
341
+ val sink: Sink[String, Int, Long] =
342
+ if (false) Sink.count
343
+ else Sink.fail("not ready")
344
+ // sink: Sink[String, Int, Long] = zio.blocks.streams.Sink$$anon$8@a09b6b
345
+
346
+ val result = Stream(1, 2, 3).run(sink)
347
+ // result: Either[String, Long] = Left("not ready")
348
+ ```
349
+
350
+ ### I/O
351
+
352
+ Write elements to Java I/O destinations:
353
+
354
+ #### `Sink.fromOutputStream` — Write Bytes
355
+
356
+ Writes every `Byte` element to a `java.io.OutputStream`:
357
+
358
+ ```scala
359
+ object Sink {
360
+ def fromOutputStream(os: java.io.OutputStream): Sink[Nothing, Byte, Unit]
361
+ }
362
+ ```
363
+ The sink does **not** close the stream when done. This is intentional: you own the stream's lifecycle, not the sink. You're responsible for closing it yourself (typically via try-with-resources or explicit `close()` calls) to flush buffers and release system resources. This design gives you flexibility to reuse the stream after the sink finishes, or to coordinate closing with other stream operations:
364
+
365
+ ```scala
366
+ import zio.blocks.streams._
367
+ import java.io.ByteArrayOutputStream
368
+
369
+ val bos = new ByteArrayOutputStream()
370
+ // bos: ByteArrayOutputStream = Hi!
371
+
372
+ // Write first batch of bytes
373
+ Stream.fromChunk(zio.blocks.chunk.Chunk[Byte](72, 105)).run(Sink.fromOutputStream(bos))
374
+ // res14: Either[Nothing, Unit] = Right(())
375
+
376
+ // Write second batch to the same stream (reuse it)
377
+ Stream.fromChunk(zio.blocks.chunk.Chunk[Byte](33)).run(Sink.fromOutputStream(bos))
378
+ // res15: Either[Nothing, Unit] = Right(())
379
+
380
+ // When done writing all batches, YOU close the stream
381
+ bos.close()
382
+
383
+ // ByteArrayOutputStream ignores close(), so you can still call toByteArray()
384
+ val allBytes = bos.toByteArray()
385
+ // allBytes: Array[Byte] = Array(72, 105, 33)
386
+ // This works because ByteArrayOutputStream doesn't maintain any closeable resources
387
+ ```
388
+
389
+ #### `Sink.fromJavaWriter` — Write Characters
390
+
391
+ Writes every `Char` element to a `java.io.Writer`. Does not close the writer when done — you own its lifecycle:
392
+
393
+ ```scala
394
+ object Sink {
395
+ def fromJavaWriter(w: java.io.Writer): Sink[Nothing, Char, Unit]
396
+ }
397
+ ```
398
+
399
+ Write a stream of characters to a StringWriter and access the accumulated text:
400
+
401
+ ```scala
402
+ import zio.blocks.streams._
403
+ import java.io.StringWriter
404
+
405
+ val writer = new StringWriter()
406
+ // writer: StringWriter = Hello World
407
+
408
+ // Write a stream of individual characters
409
+ Stream('H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd')
410
+ .run(Sink.fromJavaWriter(writer))
411
+ // res18: Either[Nothing, Unit] = Right(())
412
+
413
+ // Get the final string
414
+ val result = writer.toString()
415
+ // result: String = "Hello World"
416
+ ```
417
+
418
+ Like `Sink.fromOutputStream`, this sink intentionally does not close the writer. This gives you control over when to flush or close, allowing you to write multiple streams to the same writer or coordinate lifecycle with other operations.
419
+
420
+ ### Custom
421
+
422
+ Advanced low-level use cases with direct reader protocol access:
423
+
424
+ #### `Sink.create[E, A, Z]` — Escape Hatch
425
+
426
+ Creates a sink from a raw function that takes a `Reader[A]` and returns `Z`. This is the low-level escape hatch for writing sinks that cannot be expressed using the built-in factories:
427
+
428
+ ```scala
429
+ object Sink {
430
+ def create[E, A, Z](f: Reader[A] => Z): Sink[E, A, Z]
431
+ }
432
+ ```
433
+
434
+ :::note
435
+ `Sink.create` gives you direct access to the `Reader`, so you are responsible for using the correct read protocol (`Reader#read(sentinel)` for AnyRef, `Reader#readInt(sentinel)` for Int, etc.). Prefer the built-in sinks when possible.
436
+ :::
437
+
438
+ Here's a custom sink that computes the average of all integers in a stream:
439
+
440
+ ```scala
441
+ import zio.blocks.streams._
442
+ import zio.blocks.streams.io.Reader
443
+
444
+ // A custom sink that computes the average of Ints
445
+ val average = Sink.create[Nothing, Int, Double] { reader =>
446
+ def loop(sum: Long, count: Long): (Long, Long) = {
447
+ val v = reader.readInt(Long.MinValue)
448
+ if (v.asInstanceOf[Long] == Long.MinValue) (sum, count)
449
+ else {
450
+ val newSum = sum + v
451
+ loop(newSum, count + 1)
452
+ }
453
+ }
454
+ val (sum, count) = loop(0L, 0L)
455
+ if (count == 0) 0.0 else sum.toDouble / count
456
+ }
457
+ ```
458
+
459
+ This example shows how `Sink.create` works. The reader reads elements using `Reader#read[Any](null)` — the sentinel protocol — where `null` signals "read the next element" and the function returns `null` when the stream ends. We accumulate the sum and count via recursion, then return the average. You'd use `Sink.create` when no built-in sink provides the exact aggregation or transformation logic you need — it's powerful but requires understanding the low-level [Reader protocol](./reader.md).
460
+
461
+ ## Transforming Sinks
462
+
463
+ Every sink can be transformed using these instance methods:
464
+
465
+ ### `Sink#contramap[A2]` — Pre-Process Input
466
+
467
+ Transforms the input elements before they reach the sink. The sink's result and error types are unchanged:
468
+
469
+ ```scala
470
+ trait Sink[+E, -A, +Z] {
471
+ def contramap[A2](g: A2 => A): Sink[E, A2, Z]
472
+ }
473
+ ```
474
+
475
+ `Sink#contramap` is the dual of `Sink#map`: it transforms what goes *in*, not what comes *out*:
476
+
477
+ ```scala
478
+ import zio.blocks.streams._
479
+
480
+ // A sink that counts the length of strings
481
+ val totalLength: Sink[Nothing, String, Long] =
482
+ Sink.sumInt.contramap[String](_.length)
483
+ // totalLength: Sink[Nothing, String, Long] = zio.blocks.streams.Sink$Contramapped@5360fe09
484
+
485
+ val result = Stream("hello", "world").run(totalLength)
486
+ // result: Either[Nothing, Long] = Right(10L)
487
+ ```
488
+
489
+ ### `Sink#map[Z2]` — Transform Result
490
+
491
+ Transforms the result after the sink finishes draining:
492
+
493
+ ```scala
494
+ trait Sink[+E, -A, +Z] {
495
+ def map[Z2](f: Z => Z2): Sink[E, A, Z2]
496
+ }
497
+ ```
498
+
499
+ Transform the result after draining:
500
+
501
+ ```scala
502
+ import zio.blocks.streams._
503
+
504
+ val countAsString: Sink[Nothing, Any, String] =
505
+ Sink.count.map(n => s"Total: $n elements")
506
+ // countAsString: Sink[Nothing, Any, String] = zio.blocks.streams.Sink$Mapped@96f085b
507
+
508
+ val result = Stream(1, 2, 3).run(countAsString)
509
+ // result: Either[Nothing, String] = Right("Total: 3 elements")
510
+ ```
511
+
512
+ ### `Sink#mapError[E2]` — Transform Error
513
+
514
+ Transforms the error channel of a sink:
515
+
516
+ ```scala
517
+ trait Sink[+E, -A, +Z] {
518
+ inline def mapError[E2](f: E => E2): Sink[E2, A, Z]
519
+ }
520
+ ```
521
+
522
+ This method uses Scala 3's `inline` + `summonFrom` to perform a compile-time check: if `E` is `Nothing` (the sink never fails), the compiler elides the wrapper entirely and returns `this` cast to the new type with zero allocation:
523
+
524
+ ```scala
525
+ import zio.blocks.streams._
526
+
527
+ // No-op: drain never fails, so mapError is free
528
+ val mapped = Sink.drain.mapError[String](_.toString)
529
+ // At compile time: this is just a cast, no wrapper allocated
530
+
531
+ // Real mapping: fail can produce errors
532
+ val failing = Sink.fail("oops").mapError[RuntimeException](new RuntimeException(_))
533
+ ```
534
+
535
+ ## Integration with Stream
536
+
537
+ `Stream.run(sink)` is the primary entry point. ZIO Blocks also provides convenience methods on `Stream` that delegate to built-in sinks:
538
+
539
+ | Stream method | Equivalent Sink |
540
+ |------------------------|-----------------------------------|
541
+ | `stream.runCollect` | `stream.run(Sink.collectAll)` |
542
+ | `stream.runDrain` | `stream.run(Sink.drain)` |
543
+ | `stream.runForeach(f)` | `stream.run(Sink.foreach(f))` |
544
+ | `stream.runFold(z)(f)` | `stream.run(Sink.foldLeft(z)(f))` |
545
+ | `stream.count` | `stream.run(Sink.count)` |
546
+ | `stream.head` | `stream.run(Sink.head)` |
547
+ | `stream.last` | `stream.run(Sink.last)` |
548
+ | `stream.find(pred)` | `stream.run(Sink.find(pred))` |
549
+ | `stream.exists(pred)` | `stream.run(Sink.exists(pred))` |
550
+ | `stream.forall(pred)` | `stream.run(Sink.forall(pred))` |
551
+
552
+ The `runFold` method with primitive accumulator types (`Int`, `Long`, `Double`) uses specialized internal sink classes that keep the accumulator unboxed.
553
+
554
+ See [Stream — Running Streams](./stream.md#running-streams) for more details on terminal operations.
555
+
556
+ ## Integration with Pipeline
557
+
558
+ A [Pipeline](./pipeline.md) can be applied to a Sink using `Pipeline#andThenSink`, producing a new Sink that pre-processes input elements through the pipeline:
559
+
560
+ ```scala
561
+ import zio.blocks.streams._
562
+ import zio.blocks.chunk.Chunk
563
+
564
+ val cleanAndCollect: Sink[Nothing, String, Chunk[String]] =
565
+ Pipeline.map[String, String](_.trim.toLowerCase)
566
+ .andThenSink(Sink.collectAll[String])
567
+ // cleanAndCollect: Sink[Nothing, String, Chunk[String]] = zio.blocks.streams.Sink$Contramapped@2d273af9
568
+
569
+ val result = Stream(" Hello ", " WORLD ").run(cleanAndCollect)
570
+ // result: Either[Nothing, Chunk[String]] = Right(IndexedSeq("hello", "world"))
571
+ ```
572
+
573
+ The equivalence law holds: `stream.via(pipe).run(sink) == stream.run(pipe.andThenSink(sink))`.
574
+
575
+ See [Pipeline — Applying to a Sink](./pipeline.md#applying-to-a-sink) for more details.
576
+
577
+ ## JVM NIO Sinks
578
+
579
+ The `NioSinks` object (JVM-only) provides sinks for Java NIO (`java.nio`) buffers and channels. These exist because NIO is the standard high-performance I/O mechanism on the JVM: non-blocking, memory-efficient, and capable of handling thousands of concurrent connections. When you're writing to network sockets, memory-mapped files, or other NIO-based resources, these sinks give you a convenient way to drain streams directly into NIO data structures without intermediate allocation or copying.
580
+
581
+ Traditional Java I/O (`OutputStream`, `Writer`) blocks threads and requires manual buffering for efficiency. NIO provides non-blocking channels, but using them directly requires buffer allocation, position management, and explicit flushing. `NioSinks` bridges this gap: `NioSinks.fromChannel` handles buffering automatically (default 8KB), while typed variants like `NioSinks.fromByteBufferInt` and `NioSinks.fromByteBufferLong` eliminate boxing overhead by writing primitives directly to buffers you provide.
582
+
583
+ Choose `NioSinks.fromChannel` when you need to write to network sockets or files and cannot afford to block threads. Choose typed variants when you control buffer allocation and are streaming millions of primitives where boxing would degrade performance. **Important:** Read the Sentinel Value Limitation section below—it describes a hard constraint that affects your choice depending on whether your data can contain specific values.
584
+
585
+ Here are the available NIO sinks:
586
+
587
+ ```scala
588
+ object NioSinks {
589
+ def fromByteBuffer (buf: ByteBuffer): Sink[Nothing, Byte, Unit]
590
+ def fromByteBufferInt (buf: ByteBuffer): Sink[Nothing, Int, Unit]
591
+ def fromByteBufferLong (buf: ByteBuffer): Sink[Nothing, Long, Unit]
592
+ def fromByteBufferFloat (buf: ByteBuffer): Sink[Nothing, Float, Unit]
593
+ def fromByteBufferDouble(buf: ByteBuffer): Sink[Nothing, Double, Unit]
594
+ def fromChannel(ch: WritableByteChannel, bufSize: Int = 8192): Sink[IOException, Byte, Unit]
595
+ }
596
+ ```
597
+
598
+ ### From ByteBuffer Sinks
599
+
600
+ **`NioSinks.fromByteBuffer` and typed variants** — Write primitive streams directly into a pre-allocated NIO ByteBuffer:
601
+ - `NioSinks.fromByteBuffer` — writes individual `Byte` elements using a read sentinel of `-1`. Use only for unstructured byte data.
602
+ - `NioSinks.fromByteBufferInt`, `NioSinks.fromByteBufferLong`, `NioSinks.fromByteBufferFloat`, `NioSinks.fromByteBufferDouble` — write primitives directly using the buffer's native methods (`putInt`, `putLong`, etc.). These avoid boxing and are faster than the byte variant.
603
+
604
+ Here's an example using ByteBuffer with typed primitive writes:
605
+
606
+ ```scala
607
+ import zio.blocks.streams._
608
+ import zio.blocks.streams.NioSinks
609
+ import java.nio.ByteBuffer
610
+ import java.nio.ByteOrder
611
+
612
+ val buffer = ByteBuffer.allocate(32).order(ByteOrder.BIG_ENDIAN)
613
+ // buffer: ByteBuffer = java.nio.HeapByteBuffer[pos=32 lim=32 cap=32]
614
+
615
+ // Write a stream of Longs to the buffer
616
+ Stream(1L, 2L, 3L, 4L).run(NioSinks.fromByteBufferLong(buffer))
617
+ // res25: Either[Nothing, Unit] = Right(())
618
+
619
+ // After writing, rewind to read
620
+ buffer.rewind()
621
+ // res26: ByteBuffer = java.nio.HeapByteBuffer[pos=32 lim=32 cap=32]
622
+
623
+ val readBack = List(
624
+ buffer.getLong(),
625
+ buffer.getLong(),
626
+ buffer.getLong(),
627
+ buffer.getLong()
628
+ )
629
+ // readBack: List[Long] = List(1L, 2L, 3L, 4L)
630
+ ```
631
+
632
+ This example allocates a 32-byte buffer (4 Longs × 8 bytes each), writes four `Long` values using `NioSinks.fromByteBufferLong` (which efficiently calls `putLong` on each element), then rewinds and reads them back to verify. The typed variant is significantly faster than `NioSinks.fromByteBuffer` because it operates at the primitive level — no boxing, no element-by-element byte writing.
633
+
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 enable zero-copy batch processing.
635
+
636
+ Here is the complete example:
637
+
638
+ ```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
+ package sink
656
+
657
+ import zio.blocks.streams.*
658
+ import zio.blocks.streams.NioSinks
659
+ import java.nio.ByteBuffer
660
+ import scala.math.pow
661
+
662
+ object SinkScientificComputingExample extends App {
663
+ println("=== Batching Doubles for Scientific Computing ===\n")
664
+
665
+ // Simulate raw sensor measurements that need calibration
666
+ println("Scenario: Streaming voltage sensor measurements with calibration curve\n")
667
+
668
+ val measurementCount = 10
669
+ val bufferCapacity = measurementCount * 8 // 8 bytes per Double
670
+
671
+ println(s"Processing $measurementCount measurements...\n")
672
+
673
+ // Generate raw voltage readings (0.0 to 0.09)
674
+ val rawVoltages = (0 until measurementCount).map(i => (i * 0.01).toDouble).toList
675
+
676
+ println("Raw measurements (voltage):")
677
+ rawVoltages.zipWithIndex.foreach { case (v, i) =>
678
+ println(f" [$i] $v%.4f V")
679
+ }
680
+ println()
681
+
682
+ // Process and calibrate measurements
683
+ val buffer = processAndBufferMeasurements(measurementCount, bufferCapacity)
684
+
685
+ println("After calibration (applied quadratic correction: V' = V × (1 + 0.05×V²)):\n")
686
+
687
+ // Read back and display calibrated values
688
+ buffer.rewind()
689
+ var index = 0
690
+ while (buffer.hasRemaining) {
691
+ val calibrated = buffer.getDouble()
692
+ println(f" [$index] $calibrated%.6f V")
693
+ index += 1
694
+ }
695
+
696
+ println("\n=== Pattern Use Cases ===")
697
+ println("This pattern is used in:")
698
+ println(" • Scientific instrumentation (analog-to-digital conversion)")
699
+ println(" • Machine learning pipelines (sensor data → training batches)")
700
+ println(" • Signal processing (raw signals → preprocessed data → computation)")
701
+ println("\nKey benefits:")
702
+ println(" • Zero-copy batch processing with DirectByteBuffer")
703
+ println(" • Efficient numerical stream transformation")
704
+ println(" • Memory-friendly for large datasets")
705
+
706
+ // Process and buffer measurements using NioSinks
707
+ def processAndBufferMeasurements(
708
+ measurementCount: Int,
709
+ bufferCapacity: Int
710
+ ): ByteBuffer = {
711
+ val buffer = ByteBuffer.allocateDirect(bufferCapacity)
712
+
713
+ // Stream of raw voltage measurements (need calibration)
714
+ val voltages = Stream.range(0, measurementCount).map(i => (i * 0.01).toDouble)
715
+
716
+ // Apply calibration curve: quadratic correction
717
+ // This simulates real sensor calibration with nonlinear response
718
+ val calibrated = voltages.map { raw =>
719
+ val calibrationFactor = 1.0 + (0.05 * pow(raw, 2))
720
+ raw * calibrationFactor
721
+ }
722
+
723
+ // Write calibrated values directly to ByteBuffer using typed sink
724
+ // This is much faster than element-by-element byte writing
725
+ calibrated.run(NioSinks.fromByteBufferDouble(buffer))
726
+ buffer.flip()
727
+ buffer
728
+ }
729
+ }
730
+ ```
731
+
732
+ Run this example with:
733
+
734
+ ```bash
735
+ sbt "streams-examples/runMain sink.SinkScientificComputingExample"
736
+ ```
737
+
738
+ 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
+
740
+ :::warning[Sentinel Collisions Throw — Never Silently Truncate]
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.
895
+
896
+ ### From Channel Sink
897
+
898
+ The **`Sink.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.
899
+
900
+ 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
+
902
+ Suppose you're collecting metrics from thousands of sensors (temperature, pressure, timestamps) and need to write them to a file efficiently. Using `NioSinks.fromChannel` with a file's `WritableByteChannel` gives you automatic buffering and eliminates manual position management.
903
+
904
+ Here is the complete example:
905
+
906
+ ```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
+ package sink
924
+
925
+ import zio.blocks.streams.*
926
+ import zio.blocks.streams.NioSinks
927
+ import java.io.RandomAccessFile
928
+ import java.nio.file.Files
929
+ import scala.util.Using
930
+
931
+ object SinkTelemetryExample extends App {
932
+ println("=== Streaming Telemetry to File Channel ===\n")
933
+
934
+ // Simulated sensor readings (timestamp, temperature)
935
+ case class SensorReading(timestamp: Long, temperature: Double) {
936
+ override def toString: String = f"[$timestamp] $temperature%.2f°C"
937
+ }
938
+
939
+ // Generate mock sensor data
940
+ val sensorReadings = List(
941
+ SensorReading(1000L, 22.5),
942
+ SensorReading(1100L, 23.1),
943
+ SensorReading(1200L, 22.8),
944
+ SensorReading(1300L, 23.4),
945
+ SensorReading(1400L, 24.0)
946
+ )
947
+
948
+ println("Sensor readings to write:")
949
+ sensorReadings.foreach(r => println(s" $r"))
950
+ println()
951
+
952
+ // Write telemetry to file using NioSinks.fromChannel
953
+ val tempFile = Files.createTempFile("telemetry", ".bin")
954
+ val filePath = tempFile.toString
955
+
956
+ println(s"Writing to $filePath...")
957
+ Using(new RandomAccessFile(filePath, "rw")) { file =>
958
+ val channel = file.getChannel
959
+ channel.truncate(0) // Clear file
960
+
961
+ // Serialize readings into a single buffer: 8 bytes timestamp + 8 bytes temperature per reading
962
+ val buffer = java.nio.ByteBuffer.allocate(sensorReadings.length * 16)
963
+ sensorReadings.foreach { reading =>
964
+ buffer.putLong(reading.timestamp)
965
+ buffer.putDouble(reading.temperature)
966
+ }
967
+ buffer.flip()
968
+
969
+ // Write all bytes to file with internal buffering (8KB chunks)
970
+ val bytes = buffer.array()
971
+ val byteStream = Stream.fromChunk(zio.blocks.chunk.Chunk.fromArray(bytes))
972
+ byteStream.run(NioSinks.fromChannel(channel, bufSize = 8192))
973
+
974
+ println(s"✓ Wrote ${file.length()} bytes to disk")
975
+ }.get
976
+
977
+ // Read back and verify
978
+ println("\nVerifying written data:")
979
+ Using(new RandomAccessFile(filePath, "r")) { file =>
980
+ val buf = java.nio.ByteBuffer.allocate((8 + 8) * sensorReadings.length)
981
+ file.getChannel.read(buf)
982
+ buf.rewind()
983
+
984
+ var count = 0
985
+ while (buf.remaining() >= 16) {
986
+ val timestamp = buf.getLong()
987
+ val temperature = buf.getDouble()
988
+ println(f" [$timestamp] $temperature%.2f°C")
989
+ count += 1
990
+ }
991
+ println(s"✓ Read back $count sensor readings")
992
+ }.get
993
+
994
+ println("\n=== Pattern Use Cases ===")
995
+ println("This pattern is used in:")
996
+ println(" • IoT telemetry platforms (time-series databases)")
997
+ println(" • High-throughput logging systems")
998
+ println(" • Sensor data aggregation pipelines")
999
+ println("\nKey benefits:")
1000
+ println(" • Automatic buffering eliminates manual position management")
1001
+ println(" • Integrated with Stream composition (no boilerplate)")
1002
+ println(" • Type-safe error handling (IOException as Sink error type)")
1003
+
1004
+ Files.delete(tempFile)
1005
+ }
1006
+ ```
1007
+
1008
+
1009
+ Run it with:
1010
+
1011
+ ```bash
1012
+ sbt "streams-examples/runMain sink.SinkTelemetryExample"
1013
+ ```
1014
+
1015
+ This pattern is common in high-throughput logging systems, time-series databases, and IoT platforms where you need to write streams of telemetry data to persistent storage without blocking or allocating excessively.
1016
+
1017
+ ## Running the Examples
1018
+
1019
+ All code from this guide is available as runnable examples in the `streams-examples` module.
1020
+
1021
+ Start by cloning the repository and navigating to the project:
1022
+
1023
+ ```bash
1024
+ git clone https://github.com/zio/zio-blocks.git
1025
+ cd zio-blocks
1026
+ ```
1027
+
1028
+ Run individual examples with sbt:
1029
+
1030
+ ### Basic Usage
1031
+
1032
+ This example demonstrates the most commonly used built-in sinks: `Sink.drain`, `Sink.count`, `Sink.collectAll`, `Sink.head`, `Sink.last`, and `Sink.take`:
1033
+
1034
+ ```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
+ package sink
1052
+
1053
+ import zio.blocks.streams.*
1054
+ import zio.sbt.ExprEval.show
1055
+
1056
+ object SinkBasicUsageExample extends App {
1057
+ println("=== Sink Basic Usage ===\n")
1058
+
1059
+ val data = Stream(1, 2, 3, 4, 5)
1060
+
1061
+ // 1. Sink.drain — discard all elements
1062
+ println("1. Sink.drain — discard all elements:")
1063
+ show(data.run(Sink.drain))
1064
+
1065
+ // 2. Sink.count — count elements
1066
+ println("\n2. Sink.count — count elements:")
1067
+ show(data.run(Sink.count))
1068
+
1069
+ // 3. Sink.collectAll — collect into Chunk
1070
+ println("\n3. Sink.collectAll — collect into Chunk:")
1071
+ show(data.run(Sink.collectAll[Int]))
1072
+
1073
+ // 4. Sink.head — first element
1074
+ println("\n4. Sink.head — first element:")
1075
+ show(data.run(Sink.head[Int]))
1076
+
1077
+ println("\n Sink.head on empty stream:")
1078
+ show(Stream.empty.run(Sink.head[Int]))
1079
+
1080
+ // 5. Sink.last — last element
1081
+ println("\n5. Sink.last — last element:")
1082
+ show(data.run(Sink.last[Int]))
1083
+
1084
+ // 6. Sink.take — first n elements
1085
+ println("\n6. Sink.take — first n elements:")
1086
+ show(data.run(Sink.take(3)))
1087
+
1088
+ // 7. take on a large stream (short-circuits)
1089
+ println("\n7. Sink.take short-circuits (only reads 3 of 1000):")
1090
+ show(Stream.range(0, 1000).run(Sink.take(3)))
1091
+
1092
+ // 8. Combining stream operations with sinks
1093
+ println("\n8. Combining stream operations with explicit sinks:")
1094
+ val result = Stream(1, 2, 3, 4, 5)
1095
+ .filter(_ % 2 == 0)
1096
+ .run(Sink.collectAll[Int])
1097
+ show(result)
1098
+
1099
+ // 9. Equivalence with convenience methods
1100
+ println("\n9. Stream convenience methods delegate to sinks:")
1101
+ show(data.runCollect == data.run(Sink.collectAll[Int]))
1102
+ show(data.count == data.run(Sink.count))
1103
+ }
1104
+ ```
1105
+
1106
+ Run this example with:
1107
+
1108
+ ```bash
1109
+ sbt "streams-examples/runMain sink.SinkBasicUsageExample"
1110
+ ```
1111
+
1112
+ ### Aggregation and Search
1113
+
1114
+ This example shows aggregation sinks (`Sink.foldLeft`, `Sink.sumInt`, `Sink.sumDouble`) and search sinks (`Sink.exists`, `Sink.forall`, `Sink.find`, `Sink.foreach`):
1115
+
1116
+ ```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
+ package sink
1134
+
1135
+ import zio.blocks.streams.*
1136
+ import zio.sbt.ExprEval.show
1137
+
1138
+ object SinkAggregationExample extends App {
1139
+ println("=== Sink Aggregation and Search ===\n")
1140
+
1141
+ // 1. foldLeft — general accumulation
1142
+ println("1. Sink.foldLeft — general accumulation:")
1143
+ val sum = Stream(1, 2, 3, 4, 5).run(Sink.foldLeft(0)(_ + _))
1144
+ show(sum)
1145
+
1146
+ println("\n foldLeft with string concatenation:")
1147
+ val concat = Stream("a", "b", "c").run(Sink.foldLeft("")(_ + _))
1148
+ show(concat)
1149
+ // 2. sumInt — typed numeric sum
1150
+ println("\n2. Sink.sumInt — returns Long to avoid overflow:")
1151
+ val intSum = Stream(1, 2, 3, 4, 5).run(Sink.sumInt)
1152
+ show(intSum)
1153
+
1154
+ // 3. sumDouble — typed floating point sum
1155
+ println("\n3. Sink.sumDouble:")
1156
+ val doubleSum = Stream(1.5, 2.5, 3.0).run(Sink.sumDouble)
1157
+ show(doubleSum)
1158
+
1159
+ // 4. exists — short-circuits on first match
1160
+ println("\n4. Sink.exists — short-circuits on first match:")
1161
+ val hasNegative = Stream(1, 2, -3, 4).run(Sink.exists[Int](_ < 0))
1162
+ show(hasNegative)
1163
+
1164
+ val noNegative = Stream(1, 2, 3, 4).run(Sink.exists[Int](_ < 0))
1165
+ show(noNegative)
1166
+
1167
+ // 5. forall — all elements must match
1168
+ println("\n5. Sink.forall — all elements must match:")
1169
+ val allPositive = Stream(1, 2, 3).run(Sink.forall[Int](_ > 0))
1170
+ show(allPositive)
1171
+
1172
+ val notAllPositive = Stream(1, -2, 3).run(Sink.forall[Int](_ > 0))
1173
+ show(notAllPositive)
1174
+
1175
+ // 6. find — first element matching predicate
1176
+ println("\n6. Sink.find — first matching element:")
1177
+ val firstEven = Stream(1, 3, 4, 6, 8).run(Sink.find[Int](_ % 2 == 0))
1178
+ show(firstEven)
1179
+
1180
+ val noMatch = Stream(1, 3, 5, 7).run(Sink.find[Int](_ % 2 == 0))
1181
+ show(noMatch)
1182
+
1183
+ // 7. foreach — side effects
1184
+ println("\n7. Sink.foreach — apply side effects:")
1185
+ val items = scala.collection.mutable.Buffer[String]()
1186
+ val result = Stream("x", "y", "z").run(Sink.foreach[String](s => items += s))
1187
+ show(result)
1188
+ show(items.toList)
1189
+
1190
+ // 8. Complex aggregation: combine foldLeft with map
1191
+ println("\n8. Complex aggregation — average via foldLeft + map:")
1192
+ val average = Sink
1193
+ .foldLeft[Int, (Int, Int)]((0, 0)) { case ((sum, count), x) =>
1194
+ (sum + x, count + 1)
1195
+ }
1196
+ .map { case (sum, count) =>
1197
+ if (count == 0) 0.0 else sum.toDouble / count
1198
+ }
1199
+
1200
+ val avg = Stream(10, 20, 30, 40).run(average)
1201
+ show(avg)
1202
+ }
1203
+ ```
1204
+
1205
+ Run this example with:
1206
+
1207
+ ```bash
1208
+ sbt "streams-examples/runMain sink.SinkAggregationExample"
1209
+ ```
1210
+
1211
+ ### Transformations and Composition
1212
+
1213
+ This example demonstrates `Sink#contramap`, `Sink#map`, `Sink#mapError`, `Sink.fail`, `Sink.create`, and `Pipeline#andThenSink`:
1214
+
1215
+ ```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
+ package sink
1233
+
1234
+ import zio.blocks.streams.*
1235
+ import zio.sbt.ExprEval.show
1236
+
1237
+ object SinkTransformationExample extends App {
1238
+ println("=== Sink Transformations and Composition ===\n")
1239
+
1240
+ // 1. contramap — pre-process input
1241
+ println("1. Sink.contramap — pre-process input elements:")
1242
+ val stringLengthSum: Sink[Nothing, String, Long] =
1243
+ Sink.sumInt.contramap[String](_.length)
1244
+
1245
+ show(Stream("hello", "world").run(stringLengthSum))
1246
+
1247
+ // 2. contramap — change element type
1248
+ println("\n2. contramap to convert types:")
1249
+ val parseInts: Sink[Nothing, String, Long] =
1250
+ Sink.sumInt.contramap[String](_.toInt)
1251
+
1252
+ show(Stream("10", "20", "30").run(parseInts))
1253
+
1254
+ // 3. map — transform result
1255
+ println("\n3. Sink.map — transform the result:")
1256
+ val countFormatted: Sink[Nothing, Any, String] =
1257
+ Sink.count.map(n => s"Processed $n elements")
1258
+
1259
+ show(Stream(1, 2, 3).run(countFormatted))
1260
+
1261
+ // 4. Chaining contramap + map
1262
+ println("\n4. Chaining contramap + map:")
1263
+ val pipeline = Sink.sumInt
1264
+ .contramap[String](_.length)
1265
+ .map(total => s"Total chars: $total")
1266
+
1267
+ show(Stream("hi", "hello").run(pipeline))
1268
+
1269
+ // 5. mapError — transform error channel
1270
+ println("\n5. Sink.mapError — transform errors:")
1271
+
1272
+ sealed trait AppError
1273
+ case class ParseError(msg: String) extends AppError
1274
+
1275
+ val failingSink = Sink.fail("raw error").mapError[AppError](msg => ParseError(msg))
1276
+ show(Stream(1).run(failingSink))
1277
+
1278
+ // 6. fail — immediately fail
1279
+ println("\n6. Sink.fail — immediate failure:")
1280
+ show(Stream(1, 2, 3).run(Sink.fail("error")))
1281
+
1282
+ // 7. Pipeline.andThenSink integration
1283
+ println("\n7. Pipeline.andThenSink — pipeline pre-processes before sink:")
1284
+ val cleanAndCollect =
1285
+ Pipeline
1286
+ .map[String, String](_.trim.toLowerCase)
1287
+ .andThenSink(Sink.collectAll[String])
1288
+
1289
+ show(Stream(" Hello ", " WORLD ").run(cleanAndCollect))
1290
+
1291
+ // 8. Equivalence: via + run == andThenSink + run
1292
+ println("\n8. Equivalence law: via + run == andThenSink + run:")
1293
+ val pipe = Pipeline.filter[Int](_ > 2).andThen(Pipeline.map[Int, Int](_ * 10))
1294
+ val source = Stream(1, 2, 3, 4, 5)
1295
+
1296
+ val viaResult = source.via(pipe).run(Sink.collectAll[Int])
1297
+ val sinkResult = source.run(pipe.andThenSink(Sink.collectAll[Int]))
1298
+ show(viaResult)
1299
+ show(sinkResult)
1300
+
1301
+ // 9. Composing multiple transformations into a reusable sink
1302
+ println("\n9. Reusable composed sink:")
1303
+ case class Metric(name: String, value: Double)
1304
+
1305
+ val metricSumSink: Sink[Nothing, Metric, Double] =
1306
+ Sink.foldLeft(0.0)((acc, m: Metric) => acc + m.value)
1307
+
1308
+ val metrics = Stream(
1309
+ Metric("cpu", 45.0),
1310
+ Metric("cpu", 67.0),
1311
+ Metric("cpu", 23.0)
1312
+ )
1313
+ show(metrics.run(metricSumSink))
1314
+
1315
+ val metricAvgSink: Sink[Nothing, Metric, Double] =
1316
+ Sink
1317
+ .foldLeft[Metric, (Double, Int)]((0.0, 0)) { case ((sum, count), m) =>
1318
+ (sum + m.value, count + 1)
1319
+ }
1320
+ .map { case (sum, count) => if (count == 0) 0.0 else sum / count }
1321
+
1322
+ show(metrics.run(metricAvgSink))
1323
+ }
1324
+ ```
1325
+
1326
+ Run this example with:
1327
+
1328
+ ```bash
1329
+ sbt "streams-examples/runMain sink.SinkTransformationExample"
1330
+ ```
1331
+
1332
+ ### Sentinel Guard (NIO Typed Sinks)
1333
+
1334
+ This example demonstrates that the typed NIO sinks (`NioSinks.fromByteBufferLong`, `NioSinks.fromByteBufferDouble`) reject streams containing their sentinel value (e.g., `Long.MaxValue` for `NioSinks.fromByteBufferLong`) with an `IllegalArgumentException` instead of silently truncating — detected at zero hot-path cost via the reader's out-of-band EOF flag, consulted once after the drain loop exits:
1335
+
1336
+ ```scala title="streams-examples/src/main/scala/sink/SinkSentinelGuardExample.scala"
1337
+ /*
1338
+ * Copyright 2024-2026 John A. De Goes and the ZIO Contributors
1339
+ *
1340
+ * Licensed under the Apache License, Version 2.0 (the "License");
1341
+ * you may not use this file except in compliance with the License.
1342
+ * You may obtain a copy of the License at
1343
+ *
1344
+ * http://www.apache.org/licenses/LICENSE-2.0
1345
+ *
1346
+ * Unless required by applicable law or agreed to in writing, software
1347
+ * distributed under the License is distributed on an "AS IS" BASIS,
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.
1351
+ */
1352
+
1353
+ package sink
1354
+
1355
+ import zio.blocks.streams.*
1356
+ import zio.blocks.streams.NioSinks
1357
+ import java.nio.ByteBuffer
1358
+
1359
+ object SinkSentinelGuardExample extends App {
1360
+ println("=== Sentinel Collisions Are Rejected Loudly (Never Silently) ===\n")
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")
1385
+
1386
+ // Example 2: a sentinel-valued element is rejected with a clear error
1387
+ println("Test 2: Stream containing Long.MaxValue is rejected, not truncated")
1388
+ println("-" * 60)
1389
+
1390
+ val riskyData = List(100L, 200L, Long.MaxValue, 300L, 400L)
1391
+ println(
1392
+ f"Stream data: ${riskyData.map(v => if (v == Long.MaxValue) "Long.MaxValue" else v.toString).mkString(", ")}\n"
1393
+ )
1394
+
1395
+ val buffer2 = ByteBuffer.allocate(riskyData.length * 8)
1396
+ try {
1397
+ Stream.fromIterable(riskyData).run(NioSinks.fromByteBufferLong(buffer2))
1398
+ println("✗ UNEXPECTED: drain completed without error")
1399
+ } catch {
1400
+ case e: IllegalArgumentException =>
1401
+ println(s"✓ Rejected loudly: ${e.getMessage}")
1402
+ }
1403
+
1404
+ // Recommendations
1405
+ println("\n=== Recommendations ===")
1406
+ println("1. If your data might contain the sentinel value (Long.MaxValue for the Long")
1407
+ println(" sink, Double.MaxValue for the Double sink):")
1408
+ println(" → Use a generic sink (Sink.collectAll, Sink.foreach, Sink.foldLeft) — these")
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)")
1419
+ }
1420
+ ```
1421
+
1422
+ Run it with this command:
1423
+
1424
+ ```bash
1425
+ sbt "streams-examples/runMain sink.SinkSentinelGuardExample"
1426
+ ```