@zio.dev/zio-blocks 0.0.33 → 0.0.55

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (215) hide show
  1. package/adr/2026-07-18-data-migration.md +123 -0
  2. package/guides/async-getting-started.md +687 -0
  3. package/guides/compile-time-resource-safety-with-scope.md +21 -16
  4. package/guides/getting-started-with-mux.md +1395 -0
  5. package/guides/query-dsl-extending.md +161 -102
  6. package/guides/query-dsl-fluent-builder.md +217 -157
  7. package/guides/query-dsl-reified-optics.md +12 -10
  8. package/guides/query-dsl-sql.md +640 -165
  9. package/guides/sql-checked-interpolation.md +173 -0
  10. package/guides/sql-transactions.md +286 -0
  11. package/guides/telemetry-guide.md +1130 -0
  12. package/guides/zio-schema-migration.md +29 -22
  13. package/index.md +248 -389
  14. package/package.json +1 -1
  15. package/plans/config-follow-up-prs.md +188 -0
  16. package/plans/config-pr-assessment-roadmap.md +310 -0
  17. package/reference/MuxDataFlow.jsx +250 -0
  18. package/reference/async.md +1499 -0
  19. package/reference/chunk.md +3533 -308
  20. package/reference/codegen/case-class.md +436 -0
  21. package/reference/codegen/emitter-config.md +383 -0
  22. package/reference/codegen/examples.md +664 -0
  23. package/reference/codegen/field.md +316 -0
  24. package/reference/codegen/index.md +317 -0
  25. package/reference/codegen/scala-emitter.md +392 -0
  26. package/reference/codegen/scala-file.md +276 -0
  27. package/reference/codegen/sealed-trait.md +408 -0
  28. package/reference/codegen/type-definition.md +340 -0
  29. package/reference/codegen/type-ref.md +201 -0
  30. package/reference/combinators.md +347 -117
  31. package/reference/config/config-decoder.md +460 -0
  32. package/reference/config/config-source.md +489 -0
  33. package/reference/config/errors.md +278 -0
  34. package/reference/config/flags.md +369 -0
  35. package/reference/config/formats.md +314 -0
  36. package/reference/config/index.md +304 -0
  37. package/reference/config/rollout.md +336 -0
  38. package/reference/context.md +9 -52
  39. package/reference/data-migration.md +269 -0
  40. package/reference/datastar/attributes.md +302 -0
  41. package/reference/datastar/events.md +234 -0
  42. package/reference/datastar/index.md +256 -0
  43. package/reference/datastar/signals.md +230 -0
  44. package/reference/datastar/sse.md +295 -0
  45. package/reference/datastar.md +346 -0
  46. package/reference/docs.md +1461 -345
  47. package/reference/endpoint/auth-type.md +146 -0
  48. package/reference/endpoint/bulk-creation.md +96 -0
  49. package/reference/endpoint/endpoint.md +297 -0
  50. package/reference/endpoint/http-codec.md +249 -0
  51. package/reference/endpoint/index.md +745 -0
  52. package/reference/endpoint/path-codec.md +225 -0
  53. package/reference/endpoint/route-pattern.md +194 -0
  54. package/reference/endpoint/route-tree.md +111 -0
  55. package/reference/endpoint/segment-codec.md +199 -0
  56. package/reference/html.md +1424 -0
  57. package/reference/htmx/attribute-values.md +359 -0
  58. package/reference/htmx/hx-encoding.md +111 -0
  59. package/reference/htmx/hx-params.md +204 -0
  60. package/reference/htmx/hx-swap.md +276 -0
  61. package/reference/htmx/hx-sync.md +251 -0
  62. package/reference/htmx/hx-target.md +314 -0
  63. package/reference/htmx/hx-trigger.md +457 -0
  64. package/reference/htmx/hx-url-update.md +239 -0
  65. package/reference/htmx/index.md +807 -0
  66. package/reference/htmx/response-headers.md +240 -0
  67. package/reference/http-model/headers.md +735 -0
  68. package/reference/http-model/index.md +49 -0
  69. package/reference/http-model/model.md +1517 -0
  70. package/reference/http-model/schema-codecs.md +522 -0
  71. package/reference/http-model/schema.md +750 -0
  72. package/reference/http-model/server-sent-event.md +341 -0
  73. package/reference/jwt.md +195 -0
  74. package/reference/maybe.md +943 -0
  75. package/reference/media-type.md +2 -2
  76. package/reference/mux.md +254 -0
  77. package/reference/mux.mdx +828 -0
  78. package/reference/openapi.md +1351 -0
  79. package/reference/projection.md +654 -0
  80. package/reference/resource-management/defer-handle.md +1 -1
  81. package/reference/resource-management/resource.md +31 -98
  82. package/reference/resource-management/scope.md +28 -220
  83. package/reference/resource-management/wire.md +5 -55
  84. package/reference/ringbuffer/MpmcDiagram.jsx +717 -0
  85. package/reference/ringbuffer/MpscDiagram.jsx +618 -0
  86. package/reference/ringbuffer/SpmcDiagram.jsx +680 -0
  87. package/reference/ringbuffer/SpscDiagram.jsx +677 -0
  88. package/reference/ringbuffer/advanced.mdx +109 -0
  89. package/reference/ringbuffer/index.mdx +145 -0
  90. package/reference/ringbuffer/mpmc.mdx +185 -0
  91. package/reference/ringbuffer/mpsc.mdx +164 -0
  92. package/reference/ringbuffer/spmc.mdx +108 -0
  93. package/reference/ringbuffer/spsc.mdx +416 -0
  94. package/reference/{allows.md → schema/allows.md} +4 -100
  95. package/reference/{binding-resolver.md → schema/binding-resolver.md} +1 -1
  96. package/reference/{binding.md → schema/binding.md} +3 -4
  97. package/reference/schema/built-in-codecs/avro.md +451 -0
  98. package/reference/schema/built-in-codecs/bson.md +510 -0
  99. package/reference/schema/built-in-codecs/csv.md +564 -0
  100. package/reference/schema/built-in-codecs/index.md +77 -0
  101. package/reference/schema/built-in-codecs/json/index.md +295 -0
  102. package/reference/schema/built-in-codecs/json/json-config.md +217 -0
  103. package/reference/{json-patch.md → schema/built-in-codecs/json/json-patch.md} +5 -5
  104. package/reference/{json-schema.md → schema/built-in-codecs/json/json-schema.md} +14 -47
  105. package/reference/schema/built-in-codecs/json/json-selection.md +322 -0
  106. package/reference/{json.md → schema/built-in-codecs/json/json.md} +32 -64
  107. package/reference/schema/built-in-codecs/messagepack.md +508 -0
  108. package/reference/schema/built-in-codecs/thrift.md +433 -0
  109. package/reference/schema/built-in-codecs/toon.md +1078 -0
  110. package/reference/{xml.md → schema/built-in-codecs/xml.md} +13 -9
  111. package/reference/schema/built-in-codecs/yaml.md +552 -0
  112. package/reference/{codec.md → schema/codec.md} +11 -11
  113. package/reference/{dynamic-optic.md → schema/dynamic-optic.md} +196 -5
  114. package/reference/{dynamic-schema.md → schema/dynamic-schema.md} +8 -8
  115. package/reference/schema/format.md +92 -0
  116. package/reference/schema/index.md +52 -0
  117. package/reference/schema/migration.md +297 -0
  118. package/reference/{modifier.md → schema/modifier.md} +58 -7
  119. package/reference/{optics.md → schema/optics.md} +2 -2
  120. package/reference/{patch.md → schema/patch.md} +1 -1
  121. package/{path-interpolator.md → reference/schema/path-interpolator.md} +167 -72
  122. package/reference/schema/reflect-transformer.md +140 -0
  123. package/reference/{schema-evolution → schema/schema-evolution}/as.md +8 -8
  124. package/reference/{schema-evolution → schema/schema-evolution}/index.md +2 -2
  125. package/reference/{schema-evolution → schema/schema-evolution}/into.md +8 -8
  126. package/reference/{schema-expr.md → schema/schema-expr.md} +110 -175
  127. package/reference/schema/schema-search.md +263 -0
  128. package/reference/{schema.md → schema/schema.md} +22 -2
  129. package/reference/{structural-types.md → schema/structural-types.md} +1 -1
  130. package/reference/{type-class-derivation.md → schema/type-class-derivation.md} +63 -1
  131. package/reference/smithy.md +1032 -0
  132. package/reference/sql/db-codec-deriver.md +71 -0
  133. package/reference/sql/db-codec.md +687 -0
  134. package/reference/sql/db-con.md +271 -0
  135. package/reference/sql/db-connection.md +153 -0
  136. package/reference/sql/db-param-writer.md +77 -0
  137. package/reference/sql/db-param.md +66 -0
  138. package/reference/sql/db-result-reader.md +148 -0
  139. package/reference/sql/db-tx.md +114 -0
  140. package/reference/sql/db-value.md +41 -0
  141. package/reference/sql/ddl.md +85 -0
  142. package/reference/sql/frag.md +288 -0
  143. package/reference/sql/index.md +341 -0
  144. package/reference/sql/repo.md +600 -0
  145. package/reference/sql/sql-dialect.md +73 -0
  146. package/reference/sql/sql-logger.md +62 -0
  147. package/reference/sql/sql-name-mapper.md +70 -0
  148. package/reference/sql/table-metadata.md +134 -0
  149. package/reference/sql/table.md +448 -0
  150. package/reference/sql/transactor-zio.md +399 -0
  151. package/reference/sql/transactor.md +363 -0
  152. package/reference/sql-zio.md +112 -0
  153. package/reference/streams/core/index.md +32 -0
  154. package/reference/streams/core/pipeline.md +854 -0
  155. package/reference/streams/core/sink.md +1404 -0
  156. package/reference/streams/core/stream.md +3236 -0
  157. package/reference/streams/execution-and-compatibility/async-execution.md +822 -0
  158. package/reference/streams/execution-and-compatibility/index.md +35 -0
  159. package/reference/streams/execution-and-compatibility/platform-differences.md +297 -0
  160. package/reference/streams/execution-and-compatibility/scala-2-compatibility.md +88 -0
  161. package/reference/streams/execution-and-compatibility/zero-boxing.md +393 -0
  162. package/reference/streams/index.md +726 -0
  163. package/reference/streams/primitives/index.md +30 -0
  164. package/reference/streams/primitives/reader.md +1992 -0
  165. package/reference/streams/primitives/writer.md +1201 -0
  166. package/reference/telemetry/common/any-value.md +90 -0
  167. package/reference/telemetry/common/attribute-key.md +87 -0
  168. package/reference/telemetry/common/attributes.md +118 -0
  169. package/reference/telemetry/common/index.md +39 -0
  170. package/reference/telemetry/common/instrumentation-scope.md +24 -0
  171. package/reference/telemetry/common/resource.md +34 -0
  172. package/reference/telemetry/index.md +311 -0
  173. package/reference/telemetry/logging/index.md +197 -0
  174. package/reference/telemetry/logging/log-enrichment.md +72 -0
  175. package/reference/telemetry/logging/log-formatter.md +100 -0
  176. package/reference/telemetry/logging/log-record-processor.md +56 -0
  177. package/reference/telemetry/logging/log-record.md +44 -0
  178. package/reference/telemetry/logging/log-writer.md +64 -0
  179. package/reference/telemetry/logging/logger-provider.md +142 -0
  180. package/reference/telemetry/logging/logger.md +83 -0
  181. package/reference/telemetry/logging/severity.md +62 -0
  182. package/reference/telemetry/metrics/index.md +150 -0
  183. package/reference/telemetry/metrics/instruments.md +183 -0
  184. package/reference/telemetry/metrics/labeled-instruments.md +74 -0
  185. package/reference/telemetry/metrics/meter-provider.md +76 -0
  186. package/reference/telemetry/metrics/meter.md +98 -0
  187. package/reference/telemetry/metrics/metric-data.md +57 -0
  188. package/reference/telemetry/otel/custom-exporter.md +216 -0
  189. package/reference/telemetry/otel/index.md +212 -0
  190. package/reference/telemetry/tracing/index.md +155 -0
  191. package/reference/telemetry/tracing/sampler.md +89 -0
  192. package/reference/telemetry/tracing/span-builder.md +57 -0
  193. package/reference/telemetry/tracing/span-context.md +39 -0
  194. package/reference/telemetry/tracing/span-data.md +32 -0
  195. package/reference/telemetry/tracing/span-kind.md +55 -0
  196. package/reference/telemetry/tracing/span-processor.md +53 -0
  197. package/reference/telemetry/tracing/span-status.md +47 -0
  198. package/reference/telemetry/tracing/span.md +117 -0
  199. package/reference/telemetry/tracing/tracer-provider.md +91 -0
  200. package/reference/telemetry/tracing/tracer.md +52 -0
  201. package/reference/typeid.md +5 -83
  202. package/sidebars.js +376 -43
  203. package/undocumented-report.md +528 -270
  204. package/reference/formats.md +0 -694
  205. package/reference/http-model.md +0 -1716
  206. package/reference/streams.md +0 -989
  207. package/ringbuffer.md +0 -249
  208. /package/reference/{json-differ.md → schema/built-in-codecs/json/json-differ.md} +0 -0
  209. /package/reference/{dynamic-value.md → schema/dynamic-value.md} +0 -0
  210. /package/reference/{lazy.md → schema/lazy.md} +0 -0
  211. /package/reference/{reflect.md → schema/reflect.md} +0 -0
  212. /package/reference/{registers.md → schema/registers.md} +0 -0
  213. /package/reference/{schema-error.md → schema/schema-error.md} +0 -0
  214. /package/reference/{syntax.md → schema/syntax.md} +0 -0
  215. /package/reference/{validation.md → schema/validation.md} +0 -0
@@ -0,0 +1,1201 @@
1
+ ---
2
+ id: writer
3
+ title: "Writer"
4
+ sidebar_label: "Writer"
5
+ description: "The push-based sink for elements: the write-and-close protocol, the primitive write family, and the sixteen deferred *Async mirrors."
6
+ keywords:
7
+ - "Push-Based Writing"
8
+ - "Deferred Effects"
9
+ - "Writer Cancellation"
10
+ - "Specialized Writes"
11
+ - "Writer"
12
+ ---
13
+
14
+ `Writer[-Elem]` is a **push-based sink for elements** that accepts values one at a time until closed or filled. It is the push-based counterpart to `Reader[+Elem]` (which pulls). Elements are written on demand by the producer, making it ideal for streaming, buffering, and integration with I/O subsystems. The fundamental operations are `write(elem): Boolean` — pushes an element and returns success or closure — and `close()` — signals the end of writing and releases resources.
15
+
16
+ `Writer[-Elem]` has these key properties:
17
+
18
+ - **Lazy and Push-Based** — nothing happens until the producer calls `write()`
19
+ - **Non-Thread-Safe** — designed for single-threaded production; concurrent access requires external synchronization
20
+ - **Explicit Closure Signal** — returns `false` when closed (clean closure) or throws when error-closed
21
+
22
+ Here is the structural shape of the `Writer` type:
23
+
24
+ ```scala
25
+ abstract class Writer[-Elem] {
26
+ def write(a: Elem): Boolean
27
+ def close(): Unit
28
+ def isClosed: Boolean
29
+
30
+ // concrete defaults for fail() and writeable()
31
+ def fail(error: Throwable): Unit = close()
32
+ def writeable(): Boolean = !isClosed
33
+ }
34
+ ```
35
+
36
+ ## Motivation
37
+
38
+ Imagine you're building a data pipeline where a producer feeds items to a bounded sink. The producer doesn't control the sink's internal state—how much capacity remains, whether it's busy, or if it's permanently closed. You need to know before each write: Is the sink ready? Did the write succeed? Is the sink closed?
39
+
40
+ With Java's `OutputStream`, you call `write()` and either it succeeds (void return) or throws an exception. This leaves ambiguity: Was the exception transient (try again later) or permanent (the stream is done)? If the buffer fills, the thread blocks—but you don't know how long, or even that it will block beforehand. There's no way to check capacity upfront, so you're forced to either over-allocate buffers (wasting memory) or catch exceptions and guess the right strategy.
41
+
42
+ `Writer` makes the state explicit and non-throwing. You check readiness with `writeable()`, then push with `write()`, which returns a `Boolean` indicating success or closure. The protocol is explicit: when `write()` returns `false`, the sink is permanently closed and you should stop. It is not exception-free, though — a writer closed with `fail` may throw the stored error on a subsequent `write()`.
43
+
44
+ ## Quick Showcase
45
+
46
+ Here's how to create and push elements to a `Writer`:
47
+
48
+ ```scala
49
+ import zio.blocks.streams.io.Writer
50
+ import scala.collection.mutable.Buffer
51
+
52
+ val collected = Buffer[Int]()
53
+ // collected: Buffer[Int] = ArrayBuffer(10, 20, 30, 40, 50)
54
+ val w = new Writer[Int] {
55
+ private var closed = false
56
+
57
+ def isClosed = closed
58
+ def write(a: Int) = {
59
+ if (!closed) { collected += a; true }
60
+ else false
61
+ }
62
+ def close() = { closed = true }
63
+ override def fail(error: Throwable) = close()
64
+ override def writeable() = !isClosed
65
+ }
66
+ // w: Writer[Int] = repl.MdocSession$MdocApp0$$anon$2@33edb4e
67
+
68
+ // Push elements, checking writeable() before each write
69
+ def pushAll(elements: List[Int]): Unit = {
70
+ elements match {
71
+ case Nil => ()
72
+ case head :: tail =>
73
+ if (w.writeable() && w.write(head)) pushAll(tail)
74
+ }
75
+ }
76
+
77
+ pushAll(List(10, 20, 30, 40, 50))
78
+ w.close()
79
+
80
+ println(s"Collected: $collected")
81
+ // Collected: ArrayBuffer(10, 20, 30, 40, 50)
82
+ println(s"Writable after close: ${w.writeable()}")
83
+ // Writable after close: false
84
+ ```
85
+
86
+ ## Writing and Closure
87
+
88
+ The fundamental protocol is: call `write(element)` to push an element. It returns `true` on success, `false` only when the writer is **closed** (not when the buffer is full). Once `write()` returns `false`, the writer is permanently closed—all further writes return `false`. There is no recovery.
89
+
90
+ ```scala
91
+ import zio.blocks.streams.io.Writer
92
+
93
+ val w = Writer.single[Int]
94
+ // w: Writer[Int] = zio.blocks.streams.io.Writer$SingleWriter@4505afa6
95
+ println(s"First write: ${w.write(42)}") // true (accepted)
96
+ // First write: true
97
+ println(s"Second write: ${w.write(99)}") // false (writer auto-closed after one element)
98
+ // Second write: false
99
+ println(s"Third write: ${w.write(77)}") // false (still closed)
100
+ // Third write: false
101
+ ```
102
+
103
+ ## Asynchronous Writes
104
+
105
+ Every effectful member of `Writer` has a deferred mirror whose name ends in `Async` and whose result is an `Async`. There are sixteen of them, and together they are the entire asynchronous surface of the type; the structural combinators `concat` and `contramap` are deliberately outside it.
106
+
107
+ A mirror does one thing. It wraps a single synchronous call in an effect that has not happened yet: constructing `writer.writeAsync(42)` performs no write at all, and driving the returned effect performs `write(42)` exactly once and yields its `Boolean`. All sixteen are `final` and delegate to one private helper, which builds them on the library's internal cancellable-defer primitive `Async.deferCancelable` (`Writer.scala:57`).
108
+
109
+ The mirrors group exactly as their synchronous twins do on this page:
110
+
111
+ | Group | Synchronous member | Deferred mirror | Result |
112
+ |--------------------|--------------------------------|-------------------------------------|-----------------------|
113
+ | Lifecycle | `close()` | `closeAsync()` | `Async[Unit]` |
114
+ | Lifecycle | `fail(error)` | `failAsync(error)` | `Async[Unit]` |
115
+ | Single element | `write(a)` | `writeAsync(a)` | `Async[Boolean]` |
116
+ | Bulk | `writeAll(chunk)` | `writeAllAsync(chunk)` | `Async[Chunk[Elem1]]` |
117
+ | Specialized | `writeInt(value)` | `writeIntAsync(value)` | `Async[Boolean]` |
118
+ | Specialized | `writeLong(value)` | `writeLongAsync(value)` | `Async[Boolean]` |
119
+ | Specialized | `writeFloat(value)` | `writeFloatAsync(value)` | `Async[Boolean]` |
120
+ | Specialized | `writeDouble(value)` | `writeDoubleAsync(value)` | `Async[Boolean]` |
121
+ | Byte and character | `writeByte(b)` | `writeByteAsync(b)` | `Async[Boolean]` |
122
+ | Byte and character | `writeBytes(buf, offset, len)` | `writeBytesAsync(buf, offset, len)` | `Async[Int]` |
123
+ | Byte and character | `writeChar(value)` | `writeCharAsync(value)` | `Async[Boolean]` |
124
+ | Byte and character | `writeShort(value)` | `writeShortAsync(value)` | `Async[Boolean]` |
125
+ | Byte and character | `writeBoolean(value)` | `writeBooleanAsync(value)` | `Async[Boolean]` |
126
+ | State checks | `isClosed` | `isClosedAsync` | `Async[Boolean]` |
127
+ | State checks | `writeable()` | `writeableAsync()` | `Async[Boolean]` |
128
+ | State checks | `jvmType` | `jvmTypeAsync` | `Async[JvmType]` |
129
+
130
+ Each mirror carries the same parameters and the same implicit evidence as its twin, so the specialized mirrors still ask for the subtype witness their twin asks for:
131
+
132
+ ```scala
133
+ abstract class Writer[-Elem] {
134
+ final def closeAsync(): Async[Unit]
135
+ final def writeAsync(a: Elem): Async[Boolean]
136
+ final def writeAllAsync[Elem1 <: Elem](chunk: Chunk[Elem1]): Async[Chunk[Elem1]]
137
+ final def writeIntAsync(value: Int)(implicit ev: Int <:< Elem): Async[Boolean]
138
+ final def writeBytesAsync(buf: Array[Byte], offset: Int, len: Int)(implicit ev: Byte <:< Elem): Async[Int]
139
+ }
140
+ ```
141
+
142
+ `jvmTypeAsync` is the mirror of `jvmType`, the writer's element representation (`JvmType.AnyRef` unless a subclass overrides it). It is the only mirror whose twin is not otherwise documented on this page.
143
+
144
+ ### What `*Async` Does and Does Not Do
145
+
146
+ These are **cancellation-aware deferral adapters, not asynchronous I/O**. The library's own scaladoc says so in as many words (`Writer.scala:51`), and it is worth repeating because sixteen methods named `*Async` invite the opposite conclusion.
147
+
148
+ What a mirror does:
149
+
150
+ - **It defers one synchronous operation.** The wrapped call is first evaluated when the effect is driven, never when it is constructed, and it runs at most once however many times the effect is composed.
151
+ - **It closes the writer on cancellation.** Every mirror installs `close()` as its cancellation hook. If cancellation wins before the operation finishes, the writer is closed and the operation's result is discarded rather than published — the run then delivers nothing at all, so a cancelled handle must never be given to `block`.
152
+
153
+ What a mirror does not do:
154
+
155
+ - **It does not move the write to another thread.** Driving `writeAsync` calls `write` on whichever thread is driving.
156
+ - **It does not make a blocking write nonblocking.** When `write` blocks — a bounded buffer with no space, a socket with a full send window — driving `writeAsync` blocks in the same place for the same duration. `writeBytesAsync` on a `Writer.fromOutputStream` is a `java.io.OutputStream.write` behind an `Async`, and that call blocks.
157
+
158
+ :::warning[These methods are not nonblocking I/O]
159
+ A `Writer` whose `write` blocks still blocks when you drive its `*Async` mirror. The mirrors buy you deferral and a cancellation hook; they do not buy you a nonblocking writer. If you need writes that genuinely suspend rather than block, that is a different writer, not a different method on this one.
160
+ :::
161
+
162
+ Cancellation is cooperative and interrupts no thread, so the hook cannot abort a call already inside a blocking `write`. It calls `close()`, and that helps exactly when closing the writer is what releases the blocked call — which is true of a writer whose blocking wait is woken by closure, and false of one that ignores its own closed flag while parked. See [Running#cancel](../../async.md#runningcancel) for what a cancelled run does and does not stop.
163
+
164
+ ### Why There Is No `concatAsync` or `contramapAsync`
165
+
166
+ The structural combinators `concat` and `contramap` have no mirrors, and that is deliberate rather than an omission. The synchronous `Writer` protocol requires `write` to return its `Boolean` immediately. A composition callback that produced an `Async` would have no honest way to report that result: `write` cannot return a pending value, and inventing one — blocking on it, or guessing `true` — would break the very protocol the page opens with. Modelling asynchronous composition needs a separate async-writer architecture, not another method here.
167
+
168
+ ## Capacity and Buffering
169
+
170
+ The default `writeable()` method returns `!isClosed`—it only tells you if the writer is closed, not whether the buffer has space. Bounded implementations can override `writeable()` to reflect remaining capacity, but this is not guaranteed by the interface. The important distinction:
171
+
172
+ - **`writeable()` returns `false`**: the writer is closed (permanent state)
173
+ - **`writeable()` returns `true` but `write()` would block**: the buffer is full but not closed. What happens next is implementation-defined: a writer backed by a bounded buffer may block the calling thread until space becomes available, while the buffer-backed writers in this library instead auto-close and return `false`.
174
+
175
+ The writers behind `NioWriters.fromByteBuffer` and its typed variants auto-close when the buffer fills, turning the full state into closure. A writer you implement yourself may instead block indefinitely waiting for space.
176
+
177
+ ## Error Handling
178
+
179
+ When the writer encounters an error, signal it with `fail(error)`. By default, `fail()` closes the writer; all subsequent `write()` calls return `false`.
180
+
181
+ If you override `fail()` to store the error internally, `write()` will throw it on the next call:
182
+
183
+ ```scala
184
+ import zio.blocks.streams.io.Writer
185
+
186
+ class ErrorStoringWriter extends Writer[Int] {
187
+ private var closed = false
188
+ private var storedError: Option[Throwable] = None
189
+
190
+ def isClosed = closed
191
+ def write(a: Int): Boolean = {
192
+ if (storedError.isDefined) throw storedError.get
193
+ if (closed) false else true
194
+ }
195
+ def close() = { closed = true }
196
+ override def fail(error: Throwable) = {
197
+ storedError = Some(error)
198
+ closed = true
199
+ }
200
+ }
201
+
202
+ val w = new ErrorStoringWriter()
203
+ // w: ErrorStoringWriter = repl.MdocSession$MdocApp9$ErrorStoringWriter@3c6ebbb9
204
+ w.fail(new Exception("Stream error"))
205
+ try {
206
+ w.write(42) // throws the stored error
207
+ } catch {
208
+ case e: Exception => println(s"Caught: ${e.getMessage}")
209
+ }
210
+ // Caught: Stream error
211
+ ```
212
+
213
+ This gives you optional error propagation: use the default `fail()` for silent closure, or override it to propagate errors as exceptions.
214
+
215
+ ## Construction
216
+
217
+ Writers are created using factory methods on the companion object, from adapters wrapping Java I/O, or by direct subclassing for custom implementations:
218
+
219
+ ### Creating Predefined Writers
220
+
221
+ `Writer.closed` — A pre-closed writer that rejects all writes. Useful as a base case for empty streams:
222
+
223
+ ```scala
224
+ object Writer {
225
+ def closed: Writer[Any]
226
+ }
227
+ ```
228
+
229
+ Create a pre-closed writer that rejects all writes:
230
+
231
+ ```scala
232
+ import zio.blocks.streams.io.Writer
233
+
234
+ val w = Writer.closed
235
+ // w: Writer[Any] = zio.blocks.streams.io.Writer$$anon$1@4596ff1b
236
+ println(w.write(42)) // false (closed)
237
+ // false
238
+ println(w.isClosed) // true
239
+ // true
240
+ ```
241
+
242
+ ### Single Element
243
+
244
+ `Writer.single` — Creates a writer that accepts exactly one element, then auto-closes. The dual of `Reader.single`:
245
+
246
+ ```scala
247
+ object Writer {
248
+ def single[Elem]: Writer[Elem]
249
+ }
250
+ ```
251
+
252
+ Create a writer that accepts exactly one element, then auto-closes:
253
+
254
+ ```scala
255
+ import zio.blocks.streams.io.Writer
256
+
257
+ val w = Writer.single[Int]
258
+ // w: Writer[Int] = zio.blocks.streams.io.Writer$SingleWriter@2bcb877a
259
+ println(w.write(42)) // true
260
+ // true
261
+ println(w.write(99)) // false (already accepted one element)
262
+ // false
263
+ println(w.isClosed) // true
264
+ // true
265
+ ```
266
+
267
+ ### Limited Capacity
268
+
269
+ `Writer.limited` — Creates a writer that accepts at most `n` elements from `inner`, then becomes closed. The dual of `Stream.take`. If `inner` closes before `n` elements are accepted, the limited writer also closes immediately without consuming the remaining capacity.
270
+
271
+ :::note
272
+ The inner writer is not automatically closed—only the limited wrapper's `isClosed` returns `true` when the limit is reached. The inner writer stays open until someone explicitly calls `close()`.
273
+ :::
274
+
275
+ ```scala
276
+ object Writer {
277
+ def limited[Elem](inner: Writer[Elem], n: Long): Writer[Elem]
278
+ }
279
+ ```
280
+
281
+ Limit a writer to accept at most n elements:
282
+
283
+ ```scala
284
+ import zio.blocks.streams.io.Writer
285
+ import scala.collection.mutable.Buffer
286
+
287
+ val collected = Buffer[Int]()
288
+ // collected: Buffer[Int] = ArrayBuffer(1, 2)
289
+ val inner = new Writer[Int] {
290
+ def isClosed = false
291
+ def write(a: Int) = { collected += a; true }
292
+ def close() = ()
293
+ }
294
+ // inner: Writer[Int] = repl.MdocSession$MdocApp19$$anon$23@5ac5be8b
295
+
296
+ val limited = Writer.limited(inner, 2)
297
+ // limited: Writer[Int] = zio.blocks.streams.io.Writer$LimitedWriter@4eafb19a
298
+ println(limited.write(1)) // true
299
+ // true
300
+ println(limited.write(2)) // true (space available)
301
+ // true
302
+ println(limited.write(3)) // false (limit of 2 reached)
303
+ // false
304
+ println(s"Collected: $collected") // Collected: Buffer(1, 2)
305
+ // Collected: ArrayBuffer(1, 2)
306
+ ```
307
+
308
+ ### I/O Adapters
309
+
310
+ `Writer.fromOutputStream` — Wraps a `java.io.OutputStream` as a `Writer[Byte]`. Calling `close()` flushes and closes the underlying stream:
311
+
312
+ ```scala
313
+ object Writer {
314
+ def fromOutputStream(os: OutputStream): Writer[Byte]
315
+ }
316
+ ```
317
+
318
+ `Writer.fromWriter` — Wraps a `java.io.Writer` as a `Writer[Char]`. Calling `close()` flushes and closes the underlying writer:
319
+
320
+ ```scala
321
+ object Writer {
322
+ def fromWriter(w: java.io.Writer): Writer[Char]
323
+ }
324
+ ```
325
+
326
+ ## Core Operations
327
+
328
+ The fundamental operations on `Writer` cover pushing elements one at a time, bulk operations, specialized writes for primitives, and state checks:
329
+
330
+ Each of these operations also has a deferred mirror, listed in [Asynchronous Writes](#asynchronous-writes) above.
331
+
332
+ ### Writing Elements
333
+
334
+ `Writer#write` — Pushes one element to the writer. Returns `true` on success, `false` if the writer is closed and cannot accept more elements. Throws if the writer was closed with an error via `Writer#fail`:
335
+
336
+ ```scala
337
+ abstract class Writer[-Elem] {
338
+ def write(a: Elem): Boolean
339
+ }
340
+ ```
341
+
342
+ Write elements and observe the return value indicating success or closure:
343
+
344
+ ```scala
345
+ import zio.blocks.streams.io.Writer
346
+
347
+ val w = Writer.single[Int]
348
+ // w: Writer[Int] = zio.blocks.streams.io.Writer$SingleWriter@3e9febfe
349
+ val result1 = w.write(42)
350
+ // result1: Boolean = true
351
+ val result2 = w.write(99) // false, already closed
352
+ // result2: Boolean = false
353
+ println(s"First: $result1, Second: $result2")
354
+ // First: true, Second: false
355
+ ```
356
+
357
+ ### Bulk Writing
358
+
359
+ `Writer#writeAll` — Writes every element in a chunk. Returns the suffix not delivered. If the writer is already closed, returns the entire chunk. Exceptions from individual writes propagate to the caller:
360
+
361
+ ```scala
362
+ abstract class Writer[-Elem] {
363
+ def writeAll[Elem1 <: Elem](chunk: Chunk[Elem1]): Chunk[Elem1]
364
+ }
365
+ ```
366
+
367
+ Write a chunk and observe how many elements were delivered:
368
+
369
+ ```scala
370
+ import zio.blocks.streams.io.Writer
371
+ import zio.blocks.chunk.Chunk
372
+
373
+ val w = Writer.single[Int]
374
+ // w: Writer[Int] = zio.blocks.streams.io.Writer$SingleWriter@51958c7
375
+ val chunk = Chunk(1, 2, 3)
376
+ // chunk: Chunk[Int] = IndexedSeq(1, 2, 3)
377
+ val remaining = w.writeAll(chunk)
378
+ // remaining: Chunk[Int] = IndexedSeq(2, 3)
379
+ println(s"Remaining: $remaining") // Chunk(2, 3)
380
+ // Remaining: Chunk(2,3)
381
+ ```
382
+
383
+ ### Specialized Writes
384
+
385
+ For primitive types, specialized write methods take a subtype witness so that a writer backed by that primitive can override them and write the value without going through the generic `write`. The default bodies delegate to `write(value.asInstanceOf[Elem])`, so a writer that does not override them gains nothing.
386
+
387
+ `writeInt` — Specialized `Int` write. Requires implicit evidence that `Int` is a subtype of `Elem`:
388
+
389
+ ```scala
390
+ abstract class Writer[-Elem] {
391
+ def writeInt(value: Int)(implicit ev: Int <:< Elem): Boolean
392
+ }
393
+ ```
394
+
395
+ `writeLong` — Specialized `Long` write:
396
+
397
+ ```scala
398
+ abstract class Writer[-Elem] {
399
+ def writeLong(value: Long)(implicit ev: Long <:< Elem): Boolean
400
+ }
401
+ ```
402
+
403
+ `writeFloat` — Specialized `Float` write:
404
+
405
+ ```scala
406
+ abstract class Writer[-Elem] {
407
+ def writeFloat(value: Float)(implicit ev: Float <:< Elem): Boolean
408
+ }
409
+ ```
410
+
411
+ `writeDouble` — Specialized `Double` write:
412
+
413
+ ```scala
414
+ abstract class Writer[-Elem] {
415
+ def writeDouble(value: Double)(implicit ev: Double <:< Elem): Boolean
416
+ }
417
+ ```
418
+
419
+ ### Byte and Character Writes
420
+
421
+ `writeByte` — Specialized byte write. Avoids boxing when `Elem = Byte`. Requires evidence that `Byte` is a subtype of `Elem`:
422
+
423
+ ```scala
424
+ abstract class Writer[-Elem] {
425
+ def writeByte(b: Byte)(implicit ev: Byte <:< Elem): Boolean
426
+ }
427
+ ```
428
+
429
+ `writeBytes` — Blocking bulk byte write. Calls `writeByte` for each byte in `buf[offset, offset+len)`, stopping early if the channel closes. Returns the number of bytes successfully written:
430
+
431
+ ```scala
432
+ abstract class Writer[-Elem] {
433
+ def writeBytes(buf: Array[Byte], offset: Int, len: Int)(implicit ev: Byte <:< Elem): Int
434
+ }
435
+ ```
436
+
437
+ `writeChar` — Specialized `Char` write. Requires evidence that `Char` is a subtype of `Elem`:
438
+
439
+ ```scala
440
+ abstract class Writer[-Elem] {
441
+ def writeChar(value: Char)(implicit ev: Char <:< Elem): Boolean
442
+ }
443
+ ```
444
+
445
+ `writeShort` — Specialized `Short` write. Requires evidence that `Short` is a subtype of `Elem`:
446
+
447
+ ```scala
448
+ abstract class Writer[-Elem] {
449
+ def writeShort(value: Short)(implicit ev: Short <:< Elem): Boolean
450
+ }
451
+ ```
452
+
453
+ `writeBoolean` — Specialized `Boolean` write. Requires evidence that `Boolean` is a subtype of `Elem`:
454
+
455
+ ```scala
456
+ abstract class Writer[-Elem] {
457
+ def writeBoolean(value: Boolean)(implicit ev: Boolean <:< Elem): Boolean
458
+ }
459
+ ```
460
+
461
+ ### State Checks
462
+
463
+ `Writer#isClosed` — Returns `true` if the writer is closed. Monotone: once `true`, never returns `false`:
464
+
465
+ ```scala
466
+ abstract class Writer[-Elem] {
467
+ def isClosed: Boolean
468
+ }
469
+ ```
470
+
471
+ `writeable` — Returns `true` if the next `write()` would accept a value without blocking (space is available and the writer is not closed). Default returns `!isClosed`. A writer backed by a bounded buffer can override it for accuracy; none of the writers in this library does. Note the spelling: it is `writeable()`, not `writable()`; `Reader`'s counterpart is `readable()`.
472
+
473
+ ```scala
474
+ abstract class Writer[-Elem] {
475
+ def writeable(): Boolean
476
+ }
477
+ ```
478
+
479
+ Check writer capacity before writing:
480
+
481
+ ```scala
482
+ import zio.blocks.streams.io.Writer
483
+
484
+ val w = Writer.single[Int]
485
+ // w: Writer[Int] = zio.blocks.streams.io.Writer$SingleWriter@fda0482
486
+ println(w.writeable()) // true
487
+ // true
488
+ w.write(42)
489
+ // res30: Boolean = true
490
+ println(w.writeable()) // false (closed after accepting one)
491
+ // false
492
+ ```
493
+
494
+ ## Composition
495
+
496
+ Writers can be concatenated to chain multiple sinks together, or transformed to adapt their input types:
497
+
498
+ ### Concatenation
499
+
500
+ `Writer#concat` — Returns a `Writer` that writes to `this` until it closes, then transparently switches to `next`. If `this` closes with an error, the error is propagated immediately without consulting `next`. The dual of `Reader#concat`:
501
+
502
+ ```scala
503
+ abstract class Writer[-Elem] {
504
+ def concat[Elem1 <: Elem](next: => Writer[Elem1]): Writer[Elem1]
505
+ }
506
+ ```
507
+
508
+ `Writer#++` — Alias for `Writer#concat`. Syntactic sugar for composing writers:
509
+
510
+ ```scala
511
+ abstract class Writer[-Elem] {
512
+ def ++[Elem1 <: Elem](next: => Writer[Elem1]): Writer[Elem1]
513
+ }
514
+ ```
515
+
516
+ Here is how concatenation switches to the next writer when the first closes:
517
+
518
+ ```scala
519
+ import zio.blocks.streams.io.Writer
520
+ import scala.collection.mutable
521
+
522
+ val collected = mutable.ArrayBuffer[Int]()
523
+ // collected: ArrayBuffer[Int] = ArrayBuffer(5, 200)
524
+ val w1 = new Writer[Int] {
525
+ def isClosed = false
526
+ def write(a: Int) = {
527
+ if (a < 10) { collected += a; true; }
528
+ else false
529
+ }
530
+ def close() = ()
531
+ }
532
+ // w1: Writer[Int] = repl.MdocSession$MdocApp32$$anon$43@3790ae1
533
+
534
+ val w2 = new Writer[Int] {
535
+ def isClosed = false
536
+ def write(a: Int) = { collected += a * 10; true }
537
+ def close() = ()
538
+ }
539
+ // w2: Writer[Int] = repl.MdocSession$MdocApp32$$anon$45@15d5ddc2
540
+
541
+ val combined = w1 ++ w2
542
+ // combined: Writer[Int] = zio.blocks.streams.io.Writer$ConcatWith@48fab02c
543
+ combined.write(5)
544
+ // res33: Boolean = true
545
+ combined.write(20) // first writer rejects, switches to second
546
+ // res34: Boolean = true
547
+ println(collected.toList) // List(5, 200)
548
+ // List(5, 200)
549
+ ```
550
+
551
+ ### Transformation
552
+
553
+ `Writer#contramap` — Returns a `Writer` that transforms incoming elements with `g` before passing them to this writer. All other operations (`Writer#isClosed`, `Writer#close`, `Writer#fail`) delegate unchanged:
554
+
555
+ ```scala
556
+ abstract class Writer[-Elem] {
557
+ def contramap[Elem2](g: Elem2 => Elem): Writer[Elem2]
558
+ }
559
+ ```
560
+
561
+ Transform the input type before writing:
562
+
563
+ ```scala
564
+ import zio.blocks.streams.io.Writer
565
+
566
+ val stringWriter = new Writer[String] {
567
+ def isClosed = false
568
+ def write(a: String) = { println(s"Writing: $a"); true }
569
+ def close() = ()
570
+ }
571
+ // stringWriter: Writer[String] = repl.MdocSession$MdocApp36$$anon$51@18c962ec
572
+
573
+ val intWriter = stringWriter.contramap[Int](_.toString)
574
+ // intWriter: Writer[Int] = zio.blocks.streams.io.Writer$Contramapped@285a433
575
+ intWriter.write(42) // Prints: Writing: 42
576
+ // Writing: 42
577
+ // res37: Boolean = true
578
+ ```
579
+
580
+ ## Closure and Error Handling
581
+
582
+ Writers support both clean closure and error closure, allowing you to signal end-of-stream gracefully or with an error condition:
583
+
584
+ ### Clean Closure
585
+
586
+ `Writer#close` — Closes the writer cleanly. After this call, `write()` returns `false` and `Writer#isClosed` returns `true`. Idempotent:
587
+
588
+ ```scala
589
+ abstract class Writer[-Elem] {
590
+ def close(): Unit
591
+ }
592
+ ```
593
+
594
+ ### Error Closure
595
+
596
+ `Writer#fail` — Closes the writer with an error. After this call, `Writer#isClosed` returns `true`. Subclasses that override this method may cause `write()` to throw `error` on subsequent calls; the default simply delegates to `Writer#close`. Both `Writer#close` and `Writer#fail` are idempotent; only the first call wins:
597
+
598
+ ```scala
599
+ abstract class Writer[-Elem] {
600
+ def fail(error: Throwable): Unit
601
+ }
602
+ ```
603
+
604
+ Close a writer with an error:
605
+
606
+ ```scala
607
+ import zio.blocks.streams.io.Writer
608
+
609
+ val w = Writer.single[Int]
610
+ // w: Writer[Int] = zio.blocks.streams.io.Writer$SingleWriter@2d919436
611
+ w.write(42)
612
+ // res39: Boolean = true
613
+ w.fail(new RuntimeException("Error"))
614
+ println(w.isClosed) // true
615
+ // true
616
+ ```
617
+
618
+ ## Contravariance
619
+
620
+ `Writer` is **contravariant** in `Elem`, meaning `Writer[-Elem]` can accept narrower types. If you have a `Writer[Number]`, you can use it as a `Writer[Int]` because every `Int` is a `Number`:
621
+
622
+ ```scala
623
+ import zio.blocks.streams.io.Writer
624
+
625
+ trait Number
626
+ case class IntNum(value: Int) extends Number
627
+
628
+ val numberWriter = new Writer[Number] {
629
+ def isClosed = false
630
+ def write(a: Number) = { println(s"Number: $a"); true }
631
+ def close() = ()
632
+ }
633
+ // numberWriter: Writer[Number] = repl.MdocSession$MdocApp42$$anon$59@5cb55db1
634
+
635
+ // numberWriter is also a Writer[IntNum] due to contravariance
636
+ val intNumWriter: Writer[IntNum] = numberWriter
637
+ // intNumWriter: Writer[IntNum] = repl.MdocSession$MdocApp42$$anon$59@5cb55db1
638
+ intNumWriter.write(IntNum(42))
639
+ // Number: IntNum(42)
640
+ // res43: Boolean = true
641
+ ```
642
+
643
+ This is the dual of Reader's covariance: Reader is covariant (`+Elem`) because narrower elements flow out; Writer is contravariant (`-Elem`) because broader element types flow in.
644
+
645
+ ## Integration with Readers and Channels
646
+
647
+ While Reader is typically used with pull-based stream operations, Writer is used internally by channel-based implementations and as an I/O adapter. The pairing is natural: a Reader pulls from a source, while a Writer pushes to a sink.
648
+
649
+ For typical stream usage, you'll see Writer indirectly when writing to files, network sockets, or other I/O resources. The `Writer.fromOutputStream` and `Writer.fromWriter` factories adapt standard Java I/O to the Writer interface.
650
+
651
+ ## Implementation Notes
652
+
653
+ Understanding `Writer`'s design decisions helps you use it correctly and avoid common pitfalls:
654
+
655
+ ### Push Vs Pull
656
+
657
+ `Writer` is push-based (producer-driven), contrasting with `Reader` which is pull-based (consumer-driven):
658
+
659
+ | Aspect | Reader | Writer |
660
+ |----------------|----------------------------|-------------------------|
661
+ | **Direction** | Source → Consumer (pull) | Producer → Sink (push) |
662
+ | **Variance** | Covariant (`+Elem`) | Contravariant (`−Elem`) |
663
+ | **Blocking** | `read()` may block | `write()` may block |
664
+ | **Signal end** | Caller-supplied sentinel, or a negative count from a bulk read | `close()` or `fail()` |
665
+ | **Dual** | Sink drains Reader | Producer feeds Writer |
666
+
667
+ ### Thread Safety
668
+
669
+ `Writer` is **not thread-safe** by default. It is designed for single-threaded, push-based production. Do not share a `Writer` across threads without external synchronization. If you need concurrent production, wrap the writer in a thread-safe queue or use a concurrent streaming library.
670
+
671
+ ### Idempotency
672
+
673
+ Both `close()` and `fail()` are idempotent: only the first call wins. Subsequent calls have no effect. This simplifies error handling in try-finally blocks.
674
+
675
+ ## Running the Examples
676
+
677
+ All code from this guide is available as runnable examples in the `streams-examples` module.
678
+
679
+ **1. Clone the repository and navigate to the project:**
680
+
681
+ Run these commands to set up the examples:
682
+
683
+ ```bash
684
+ git clone https://github.com/zio/zio-blocks.git
685
+ cd zio-blocks
686
+ ```
687
+
688
+ **2. Run individual examples with sbt:**
689
+
690
+ ### Basic Writer Construction
691
+
692
+ This example demonstrates the most common writer factories: `Writer.single`, `Writer.limited`, `Writer.closed`, and custom writers via subclassing:
693
+
694
+ ```scala title="streams-examples/src/main/scala/writer/WriterBasicConstructionExample.scala"
695
+ package writer
696
+
697
+ import zio.blocks.streams.io.Writer
698
+ import zio.blocks.chunk.Chunk
699
+ import scala.collection.mutable
700
+
701
+ /**
702
+ * Demonstrates the most common Writer factories: single, limited, closed, and
703
+ * custom writers via subclassing. Each writer is fed manually with write() to
704
+ * show how to produce elements.
705
+ */
706
+ object WriterBasicConstructionExample extends App {
707
+
708
+ println("=== Writer.single ===")
709
+ val singleWriter = Writer.single[Int]
710
+ println(s"Write 42: ${singleWriter.write(42)}")
711
+ println(s"Write 99 (closed): ${singleWriter.write(99)}")
712
+ println(s"isClosed: ${singleWriter.isClosed}")
713
+
714
+ println("\n=== Writer.closed ===")
715
+ val closedWriter = Writer.closed
716
+ println(s"Write to closed: ${closedWriter.write(1)}")
717
+ println(s"isClosed: ${closedWriter.isClosed}")
718
+
719
+ println("\n=== Writer.limited ===")
720
+ val limitedWriter = Writer.limited(Writer.single[String], 2)
721
+ val a = "a"
722
+ val b = "b"
723
+ val c = "c"
724
+ println(s"Write 'a': ${limitedWriter.write(a)}")
725
+ println(s"Write 'b': ${limitedWriter.write(b)}")
726
+ println(s"Write 'c': ${limitedWriter.write(c)}")
727
+ println(s"isClosed: ${limitedWriter.isClosed}")
728
+
729
+ println("\n=== writeAll: bulk write ===")
730
+ val collected = mutable.ArrayBuffer[Int]()
731
+ val collectWriter = new Writer[Int] {
732
+ def isClosed = false
733
+ def write(a: Int) = { collected += a; true }
734
+ def close(): Unit = ()
735
+ }
736
+
737
+ val chunk = Chunk(10, 20, 30)
738
+ val remaining = collectWriter.writeAll(chunk)
739
+ println(s"Collected: ${collected.toList}")
740
+ println(s"Remaining: $remaining")
741
+
742
+ println("\n=== writeable: check capacity ===")
743
+ val capWriter = Writer.single[Int]
744
+ println(s"writeable before: ${capWriter.writeable()}")
745
+ capWriter.write(42)
746
+ println(s"writeable after: ${capWriter.writeable()}")
747
+
748
+ println("\n=== Custom Writer ===")
749
+ val upperWriter = new Writer[String] {
750
+ private val buffer = mutable.ArrayBuffer[String]()
751
+ def isClosed = false
752
+ def write(a: String) = {
753
+ buffer += a.toUpperCase()
754
+ true
755
+ }
756
+ def close(): Unit = println(s"Final buffer: $buffer")
757
+ }
758
+
759
+ upperWriter.write("hello")
760
+ upperWriter.write("world")
761
+ upperWriter.close()
762
+ }
763
+ ```
764
+
765
+ Run this example with:
766
+
767
+ ```bash
768
+ sbt "streams-examples/runMain writer.WriterBasicConstructionExample"
769
+ ```
770
+
771
+ ### Composition and Transformation
772
+
773
+ This example shows writer composition with `Writer#++` (concat), transformation with `Writer#contramap`, and bulk writes with `Writer#writeAll`:
774
+
775
+ ```scala title="streams-examples/src/main/scala/writer/WriterCompositionExample.scala"
776
+ package writer
777
+
778
+ import zio.blocks.streams.io.Writer
779
+ import zio.blocks.chunk.Chunk
780
+ import scala.collection.mutable
781
+
782
+ /**
783
+ * Demonstrates writer composition with ++ (concat), transformation with
784
+ * contramap, and error handling via fail(). Shows how multiple writers can be
785
+ * chained and how transformations are applied before writing.
786
+ */
787
+ object WriterCompositionExample extends App {
788
+
789
+ println("=== concat: ++ operator ===")
790
+ val results = mutable.ArrayBuffer[Int]()
791
+
792
+ val w1 = new Writer[Int] {
793
+ def isClosed = false
794
+ def write(a: Int) = {
795
+ results += a * 10
796
+ a < 50 // reject values >= 50
797
+ }
798
+ def close(): Unit = ()
799
+ }
800
+
801
+ val w2 = new Writer[Int] {
802
+ def isClosed = false
803
+ def write(a: Int) = {
804
+ results += a * 100
805
+ true
806
+ }
807
+ def close(): Unit = ()
808
+ }
809
+
810
+ val combined = w1 ++ w2
811
+
812
+ combined.write(10) // accepted by w1 (10 < 50)
813
+ combined.write(60) // rejected by w1, switches to w2
814
+ println(s"Results: ${results.toList}")
815
+
816
+ println("\n=== contramap: transform elements ===")
817
+ val stringResults = mutable.ArrayBuffer[String]()
818
+ val stringWriter = new Writer[String] {
819
+ def isClosed = false
820
+ def write(a: String) = {
821
+ stringResults += a
822
+ true
823
+ }
824
+ def close(): Unit = ()
825
+ }
826
+
827
+ val intWriter = stringWriter.contramap[Int](_.toString)
828
+ intWriter.write(42)
829
+ intWriter.write(99)
830
+ println(s"String results: ${stringResults.toList}")
831
+
832
+ println("\n=== Multiple contramap: chained transformations ===")
833
+ val doubleResults = mutable.ArrayBuffer[String]()
834
+ val doubleStringWriter = new Writer[String] {
835
+ def isClosed = false
836
+ def write(a: String) = {
837
+ doubleResults += a
838
+ true
839
+ }
840
+ def close(): Unit = ()
841
+ }
842
+
843
+ val intDoubleWriter = doubleStringWriter
844
+ .contramap[Double](d => s"${d * 2}")
845
+ .contramap[Int](i => i.toDouble)
846
+
847
+ intDoubleWriter.write(5) // 5 -> 5.0 -> "10.0"
848
+ intDoubleWriter.write(10) // 10 -> 10.0 -> "20.0"
849
+ println(s"Double results: ${doubleResults.toList}")
850
+
851
+ println("\n=== fail: error closure ===")
852
+ val failWriter = new Writer[Int] {
853
+ private var closed = false
854
+ def isClosed = closed
855
+ def write(a: Int) =
856
+ if (closed) false else { println(s"Write: $a"); true }
857
+ def close(): Unit = closed = true
858
+ override def fail(error: Throwable): Unit = {
859
+ closed = true
860
+ println(s"Failed with: ${error.getMessage}")
861
+ }
862
+ }
863
+
864
+ failWriter.write(1)
865
+ failWriter.fail(new RuntimeException("Oops"))
866
+ println(s"isClosed after fail: ${failWriter.isClosed}")
867
+
868
+ println("\n=== writeAll: bulk operations ===")
869
+ val bulkResults = mutable.ArrayBuffer[Int]()
870
+ val bulkWriter = Writer.limited(
871
+ new Writer[Int] {
872
+ def isClosed = false
873
+ def write(a: Int) = { bulkResults += a; true }
874
+ def close(): Unit = ()
875
+ },
876
+ 2
877
+ )
878
+
879
+ val chunk = Chunk(1, 2, 3, 4)
880
+ val unwritten = bulkWriter.writeAll(chunk)
881
+ println(s"Bulk results: ${bulkResults.toList}")
882
+ println(s"Unwritten: $unwritten")
883
+ }
884
+ ```
885
+
886
+ Run this example with:
887
+
888
+ ```bash
889
+ sbt "streams-examples/runMain writer.WriterCompositionExample"
890
+ ```
891
+
892
+ ### I/O Adapters
893
+
894
+ This example demonstrates I/O integration with `Writer.fromOutputStream` and `Writer.fromWriter` for streaming to files or character streams:
895
+
896
+ ```scala title="streams-examples/src/main/scala/writer/WriterIOAdapterExample.scala"
897
+ package writer
898
+
899
+ import zio.blocks.streams.io.Writer
900
+ import java.io.{ByteArrayOutputStream, StringWriter}
901
+
902
+ /**
903
+ * Demonstrates I/O integration with Writer via fromOutputStream and fromWriter.
904
+ * Shows how to write bytes to streams and characters to writers using the
905
+ * Writer interface.
906
+ */
907
+ object WriterIOAdapterExample extends App {
908
+
909
+ println("=== Writer.fromOutputStream ===")
910
+ val byteStream = new ByteArrayOutputStream()
911
+ val byteWriter = Writer.fromOutputStream(byteStream)
912
+
913
+ byteWriter.write(72.toByte) // 'H'
914
+ byteWriter.write(105.toByte) // 'i'
915
+ byteWriter.write(33.toByte) // '!'
916
+ byteWriter.close()
917
+
918
+ println(s"Output: ${byteStream.toString("UTF-8")}")
919
+
920
+ println("\n=== writeBytes: bulk byte write ===")
921
+ val byteStream2 = new ByteArrayOutputStream()
922
+ val byteWriter2 = Writer.fromOutputStream(byteStream2)
923
+
924
+ val message = "Hello".getBytes("UTF-8")
925
+ val bytesWritten = byteWriter2.writeBytes(message, 0, message.length)
926
+ byteWriter2.close()
927
+
928
+ println(s"Bytes written: $bytesWritten")
929
+ println(s"Output: ${byteStream2.toString("UTF-8")}")
930
+
931
+ println("\n=== Writer.fromWriter ===")
932
+ val charStream = new StringWriter()
933
+ val charWriter = Writer.fromWriter(charStream)
934
+
935
+ charWriter.write('H')
936
+ charWriter.write('e')
937
+ charWriter.write('l')
938
+ charWriter.write('l')
939
+ charWriter.write('o')
940
+ charWriter.close()
941
+
942
+ println(s"Output: ${charStream.toString}")
943
+
944
+ println("\n=== writeChar: individual character writes ===")
945
+ val charStream2 = new StringWriter()
946
+ val charWriter2 = Writer.fromWriter(charStream2)
947
+
948
+ val greeting = "Hi!"
949
+ for (c <- greeting) {
950
+ val result = charWriter2.writeChar(c)
951
+ println(s"Write '$c': $result")
952
+ }
953
+ charWriter2.close()
954
+
955
+ println(s"Output: ${charStream2.toString}")
956
+
957
+ println("\n=== Specialized numeric writes ===")
958
+ val charStream3 = new StringWriter()
959
+ val charWriter3 = Writer.fromWriter(charStream3)
960
+
961
+ // Note: These specialized methods require the writer to be typed to accept them
962
+ // For a demo, we'll just show the interface exists
963
+ println("Specialized write methods available:")
964
+ println(" - writeInt(value: Int)")
965
+ println(" - writeLong(value: Long)")
966
+ println(" - writeFloat(value: Float)")
967
+ println(" - writeDouble(value: Double)")
968
+ println(" - writeBoolean(value: Boolean)")
969
+ println(" - writeShort(value: Short)")
970
+
971
+ charWriter3.close()
972
+
973
+ println("\n=== Error handling in I/O ===")
974
+ val closedStream = new ByteArrayOutputStream()
975
+ closedStream.close()
976
+ val failingWriter = Writer.fromOutputStream(closedStream)
977
+
978
+ val writeResult = failingWriter.write(65.toByte) // 'A'
979
+ println(s"Write to closed stream: $writeResult")
980
+ println(s"Writer is closed: ${failingWriter.isClosed}")
981
+ }
982
+ ```
983
+
984
+ Run this example with:
985
+
986
+ ```bash
987
+ sbt "streams-examples/runMain writer.WriterIOAdapterExample"
988
+ ```
989
+
990
+ ### Bounded Implementation
991
+
992
+ This example shows how to implement a bounded Writer that wraps a fixed-capacity container and auto-closes when full. It demonstrates the protocol: `write()` returns `false` only on closure (not buffer fullness), and `writeable()` reflects closure state:
993
+
994
+ ```scala title="streams-examples/src/main/scala/writer/WriterBoundedImplementationExample.scala"
995
+ package writer
996
+
997
+ import zio.blocks.streams.io.Writer
998
+ import scala.collection.mutable
999
+
1000
+ /**
1001
+ * Demonstrates implementing a bounded Writer that auto-closes when capacity is
1002
+ * reached. Shows how write() returns false only on closure, not on buffer
1003
+ * fullness, and how writeable() reflects the closure state.
1004
+ */
1005
+ object WriterBoundedImplementationExample extends App {
1006
+
1007
+ class BoundedWriter[A](maxCapacity: Int) extends Writer[A] {
1008
+ private val buffer = mutable.Buffer[A]()
1009
+ private var closed = false
1010
+
1011
+ def isClosed: Boolean = closed
1012
+
1013
+ def write(a: A): Boolean =
1014
+ if (closed) false
1015
+ else if (buffer.size < maxCapacity) {
1016
+ buffer += a
1017
+ true
1018
+ } else {
1019
+ // Buffer full: auto-close and reject
1020
+ closed = true
1021
+ false
1022
+ }
1023
+
1024
+ def close(): Unit = closed = true
1025
+
1026
+ override def fail(error: Throwable): Unit = close()
1027
+
1028
+ def contents: mutable.Buffer[A] = buffer
1029
+ }
1030
+
1031
+ println("=== Bounded Writer with auto-close ===")
1032
+ val bounded = new BoundedWriter[Int](3)
1033
+
1034
+ println(s"Write 10: ${bounded.write(10)}")
1035
+ println(s"Write 20: ${bounded.write(20)}")
1036
+ println(s"Write 30: ${bounded.write(30)}")
1037
+ println(s"Write 40 (buffer full, auto-closes): ${bounded.write(40)}")
1038
+ println(s"Write 50 (closed): ${bounded.write(50)}")
1039
+
1040
+ println(s"\nBuffer contents: ${bounded.contents}")
1041
+ println(s"Writer closed: ${bounded.isClosed}")
1042
+ println(s"Writeable: ${bounded.writeable()}")
1043
+
1044
+ println("\n=== Behavior summary ===")
1045
+ println("• write() returns true while space exists")
1046
+ println("• When buffer fills, write() auto-closes and returns false")
1047
+ println("• All subsequent write() calls return false (closure is permanent)")
1048
+ println("• writeable() reflects closure state, not buffer capacity")
1049
+ }
1050
+ ```
1051
+
1052
+ Run this example with:
1053
+
1054
+ ```bash
1055
+ sbt "streams-examples/runMain writer.WriterBoundedImplementationExample"
1056
+ ```
1057
+
1058
+ ### Deferred and Cancellable Writes
1059
+
1060
+ This example makes the `*Async` caveat concrete. It shows that constructing a mirror writes nothing while driving it writes once, composes three mirrors into one effect, and then cancels a driven `writeAsync` whose write is parked — observing that cancellation closed the writer, that no element was recorded, and that the run delivers no value at all:
1061
+
1062
+ ```scala title="streams-examples/src/main/scala/writer/WriterAsyncExample.scala"
1063
+ package writer
1064
+
1065
+ import zio.blocks.async.*
1066
+ import zio.blocks.chunk.Chunk
1067
+ import zio.blocks.streams.io.Writer
1068
+
1069
+ import java.util.concurrent.{ConcurrentLinkedQueue, CountDownLatch}
1070
+
1071
+ /**
1072
+ * The `*Async` mirrors on `Writer`, and the two properties that define them:
1073
+ * each mirror defers exactly one synchronous writer operation until the
1074
+ * returned effect is driven, and cancellation closes the writer and suppresses
1075
+ * the stale result.
1076
+ *
1077
+ * Neither property makes a write nonblocking, and neither moves it to another
1078
+ * thread. The last section only observes cancellation at all because the
1079
+ * writer's own `close()` is what releases the parked write.
1080
+ *
1081
+ * JVM only, because it ends in `.block` to turn an `Async` into a value for
1082
+ * `main`.
1083
+ */
1084
+ object WriterAsyncExample {
1085
+
1086
+ /** Records what actually reached the writer, so deferral is observable. */
1087
+ final class RecordingWriter extends Writer[Int] {
1088
+ private val recorded = scala.collection.mutable.ArrayBuffer.empty[Int]
1089
+ private var closed = false
1090
+ def isClosed: Boolean = closed
1091
+ def write(a: Int): Boolean = if (closed) false else { recorded += a; true }
1092
+ def close(): Unit = closed = true
1093
+ def snapshot: List[Int] = recorded.toList
1094
+ }
1095
+
1096
+ /**
1097
+ * A writer whose `write` parks until someone closes it. `close()` is the
1098
+ * cancellation hook every `*Async` mirror installs, so cancelling a driven
1099
+ * `writeAsync` is what wakes this writer up again.
1100
+ */
1101
+ final class GatedWriter extends Writer[Int] {
1102
+ private val gate = new CountDownLatch(1)
1103
+ private val recorded = new ConcurrentLinkedQueue[Int]
1104
+ @volatile private var closed = false
1105
+
1106
+ /** Counts down once the deferred thunk has entered `write`. */
1107
+ val entered = new CountDownLatch(1)
1108
+
1109
+ /** Counts down once `close()` has run. */
1110
+ val wasClosed = new CountDownLatch(1)
1111
+
1112
+ /** Counts down once the parked `write` has returned. */
1113
+ val finished = new CountDownLatch(1)
1114
+
1115
+ def isClosed: Boolean = closed
1116
+
1117
+ def write(a: Int): Boolean = {
1118
+ entered.countDown()
1119
+ gate.await()
1120
+ val accepted =
1121
+ if (closed) false
1122
+ else { recorded.add(a); true }
1123
+ finished.countDown()
1124
+ accepted
1125
+ }
1126
+
1127
+ def close(): Unit = {
1128
+ closed = true
1129
+ gate.countDown()
1130
+ wasClosed.countDown()
1131
+ }
1132
+
1133
+ def recordedCount: Int = recorded.size
1134
+ }
1135
+
1136
+ def main(args: Array[String]): Unit = {
1137
+ deferral()
1138
+ sequence()
1139
+ cancellation()
1140
+ }
1141
+
1142
+ /** Constructing a mirror writes nothing; driving it writes exactly once. */
1143
+ private def deferral(): Unit = {
1144
+ val writer = new RecordingWriter
1145
+ val pending = writer.writeAsync(1)
1146
+
1147
+ println(s"deferral: after construction recorded=${writer.snapshot}")
1148
+ println(s"deferral: after driving once accepted=${pending.block}, recorded=${writer.snapshot}")
1149
+ }
1150
+
1151
+ /** The mirrors compose like any other `Async`, one operation per step. */
1152
+ private def sequence(): Unit = {
1153
+ val writer = new RecordingWriter
1154
+
1155
+ val program: Async[Chunk[Int]] =
1156
+ writer
1157
+ .writeAsync(10)
1158
+ .flatMap(_ => writer.writeAllAsync(Chunk(20, 30, 40)))
1159
+ .flatMap(undelivered => writer.closeAsync().map(_ => undelivered))
1160
+
1161
+ val undelivered = program.block
1162
+ println(s"sequence: recorded=${writer.snapshot}, undelivered=$undelivered, closed=${writer.isClosed}")
1163
+ }
1164
+
1165
+ /**
1166
+ * Cancellation closes the writer and discards the result it was about to
1167
+ * produce.
1168
+ */
1169
+ private def cancellation(): Unit = {
1170
+ val writer = new GatedWriter
1171
+ val running = writer.writeAsync(99).start
1172
+
1173
+ // Wait until the deferred thunk is parked inside `write`, so cancellation
1174
+ // races a genuinely in-flight operation rather than an unstarted one.
1175
+ writer.entered.await()
1176
+ running.cancel()
1177
+
1178
+ // Cancellation ran `close()`, which released the parked `write`.
1179
+ writer.wasClosed.await()
1180
+ writer.finished.await()
1181
+
1182
+ // The `false` that `write` then returned lost the race to publish, so this
1183
+ // run never delivers a value. Never call `.block` on a cancelled handle.
1184
+ println(s"cancellation: closed=${writer.isClosed}, recorded=${writer.recordedCount}")
1185
+ }
1186
+ }
1187
+ ```
1188
+
1189
+ Run this example with:
1190
+
1191
+ ```bash
1192
+ sbt "streams-examples/runMain writer.WriterAsyncExample"
1193
+ ```
1194
+
1195
+ ## See Also
1196
+
1197
+ - [Asynchronous Stream Execution](../execution-and-compatibility/async-execution.md#cancellation) — how cancellation reaches a stream's resources, and the asynchronous stream API the deferred mirrors sit beside
1198
+ - [Async Reference](../../async.md#runningcancel) — what `Running#cancel` stops, why a cancelled run never delivers, and why the cancel hook cannot interrupt a blocked thread
1199
+ - [Reader](./reader.md) — the pull-based dual of this type, and the reader kinds a sink drains
1200
+ - [Sink](../core/sink.md) — the consumer side of a stream, which drains a `Reader` rather than feeding a `Writer`
1201
+ - [Zero-Boxing Streams](../execution-and-compatibility/zero-boxing.md) — why the specialized write family exists and how a primitive lane is chosen