@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,823 @@
1
+ ---
2
+ id: mux
3
+ title: "Mux"
4
+ ---
5
+
6
+ import Tabs from '@theme/Tabs';
7
+ import TabItem from '@theme/TabItem';
8
+ import MuxDataFlow from './MuxDataFlow.jsx';
9
+
10
+ `zio-blocks-mux` is a **high-performance multiplexer for ID-multiplexed protocols** (HTTP/2, QUIC, WebSockets with multiplexing, and other stream-based transports). It manages multiple concurrent independent streams over a shared transport, each identified by a unique ID, with separate inbound/outbound message queues and automatic state machine lifecycle management.
11
+
12
+ Core types: `Mux`, `MuxStream`, `MuxError`.
13
+
14
+ Create a mux and exchange messages:
15
+
16
+ ```scala
17
+ import zio.blocks.mux._
18
+
19
+ val mux = Mux[Int, String, String](100)
20
+ val streamOrError = mux.open(1)
21
+ val stream = streamOrError match {
22
+ case s: MuxStream[Int, String, String] => s
23
+ case _: MuxError => sys.error("Failed to open stream")
24
+ }
25
+ stream.send("hello")
26
+ ```
27
+
28
+ ## Introduction
29
+
30
+ Multiplexing allows a single transport connection (TCP, QUIC, WebSocket) to carry multiple independent logical streams simultaneously. Each stream has its own ID, message queues, and lifecycle independent from others. The `Mux` primitive handles all the bookkeeping: stream creation, capacity enforcement, message queuing with backpressure, and graceful shutdown semantics.
31
+
32
+ ## Motivation
33
+
34
+ Without multiplexing, protocols must open a new connection per concurrent operation (HTTP/1.1 with keep-alive), which is expensive and scales poorly. With multiplexing (HTTP/2, QUIC), one connection carries many streams, reducing connection overhead and latency while maintaining logical independence.
35
+
36
+ `Mux` provides:
37
+ - **Thread-safe stream registry** — concurrent `open`, `get`, `cancel` operations on the registry (lock-based on JVM, lock-free on JS)
38
+ - **Lock-free per-stream queues** — `send()` and `offerInbound()` use lock-free ring buffers on JVM for high-throughput messaging
39
+ - **Separate inbound/outbound queues** — independent message directions, two-way communication
40
+ - **Automatic state machine** — stream lifecycle (OPEN → HALF_CLOSED_LOCAL/REMOTE → CLOSED) with proper half-close semantics
41
+ - **Backpressure** — per-stream and mux-level capacity limits to prevent unbounded buffering
42
+ - **Graceful shutdown** — `closeAll` atomically closes all streams with a terminal error
43
+ - **Thread-safe per-stream operations** — `send` and `offerInbound` are multi-thread safe; `receive` and `takeOutbound` follow single-consumer contract
44
+
45
+ ## Installation
46
+
47
+ Add the dependency to your build:
48
+
49
+ ```scala
50
+ libraryDependencies += "dev.zio" %% "zio-blocks-mux" % "@VERSION@"
51
+ ```
52
+
53
+ For Scala.js:
54
+
55
+ ```scala
56
+ libraryDependencies += "dev.zio" %%% "zio-blocks-mux" % "@VERSION@"
57
+ ```
58
+
59
+ Supported Scala versions: 2.13.x and 3.x
60
+
61
+ :::tip[Getting Started]
62
+ New to Mux? Check out the [Getting Started with Mux](../guides/getting-started-with-mux.md) tutorial for a comprehensive step-by-step guide that teaches you the core concepts, common patterns, and best practices. The tutorial is designed for newcomers and includes runnable examples.
63
+ :::
64
+
65
+ ## Overview
66
+
67
+ **IMPORTANT: Error Handling with Union Types and Either**
68
+
69
+ In Scala 3, methods return union types (e.g., `Option[Out] | MuxError`). In Scala 2, they return `Either[MuxError, Option[Out]]`. You must use pattern matching to safely handle all branches:
70
+ - Success cases: `Some(msg)` or `None` (in Scala 3) / `Right(Some(msg))` or `Right(None)` (in Scala 2)
71
+ - Error cases: `MuxError` (in Scala 3) / `Left(error)` (in Scala 2)
72
+
73
+ Using higher-order functions like `.forEach()` or `.map()` without pattern matching will silently ignore error conditions and lose error information. Always explicitly pattern-match on all branches to handle both success and failure paths correctly.
74
+
75
+ The multiplexer has three core concepts:
76
+
77
+ **`Mux[Id, In, Out]`** is the entry point. You create it with a fixed capacity (maximum concurrent streams), then open, retrieve, cancel, or close streams. It enforces capacity limits and maintains a registry of active streams.
78
+
79
+ **`MuxStream[Id, In, Out]`** represents a single logical stream within the mux. It has two independent message queues: one for messages you send (outbound, drained by the protocol), and one for messages delivered to you (inbound, enqueued by the protocol). It also manages a state machine that enforces correct sequencing of sends and receives as the stream progresses through OPEN, HALF_CLOSED_LOCAL, HALF_CLOSED_REMOTE, and CLOSED states.
80
+
81
+ **`MuxError`** is a sealed trait representing all failure cases: `StreamClosed` (attempting operations on a closed stream), `CapacityExceeded` (too many concurrent streams), `QueueFull` (per-stream message queue exhausted), `Cancelled` (stream was cancelled by peer), `MuxClosed` (mux itself is closed), and `ProtocolError` (invalid state transition, duplicate ID, null message).
82
+
83
+ ## How They Work Together
84
+
85
+ The typical flow is:
86
+
87
+ **1. Create a mux** with a fixed capacity:
88
+
89
+ ```scala
90
+ import zio.blocks.mux._
91
+
92
+ val mux = Mux[Int, String, String](100)
93
+ ```
94
+
95
+ **2. Open a stream** (allocates a slot in the mux's capacity):
96
+
97
+ ```scala
98
+ import zio.blocks.mux._
99
+
100
+ val mux = Mux[Int, String, String](100)
101
+ val streamOrError = mux.open(1)
102
+ val stream = streamOrError match {
103
+ case s: MuxStream[Int, String, String] => s
104
+ case error: MuxError => throw new RuntimeException(s"Failed: $error")
105
+ }
106
+ ```
107
+
108
+ **3. Exchange messages** via the stream's two-way queue:
109
+ - **Your side sends messages** via `stream.send(msg)`, placing them in the outbound queue.
110
+ - **The protocol drains** those messages via `stream.takeOutbound()` and transmits them over the shared transport.
111
+ - **The protocol receives** messages from the peer and delivers them via `stream.offerInbound(msg)`, placing them in the inbound queue.
112
+ - **Your side receives messages** via `stream.receive()`, reading from the inbound queue.
113
+
114
+ **4. Signal end-of-stream** when either side is done sending:
115
+ - Your side calls `stream.halfClose()` to signal you're done sending (local close).
116
+ - The protocol calls `stream.signalRemoteClose()` when the peer signals end-of-stream (remote close).
117
+ - Once both sides close, the stream transitions to CLOSED.
118
+
119
+ **5. Close or cancel** the stream to release capacity:
120
+ - Call `stream.close()` to forcibly close a single stream.
121
+ - Call `mux.cancel(id, reason)` to cancel a stream externally (e.g., protocol error).
122
+ - Call `mux.closeAll(reason)` to atomically close all active streams and reject new opens.
123
+
124
+ This design mirrors **HTTP/2 stream lifecycle**: each stream is independent, supports half-closed states for proper shutdown, and the mux enforces capacity limits and graceful shutdown semantics.
125
+
126
+ The architecture shows how application code, mux streams, and protocol layers interact:
127
+
128
+ ```
129
+ ┌─────────────────────────────────────────────────────────┐
130
+ │ Your Application │
131
+ └──────────────────────┬──────────────────────────────────┘
132
+ │
133
+ send(msg) │ receive(msg)
134
+ ────────┬┴────────
135
+ │
136
+ ┌─────────────▼──────────────┐
137
+ │ MuxStream (Stream 1) │
138
+ │ ┌─────────────────────────┐│
139
+ │ │ Outbound Queue ││
140
+ │ │ (messages to peer) ││
141
+ │ └─────────────┬───────────┘│
142
+ │ │ │
143
+ │ ┌─────────────┴───────────┐│
144
+ │ │ Inbound Queue ││
145
+ │ │ (messages from peer) ││
146
+ │ └─────────────┬───────────┘│
147
+ └───────────────┼────────────┘
148
+ │
149
+ takeOutbound() │ offerInbound()
150
+ ────────┬─┴──────────
151
+ │
152
+ ┌─────────────────────▼──────────────────────────────────┐
153
+ │ Protocol Layer (HTTP/2 framing, etc.) │
154
+ └─────────────────────┬──────────────────────────────────┘
155
+ │
156
+ Shared Transport (TCP, QUIC, etc.)
157
+ ```
158
+
159
+ The mux holds multiple streams in a concurrent map. Each stream can be accessed independently:
160
+
161
+ <Tabs groupId="scala-version" defaultValue="scala3">
162
+ <TabItem value="scala2" label="Scala 2">
163
+
164
+ ```scala
165
+ import zio.blocks.mux._
166
+
167
+ val mux = Mux[Int, String, String](100)
168
+ val s1: Either[MuxError, MuxStream[Int, String, String]] = mux.open(1)
169
+ val stream1 = s1 match {
170
+ case Right(s) => s
171
+ case Left(error) => throw new RuntimeException(s"Failed: $error")
172
+ }
173
+
174
+ val s2: Either[MuxError, MuxStream[Int, String, String]] = mux.open(2)
175
+ val stream2 = s2 match {
176
+ case Right(s) => s
177
+ case Left(error) => throw new RuntimeException(s"Failed: $error")
178
+ }
179
+
180
+ stream1.send("hello")
181
+ stream2.send("world")
182
+ mux.get(1).foreach { stream =>
183
+ stream.receive() match {
184
+ case Right(Some(msg)) => println(s"Received: $msg")
185
+ case Right(None) => println("No message yet")
186
+ case Left(error) => println(s"Error: $error")
187
+ }
188
+ }
189
+ mux.get(2).foreach { stream =>
190
+ stream.receive() match {
191
+ case Right(Some(msg)) => println(s"Received: $msg")
192
+ case Right(None) => println("No message yet")
193
+ case Left(error) => println(s"Error: $error")
194
+ }
195
+ }
196
+ ```
197
+
198
+ </TabItem>
199
+ <TabItem value="scala3" label="Scala 3">
200
+
201
+ ```scala
202
+ import zio.blocks.mux._
203
+
204
+ val mux = Mux[Int, String, String](100)
205
+
206
+ val s1: MuxStream[Int, String, String] | MuxError = mux.open(1)
207
+ val stream1 = s1 match {
208
+ case s: MuxStream[Int, String, String] => s
209
+ case error: MuxError => throw new RuntimeException(s"Failed: $error")
210
+ }
211
+
212
+ val s2: MuxStream[Int, String, String] | MuxError = mux.open(2)
213
+ val stream2 = s2 match {
214
+ case s: MuxStream[Int, String, String] => s
215
+ case error: MuxError => throw new RuntimeException(s"Failed: $error")
216
+ }
217
+
218
+ stream1.send("hello")
219
+ stream2.send("world")
220
+ mux.get(1).foreach { stream =>
221
+ stream.receive() match {
222
+ case Some(msg) => println(s"Received: $msg")
223
+ case None => println("No message yet")
224
+ case error: MuxError => println(s"Error: $error")
225
+ }
226
+ }
227
+ mux.get(2).foreach { stream =>
228
+ stream.receive() match {
229
+ case Some(msg) => println(s"Received: $msg")
230
+ case None => println("No message yet")
231
+ case error: MuxError => println(s"Error: $error")
232
+ }
233
+ }
234
+ ```
235
+
236
+ </TabItem>
237
+ </Tabs>
238
+
239
+ ## Diagram
240
+
241
+ To see the mux data flow in action, use this interactive explorer. It shows three concurrent streams sharing the same mux. Select a stream tab, then click actions in either zone to watch messages move through the queues and observe how the state machine responds.
242
+
243
+ - **Application code** (top) — calls `send()` to enqueue outbound messages and `receive()` to dequeue inbound messages.
244
+ - **MuxStream** (middle) — holds the outbound and inbound ring-buffer queues and tracks stream state (`OPEN`, `HALF_CLOSED_LOCAL`, `HALF_CLOSED_REMOTE`, `CLOSED`).
245
+ - **Protocol layer** (bottom) — calls `takeOutbound()` to drain messages for transmission and `offerInbound()` to deliver messages arriving from the peer.
246
+
247
+ Use the **Lifecycle** controls to drive the state machine: `halfClose()` signals your side is done sending; `signalRemoteClose()` signals the peer is done; `close()` forces immediate closure. The event log shows the exact return values the API would produce, including `QueueFull` and `StreamClosed` errors.
248
+
249
+ <MuxDataFlow />
250
+
251
+ ## Common Patterns
252
+
253
+ **Capacity Management**
254
+
255
+ Mux enforces a fixed capacity (e.g., 100 concurrent streams). When the limit is reached, `open` returns `CapacityExceeded`. Close or cancel streams to free capacity:
256
+
257
+ ```scala
258
+ import zio.blocks.mux._
259
+
260
+ val mux = Mux[Int, String, String](5)
261
+ for (i <- 1 to 5) {
262
+ mux.open(i) match {
263
+ case _: MuxStream[Int, String, String] => ()
264
+ case e: MuxError => println(s"Failed to open stream $i: $e")
265
+ }
266
+ }
267
+ val result = mux.open(6) // returns CapacityExceeded — mux is full
268
+
269
+ mux.cancel(1, MuxError.Cancelled(1, "freed"))
270
+ val newStream = mux.open(6) // succeeds now that stream 1 was freed
271
+ ```
272
+
273
+ **Half-Close Shutdown**
274
+
275
+ Streams support half-closed states (like HTTP/2). Your side calls `halfClose()` to signal it's done sending; the protocol calls `signalRemoteClose()` when the peer closes their sending side:
276
+
277
+ ```scala
278
+ import zio.blocks.mux._
279
+
280
+ val mux = Mux[Int, String, String](100)
281
+ val stream = mux.open(1) match {
282
+ case s: MuxStream[Int, String, String] => s
283
+ case _: MuxError => sys.error("open failed")
284
+ }
285
+
286
+ stream.halfClose()
287
+ stream.receive()
288
+ stream.signalRemoteClose()
289
+ val sendResult = stream.send("fail")
290
+ ```
291
+
292
+ **Backpressure and Queue Management**
293
+
294
+ Each stream has separate inbound and outbound queues (capacity 256 per queue). If the protocol producer is faster than the consumer, the queue fills and `offerInbound` or `send` returns `QueueFull`:
295
+
296
+ ```scala
297
+ import zio.blocks.mux._
298
+
299
+ val mux = Mux[Int, String, String](100)
300
+ val stream = mux.open(1) match {
301
+ case s: MuxStream[Int, String, String] => s
302
+ case _: MuxError => sys.error("open failed")
303
+ }
304
+
305
+ for (i <- 1 to 256) {
306
+ stream.send(s"msg-$i") match {
307
+ case () => ()
308
+ case e: MuxError => sys.error(s"send failed: $e")
309
+ }
310
+ }
311
+ val result = stream.send("overflow")
312
+ stream.takeOutbound()
313
+ val recovered = stream.send("now-ok")
314
+ ```
315
+
316
+ **Graceful Shutdown**
317
+
318
+ Call `closeAll` to atomically close all streams and prevent new opens. After closure, `receive()` / `takeOutbound()` return the terminal error once their queues are drained:
319
+
320
+ ```scala
321
+ import zio.blocks.mux._
322
+
323
+ val mux = Mux[Int, String, String](100)
324
+ for (i <- 1 to 10) {
325
+ mux.open(i) match {
326
+ case _: MuxStream[?, ?, ?] => ()
327
+ case e: MuxError => sys.error(s"Failed to open stream $i: $e")
328
+ }
329
+ }
330
+
331
+ mux.closeAll(MuxError.MuxClosed)
332
+
333
+ mux.activeCount
334
+ val newOpen = mux.open(11)
335
+ ```
336
+
337
+ ## Integration Points
338
+
339
+ `Mux` is designed as a protocol-independent multiplexing layer. It integrates with:
340
+
341
+ - **Transport protocols**: HTTP/2, QUIC, multiplexed WebSockets — any ID-based multiplexed protocol
342
+ - **Message types**: Generic over message types (`In`, `Out`) — use your protocol's frame/message types
343
+ - **Stream IDs**: Generic over ID type — use the protocol's stream ID type (Int for HTTP/2, Long for QUIC, etc.)
344
+ - **Error handling**: Terminal errors from protocol errors or local cancellation become available to user code via `receive()` and `takeOutbound()`
345
+
346
+ The mux does not depend on external modules except `zio-blocks-ringbuffer` for its lock-free ring buffer queues. It is pure and zero-dependency beyond that.
347
+
348
+ ---
349
+
350
+ ## MuxError
351
+
352
+ `MuxError` is a sealed trait representing all failure conditions in mux operations.
353
+
354
+ <Tabs groupId="scala-version" defaultValue="scala2">
355
+ <TabItem value="scala2" label="Scala 2">
356
+
357
+ Here are the error type definitions for both Scala versions:
358
+
359
+ ```scala
360
+ sealed trait MuxError
361
+
362
+ object MuxError {
363
+ final case class StreamClosed(id: Any) extends MuxError
364
+ final case class CapacityExceeded(limit: Int) extends MuxError
365
+ final case class QueueFull(queueCapacity: Int) extends MuxError
366
+ final case class Cancelled(id: Any, reason: String) extends MuxError
367
+ case object MuxClosed extends MuxError
368
+ final case class ProtocolError(message: String) extends MuxError
369
+ }
370
+ ```
371
+
372
+ </TabItem>
373
+ <TabItem value="scala3" label="Scala 3">
374
+
375
+ Scala 3 uses the same structure with union return types in method signatures:
376
+
377
+ ```scala
378
+ sealed trait MuxError
379
+
380
+ object MuxError {
381
+ final case class StreamClosed(id: Any) extends MuxError
382
+ final case class CapacityExceeded(limit: Int) extends MuxError
383
+ final case class QueueFull(queueCapacity: Int) extends MuxError
384
+ final case class Cancelled(id: Any, reason: String) extends MuxError
385
+ case object MuxClosed extends MuxError
386
+ final case class ProtocolError(message: String) extends MuxError
387
+ }
388
+ ```
389
+
390
+ </TabItem>
391
+ </Tabs>
392
+
393
+ **Variants**
394
+
395
+ **`StreamClosed(id: Any)`** — Returned when attempting to send or receive on a stream that is already closed. The `id` field is typed as `Any` to keep `MuxError` non-generic; you can pattern-match on it or cast it at the use site if you need the concrete type.
396
+
397
+ **`CapacityExceeded(limit: Int)`** — Returned by `open` when the number of active streams has reached the mux's capacity limit. The `limit` field shows the configured capacity. Close or cancel other streams to free capacity.
398
+
399
+ **`QueueFull(queueCapacity: Int)`** — Returned by `send` (outbound queue full) or `offerInbound` (inbound queue full) when the per-stream message queue has exhausted its capacity (typically 256). Drain the queue (by calling `takeOutbound()` or `receive()`) to resume sending/receiving.
400
+
401
+ **`Cancelled(id: Any, reason: String)`** — Returned when a stream gets cancelled via `mux.cancel(id, reason)`. The `reason` field explains why the stream gets cancelled. Pending `receive()` calls return this error.
402
+
403
+ **`MuxClosed`** — Set when the mux itself is closed via `closeAll`. After this, `open` returns this error and all active streams transition to CLOSED.
404
+
405
+ **`ProtocolError(message: String)`** — Returned when the protocol contract is violated (e.g., duplicate stream ID, null message, invalid state transition). These indicate bugs in the protocol implementation or incorrect API usage.
406
+
407
+ ---
408
+
409
+ ## Mux
410
+
411
+ `Mux[Id, In, Out]` is the entry point for multiplexed stream coordination. It manages a registry of active streams, enforces capacity limits, and provides operations to open, retrieve, cancel, and close streams.
412
+
413
+ **Factory**
414
+
415
+ To create a multiplexer, use the factory constructor with a fixed capacity:
416
+
417
+ ```scala
418
+ import zio.blocks.mux._
419
+
420
+ val mux = Mux[Int, String, String](100)
421
+ ```
422
+
423
+ The capacity must be positive; zero or negative capacity throws `IllegalArgumentException`. Capacity is fixed at construction and never changes.
424
+
425
+ **Core Operations**
426
+
427
+ **`def open(id: Id): MuxStream[Id, In, Out] | MuxError`** (Scala 3) / **`def open(id: Id): Either[MuxError, MuxStream[Id, In, Out]]`** (Scala 2) — Open a new stream with the given ID. Transitions the stream from IDLE to OPEN state. Returns the new stream on success, or an error if:
428
+ - Mux is closed (returns `MuxClosed`)
429
+ - Stream ID already exists (returns `ProtocolError`)
430
+ - Capacity exceeded (returns `CapacityExceeded`)
431
+
432
+ Opening a stream with error handling:
433
+
434
+ <Tabs groupId="scala-version" defaultValue="scala3">
435
+ <TabItem value="scala2" label="Scala 2">
436
+
437
+ ```scala
438
+ import zio.blocks.mux._
439
+
440
+ val mux = Mux[Int, String, String](100)
441
+
442
+ mux.open(1) match {
443
+ case Right(stream) => println(s"Stream ${stream.id} opened")
444
+ case Left(error) => println(s"Failed: $error")
445
+ }
446
+ ```
447
+
448
+ </TabItem>
449
+ <TabItem value="scala3" label="Scala 3">
450
+
451
+ ```scala
452
+ import zio.blocks.mux._
453
+
454
+ val mux = Mux[Int, String, String](100)
455
+
456
+ mux.open(1) match {
457
+ case stream: MuxStream[Int, String, String] => println(s"Stream ${stream.id} opened")
458
+ case error: MuxError => println(s"Failed: $error")
459
+ }
460
+ ```
461
+
462
+ </TabItem>
463
+ </Tabs>
464
+
465
+ **`def get(id: Id): Option[MuxStream[Id, In, Out]]`** — Retrieve an existing stream by ID. Returns `Some(stream)` if the stream is open, `None` otherwise. This is a non-blocking lookup and does not modify any state.
466
+
467
+ Looking up a stream:
468
+
469
+ ```scala
470
+ import zio.blocks.mux._
471
+
472
+ val mux = Mux[Int, String, String](100)
473
+ val result = mux.open(1)
474
+ val retrieved = mux.get(1)
475
+ ```
476
+
477
+ **`def cancel(id: Id, reason: MuxError): Unit`** — Cancel a stream by ID, removing it from the mux and setting a terminal error. The stream transitions to CLOSED and any pending `receive()` calls on that stream return the terminal error. Cancelling a non-existent stream is a no-op.
478
+
479
+ Cancelling a stream:
480
+
481
+ ```scala
482
+ import zio.blocks.mux._
483
+
484
+ val mux = Mux[Int, String, String](100)
485
+ mux.open(1)
486
+ mux.cancel(1, MuxError.Cancelled(1, "peer error"))
487
+ val retrieved = mux.get(1)
488
+ ```
489
+
490
+ **`def closeAll(reason: MuxError): Unit`** — Close all active streams atomically with a terminal error. Transitions all streams to CLOSED, clears the stream registry, and sets the mux to a closed state. After `closeAll`, `open` returns `MuxClosed`.
491
+
492
+ Closing all streams:
493
+
494
+ ```scala
495
+ import zio.blocks.mux._
496
+
497
+ val mux = Mux[Int, String, String](100)
498
+ for (i <- 1 to 10) {
499
+ mux.open(i) match {
500
+ case _: MuxStream[?, ?, ?] => ()
501
+ case e: MuxError => sys.error(s"Failed to open stream $i: $e")
502
+ }
503
+ }
504
+ mux.closeAll(MuxError.MuxClosed)
505
+ val count = mux.activeCount
506
+ ```
507
+
508
+ **`def activeCount: Int`** — Return the current number of active (open) streams. This is a point-in-time snapshot; the count may change immediately after if other threads open or close streams.
509
+
510
+ Checking the number of active streams:
511
+
512
+ ```scala
513
+ import zio.blocks.mux._
514
+
515
+ val mux = Mux[Int, String, String](100)
516
+ mux.open(1)
517
+ mux.open(2)
518
+ val count = mux.activeCount
519
+ ```
520
+
521
+ ---
522
+
523
+ ## MuxStream
524
+
525
+ `MuxStream[Id, In, Out]` represents a single logical stream within a mux. It has two independent message queues (inbound and outbound), a state machine, and operations to send/receive messages and manage lifecycle.
526
+
527
+ **Stream State Machine**
528
+
529
+ A stream progresses through four states:
530
+
531
+ - **OPEN** — Initial state after `open`. Both sides can send and receive.
532
+ - **HALF_CLOSED_LOCAL** — Local side calls `halfClose()`. Local side cannot send; remote side can still send (which transitions to CLOSED via `signalRemoteClose()`).
533
+ - **HALF_CLOSED_REMOTE** — Remote side calls `signalRemoteClose()`. Remote side cannot send; local side can still send (which transitions to CLOSED via `halfClose()`).
534
+ - **CLOSED** — Both sides closed or stream was forcibly closed. No operations allowed except state queries.
535
+
536
+ Attempting to send on a HALF_CLOSED_LOCAL or CLOSED stream returns `StreamClosed`. Attempting to offer inbound on a HALF_CLOSED_REMOTE or CLOSED stream returns `StreamClosed`. Attempting to receive on a fully CLOSED stream returns the terminal error after the queue drains.
537
+
538
+ The state transitions form a finite state machine:
539
+
540
+ ```
541
+ ┌─────────────────────────────────────────────┐
542
+ │ OPEN │
543
+ │ (both sides can send and receive) │
544
+ └─┬─────────────────────────────────────────┬─┘
545
+ │ │
546
+ │ halfClose() signalRemoteClose()
547
+ │ │
548
+ ┌─────┴──────────────────┐ ┌────────────┴──────────┐
549
+ │ HALF_CLOSED_LOCAL │ │ HALF_CLOSED_REMOTE │
550
+ │ (local: done sending) │ │ (remote: done sending)│
551
+ │ (remote: can still rx) │ │ (local: can still tx) │
552
+ └─┬──────────────────────┘ └──────────┬────────────┘
553
+ │ │
554
+ │ signalRemoteClose() halfClose() │
555
+ │ │
556
+ └──────────────┬────────────────────────────┘
557
+ │
558
+ ┌───┴────┐
559
+ │ CLOSED │
560
+ └────────┘
561
+ ```
562
+
563
+ **Query Operations**
564
+
565
+ **`def id: Id`** — Return the stream's unique identifier within the mux.
566
+
567
+ **`def isClosed: Boolean`** — Return true if the stream is in CLOSED state, false otherwise.
568
+
569
+ **`def isHalfClosed: Boolean`** — Return true if the stream is in HALF_CLOSED_LOCAL or HALF_CLOSED_REMOTE state, false otherwise.
570
+
571
+ **Message Operations**
572
+
573
+ **`def send(msg: In): Unit | MuxError`** (Scala 3) / **`def send(msg: In): Either[MuxError, Unit]`** (Scala 2) — Send a message on this stream. Places the message into the outbound queue for the protocol to drain. The protocol is responsible for transmitting the message over the shared transport.
574
+
575
+ Returns `Unit` on success, or an error if:
576
+ - Stream is closed (returns `StreamClosed`)
577
+ - Stream is in HALF_CLOSED_LOCAL state (returns `StreamClosed`)
578
+ - Message is null (returns `ProtocolError`)
579
+ - Outbound queue is full (returns `QueueFull`)
580
+
581
+ Sending a message on a stream:
582
+
583
+ <Tabs groupId="scala-version" defaultValue="scala3">
584
+ <TabItem value="scala2" label="Scala 2">
585
+
586
+ ```scala
587
+ import zio.blocks.mux._
588
+
589
+ val mux = Mux[Int, String, String](100)
590
+ val stream = mux.open(1) match {
591
+ case Right(s) => s
592
+ case Left(_) => sys.error("open failed")
593
+ }
594
+
595
+ val sendResult = stream.send("hello")
596
+ ```
597
+
598
+ </TabItem>
599
+ <TabItem value="scala3" label="Scala 3">
600
+
601
+ ```scala
602
+ import zio.blocks.mux._
603
+
604
+ val mux = Mux[Int, String, String](100)
605
+ val stream = mux.open(1) match {
606
+ case s: MuxStream[Int, String, String] => s
607
+ case _: MuxError => sys.error("open failed")
608
+ }
609
+
610
+ val sendResult = stream.send("hello")
611
+ ```
612
+
613
+ </TabItem>
614
+ </Tabs>
615
+
616
+ **`def receive(): Option[Out] | MuxError`** (Scala 3) / **`def receive(): Either[MuxError, Option[Out]]`** (Scala 2) — Receive a message from the inbound queue. This is non-blocking: it returns immediately with whatever is available.
617
+
618
+ Returns:
619
+ - `Some(msg)` if a message is available in the inbound queue
620
+ - `None` if the queue is empty (no message yet, but stream is still open)
621
+ - A `MuxError` if the stream is closed (queue has drained and a terminal error is set)
622
+
623
+ Use this in a polling loop or with a reactor to wait for messages:
624
+
625
+ <Tabs groupId="scala-version" defaultValue="scala3">
626
+ <TabItem value="scala2" label="Scala 2">
627
+
628
+ ```scala
629
+ import zio.blocks.mux._
630
+
631
+ val mux = Mux[Int, String, String](100)
632
+ val stream = mux.open(1) match {
633
+ case Right(s) => s
634
+ case Left(_) => sys.error("open failed")
635
+ }
636
+
637
+ val result = stream.receive()
638
+ ```
639
+
640
+ </TabItem>
641
+ <TabItem value="scala3" label="Scala 3">
642
+
643
+ ```scala
644
+ import zio.blocks.mux._
645
+
646
+ val mux = Mux[Int, String, String](100)
647
+ val stream = mux.open(1) match {
648
+ case s: MuxStream[Int, String, String] => s
649
+ case _: MuxError => sys.error("open failed")
650
+ }
651
+
652
+ val result = stream.receive()
653
+ ```
654
+
655
+ </TabItem>
656
+ </Tabs>
657
+
658
+ **`def takeOutbound(): Option[In] | MuxError`** (Scala 3) / **`def takeOutbound(): Either[MuxError, Option[In]]`** (Scala 2) — Take the next message from the outbound queue. Called by the protocol to drain messages sent via `send()` and transmit them over the shared transport. Non-blocking: returns immediately.
659
+
660
+ Returns:
661
+ - `Some(msg)` if a message is available in the outbound queue
662
+ - `None` if the queue is empty (no outbound message yet, but stream is still open)
663
+ - A `MuxError` if the stream is closed
664
+
665
+ Draining messages from the outbound queue:
666
+
667
+ <Tabs groupId="scala-version" defaultValue="scala3">
668
+ <TabItem value="scala2" label="Scala 2">
669
+
670
+ ```scala
671
+ import zio.blocks.mux._
672
+
673
+ val mux = Mux[Int, String, String](100)
674
+ val stream = mux.open(1) match {
675
+ case Right(s) => s
676
+ case Left(_) => sys.error("open failed")
677
+ }
678
+
679
+ stream.send("outgoing")
680
+ val outbound = stream.takeOutbound()
681
+ ```
682
+
683
+ </TabItem>
684
+ <TabItem value="scala3" label="Scala 3">
685
+
686
+ ```scala
687
+ import zio.blocks.mux._
688
+
689
+ val mux = Mux[Int, String, String](100)
690
+ val stream = mux.open(1) match {
691
+ case s: MuxStream[Int, String, String] => s
692
+ case _: MuxError => sys.error("open failed")
693
+ }
694
+
695
+ stream.send("outgoing")
696
+ val outbound = stream.takeOutbound()
697
+ ```
698
+
699
+ </TabItem>
700
+ </Tabs>
701
+
702
+ **`def offerInbound(msg: Out): Unit | MuxError`** (Scala 3) / **`def offerInbound(msg: Out): Either[MuxError, Unit]`** (Scala 2) — Deliver a message to this stream's inbound queue. Called by the protocol when a message arrives from the peer.
703
+
704
+ Returns `Unit` on success, or an error if:
705
+ - Stream is closed (returns `StreamClosed`)
706
+ - Stream is in HALF_CLOSED_REMOTE state (returns `StreamClosed`)
707
+ - Message is null (returns `ProtocolError`)
708
+ - Inbound queue is full (returns `QueueFull`)
709
+
710
+ Delivering a message to the inbound queue:
711
+
712
+ <Tabs groupId="scala-version" defaultValue="scala3">
713
+ <TabItem value="scala2" label="Scala 2">
714
+
715
+ ```scala
716
+ import zio.blocks.mux._
717
+
718
+ val mux = Mux[Int, String, String](100)
719
+ val stream = mux.open(1) match {
720
+ case Right(s) => s
721
+ case Left(_) => sys.error("open failed")
722
+ }
723
+
724
+ stream.offerInbound("response from peer")
725
+ val received = stream.receive()
726
+ ```
727
+
728
+ </TabItem>
729
+ <TabItem value="scala3" label="Scala 3">
730
+
731
+ ```scala
732
+ import zio.blocks.mux._
733
+
734
+ val mux = Mux[Int, String, String](100)
735
+ val stream = mux.open(1) match {
736
+ case s: MuxStream[Int, String, String] => s
737
+ case _: MuxError => sys.error("open failed")
738
+ }
739
+
740
+ stream.offerInbound("response from peer")
741
+ val received = stream.receive()
742
+ ```
743
+
744
+ </TabItem>
745
+ </Tabs>
746
+
747
+ **Lifecycle Operations**
748
+
749
+ **`def halfClose(): Unit`** — Signal that the local side is done sending. Transitions the stream based on the current state:
750
+ - OPEN → HALF_CLOSED_LOCAL (local side cannot send; remote can still receive pending messages)
751
+ - HALF_CLOSED_REMOTE → CLOSED (both sides are now closed)
752
+
753
+ After `halfClose()`, `send()` returns `StreamClosed`. You can still call `receive()` to drain any buffered inbound messages.
754
+
755
+ Signalling local half-close:
756
+
757
+ ```scala
758
+ import zio.blocks.mux._
759
+
760
+ val mux = Mux[Int, String, String](100)
761
+ val stream = mux.open(1) match {
762
+ case s: MuxStream[Int, String, String] => s
763
+ case _: MuxError => sys.error("open failed")
764
+ }
765
+
766
+ stream.halfClose()
767
+ val isHc = stream.isHalfClosed
768
+ val sendAfter = stream.send("fail")
769
+ val recAfter = stream.receive()
770
+ ```
771
+
772
+ **`def signalRemoteClose(): Unit`** — Signal that the remote side is done sending. Called by the protocol when the peer signals END_STREAM. Transitions the stream based on the current state:
773
+ - OPEN → HALF_CLOSED_REMOTE (remote side cannot send; local can still receive pending messages)
774
+ - HALF_CLOSED_LOCAL → CLOSED (both sides are now closed)
775
+
776
+ After `signalRemoteClose()`, `offerInbound()` returns `StreamClosed`. You can still call `receive()` to drain any buffered inbound messages.
777
+
778
+ Signalling remote half-close:
779
+
780
+ ```scala
781
+ import zio.blocks.mux._
782
+
783
+ val mux = Mux[Int, String, String](100)
784
+ val stream = mux.open(1) match {
785
+ case s: MuxStream[Int, String, String] => s
786
+ case _: MuxError => sys.error("open failed")
787
+ }
788
+
789
+ stream.signalRemoteClose()
790
+ val isHc = stream.isHalfClosed
791
+ val offerAfter = stream.offerInbound("fail")
792
+ ```
793
+
794
+ **`def close(): Unit`** — Forcibly close this stream immediately, transitioning it to CLOSED state and removing it from the mux. Any pending `receive()` calls on this stream will return the terminal error after the queue drains.
795
+
796
+ Forcibly closing a stream:
797
+
798
+ ```scala
799
+ import zio.blocks.mux._
800
+
801
+ val mux = Mux[Int, String, String](100)
802
+ val stream = mux.open(1) match {
803
+ case s: MuxStream[Int, String, String] => s
804
+ case _: MuxError => sys.error("open failed")
805
+ }
806
+
807
+ stream.close()
808
+ val isClosed = stream.isClosed
809
+ val inMux = mux.get(1)
810
+ ```
811
+
812
+ **Thread Safety**
813
+
814
+ - `send()` and `offerInbound()` are **multi-thread safe**: multiple threads can safely call these concurrently on the same stream.
815
+ - Call `receive()` and `takeOutbound()` from the same thread only. Concurrent calls to the same ring buffer produce data races and unpredictable behavior. These are single-consumer operations and must be called from a single thread. Concurrent calls will corrupt the ring buffer state and lead to incorrect results or lost messages.
816
+ - State queries (`id`, `isClosed`, `isHalfClosed`) are always safe to call from any thread.
817
+
818
+ ## Performance
819
+
820
+ - JVM: `MpscRingBuffer` for inbound and outbound, lock-free and zero-alloc for multi-producer writes
821
+ - JVM: `VarHandle` CAS for stream state transitions
822
+ - JS: `ArrayDeque` fallback
823
+ - No `synchronized`, so it stays friendly to virtual threads