@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,1284 @@
1
+ ---
2
+ id: reader
3
+ title: "Reader"
4
+ ---
5
+
6
+ `Reader[+Elem]` is the **pull-based source that powers ZIO Blocks streams**. When you call a terminal operation like `stream.run(sink)`, the stream compiles into a `Reader`, which yields values one at a time on demand until closed.
7
+
8
+ The fundamental operations are `read(sentinel)` — returns the next element or a sentinel when exhausted — and `close()` — signals stream end and releases resources. Most users never interact with `Reader` directly, but understanding it clarifies how streams work internally.
9
+
10
+ The compilation and execution flow:
11
+
12
+ ```
13
+ Stream[E, A] ──(compile)──> Reader[A]
14
+ │
15
+ └─(drain via Sink)──> Either[E, Z]
16
+ ```
17
+
18
+ `Reader`:
19
+ - Is lazy and pull-based — `Stream` transformations don't run until `read()` is called, running in constant space one element at a time
20
+ - Is not thread-safe — designed for single-threaded consumption
21
+ - Uses a sentinel protocol where callers specify the end-of-stream value; for primitives, specialized methods like `Reader#readInt(sentinel)` avoid boxing entirely
22
+ - Dispatches on `Reader#jvmType` to use specialized, unboxed reads for primitive types
23
+ - Is the compilation target of `Stream` — when a stream runs, it becomes a `Reader`
24
+ - Guarantees resource safety by tracking and closing files, database connections, and buffers via `finally` blocks, even if consumption stops early or fails
25
+ - Supports composition by chaining readers through transformations without materializing intermediate data
26
+
27
+ Here is the core `Reader` interface with the most essential methods:
28
+
29
+ ```scala
30
+ abstract class Reader[+Elem] {
31
+ def read[A >: Elem](sentinel: A): A
32
+ def close(): Unit
33
+ def isClosed: Boolean
34
+ def readable(): Boolean
35
+ }
36
+ ```
37
+
38
+ ## Quick Showcase
39
+
40
+ Here's how to create and drain a `Reader`:
41
+
42
+ ```scala
43
+ import zio.blocks.streams.io.Reader
44
+ import zio.blocks.chunk.Chunk
45
+ import scala.collection.mutable.Buffer
46
+
47
+ val r = Reader.fromChunk(Chunk(1, 2, 3, 4, 5))
48
+ // r: Reader[Int] = zio.blocks.streams.io.Reader$FromChunkInt@2305ca8e
49
+ val collected = Buffer[Int]()
50
+ // collected: Buffer[Int] = ArrayBuffer(1, 2, 3, 4, 5)
51
+
52
+ // Pull elements until sentinel
53
+ def drainAll(): Unit = {
54
+ val elem = r.read(-1)
55
+ if (elem != -1) {
56
+ collected += elem
57
+ drainAll()
58
+ }
59
+ }
60
+ drainAll()
61
+
62
+ println(s"Collected: $collected")
63
+ // Collected: ArrayBuffer(1, 2, 3, 4, 5)
64
+ ```
65
+
66
+ ## Motivation
67
+
68
+ Imagine you're processing a massive CSV file—millions of rows of customer data. Your first instinct is to load it all into memory as a `List[Row]`, transform it, filter it, and then write the results. This works fine for small files, but one day someone feeds you a 50GB dataset and your application crashes with `OutOfMemoryError`. You've hit the fundamental problem of eager evaluation: **you must load everything before you can do anything**, and if the data is bigger than available memory, you're stuck.
69
+
70
+ Even if you manage to fit the data in memory, you've paid the startup cost upfront. If your pipeline only needs the first 100 rows to produce a result, you've wasted time and energy materializing the other millions. And if something fails partway through—a database connection drops, a file is corrupted—you've already consumed resources and may have inconsistencies to clean up.
71
+
72
+ The streaming intuition is different: instead of pulling all data at once, what if the consumer asked the producer "give me the next element?" one at a time? This way, you never hold more than one element in memory, you only do work on elements you actually use, and you can stop immediately when you have enough.
73
+
74
+ `Reader` embodies this pull-based philosophy. Rather than materializing a `List`, a `Stream` compiles down to a `Reader`—a stateful object that produces one element each time you call `read()`. The consumer (a `Sink`) drives the pace: it calls `read()` when ready, and the `Reader` computes and returns the next value. When the stream is exhausted, `Reader` returns a sentinel—a special value you provide—signaling "no more data." No exceptions, no null, no boxing overhead.
75
+
76
+ `Reader` shines when you're processing large, unbounded, or expensive-to-produce data sources: database result sets, network streams, log files, sensor data, or any pipeline where memory or time efficiency matters. Instead of hoping your data fits in memory, you pay a constant, predictable cost per element.
77
+
78
+ ## Construction
79
+
80
+ Several ways to create a `Reader`, from predefined singletons to collections and I/O sources:
81
+
82
+ ### Creating Predefined Readers
83
+
84
+ `Reader.closed` — An already-closed reader that emits no elements. Useful as a base case or for empty streams:
85
+
86
+ ```scala
87
+ object Reader {
88
+ def closed: Reader[Nothing]
89
+ }
90
+ ```
91
+
92
+ Here's how to create and use a closed reader:
93
+
94
+ ```scala
95
+ import zio.blocks.streams.io.Reader
96
+
97
+ val r = Reader.closed
98
+ // r: Reader[Nothing] = zio.blocks.streams.io.Reader$ClosedReader$@55f2a900
99
+ println(r.isClosed) // true
100
+ // true
101
+ println(r.read(-1)) // -1 (the sentinel)
102
+ // -1
103
+ ```
104
+
105
+ ### From Collections
106
+
107
+ `Reader.fromChunk` — Creates a reader backed by a `Chunk`. Dispatches on the element type to use specialized, unboxed reads for primitives:
108
+
109
+ ```scala
110
+ object Reader {
111
+ def fromChunk[A](chunk: Chunk[A])(implicit jt: JvmType.Infer[A]): Reader[A]
112
+ }
113
+ ```
114
+
115
+ Create a reader from a chunk and drain its elements:
116
+
117
+ ```scala
118
+ import zio.blocks.streams.io.Reader
119
+ import zio.blocks.chunk.Chunk
120
+
121
+ val chunk = Chunk(10, 20, 30)
122
+ // chunk: Chunk[Int] = IndexedSeq(10, 20, 30)
123
+ val r = Reader.fromChunk(chunk)
124
+ // r: Reader[Int] = zio.blocks.streams.io.Reader$FromChunkInt@8388986
125
+
126
+ def drain(): Unit = {
127
+ val v = r.read(-1)
128
+ if (v != -1) {
129
+ println(v)
130
+ drain()
131
+ }
132
+ }
133
+ drain()
134
+ // 10
135
+ // 20
136
+ // 30
137
+ // Output: 10, 20, 30
138
+ ```
139
+
140
+ `Reader.fromIterable` — Creates a reader from any `Iterable`. Works with lists, sets, vectors, and other collections:
141
+
142
+ ```scala
143
+ object Reader {
144
+ def fromIterable[A](it: Iterable[A]): Reader[A]
145
+ }
146
+ ```
147
+
148
+ Create a reader from a list and consume its elements:
149
+
150
+ ```scala
151
+ import zio.blocks.streams.io.Reader
152
+
153
+ val list = List("a", "b", "c")
154
+ // list: List[String] = List("a", "b", "c")
155
+ val r = Reader.fromIterable(list)
156
+ // r: Reader[String] = zio.blocks.streams.io.Reader$FromIterable@76a099be
157
+
158
+ def drain(): Unit = {
159
+ val v = r.read(null)
160
+ if (v != null) {
161
+ println(v)
162
+ drain()
163
+ }
164
+ }
165
+ drain()
166
+ // a
167
+ // b
168
+ // c
169
+ // Output: a, b, c
170
+ ```
171
+
172
+ `Reader.fromRange` — Creates a reader from a Scala `Range`. Optimized for integer ranges without allocation:
173
+
174
+ ```scala
175
+ object Reader {
176
+ def fromRange(range: Range): Reader[Int]
177
+ }
178
+ ```
179
+
180
+ Create a reader from a range and drain the integers:
181
+
182
+ ```scala
183
+ import zio.blocks.streams.io.Reader
184
+
185
+ val r = Reader.fromRange(1 to 5)
186
+ // r: Reader[Int] = zio.blocks.streams.io.Reader$FromRange@5a4e4971
187
+
188
+ def drain(): Unit = {
189
+ val v = r.read(-1)
190
+ if (v != -1) {
191
+ println(v)
192
+ drain()
193
+ }
194
+ }
195
+ drain()
196
+ // 1
197
+ // 2
198
+ // 3
199
+ // 4
200
+ // 5
201
+ // Output: 1, 2, 3, 4, 5
202
+ ```
203
+
204
+ ### From I/O
205
+
206
+ `Reader.fromInputStream` — Wraps a `java.io.InputStream` as a `Reader[Int]`, where each byte is widened to `Int` (0–255). This avoids boxing on `.map`/`.filter` since `Function1` is specialized for `Int`:
207
+
208
+ ```scala
209
+ object Reader {
210
+ def fromInputStream(is: InputStream): Reader[Int]
211
+ }
212
+ ```
213
+
214
+ `Reader.fromReader` — Wraps a `java.io.Reader` as a `Reader[Char]` for character-based I/O:
215
+
216
+ ```scala
217
+ object Reader {
218
+ def fromReader(r: java.io.Reader): Reader[Char]
219
+ }
220
+ ```
221
+
222
+ ### Single Element
223
+
224
+ `Reader.single` — Creates a reader that emits exactly one element, then closes. Primitive types use specialized variants for zero-boxing:
225
+
226
+ ```scala
227
+ object Reader {
228
+ def single[A](value: A)(implicit jt: JvmType.Infer[A]): Reader[A]
229
+ def singleInt(value: Int): Reader[Int]
230
+ def singleLong(value: Long): Reader[Long]
231
+ def singleFloat(value: Float): Reader[Float]
232
+ def singleDouble(value: Double): Reader[Double]
233
+ def singleChar(value: Char): Reader[Char]
234
+ def singleShort(value: Short): Reader[Short]
235
+ def singleByte(value: Byte): Reader[Int]
236
+ def singleBoolean(value: Boolean): Reader[Boolean]
237
+ }
238
+ ```
239
+
240
+ When you use `Reader.single`, behavior differs between reference types and primitives. The `JvmType.Infer[A]` implicit parameter enables compile-time type detection, automatically selecting the appropriate implementation (specialized primitive or reference-type generic).
241
+
242
+ For reference types like String, `Reader.single("hello")` stores the element directly and uses an internal sentinel object (`EndOfStream`) to signal end-of-stream. You read via the generic `Reader#read[A](sentinel)` method, passing your own sentinel value. On the first call, you get your string; on subsequent calls, you receive the sentinel you provided, allowing you to detect stream closure.
243
+
244
+ For primitive types, `Reader.single(42)` could naively box the integer, but the library avoids this penalty entirely via `SingletonPrim`—a zero-boxing specialization that stores the primitive unboxed in memory. The `JvmType.Infer` implicit detects this at compile time and routes you through specialized factory methods (`Reader.singleInt`, `Reader.singleLong`, etc.) and specialized read methods (`Reader#readInt`, `Reader#readLong`, etc.). Both storage and retrieval stay unboxed, maintaining zero-copy efficiency.
245
+
246
+ Note: `Reader.singleByte` returns `Reader[Int]` (not `Reader[Byte]`) because Java's primitive byte type is typically widened to int in arrays and I/O contexts; this aligns with JVM conventions for byte-level operations. When reading, use `Reader#readInt(sentinel: Long): Long`, which returns a long to maintain the sentinel protocol—extract the int via casting if needed.
247
+
248
+ Create and read from a single-element reference-type reader with a custom sentinel:
249
+
250
+ ```scala
251
+ import zio.blocks.streams.io.Reader
252
+
253
+ val r = Reader.single("hello")
254
+ // r: Reader[String] = zio.blocks.streams.io.Reader$SingletonGeneric@4ccaf3f7
255
+ val sentinel = "END"
256
+ // sentinel: String = "END"
257
+ println(r.read(sentinel)) // hello
258
+ // hello
259
+ println(r.read(sentinel)) // END (sentinel, reader is closed)
260
+ // END
261
+ ```
262
+
263
+ For primitive types, use the specialized factory and read methods. The `Reader#readInt` method takes a `Long` sentinel (to avoid confusion with sentinel values that fit in int range) and returns `Long` so you can distinguish the actual int value from the sentinel:
264
+
265
+ ```scala
266
+ import zio.blocks.streams.io.Reader
267
+
268
+ val r = Reader.singleInt(100)
269
+ // r: Reader[Int] = zio.blocks.streams.io.Reader$SingletonPrim@56f86b9b
270
+ val sentinel = Long.MinValue
271
+ // sentinel: Long = -9223372036854775808L
272
+ val v1 = r.readInt(sentinel)
273
+ // v1: Long = 100L
274
+ println(v1) // 100
275
+ // 100
276
+ val v2 = r.readInt(sentinel)
277
+ // v2: Long = -9223372036854775808L
278
+ println(v2) // -9223372036854775808 (sentinel, reader is closed)
279
+ // -9223372036854775808
280
+ ```
281
+
282
+ ### Infinite & Repeating
283
+
284
+ `Reader.repeat` — Creates an infinite reader that always emits the same value:
285
+
286
+ ```scala
287
+ object Reader {
288
+ def repeat[A](a: A)(implicit jt: JvmType.Infer[A]): Reader[A]
289
+ }
290
+ ```
291
+
292
+ Create an infinite reader that repeatedly emits the same value:
293
+
294
+ ```scala
295
+ import zio.blocks.streams.io.Reader
296
+
297
+ val r = Reader.repeat(1)
298
+ // r: Reader[Int] = zio.blocks.streams.io.Reader$SingletonPrim@7c91cdb2
299
+
300
+ def drainN(n: Int): Unit = {
301
+ if (n > 0) {
302
+ val v = r.read(-1)
303
+ println(v)
304
+ drainN(n - 1)
305
+ }
306
+ }
307
+ drainN(3)
308
+ // 1
309
+ // 1
310
+ // 1
311
+ // Output: 1, 1, 1
312
+ ```
313
+
314
+ `Reader.repeated` — Restarts an inner reader each time it closes cleanly. Used by `Stream.repeated` to create indefinitely repeating streams:
315
+
316
+ ```scala
317
+ object Reader {
318
+ def repeated[A](inner: Reader[A]): Reader[A]
319
+ }
320
+ ```
321
+
322
+ ### Unfold (State Machine)
323
+
324
+ `Reader.unfold` — Creates a reader by unfolding state with a function. Returns `None` to signal completion, or `Some((elem, nextState))` to emit an element and advance state:
325
+
326
+ ```scala
327
+ object Reader {
328
+ def unfold[S, A](s: S)(f: S => Option[(A, S)]): Reader[A]
329
+ }
330
+ ```
331
+
332
+ Create a reader that unfolds state incrementally until completion:
333
+
334
+ ```scala
335
+ import zio.blocks.streams.io.Reader
336
+
337
+ val r = Reader.unfold(1) { s =>
338
+ if (s > 3) None else Some((s, s + 1))
339
+ }
340
+ // r: Reader[Int] = zio.blocks.streams.io.Reader$Unfold@65f92a5f
341
+
342
+ def drain(): Unit = {
343
+ val v = r.read(-1)
344
+ if (v != -1) {
345
+ println(v)
346
+ drain()
347
+ }
348
+ }
349
+ drain()
350
+ // 1
351
+ // 2
352
+ // 3
353
+ // Output: 1, 2, 3
354
+ ```
355
+
356
+ ## Core Operations
357
+
358
+ These methods form the primary interface for consuming elements and querying reader state:
359
+
360
+ ### Pulling Elements
361
+
362
+ `Reader#read` — Pulls the next element, or returns `sentinel` if the reader is closed and empty. This is the fundamental operation: call it repeatedly to consume all elements until it returns your sentinel value:
363
+
364
+ ```scala
365
+ abstract class Reader[+Elem] {
366
+ def read[A >: Elem](sentinel: A): A
367
+ }
368
+ ```
369
+
370
+ The sentinel value is caller-chosen and should never appear as a real element. For reference types, `null` is convenient. For primitives, use a value outside the domain (e.g., `-1` for unsigned bytes, `Long.MinValue` for `Int`):
371
+
372
+ ```scala
373
+ import zio.blocks.streams.io.Reader
374
+ import zio.blocks.chunk.Chunk
375
+
376
+ val r = Reader.fromChunk(Chunk(10, 20))
377
+ // r: Reader[Int] = zio.blocks.streams.io.Reader$FromChunkInt@677263ef
378
+ val v1 = r.read(-1) // 10
379
+ // v1: Int = 10
380
+ val v2 = r.read(-1) // 20
381
+ // v2: Int = 20
382
+ val v3 = r.read(-1) // -1 (sentinel, reader is closed)
383
+ // v3: Int = -1
384
+ ```
385
+
386
+ ### Primitive Specialization
387
+
388
+ For primitive types, specialized methods avoid boxing by widening the return type.
389
+
390
+ `Reader#readInt` — Sentinel-return `Int` pull. Returns the element widened to `Long`, or `sentinel` when closed. The sentinel must lie outside `[Int.MinValue, Int.MaxValue]` (typically `Long.MinValue`):
391
+
392
+ ```scala
393
+ abstract class Reader[+Elem] {
394
+ def readInt(sentinel: Long)(using Elem <:< Int): Long
395
+ }
396
+ ```
397
+
398
+ Why widen to `Long`? If `Reader#readInt` returned `Int`, you couldn't distinguish a real element from the sentinel—both would fit in the int range. By widening to `Long`, the sentinel (e.g., `Long.MinValue`) lies outside the possible int domain, allowing reliable end-of-stream detection. Cast the result back to `Int` if needed: `r.readInt(Long.MinValue).toInt`.
399
+
400
+ `Reader#readLong` — Sentinel-return `Long` pull. Returns the element, or `sentinel` when closed. The sentinel must be a value that never appears in the stream (typically `Long.MaxValue`):
401
+
402
+ ```scala
403
+ abstract class Reader[+Elem] {
404
+ def readLong(sentinel: Long)(using Elem <:< Long): Long
405
+ }
406
+ ```
407
+
408
+ :::note[Sentinel Collisions Are Disambiguated]
409
+ Unlike `Reader#readInt` which widens to `Long`, `Reader#readLong` has no wider type to safely house the sentinel — a real `Long.MaxValue` element and end-of-stream both come back as the sentinel value. To disambiguate, every read records an out-of-band flag, exposed as `Reader#lastReadWasEOF`: after a read that returned the sentinel, `lastReadWasEOF` is `true` only for genuine end-of-stream. The library's own drain loops test `v == sentinel && reader.lastReadWasEOF`, so streams containing the sentinel value are processed losslessly; manual pull loops should do the same.
410
+
411
+ **Performance Tradeoff:** `Reader#readLong` avoids boxing on every read—the long stays unboxed in memory, and retrieval is a simple memory fetch. In contrast, `Reader#read[Long](sentinel)` boxes each long into a generic `Any` reference, forcing allocation and garbage collection pressure in hot loops. For latency-sensitive or high-throughput workloads (millions of elements per second), this difference is measurable. The `lastReadWasEOF` check costs nothing on the hot path — it only needs consulting on the rare value/sentinel collision.
412
+ :::
413
+
414
+ `Reader#readFloat` — Sentinel-return `Float` pull. Returns the element widened to `Double`, or `sentinel` when closed:
415
+
416
+ ```scala
417
+ abstract class Reader[+Elem] {
418
+ def readFloat(sentinel: Double)(using Elem <:< Float): Double
419
+ }
420
+ ```
421
+
422
+ Like `Reader#readInt`, widening to `Double` allows the sentinel to lie safely outside the float domain. A float value will always fit in the lower precision bits of the double result, and the sentinel (typically `Double.MaxValue`) occupies the upper range. This ensures you can reliably distinguish real float elements from end-of-stream. Cast back to `Float` if needed: `r.readFloat(Double.MaxValue).toFloat`.
423
+
424
+ `Reader#readDouble` — Sentinel-return `Double` pull. Returns the element, or `sentinel` when closed. The sentinel must be a value outside the domain (typically `Double.MaxValue`):
425
+
426
+ ```scala
427
+ abstract class Reader[+Elem] {
428
+ def readDouble(sentinel: Double)(using Elem <:< Double): Double
429
+ }
430
+ ```
431
+
432
+ :::danger[Sentinel Collision Risk for Doubles]
433
+ Like `Reader#readLong`, `Reader#readDouble` has no wider type to safely contain the sentinel. If your actual data stream contains `Double.MaxValue` or the sentinel you chose, you will incorrectly detect end-of-stream mid-stream. Always verify that your data domain excludes the chosen sentinel value. Alternatively, use `Reader#read[Double](sentinel)` (the generic method) if you need the flexibility to choose any sentinel regardless of your data—this trades performance (boxing on every read) for safety.
434
+ :::
435
+
436
+ These specialized methods are the hot path for primitive streams — they avoid allocation and boxing entirely:
437
+
438
+ ```scala
439
+ import zio.blocks.streams.io.Reader
440
+ import zio.blocks.chunk.Chunk
441
+
442
+ val r = Reader.fromChunk(Chunk(10, 20, 30))
443
+ // r: Reader[Int] = zio.blocks.streams.io.Reader$FromChunkInt@f89374e
444
+ val sentinel = Long.MinValue
445
+ // sentinel: Long = -9223372036854775808L
446
+
447
+ val v = r.readInt(sentinel)
448
+ // v: Long = 10L
449
+ ```
450
+
451
+ ### Byte-Level Reading
452
+
453
+ `Reader#readByte` — Reads a single byte (0–255), widened to `Int`. Returns `-1` when the reader is closed. Dispatches on `Reader#jvmType` for zero-boxing when the reader is specialized:
454
+
455
+ ```scala
456
+ abstract class Reader[+Elem] {
457
+ def readByte(): Int
458
+ }
459
+ ```
460
+
461
+ Read bytes one at a time from a reader until end-of-stream:
462
+
463
+ ```scala
464
+ import zio.blocks.streams.io.Reader
465
+ import java.io.ByteArrayInputStream
466
+
467
+ val bytes = Array[Byte](72, 101, 108, 108, 111) // Hello in ASCII bytes
468
+ // bytes: Array[Byte] = Array(72, 101, 108, 108, 111)
469
+ val is = new ByteArrayInputStream(bytes)
470
+ // is: ByteArrayInputStream = java.io.ByteArrayInputStream@78144ba7
471
+ val r = Reader.fromInputStream(is)
472
+ // r: Reader[Byte] = zio.blocks.streams.io.Reader$InputStreamReader@aafbf86
473
+
474
+ def drainBytes(): Unit = {
475
+ val b = r.readByte()
476
+ if (b != -1) {
477
+ println(s"Byte: $b (${b.toChar})")
478
+ drainBytes()
479
+ }
480
+ }
481
+ drainBytes()
482
+ // Byte: 72 (H)
483
+ // Byte: 101 (e)
484
+ // Byte: 108 (l)
485
+ // Byte: 108 (l)
486
+ // Byte: 111 (o)
487
+ // Output:
488
+ // Byte: 72 (H)
489
+ // Byte: 101 (e)
490
+ // Byte: 108 (l)
491
+ // Byte: 108 (l)
492
+ // Byte: 111 (o)
493
+ ```
494
+
495
+ `Reader#readBytes` — Bulk byte read into a caller-supplied buffer, mirroring `java.io.InputStream#read(byte[], int, int)`. The behavior is:
496
+
497
+ - Blocks until at least 1 byte is available.
498
+ - Returns the number of bytes read (`1 <= r <= len`).
499
+ - Returns `-1` when closed and empty.
500
+ - Returns `0` immediately when `len == 0`.
501
+
502
+ The method signature is:
503
+
504
+ ```scala
505
+ abstract class Reader[+Elem] {
506
+ def readBytes(buf: Array[Byte], offset: Int, len: Int): Int
507
+ }
508
+ ```
509
+
510
+ Read multiple bytes into a buffer in bulk with a loop pattern:
511
+
512
+ ```scala
513
+ import zio.blocks.streams.io.Reader
514
+ import java.io.ByteArrayInputStream
515
+
516
+ val bytes = Array[Byte](72, 101, 108, 108, 111) // The word Hello
517
+ // bytes: Array[Byte] = Array(72, 101, 108, 108, 111)
518
+ val is = new ByteArrayInputStream(bytes)
519
+ // is: ByteArrayInputStream = java.io.ByteArrayInputStream@1ba77670
520
+ val r = Reader.fromInputStream(is)
521
+ // r: Reader[Byte] = zio.blocks.streams.io.Reader$InputStreamReader@144c778
522
+
523
+ val buffer = new Array[Byte](3)
524
+ // buffer: Array[Byte] = Array(108, 111, 108)
525
+
526
+ def drainBulk(): Unit = {
527
+ val bytesRead = r.readBytes(buffer, 0, 3)
528
+ if (bytesRead > 0) {
529
+ val chunk = buffer.take(bytesRead).map(_.toChar).mkString
530
+ println(s"Read $bytesRead bytes: $chunk")
531
+ drainBulk()
532
+ }
533
+ }
534
+ drainBulk()
535
+ // Read 3 bytes: Hel
536
+ // Read 2 bytes: lo
537
+ // Output:
538
+ // Read 3 bytes: Hel
539
+ // Read 2 bytes: lo
540
+ ```
541
+
542
+ ### Character and Numeric Specialization
543
+
544
+ `Reader#readChar` — Sentinel-return `Char` pull. Returns the element widened to `Int`, or `sentinel` when closed. Requires evidence that `Elem <:< Char`:
545
+
546
+ ```scala
547
+ abstract class Reader[+Elem] {
548
+ def readChar(sentinel: Int)(using Elem <:< Char): Int
549
+ }
550
+ ```
551
+
552
+ `Reader#readShort` — Sentinel-return `Short` pull. Returns the element widened to `Int`, or `sentinel` when closed:
553
+
554
+ ```scala
555
+ abstract class Reader[+Elem] {
556
+ def readShort(sentinel: Int)(using Elem <:< Short): Int
557
+ }
558
+ ```
559
+
560
+ `Reader#readBoolean` — Sentinel-return `Boolean` pull. Returns `1` for `true`, `0` for `false`, or `sentinel` when closed. The sentinel must lie outside `[0, 1]` (typically `-1`):
561
+
562
+ ```scala
563
+ abstract class Reader[+Elem] {
564
+ def readBoolean(sentinel: Int)(using Elem <:< Boolean): Int
565
+ }
566
+ ```
567
+
568
+ ### Bulk Operations
569
+
570
+ `Reader#readAll` — Drains the entire reader into a `Chunk`. Dispatches on `Reader#jvmType` for zero-boxing on primitive readers:
571
+
572
+ ```scala
573
+ abstract class Reader[+Elem] {
574
+ def readAll[A >: Elem](): Chunk[A]
575
+ }
576
+ ```
577
+
578
+ The result is a new chunk containing all remaining elements:
579
+
580
+ ```scala
581
+ import zio.blocks.streams.io.Reader
582
+ import zio.blocks.chunk.Chunk
583
+
584
+ val r = Reader.fromChunk(Chunk(10, 20, 30))
585
+ // r: Reader[Int] = zio.blocks.streams.io.Reader$FromChunkInt@22ee08e9
586
+ val all = r.readAll()
587
+ // all: Chunk[Int] = IndexedSeq(10, 20, 30)
588
+ println(all) // Chunk(10, 20, 30)
589
+ // Chunk(10,20,30)
590
+ ```
591
+
592
+ `Reader#skip` — Eagerly discards the first `n` elements. Dispatches on `Reader#jvmType` for zero-boxing when possible:
593
+
594
+ ```scala
595
+ abstract class Reader[+Elem] {
596
+ def skip(n: Long): Unit
597
+ }
598
+ ```
599
+
600
+ ### State Queries
601
+
602
+ `Reader#isClosed` — Returns `true` if the reader is closed. Monotone: once `true`, never returns `false`:
603
+
604
+ ```scala
605
+ abstract class Reader[+Elem] {
606
+ def isClosed: Boolean
607
+ }
608
+ ```
609
+
610
+ `Reader#readable` — Returns `true` if the next `read()` would return a value (not the sentinel). Default implementation returns `!isClosed`. Buffered readers can override `readable()` for accuracy to peek ahead without consuming:
611
+
612
+ ```scala
613
+ abstract class Reader[+Elem] {
614
+ def readable(): Boolean
615
+ }
616
+ ```
617
+
618
+ Use `readable()` to check if elements are available before calling `read()`:
619
+
620
+ ```scala
621
+ import zio.blocks.streams.io.Reader
622
+ import zio.blocks.chunk.Chunk
623
+
624
+ val r = Reader.fromChunk(Chunk(1, 2))
625
+ // r: Reader[Int] = zio.blocks.streams.io.Reader$FromChunkInt@65ea641
626
+ println(r.readable()) // true
627
+ // true
628
+ r.read(-1)
629
+ // res32: Int = 1
630
+ println(r.readable()) // true
631
+ // true
632
+ r.read(-1)
633
+ // res34: Int = 2
634
+ println(r.readable()) // false
635
+ // false
636
+ ```
637
+
638
+ ## Composition
639
+
640
+ Combine multiple readers to build more complex sources:
641
+
642
+ ### Concatenation
643
+
644
+ `Reader#concat` — Concatenates this reader with `next`. When this reader is exhausted, it is closed and elements are pulled from `next` (evaluated lazily). Optimized for left-associative chains:
645
+
646
+ ```scala
647
+ abstract class Reader[+Elem] {
648
+ def concat[Elem2 >: Elem](next: () => Reader[Elem2]): Reader[Elem2]
649
+ }
650
+ ```
651
+
652
+ `Reader#++` — Alias for `Reader#concat`. Syntactic sugar for composing readers:
653
+
654
+ ```scala
655
+ abstract class Reader[+Elem] {
656
+ def ++[Elem2 >: Elem](next: => Reader[Elem2]): Reader[Elem2]
657
+ }
658
+ ```
659
+
660
+ Here is how concatenation chains multiple readers together:
661
+
662
+ ```scala
663
+ import zio.blocks.streams.io.Reader
664
+ import zio.blocks.chunk.Chunk
665
+
666
+ val r1 = Reader.fromChunk(Chunk(1, 2))
667
+ // r1: Reader[Int] = zio.blocks.streams.io.Reader$FromChunkInt@2f0f9a51
668
+ val r2 = Reader.fromChunk(Chunk(3, 4))
669
+ // r2: Reader[Int] = zio.blocks.streams.io.Reader$FromChunkInt@4a811b20
670
+ val combined = r1 ++ r2
671
+ // combined: Reader[Int] = zio.blocks.streams.io.Reader$ConcatReader@1eb0ee36
672
+
673
+ def drain(): Unit = {
674
+ val v = combined.read(-1)
675
+ if (v != -1) {
676
+ println(v)
677
+ drain()
678
+ }
679
+ }
680
+ drain()
681
+ // 1
682
+ // 2
683
+ // 3
684
+ // 4
685
+ // Output: 1, 2, 3, 4
686
+ ```
687
+
688
+ **Optimization**: If this reader is already a `ConcatReader`, the thunk is appended to its internal array and `this` is returned (mutable append, O(1) amortized). Otherwise a new `ConcatReader` is created. This ensures that left-associative chains like `a ++ b ++ c ++ d` compile into a single flat `ConcatReader` with O(1) per-element read, rather than O(n) nested wrappers.
689
+
690
+ ## Resource Management
691
+
692
+ Close readers and attach cleanup callbacks:
693
+
694
+ ### Closing
695
+
696
+ `Reader#close` — Signals end-of-stream from the consumer side and releases any held resources. Implementations set internal closed state and wake any blocked readers. This is always called in a `finally` block by sinks to guarantee resource cleanup:
697
+
698
+ ```scala
699
+ abstract class Reader[+Elem] {
700
+ def close(): Unit
701
+ }
702
+ ```
703
+
704
+ `Reader#withRelease` — Wraps this reader so that `release` runs after `Reader#close()`. Useful for attaching cleanup logic:
705
+
706
+ ```scala
707
+ abstract class Reader[+Elem] {
708
+ def withRelease(release: () => Unit): Reader[Elem]
709
+ }
710
+ ```
711
+
712
+ Here is how cleanup logic is attached to a reader:
713
+
714
+ ```scala
715
+ import zio.blocks.streams.io.Reader
716
+ import zio.blocks.chunk.Chunk
717
+ import scala.sys.Prop
718
+
719
+ val cleanupRef = scala.collection.mutable.ListBuffer[String]()
720
+ // cleanupRef: ListBuffer[String] = ListBuffer("cleaned")
721
+ val r = Reader.fromChunk(Chunk(1, 2)).withRelease { () =>
722
+ cleanupRef += "cleaned"
723
+ println("Cleaned up")
724
+ }
725
+ // r: Reader[Int] = zio.blocks.streams.io.Reader$$anon$1@697b73b8
726
+
727
+ r.close()
728
+ // Cleaned up
729
+ println(cleanupRef.nonEmpty) // true
730
+ // true
731
+ ```
732
+
733
+ ## Pushdown Operations
734
+
735
+ Readers can sometimes handle skip, limit, and repeat operations natively (O(1), zero per-element cost). These methods attempt that; if the reader cannot handle it natively, they return `false` and the caller must wrap the reader.
736
+
737
+ `Reader#setSkip` — Attempts to set a skip (drop) on this reader. Returns `true` if handled natively, `false` if the caller must wrap. When `true`, the next n elements are discarded before producing. After `Reader#reset()`, the skip is re-applied:
738
+
739
+ ```scala
740
+ abstract class Reader[+Elem] {
741
+ def setSkip(n: Long): Boolean
742
+ }
743
+ ```
744
+
745
+ Set a skip to discard the first two elements:
746
+
747
+ ```scala
748
+ import zio.blocks.streams.io.Reader
749
+ import zio.blocks.chunk.Chunk
750
+
751
+ val r = Reader.fromChunk(Chunk(1, 2, 3, 4, 5))
752
+ // r: Reader[Int] = zio.blocks.streams.io.Reader$FromChunkInt@377387c0
753
+ val handled = r.setSkip(2)
754
+ // handled: Boolean = true
755
+ println(s"Skip handled natively: $handled")
756
+ // Skip handled natively: true
757
+
758
+ def drain(): Unit = {
759
+ val v = r.read(-1)
760
+ if (v != -1) {
761
+ println(v)
762
+ drain()
763
+ }
764
+ }
765
+ drain()
766
+ // 3
767
+ // 4
768
+ // 5
769
+ // Output:
770
+ // Skip handled natively: true
771
+ // 3
772
+ // 4
773
+ // 5
774
+ ```
775
+
776
+ `Reader#setLimit` — Attempts to set a limit on this reader so it produces at most `n` elements. Returns `true` if handled natively, `false` if the caller must wrap. After `reset()`, the limit is re-applied from the new start position:
777
+
778
+ ```scala
779
+ abstract class Reader[+Elem] {
780
+ def setLimit(n: Long): Boolean
781
+ }
782
+ ```
783
+
784
+ Set a limit to produce only three elements:
785
+
786
+ ```scala
787
+ import zio.blocks.streams.io.Reader
788
+ import zio.blocks.chunk.Chunk
789
+
790
+ val r = Reader.fromChunk(Chunk(1, 2, 3, 4, 5))
791
+ // r: Reader[Int] = zio.blocks.streams.io.Reader$FromChunkInt@2e287808
792
+ val handled = r.setLimit(3)
793
+ // handled: Boolean = true
794
+ println(s"Limit handled natively: $handled")
795
+ // Limit handled natively: true
796
+
797
+ def drain(): Unit = {
798
+ val v = r.read(-1)
799
+ if (v != -1) {
800
+ println(v)
801
+ drain()
802
+ }
803
+ }
804
+ drain()
805
+ // 1
806
+ // 2
807
+ // 3
808
+ // Output:
809
+ // Limit handled natively: true
810
+ // 1
811
+ // 2
812
+ // 3
813
+ ```
814
+
815
+ `Reader#setRepeat` — Attempts to set this reader into repeat-forever mode, so it restarts from the beginning whenever it would otherwise close. Returns `true` if handled natively, `false` if the caller must wrap:
816
+
817
+ ```scala
818
+ abstract class Reader[+Elem] {
819
+ def setRepeat(): Boolean
820
+ }
821
+ ```
822
+
823
+ Set repeat mode to emit elements multiple times:
824
+
825
+ ```scala
826
+ import zio.blocks.streams.io.Reader
827
+ import zio.blocks.chunk.Chunk
828
+
829
+ val r = Reader.fromChunk(Chunk(1, 2))
830
+ // r: Reader[Int] = zio.blocks.streams.io.Reader$FromChunkInt@742ce9f
831
+ val handled = r.setRepeat()
832
+ // handled: Boolean = false
833
+ println(s"Repeat handled natively: $handled")
834
+ // Repeat handled natively: false
835
+
836
+ def drain(count: Int): Unit = {
837
+ if (count < 6) {
838
+ val v = r.read(-1)
839
+ println(v)
840
+ drain(count + 1)
841
+ }
842
+ }
843
+ drain(0)
844
+ // 1
845
+ // 2
846
+ // -1
847
+ // -1
848
+ // -1
849
+ // -1
850
+ // Output:
851
+ // Repeat handled natively: true
852
+ // 1
853
+ // 2
854
+ // 1
855
+ // 2
856
+ // 1
857
+ // 2
858
+ ```
859
+
860
+ `Reader#reset` — Rewinds this reader to its initial state, as if freshly constructed. After `Reader#reset()`, all elements are available again from the beginning. Not all readers support this; readers backed by one-shot resources (InputStreams, `java.io.Reader`s) throw `UnsupportedOperationException`:
861
+
862
+ ```scala
863
+ abstract class Reader[+Elem] {
864
+ def reset(): Unit
865
+ }
866
+ ```
867
+
868
+ After rewinding, the reader starts from the beginning:
869
+
870
+ ```scala
871
+ import zio.blocks.streams.io.Reader
872
+ import zio.blocks.chunk.Chunk
873
+
874
+ val r = Reader.fromChunk(Chunk(1, 2, 3))
875
+ // r: Reader[Int] = zio.blocks.streams.io.Reader$FromChunkInt@37838f3b
876
+ println(r.read(-1)) // 1
877
+ // 1
878
+ r.reset()
879
+ println(r.read(-1)) // 1 (back to the beginning)
880
+ // 1
881
+ ```
882
+
883
+ ## Integration with Stream
884
+
885
+ `Reader` is the compilation target of `Stream`. When you call a terminal operation, the stream compiles to a `Reader`, which is then consumed.
886
+
887
+ You can also open a stream for manual element-by-element pulling using `Stream#start`:
888
+
889
+ ```scala
890
+ import zio.blocks.streams.*
891
+ import zio.blocks.streams.io.Reader
892
+ import zio.blocks.scope.*
893
+
894
+ Scope.global.scoped { scope =>
895
+ import scope.*
896
+
897
+ val reader: $[Reader[Int]] = Stream.range(1, 6).start(using scope)
898
+
899
+ $(reader) { r =>
900
+ def drain(): Unit = {
901
+ val v = r.read(-1)
902
+ if (v != -1) {
903
+ println(v) // prints 1, 2, 3, 4, 5
904
+ drain()
905
+ }
906
+ }
907
+ drain()
908
+ }
909
+ // reader is closed automatically when scope exits
910
+ }
911
+ ```
912
+
913
+ :::caution
914
+ Avoid holding references to a `Reader` obtained via `Stream#start` outside its `Scope`. The scope guarantees cleanup; escaping the reader defeats that guarantee.
915
+ :::
916
+
917
+ ## Integration with Sink
918
+
919
+ `Reader` and `Sink` are dual: `Reader` is the source, `Sink` is the consumer. When you call `stream.run(sink)`, the stream compiles to a `Reader`, and the sink drains it:
920
+
921
+ ```scala
922
+ abstract class Sink[+E, -A, +Z] {
923
+ def drain[A2 <: A](reader: Reader[A2]): Either[E, Z]
924
+ }
925
+ ```
926
+
927
+ The sink calls `read()` repeatedly until the reader is closed, transforming the sequence of elements into a result of type `Z`.
928
+
929
+ For example, `Sink.collectAll` drains all elements and returns them as a `Chunk`:
930
+
931
+ ```scala
932
+ import zio.blocks.streams._
933
+
934
+ val result = Stream.range(1, 10)
935
+ .run(Sink.collectAll[Int])
936
+ // result: Either[Nothing, Chunk[Int]] = Right(
937
+ // IndexedSeq(1, 2, 3, 4, 5, 6, 7, 8, 9)
938
+ // )
939
+ ```
940
+
941
+ ## Implementation Notes
942
+
943
+ Understand the design choices and mechanisms that power `Reader`:
944
+
945
+ ### Sentinel Protocol
946
+
947
+ The `read(sentinel)` method uses a caller-chosen sentinel value to signal end-of-stream. This avoids the allocation and boxing of wrapping results in `Option` or `Either`. The sentinel must be a value that never appears as a real element.
948
+
949
+ For reference types, `null` is the natural sentinel. For primitives, specialized methods widen the return type and use fixed sentinels:
950
+
951
+ | Type | Sentinel | Method | Return Type |
952
+ |--------|--------------|-------------------|-------------|
953
+ | `Int` | `Long.MinValue` | `readInt(sentinel: Long)` | `Long` |
954
+ | `Long` | `Long.MaxValue` | `readLong(sentinel: Long)` | `Long` |
955
+ | `Float` | `Double.MaxValue` | `readFloat(sentinel: Double)` | `Double` |
956
+ | `Double` | `Double.MaxValue` | `readDouble(sentinel: Double)` | `Double` |
957
+
958
+ :::note
959
+ The `Long.MaxValue` and `Double.MaxValue` sentinels coincide with valid data values. To keep specialized paths lossless, every read additionally records an out-of-band `Reader#lastReadWasEOF` flag: a sentinel-valued result means end-of-stream only when the flag is set. Streams containing exactly those values are therefore processed without truncation, at zero cost on the hot path.
960
+ :::
961
+
962
+ ### JVM Type Dispatch
963
+
964
+ `Reader` dispatches on `jvmType` to choose between unboxed and boxed pull paths. Subclasses with primitive specialization override `jvmType`:
965
+
966
+ ```scala
967
+ abstract class Reader[+Elem] {
968
+ def jvmType: JvmType = JvmType.AnyRef
969
+ }
970
+ ```
971
+
972
+ For example, a `Reader[Int]` backed by a `Chunk[Int]` overrides `jvmType` to return `JvmType.Int`. Then, methods like `Reader#readAll` check `Reader#jvmType` and dispatch to the unboxed `Reader#readInt(sentinel: Long)` path instead of boxing.
973
+
974
+ ### Thread Safety
975
+
976
+ `Reader` is **not thread-safe**. It is designed for single-threaded, pull-based consumption. Do not share a `Reader` across threads without external synchronization. If you need concurrent consumption, wrap the reader in a thread-safe queue or use a concurrent streaming library.
977
+
978
+ ## Running the Examples
979
+
980
+ All code from this guide is available as runnable examples in the `streams-examples` module. Follow these steps to run them:
981
+
982
+ **Step 1** — Clone the repository and navigate to the project:
983
+
984
+ ```bash
985
+ git clone https://github.com/zio/zio-blocks.git
986
+ cd zio-blocks
987
+ ```
988
+
989
+ **Step 2** — Run individual examples with sbt:
990
+
991
+ ### Basic Reader Construction
992
+
993
+ This example demonstrates the most common reader factories: `Reader.fromChunk`, `Reader.fromIterable`, `Reader.fromRange`, and `Reader.single`. Embed the source:
994
+
995
+ ```scala title="streams-examples/src/main/scala/reader/ReaderBasicConstructionExample.scala"
996
+ /*
997
+ * Copyright 2024-2026 John A. De Goes and the ZIO Contributors
998
+ *
999
+ * Licensed under the Apache License, Version 2.0 (the "License");
1000
+ * you may not use this file except in compliance with the License.
1001
+ * You may obtain a copy of the License at
1002
+ *
1003
+ * http://www.apache.org/licenses/LICENSE-2.0
1004
+ *
1005
+ * Unless required by applicable law or agreed to in writing, software
1006
+ * distributed under the License is distributed on an "AS IS" BASIS,
1007
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1008
+ * See the License for the specific language governing permissions and
1009
+ * limitations under the License.
1010
+ */
1011
+
1012
+ package reader
1013
+
1014
+ import zio.blocks.chunk.Chunk
1015
+ import zio.blocks.streams.io.Reader
1016
+
1017
+ /**
1018
+ * Demonstrates the most common Reader factories: fromChunk, fromIterable,
1019
+ * fromRange, single, and unfold. Each reader is drained manually with read() to
1020
+ * show how to consume elements.
1021
+ */
1022
+ object ReaderBasicConstructionExample extends App {
1023
+
1024
+ println("=== Reader.fromChunk ===")
1025
+ val chunkReader = Reader.fromChunk(Chunk(10, 20, 30))
1026
+ var v = chunkReader.read(-1)
1027
+ while (v != -1) {
1028
+ println(s"Read: $v")
1029
+ v = chunkReader.read(-1)
1030
+ }
1031
+
1032
+ println("\n=== Reader.fromRange ===")
1033
+ val rangeReader = Reader.fromRange(1 to 3)
1034
+ v = rangeReader.read(-1)
1035
+ while (v != -1) {
1036
+ println(s"Read: $v")
1037
+ v = rangeReader.read(-1)
1038
+ }
1039
+
1040
+ println("\n=== Reader.fromIterable ===")
1041
+ val listReader = Reader.fromIterable(List("a", "b", "c"))
1042
+ var sv = listReader.read(null: String)
1043
+ while (sv != null) {
1044
+ println(s"Read: $sv")
1045
+ sv = listReader.read(null: String)
1046
+ }
1047
+
1048
+ println("\n=== Reader.single ===")
1049
+ val singleReader = Reader.single(42)
1050
+ println(s"Read: ${singleReader.read(-1)}")
1051
+ println(s"Read again (closed): ${singleReader.read(-1)}")
1052
+
1053
+ println("\n=== Reader.unfold ===")
1054
+ val unfoldReader = Reader.unfold(1) { s =>
1055
+ if (s > 3) None else Some((s * 10, s + 1))
1056
+ }
1057
+ v = unfoldReader.read(-1)
1058
+ while (v != -1) {
1059
+ println(s"Read: $v")
1060
+ v = unfoldReader.read(-1)
1061
+ }
1062
+
1063
+ println("\n=== Reader state ===")
1064
+ val stateReader = Reader.fromChunk(Chunk(5, 6))
1065
+ println(s"readable before: ${stateReader.readable()}")
1066
+ stateReader.read(-1)
1067
+ println(s"readable after one read: ${stateReader.readable()}")
1068
+ stateReader.read(-1)
1069
+ println(s"readable after exhaustion: ${stateReader.readable()}")
1070
+ println(s"isClosed: ${stateReader.isClosed}")
1071
+ }
1072
+ ```
1073
+
1074
+ Run it with:
1075
+
1076
+ ```bash
1077
+ sbt "streams-examples/runMain reader.ReaderBasicConstructionExample"
1078
+ ```
1079
+
1080
+ ### Primitive Specialization and Bulk Operations
1081
+
1082
+ This example shows how primitive readers avoid boxing through `Reader#jvmType` dispatch, and demonstrates `Reader#readAll` and `Reader#skip` for bulk operations. Embed the source:
1083
+
1084
+ ```scala title="streams-examples/src/main/scala/reader/ReaderPrimitiveSpecializationExample.scala"
1085
+ /*
1086
+ * Copyright 2024-2026 John A. De Goes and the ZIO Contributors
1087
+ *
1088
+ * Licensed under the Apache License, Version 2.0 (the "License");
1089
+ * you may not use this file except in compliance with the License.
1090
+ * You may obtain a copy of the License at
1091
+ *
1092
+ * http://www.apache.org/licenses/LICENSE-2.0
1093
+ *
1094
+ * Unless required by applicable law or agreed to in writing, software
1095
+ * distributed under the License is distributed on an "AS IS" BASIS,
1096
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1097
+ * See the License for the specific language governing permissions and
1098
+ * limitations under the License.
1099
+ */
1100
+
1101
+ package reader
1102
+
1103
+ import zio.blocks.chunk.Chunk
1104
+ import zio.blocks.streams.io.Reader
1105
+
1106
+ /**
1107
+ * Demonstrates primitive specialization in readers. When a Reader is backed by
1108
+ * primitive types (Int, Long, Float, Double), specialized factory methods like
1109
+ * singleInt, singleLong, etc. avoid boxing entirely. This example also shows
1110
+ * readAll for bulk consumption and skip for advancing the reader.
1111
+ */
1112
+ object ReaderPrimitiveSpecializationExample extends App {
1113
+
1114
+ println("=== singleInt (zero-boxed) ===")
1115
+ val intReader = Reader.singleInt(42)
1116
+ println(s"Read: ${intReader.read(-1)}")
1117
+
1118
+ println("\n=== singleLong (zero-boxed) ===")
1119
+ val longReader = Reader.singleLong(9999999999L)
1120
+ println(s"Read: ${longReader.read(Long.MaxValue)}")
1121
+
1122
+ println("\n=== singleFloat (zero-boxed) ===")
1123
+ val floatReader = Reader.singleFloat(3.14f)
1124
+ println(s"Read: ${floatReader.readFloat(Float.MaxValue)}")
1125
+
1126
+ println("\n=== singleDouble (zero-boxed) ===")
1127
+ val doubleReader = Reader.singleDouble(2.718)
1128
+ println(s"Read: ${doubleReader.read(Double.MaxValue)}")
1129
+
1130
+ println("\n=== readAll: bulk drain to Chunk ===")
1131
+ val bulkReader = Reader.fromChunk(Chunk(1, 2, 3, 4, 5))
1132
+ val allElements = bulkReader.readAll()
1133
+ println(s"All elements: $allElements")
1134
+
1135
+ println("\n=== skip: discard n elements ===")
1136
+ val skipReader = Reader.fromRange(10 to 15)
1137
+ skipReader.skip(2)
1138
+ // Should now read from 12 onward
1139
+ var v = skipReader.read(-1)
1140
+ val remaining = scala.collection.mutable.ArrayBuffer[Int]()
1141
+ while (v != -1) {
1142
+ remaining += v
1143
+ v = skipReader.read(-1)
1144
+ }
1145
+ println(s"After skipping 2: ${remaining.toList}")
1146
+
1147
+ println("\n=== reset: rewind to beginning ===")
1148
+ val resetReader = Reader.fromChunk(Chunk("x", "y", "z"))
1149
+ var elem = resetReader.read(null: String)
1150
+ println(s"First read: $elem")
1151
+ resetReader.reset()
1152
+ elem = resetReader.read(null: String)
1153
+ println(s"After reset: $elem")
1154
+
1155
+ println("\n=== readable: check if elements remain ===")
1156
+ val checkReader = Reader.fromChunk(Chunk(100, 200))
1157
+ println(s"readable before read: ${checkReader.readable()}")
1158
+ checkReader.read(-1)
1159
+ println(s"readable after one read: ${checkReader.readable()}")
1160
+ checkReader.read(-1)
1161
+ println(s"readable after exhaustion: ${checkReader.readable()}")
1162
+ }
1163
+ ```
1164
+
1165
+ Run it with:
1166
+
1167
+ ```bash
1168
+ sbt "streams-examples/runMain reader.ReaderPrimitiveSpecializationExample"
1169
+ ```
1170
+
1171
+ ### Composition and Resource Management
1172
+
1173
+ This example demonstrates reader composition with `Reader#++`, resource cleanup with `Reader#withRelease`, and integration with `Stream.start` for manual pulling. Embed the source:
1174
+
1175
+ ```scala title="streams-examples/src/main/scala/reader/ReaderCompositionExample.scala"
1176
+ /*
1177
+ * Copyright 2024-2026 John A. De Goes and the ZIO Contributors
1178
+ *
1179
+ * Licensed under the Apache License, Version 2.0 (the "License");
1180
+ * you may not use this file except in compliance with the License.
1181
+ * You may obtain a copy of the License at
1182
+ *
1183
+ * http://www.apache.org/licenses/LICENSE-2.0
1184
+ *
1185
+ * Unless required by applicable law or agreed to in writing, software
1186
+ * distributed under the License is distributed on an "AS IS" BASIS,
1187
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1188
+ * See the License for the specific language governing permissions and
1189
+ * limitations under the License.
1190
+ */
1191
+
1192
+ package reader
1193
+
1194
+ import zio.blocks.chunk.Chunk
1195
+ import zio.blocks.streams.io.Reader
1196
+ import zio.blocks.streams.Stream
1197
+ import zio.blocks.scope.Scope
1198
+
1199
+ /**
1200
+ * Demonstrates reader composition with ++ (concat), resource cleanup with
1201
+ * withRelease, and integration with Stream.start for manual element-by-element
1202
+ * pulling within a Scope.
1203
+ */
1204
+ object ReaderCompositionExample extends App {
1205
+
1206
+ println("=== concat: ++ operator ===")
1207
+ val r1 = Reader.fromChunk(Chunk(1, 2, 3))
1208
+ val r2 = Reader.fromChunk(Chunk(4, 5, 6))
1209
+ val combined = r1 ++ r2
1210
+
1211
+ var v = combined.read(-1)
1212
+ val allCombined = scala.collection.mutable.ArrayBuffer[Int]()
1213
+ while (v != -1) {
1214
+ allCombined += v
1215
+ v = combined.read(-1)
1216
+ }
1217
+ println(s"Combined result: ${allCombined.toList}")
1218
+
1219
+ println("\n=== Multiple concat: a ++ b ++ c ===")
1220
+ val ra = Reader.fromChunk(Chunk("a"))
1221
+ val rb = Reader.fromChunk(Chunk("b"))
1222
+ val rc = Reader.fromChunk(Chunk("c"))
1223
+ val multi = ra ++ rb ++ rc
1224
+
1225
+ var sv = multi.read(null: String)
1226
+ val result = scala.collection.mutable.ArrayBuffer[String]()
1227
+ while (sv != null) {
1228
+ result += sv
1229
+ sv = multi.read(null: String)
1230
+ }
1231
+ println(s"Multiple concat: ${result.toList}")
1232
+
1233
+ println("\n=== withRelease: cleanup on close ===")
1234
+ var cleanupCalled = false
1235
+ val resourceReader = Reader.fromChunk(Chunk(10, 20)).withRelease { () =>
1236
+ cleanupCalled = true
1237
+ println(" Cleanup executed!")
1238
+ }
1239
+ var res = resourceReader.read(-1)
1240
+ while (res != -1) {
1241
+ res = resourceReader.read(-1)
1242
+ }
1243
+ resourceReader.close()
1244
+ println(s"Cleanup was called: $cleanupCalled")
1245
+
1246
+ println("\n=== Stream.start: manual pull with Scope ===")
1247
+ Scope.global.scoped { scope =>
1248
+ import scope.*
1249
+
1250
+ // Create a stream and open it for manual pulling
1251
+ val reader: scope.$[Reader[Int]] = Stream.range(1, 6).start(using scope)
1252
+
1253
+ $(reader) { r =>
1254
+ var streamV = r.read(-1)
1255
+ val manualResult = scala.collection.mutable.ArrayBuffer[Int]()
1256
+ while (streamV != -1) {
1257
+ manualResult += streamV
1258
+ streamV = r.read(-1)
1259
+ }
1260
+ println(s"Manual stream pull: ${manualResult.toList}")
1261
+ }
1262
+ // reader is automatically closed when scope exits
1263
+ }
1264
+
1265
+ println("\n=== repeat: infinite reader ===")
1266
+ val infiniteReader = Reader.repeat(99)
1267
+ infiniteReader.setRepeat()
1268
+
1269
+ var repeatCount = 0
1270
+ var repV = infiniteReader.read(-1)
1271
+ while (repeatCount < 3) {
1272
+ println(s"Infinite read $repeatCount: $repV")
1273
+ repV = infiniteReader.read(-1)
1274
+ repeatCount += 1
1275
+ }
1276
+ infiniteReader.close()
1277
+ }
1278
+ ```
1279
+
1280
+ Run it with:
1281
+
1282
+ ```bash
1283
+ sbt "streams-examples/runMain reader.ReaderCompositionExample"
1284
+ ```