@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,1507 @@
1
+ ---
2
+ id: getting-started-with-mux
3
+ title: "Getting Started with Mux"
4
+ description: "Learn how to manage multiplexed bidirectional message streams with capacity limits."
5
+ keywords: ["mux", "multiplexing", "message streams", "concurrency", "state machine"]
6
+ ---
7
+
8
+ Welcome to Getting Started with Mux! This tutorial is for developers who want to understand how to manage concurrent, request-response style communication where multiple independent conversations happen over a single channel. You don't need any prior experience with multiplexing or the Mux library to follow along — we'll build your understanding step by step.
9
+
10
+ ## Learning Objectives
11
+
12
+ By the end of this tutorial, you will understand:
13
+
14
+ - What a `Mux` is and the problem it solves
15
+ - How to create a mux and open streams within it
16
+ - The relationship between a protocol and your application code
17
+ - How `send()` and `receive()` work for application messages
18
+ - How `takeOutbound()` and `offerInbound()` work for protocol code
19
+ - The stream lifecycle: OPEN → HALF_CLOSED → CLOSED
20
+ - How to perform graceful shutdown (halfClose/signalRemoteClose) vs immediate closure (close)
21
+ - How to work with multiple independent streams
22
+ - When and how to handle errors from capacity limits
23
+ - Why thread safety matters and what it means in practice
24
+
25
+ We'll learn these concepts through:
26
+
27
+ 1. **The Big Picture** — What Mux does and why it's useful
28
+ 2. **Creating a Mux** — Your first mux and opening a stream
29
+ 3. **Understanding Streams and Message Queues** — Two-way communication
30
+ 4. **The Stream Lifecycle** — Open, half-close, and close
31
+ 5. **Working with Multiple Streams** — Independence and isolation
32
+ 6. **Managing Capacity** — When limits matter and how to handle them
33
+ 7. **Thread Safety** — Who can call what, and from where
34
+ 8. **Putting It Together** — A complete, runnable example
35
+ 9. **Running the Examples** — Step-by-step commands
36
+
37
+ We recommend reading from top to bottom — each section builds on the previous one.
38
+
39
+ :::info[Scala 2 and Scala 3]
40
+ The code examples in this tutorial use **Scala 3 syntax**. In Scala 2.13, the API returns `Either[MuxError, T]` instead of union types, and pattern matching uses `case Right(value)` and `case Left(error)` instead of `case value` and `case error`. For complete API signatures with both syntaxes, see the [Mux Reference](../reference/mux.mdx).
41
+
42
+ **Example: Scala 2 equivalent:**
43
+ ```scala
44
+ // Scala 3
45
+ stream.send("Hello") match {
46
+ case () => println("Success")
47
+ case error: MuxError => println(s"Error: $error")
48
+ }
49
+
50
+ // Scala 2
51
+ stream.send("Hello") match {
52
+ case Right(()) => println("Success")
53
+ case Left(error) => println(s"Error: $error")
54
+ }
55
+ ```
56
+ :::
57
+
58
+ ## The Big Picture
59
+
60
+ Imagine you're building a communication protocol: a client sends requests to a server over a network connection, and the server sends responses back. Both directions use the same channel, but they're independent of each other. The client doesn't need to wait for one response before sending the next request — it can send five requests in a row, get three responses, send one more request, then get two more responses.
61
+
62
+ This is multiplexing: packing multiple independent conversations into a single bidirectional channel.
63
+
64
+ A `Mux` is a registry of these conversations. It manages:
65
+
66
+ - **Streams**: Each stream is one independent conversation, identified by a numeric ID.
67
+ - **Two message queues per stream**: One for messages going out (outbound), one for messages coming in (inbound).
68
+ - **Capacity limits**: A per-stream queue capacity and a per-mux overall capacity to prevent memory exhaustion.
69
+ - **Lifecycle management**: Each stream transitions through well-defined states (OPEN, HALF_CLOSED, CLOSED) to ensure orderly shutdown.
70
+
71
+ The key insight: application code calls `send()` and `receive()` to exchange messages with its peer. Protocol code (your networking layer) calls `takeOutbound()` and `offerInbound()` to move messages in and out of the mux. The mux itself never touches the network — it just manages queues and state.
72
+
73
+ ## 1. Creating a Mux
74
+
75
+ The simplest starting point is to create a mux and open your first stream. A `Mux` is parameterized by three types:
76
+
77
+ - The **stream ID type** — typically `Int` or `Long`
78
+ - The **outbound message type** — what your application sends out
79
+ - The **inbound message type** — what your application receives
80
+
81
+ Let's create a mux that carries `String` messages in both directions, with stream IDs as integers:
82
+
83
+ ```scala
84
+ import zio.blocks.mux._
85
+
86
+ // Create a mux that carries String messages in both directions
87
+ val mux = Mux[Int, String, String](100)
88
+ ```
89
+
90
+ The code above:
91
+ - `Mux[Int, String, String]` — creates a mux where streams are identified by `Int`, outbound messages are `String`, and inbound messages are also `String`
92
+ - `(100)` — the mux can have up to 100 concurrent streams. Each stream has its own message queues with a fixed capacity of 256 messages per direction (inbound and outbound)
93
+
94
+ Now let's open a stream and pattern-match on the result (because opening can fail if capacity is exceeded):
95
+
96
+ ```scala
97
+ import zio.blocks.mux._
98
+
99
+ val mux = Mux[Int, String, String](100)
100
+
101
+ val streamOrError = mux.open(1) // Try to open stream 1
102
+ streamOrError match {
103
+ case stream: MuxStream[Int, String, String] =>
104
+ println(s"Opened stream with ID 1: $stream")
105
+ case error: MuxError =>
106
+ println(s"Failed to open stream: $error")
107
+ }
108
+ ```
109
+
110
+ The code above:
111
+ - `mux.open(1)` — attempts to open a new stream with ID `1`
112
+ - The result is either a `MuxStream` (success) or a `MuxError` (failure — e.g., capacity exceeded)
113
+ - We pattern-match to handle both cases
114
+
115
+ :::note[Why Pattern-Matching?]
116
+ Opening a stream can fail. If the mux has reached its capacity limit, `open()` returns a `CapacityExceeded` error instead of a stream. By pattern-matching, we ensure our code handles both outcomes.
117
+ :::
118
+
119
+ ## 2. Understanding Streams and Message Queues
120
+
121
+ Once you've opened a stream, you have access to a two-way communication channel. The key insight is that there are two different perspectives:
122
+
123
+ - **Application perspective**: You call `send()` to send outbound messages and `receive()` to get inbound messages.
124
+ - **Protocol perspective**: The protocol calls `takeOutbound()` to extract messages your application sent, and `offerInbound()` to deliver messages it received from the peer.
125
+
126
+ Let's see how this works:
127
+
128
+ ```scala
129
+ import zio.blocks.mux._
130
+
131
+ val mux = Mux[Int, String, String](100)
132
+ val stream = mux.open(1) match {
133
+ case s: MuxStream[Int, String, String] => s
134
+ case e: MuxError => sys.error(s"Failed to open stream: $e")
135
+ }
136
+
137
+ // Application code: send a message
138
+ stream.send("Hello from app") match {
139
+ case () => println("✓ Message sent")
140
+ case error: MuxError => println(s"✗ Send failed: $error")
141
+ }
142
+
143
+ // Protocol code: extract the message to send over network
144
+ val outbound = stream.takeOutbound() match {
145
+ case Some(msg) => msg
146
+ case None => "no message available"
147
+ case error: MuxError => s"error: $error"
148
+ }
149
+ println(s"Protocol will send: $outbound")
150
+
151
+ // Protocol code: deliver a response from the peer
152
+ stream.offerInbound("Hello from peer") match {
153
+ case () => println("✓ Response delivered")
154
+ case error: MuxError => println(s"✗ Offer failed: $error")
155
+ }
156
+
157
+ // Application code: receive the response
158
+ val inbound = stream.receive() match {
159
+ case Some(msg) => msg
160
+ case None => "no message available"
161
+ case error: MuxError => s"error: $error"
162
+ }
163
+ println(s"Application received: $inbound")
164
+ ```
165
+
166
+ The code above demonstrates the `MuxStream` API:
167
+ - `MuxStream#send(msg)` returns `Unit | MuxError` (success or error); we pattern-match to handle both cases
168
+ - `MuxStream#takeOutbound()` returns `Option[String] | MuxError` (Some/None/error); we match all three
169
+ - `MuxStream#offerInbound(msg)` returns `Unit | MuxError` (success or error); we pattern-match to handle both
170
+ - `MuxStream#receive()` returns `Option[String] | MuxError` (Some/None/error); we match all three cases
171
+
172
+ :::tip[Two Queues, Two Roles]
173
+ Think of it as a pipeline: your application uses `send()` and `receive()`, the protocol uses `takeOutbound()` and `offerInbound()`. The mux sits in the middle, managing two FIFO queues per stream. This separation is intentional — it lets you reason about sending independently from receiving.
174
+ :::
175
+
176
+ ## 3. The Stream Lifecycle
177
+
178
+ Every stream has a well-defined lifecycle with three states: OPEN, HALF_CLOSED, and CLOSED. This ensures graceful shutdown when both sides need to agree that communication is complete.
179
+
180
+ The state transitions look like this:
181
+
182
+ ```
183
+ OPEN
184
+ ├─ (local side calls halfClose()) ──→ HALF_CLOSED_LOCAL
185
+ │ └─ (remote side calls signalRemoteClose()) ──→ CLOSED
186
+ │
187
+ └─ (remote side calls signalRemoteClose()) ──→ HALF_CLOSED_REMOTE
188
+ └─ (local side calls halfClose()) ──→ CLOSED
189
+ ```
190
+
191
+ Let's walk through a complete lifecycle:
192
+
193
+ ```scala
194
+ import zio.blocks.mux._
195
+
196
+ val mux = Mux[Int, String, String](100)
197
+ val stream = mux.open(1) match {
198
+ case s: MuxStream[Int, String, String] => s
199
+ case e: MuxError => sys.error(s"Failed to open stream: $e")
200
+ }
201
+
202
+ // Stream starts in OPEN state
203
+ println(s"Is stream closed? ${stream.isClosed}")
204
+ println(s"Is stream half-closed? ${stream.isHalfClosed}")
205
+
206
+ // Application is done sending, signals half-close
207
+ stream.halfClose()
208
+ println(s"After halfClose(), is half-closed? ${stream.isHalfClosed}")
209
+
210
+ // Protocol receives a close signal from the peer
211
+ stream.signalRemoteClose()
212
+ println(s"After signalRemoteClose(), is closed? ${stream.isClosed}")
213
+ ```
214
+
215
+ The code above:
216
+ - `stream.isClosed` — checks if the stream is in CLOSED state
217
+ - `stream.isHalfClosed` — checks if the stream is in HALF_CLOSED state (one side done)
218
+ - `stream.halfClose()` — signals that this side is done sending (but can still receive)
219
+ - `stream.signalRemoteClose()` — signals that the peer is done sending
220
+ - Once you call both `halfClose()` and `signalRemoteClose()`, the stream becomes fully closed
221
+
222
+ However, sometimes you don't need a graceful handshake—you just want to close the stream immediately. The `close()` method transitions directly from any state to CLOSED without the half-close protocol:
223
+
224
+ ```scala
225
+ import zio.blocks.mux._
226
+
227
+ val mux = Mux[Int, String, String](100)
228
+ val stream = mux.open(1) match {
229
+ case s: MuxStream[Int, String, String] => s
230
+ case e: MuxError => sys.error(s"Failed to open stream: $e")
231
+ }
232
+
233
+ // Application decides to close immediately (e.g., due to an error)
234
+ println(s"Before close: isClosed = ${stream.isClosed}")
235
+
236
+ stream.close()
237
+ println(s"After close: isClosed = ${stream.isClosed}")
238
+
239
+ // The close was synchronous and immediate
240
+ // Capacity is released, no handshake needed
241
+ ```
242
+
243
+ The code above:
244
+ - `stream.close()` — transitions the stream from OPEN (or any state) immediately to CLOSED
245
+ - The `close()` method also enqueues a terminal error (`StreamClosed`) that `receive()` returns after all buffered messages are drained, allowing applications to detect closure
246
+ - This bypasses the graceful half-close handshake and releases capacity immediately
247
+ - Use `close()` when you need abrupt termination (error conditions, cleanup)
248
+ - Use `halfClose()` + `signalRemoteClose()` for orderly shutdown where both sides need to agree
249
+
250
+ :::tip[When to Use Each Closure Method]
251
+ - **Graceful (halfClose + signalRemoteClose):** Normal shutdown where both sides need to drain pending messages and agree to close
252
+ - **Immediate (close):** When the stream should terminate right now (error recovery, cleanup, timeout)
253
+ - **External (mux.cancel(id, reason)):** When you need to close a stream from outside its context (thread-safe operation)
254
+ :::
255
+
256
+ ### External Stream Cancellation: `mux.cancel(id, reason)`
257
+
258
+ While `halfClose()` and `close()` are stream-level operations that modify the stream's state directly, **`mux.cancel(id, reason)` is a thread-safe mux-level operation** that cancels a stream from any thread without needing to access the stream object itself. This is useful for cleanup handlers, timeouts, and other scenarios where you can't guarantee single-threaded access.
259
+
260
+ Here's how to use `mux.cancel()` to externally cancel a stream:
261
+
262
+ ```scala
263
+ import zio.blocks.mux._
264
+
265
+ val mux = Mux[Int, String, String](100)
266
+ val stream = mux.open(1) match {
267
+ case s: MuxStream[Int, String, String] => s
268
+ case e: MuxError => sys.error(s"Failed to open stream: $e")
269
+ }
270
+
271
+ // From any thread, you can cancel a stream by ID
272
+ // This is thread-safe and doesn't require access to the stream object
273
+ mux.cancel(1, MuxError.Cancelled(1, "Cleanup: application shutting down"))
274
+ println("✓ Stream 1 cancelled successfully")
275
+
276
+ // After cancellation, the stream is closed
277
+ println(s"Stream isClosed: ${stream.isClosed}")
278
+ ```
279
+
280
+ The code above:
281
+ - `mux.cancel(streamId, reason)` — cancels the stream with the given ID and enqueues a terminal error on its inbound queue
282
+ - This operation is **thread-safe** and you can call it from any thread
283
+ - Unlike `close()`, which requires direct access to the stream, `cancel()` works with just the stream ID
284
+ - The `reason` parameter becomes the terminal error that `receive()` returns
285
+
286
+ :::note[Important: `offerInbound()` After `signalRemoteClose()`]
287
+ When you call `signalRemoteClose()`, the stream transitions to `HALF_CLOSED_REMOTE` state. In this state, subsequent `offerInbound()` calls fail with `StreamClosed` error because the remote side signals it will not send any more messages. This prevents the protocol layer from enqueuing messages after the peer declares it's done sending.
288
+ :::
289
+
290
+ :::caution[Closing Is Coordinated]
291
+ Calling `halfClose()` when the stream is already in HALF_CLOSED_LOCAL or beyond is idempotent—the second call has no effect. Similarly, calling `signalRemoteClose()` when the stream is already in HALF_CLOSED_REMOTE or CLOSED has no effect. Calling `close()` at any time immediately transitions to CLOSED, bypassing any handshake.
292
+ :::
293
+
294
+ ## 4. Working with Multiple Streams
295
+
296
+ The real power of mux comes from managing multiple independent streams simultaneously. Each stream is isolated: messages in stream 1 never appear in stream 2, and capacity is tracked separately.
297
+
298
+ Let's open three streams and exchange messages on each one:
299
+
300
+ ```scala
301
+ import zio.blocks.mux._
302
+
303
+ val mux = Mux[Int, String, String](100)
304
+
305
+ // Open three streams
306
+ val stream1 = mux.open(1) match {
307
+ case s: MuxStream[Int, String, String] => s
308
+ case e: MuxError => sys.error(s"Failed to open stream 1: $e")
309
+ }
310
+ val stream2 = mux.open(2) match {
311
+ case s: MuxStream[Int, String, String] => s
312
+ case e: MuxError => sys.error(s"Failed to open stream 2: $e")
313
+ }
314
+ val stream3 = mux.open(3) match {
315
+ case s: MuxStream[Int, String, String] => s
316
+ case e: MuxError => sys.error(s"Failed to open stream 3: $e")
317
+ }
318
+
319
+ // Send different messages on each stream
320
+ stream1.send("Message for stream 1") match { case () => (); case e: MuxError => println(s"Stream 1 send error: $e") }
321
+ stream2.send("Message for stream 2") match { case () => (); case e: MuxError => println(s"Stream 2 send error: $e") }
322
+ stream3.send("Message for stream 3") match { case () => (); case e: MuxError => println(s"Stream 3 send error: $e") }
323
+
324
+ // Extract messages — they stay on their own stream
325
+ val out1 = stream1.takeOutbound() match { case Some(msg) => msg; case None => "(empty)"; case e: MuxError => s"error: $e" }
326
+ val out2 = stream2.takeOutbound() match { case Some(msg) => msg; case None => "(empty)"; case e: MuxError => s"error: $e" }
327
+ val out3 = stream3.takeOutbound() match { case Some(msg) => msg; case None => "(empty)"; case e: MuxError => s"error: $e" }
328
+ println(s"Stream 1 outbound: $out1")
329
+ println(s"Stream 2 outbound: $out2")
330
+ println(s"Stream 3 outbound: $out3")
331
+
332
+ // Deliver responses to different streams
333
+ stream1.offerInbound("Response for stream 1") match { case () => (); case e: MuxError => println(s"Stream 1 offer error: $e") }
334
+ stream2.offerInbound("Response for stream 2") match { case () => (); case e: MuxError => println(s"Stream 2 offer error: $e") }
335
+ stream3.offerInbound("Response for stream 3") match { case () => (); case e: MuxError => println(s"Stream 3 offer error: $e") }
336
+
337
+ // Receive on each stream — no crosstalk
338
+ val in1 = stream1.receive() match { case Some(msg) => msg; case None => "(empty)"; case e: MuxError => s"error: $e" }
339
+ val in2 = stream2.receive() match { case Some(msg) => msg; case None => "(empty)"; case e: MuxError => s"error: $e" }
340
+ val in3 = stream3.receive() match { case Some(msg) => msg; case None => "(empty)"; case e: MuxError => s"error: $e" }
341
+ println(s"Stream 1 inbound: $in1")
342
+ println(s"Stream 2 inbound: $in2")
343
+ println(s"Stream 3 inbound: $in3")
344
+ ```
345
+
346
+ The code above:
347
+ - We open three streams with IDs 1, 2, and 3
348
+ - Each stream has its own outbound and inbound queues
349
+ - Sending on stream 1 doesn't affect stream 2 or stream 3
350
+ - Messages stay on their originating stream — there is no crosstalk
351
+
352
+ :::tip[Streams Are Independent]
353
+ Each stream is a completely independent message channel. If stream 1 fills up its queue, stream 2 is unaffected. If you close stream 1, streams 2 and 3 continue operating normally. This isolation is the core benefit of multiplexing.
354
+ :::
355
+
356
+ ## 5. Managing Capacity
357
+
358
+ The mux enforces two separate capacity limits: **mux-level** (controlling how many streams can exist concurrently) and **per-stream** (controlling how many messages each stream's queues can hold).
359
+
360
+ - **Mux-level capacity** (set at creation): The total number of concurrent streams. When exceeded, `open()` fails with `CapacityExceeded`.
361
+ - **Per-stream capacity** (fixed at 256 messages per direction): Each stream's outbound and inbound queues. When exceeded, `send()` and `offerInbound()` fail with `QueueFull`.
362
+
363
+ :::note[Null Message Rejection]
364
+ Both `send()` and `offerInbound()` reject `null` messages with `MuxError.ProtocolError("null message")`. This ensures the protocol layer never enqueues null values, which helps catch bugs early.
365
+ :::
366
+
367
+ Let's see what happens when we hit these limits:
368
+
369
+ ```scala
370
+ import zio.blocks.mux._
371
+
372
+ // Create a tiny mux with a small capacity
373
+ val mux = Mux[Int, String, String](10)
374
+ val stream = mux.open(1) match {
375
+ case s: MuxStream[Int, String, String] => s
376
+ case e: MuxError => sys.error(s"Failed to open stream: $e")
377
+ }
378
+
379
+ // Try to send many messages until the queue fills (per-stream capacity is 256 per direction)
380
+ for (i <- 1 to 300) {
381
+ val result = stream.send(s"Message $i")
382
+ result match {
383
+ case () => if (i <= 5 || i > 255) println(s"✓ Sent message $i")
384
+ case error: MuxError => println(s"✗ Message $i failed: $error")
385
+ }
386
+ }
387
+ ```
388
+
389
+ The code above:
390
+ - We create a mux with total capacity 10 (very small)
391
+ - Each `send()` either succeeds (returns `Unit`) or fails with a `MuxError`
392
+ - When the per-stream queue is full, further sends return `MuxError.QueueFull`
393
+ - Each stream has its own per-stream message queues with a fixed capacity of 256 messages per direction
394
+
395
+ The standard recovery pattern is to drain messages from the outbound queue (by calling `takeOutbound()` repeatedly) until space is available:
396
+
397
+ ```scala
398
+ import zio.blocks.mux._
399
+
400
+ val mux = Mux[Int, String, String](10)
401
+ val stream = mux.open(1) match {
402
+ case s: MuxStream[Int, String, String] => s
403
+ case e: MuxError => sys.error(s"Failed to open stream: $e")
404
+ }
405
+
406
+ // Fill the queue
407
+ for (i <- 1 to 5) {
408
+ stream.send(s"Message $i") match {
409
+ case () => ()
410
+ case e: MuxError => sys.error(s"send failed: $e")
411
+ }
412
+ }
413
+
414
+ // Drain one message from the outbound queue
415
+ println(s"Drained: ${stream.takeOutbound()}")
416
+
417
+ // Now we have space to send again
418
+ val result = stream.send("Message 6")
419
+ println(s"Sent after draining: $result is Unit")
420
+ ```
421
+
422
+ The code above:
423
+ - We fill the queue with 5 messages
424
+ - We call `takeOutbound()` once to simulate the protocol extracting and sending a message
425
+ - This frees space in the queue, and the next `send()` succeeds
426
+
427
+ :::caution[Handling QueueFull in Real Code]
428
+ In a real application, `QueueFull` means the protocol is not keeping up with the application. You should either:
429
+ - Wait for the protocol to drain messages (using a poll loop or a callback mechanism)
430
+ - Close the stream if you decide recovery is impossible
431
+ - Implement a backpressure mechanism that slows the application
432
+ Never ignore `QueueFull` — it indicates a broken contract between layers.
433
+ :::
434
+
435
+ ## 6. Thread Safety
436
+
437
+ The mux implementation is **thread-safe** for certain operations and **single-threaded** for others. Understanding this boundary is essential.
438
+
439
+ **Mux-level thread-safe operations** (call from any thread on the mux object):
440
+ - `mux.open(id)` — open a new stream
441
+ - `mux.get(id)` — retrieve a stream by ID
442
+ - `mux.cancel(id, reason)` — externally cancel a stream (thread-safe alternative to `close()`)
443
+ - `mux.closeAll(reason)` — close all streams
444
+
445
+ **Stream-level thread-safe operations** (call from multiple threads on a stream):
446
+ - `stream.send(msg)` — queue a message to send (uses lock-free ring buffer on JVM)
447
+ - `stream.offerInbound(msg)` — deliver a received message (uses lock-free ring buffer on JVM)
448
+
449
+ **Stream-level single-threaded operations** (call from the same dedicated thread only):
450
+ - `stream.receive()` — dequeue an inbound message
451
+ - `stream.takeOutbound()` — dequeue an outbound message
452
+ - `stream.halfClose()` — signal local side is done sending
453
+ - `stream.signalRemoteClose()` — signal remote side is done sending
454
+ - `stream.close()` — immediately close the stream
455
+
456
+ **Why?** The outbound and inbound queues are lock-free (on JVM) or single-threaded (on JS), which requires that the consumer threads remain stable. State transitions (`halfClose()`, `signalRemoteClose()`, `close()`) are not locked and should be coordinated from a single thread or with external synchronization.
457
+
458
+ Here's a correct usage pattern:
459
+
460
+ ```scala
461
+ import zio.blocks.mux._
462
+
463
+ val mux = Mux[Int, String, String](100)
464
+ val stream = mux.open(1) match {
465
+ case s: MuxStream[Int, String, String] => s
466
+ case e: MuxError => sys.error(s"Failed to open stream: $e")
467
+ }
468
+
469
+ // Thread A: application code (single thread)
470
+ // Always call send() from the same thread
471
+ stream.send("Message 1") match { case () => println("✓ Sent 1"); case e: MuxError => println(s"✗ Error: $e") }
472
+ stream.send("Message 2") match { case () => println("✓ Sent 2"); case e: MuxError => println(s"✗ Error: $e") }
473
+
474
+ // Thread B: protocol code (single thread)
475
+ // Always call takeOutbound() from the same thread
476
+ stream.takeOutbound() match { case Some(msg) => println(s"Extracted: $msg"); case None => println("(empty)"); case e: MuxError => println(s"✗ Error: $e") }
477
+ stream.takeOutbound() match { case Some(msg) => println(s"Extracted: $msg"); case None => println("(empty)"); case e: MuxError => println(s"✗ Error: $e") }
478
+
479
+ // Thread C, D, E, ... : multiple threads can safely deliver inbound messages
480
+ // This is thread-safe
481
+ for (_ <- 1 to 3) {
482
+ stream.offerInbound("Response from peer") match { case () => (); case e: MuxError => println(s"✗ Offer error: $e") }
483
+ }
484
+
485
+ // Thread A: back on the application thread
486
+ // Always receive() on the same thread
487
+ stream.receive() match { case Some(msg) => println(s"Received: $msg"); case None => println("(empty)"); case e: MuxError => println(s"✗ Error: $e") }
488
+ stream.receive() match { case Some(msg) => println(s"Received: $msg"); case None => println("(empty)"); case e: MuxError => println(s"✗ Error: $e") }
489
+ stream.receive() match { case Some(msg) => println(s"Received: $msg"); case None => println("(empty)"); case e: MuxError => println(s"✗ Error: $e") }
490
+ ```
491
+
492
+ The code above:
493
+ - Call `send()` and `receive()` each from a single dedicated thread, but they **can be different threads**
494
+ - Call `takeOutbound()` from a single dedicated thread (the protocol/reading thread)
495
+ - Multiple threads can safely call `offerInbound()` to deliver inbound messages
496
+ - Violating the single-threaded requirement for `receive()` and `takeOutbound()` corrupts the ring buffer
497
+
498
+ :::tip[Comparing Closure Methods]
499
+ There are three ways to close a stream, each with different semantics and thread safety properties:
500
+
501
+ | Method | Thread-Safe | Semantics | When to Use |
502
+ |--------|:---:|-----------|------------|
503
+ | **halfClose() + signalRemoteClose()** | ❌ Single-threaded | Graceful shutdown: both sides agree to close, pending messages drained | Normal orderly shutdown |
504
+ | **close()** | ❌ Single-threaded | Immediate closure: transitions to CLOSED right now, capacity released | Error recovery, cleanup, timeouts |
505
+ | **mux.cancel(id, reason)** | ✅ Thread-safe | External cancellation: close from any thread with an error reason | Cleanup from outside the stream, concurrent scenarios |
506
+
507
+ For example, use `mux.cancel(streamId, error)` when you're shutting down the entire application and need to close streams from a cleanup handler (which might run on a different thread).
508
+ :::
509
+
510
+ :::danger[Thread Safety Violation]
511
+ Do **not** call `receive()` or `takeOutbound()` from multiple threads on the same stream. These methods operate on lock-free ring buffers (JVM) or mutable arrays (JS) that are not thread-safe for concurrent access. Violating this will cause data corruption or panics.
512
+ :::
513
+
514
+ ## 7. Putting It Together
515
+
516
+ Now let's write a complete example that demonstrates all the concepts: creating a mux, opening streams, exchanging messages, handling errors, managing capacity, and closing gracefully:
517
+
518
+ ```scala
519
+ import zio.blocks.mux._
520
+
521
+ object MuxExample {
522
+ def main(args: Array[String]): Unit = {
523
+ // Create a mux: stream IDs are Int, messages are String in both directions
524
+ val mux = Mux[Int, String, String](100)
525
+
526
+ println("=== Opening Streams ===")
527
+ // Open three streams, handling potential capacity errors
528
+ val streams = (1 to 3).map { id =>
529
+ mux.open(id) match {
530
+ case stream: MuxStream[Int, String, String] =>
531
+ println(s"Opened stream $id")
532
+ stream
533
+ case error: MuxError =>
534
+ println(s"Failed to open stream $id: $error")
535
+ throw new RuntimeException(s"Cannot proceed without stream $id")
536
+ }
537
+ }.toList
538
+
539
+ println("\n=== Application Sends Messages ===")
540
+ // Application sends messages on each stream
541
+ streams.zipWithIndex.foreach { case (stream, idx) =>
542
+ val msg = s"Hello from stream ${idx + 1}"
543
+ stream.send(msg) match {
544
+ case () => println(s"Stream ${idx + 1} sent: $msg")
545
+ case error: MuxError => println(s"Stream ${idx + 1} send failed: $error")
546
+ }
547
+ }
548
+
549
+ println("\n=== Protocol Extracts Outbound Messages ===")
550
+ // Protocol extracts messages to send over the network
551
+ streams.zipWithIndex.foreach { case (stream, idx) =>
552
+ stream.takeOutbound() match {
553
+ case Some(msg) => println(s"Protocol will send on stream ${idx + 1}: $msg")
554
+ case None => println(s"Stream ${idx + 1} has no outbound messages")
555
+ case error: MuxError => println(s"Stream ${idx + 1} takeOutbound failed: $error")
556
+ }
557
+ }
558
+
559
+ println("\n=== Protocol Delivers Inbound Messages ===")
560
+ // Protocol delivers responses received from the peer
561
+ streams.zipWithIndex.foreach { case (stream, idx) =>
562
+ val response = s"Response from peer on stream ${idx + 1}"
563
+ stream.offerInbound(response) match {
564
+ case () => println(s"Protocol delivered on stream ${idx + 1}: $response")
565
+ case error: MuxError => println(s"Stream ${idx + 1} offerInbound failed: $error")
566
+ }
567
+ }
568
+
569
+ println("\n=== Application Receives Messages ===")
570
+ // Application receives the responses
571
+ streams.zipWithIndex.foreach { case (stream, idx) =>
572
+ stream.receive() match {
573
+ case Some(msg) => println(s"Stream ${idx + 1} received: $msg")
574
+ case None => println(s"Stream ${idx + 1} has no inbound messages")
575
+ case error: MuxError => println(s"Stream ${idx + 1} receive failed: $error")
576
+ }
577
+ }
578
+
579
+ println("\n=== Graceful Shutdown ===")
580
+ // Streams close gracefully
581
+ val stream1 = streams(0)
582
+ stream1.halfClose()
583
+ println("Stream 1: halfClose() called")
584
+
585
+ stream1.signalRemoteClose()
586
+ println(s"Stream 1: signalRemoteClose() called, isClosed = ${stream1.isClosed}")
587
+
588
+ // Verify the stream is closed
589
+ assert(stream1.isClosed, "Stream 1 should be closed")
590
+
591
+ println("\n=== Verify Independence ===")
592
+ // Streams 2 and 3 are unaffected by stream 1 closing
593
+ println(s"Stream 2 is open: ${!streams(1).isClosed}")
594
+ println(s"Stream 3 is open: ${!streams(2).isClosed}")
595
+
596
+ println("\nExample complete!")
597
+ }
598
+ }
599
+ ```
600
+
601
+ This example demonstrates:
602
+ - Creating a mux with a specific capacity
603
+ - Opening multiple streams and handling errors
604
+ - Application code sending messages and receiving responses
605
+ - Protocol code extracting and delivering messages
606
+ - Graceful stream closure using halfClose and signalRemoteClose
607
+ - Verifying that streams remain independent
608
+
609
+ ## What You've Learned
610
+
611
+ In this tutorial, you learned:
612
+
613
+ - What a `Mux` is: a registry that manages multiple independent, multiplexed message streams
614
+ - How to create a mux and open streams within it, handling capacity errors
615
+ - The two-role pattern: application code uses `send()` and `receive()`, protocol code uses `takeOutbound()` and `offerInbound()`
616
+ - How streams transition through a well-defined lifecycle (OPEN → HALF_CLOSED → CLOSED)
617
+ - How to perform graceful shutdown (halfClose/signalRemoteClose) vs immediate closure (close)
618
+ - Why multiple streams are completely independent with no crosstalk
619
+ - How capacity limits work and the standard recovery pattern (drain to free space)
620
+ - The thread-safety contract: only `send()`, `offerInbound()`, and `cancel()` are multi-threaded safe; state transitions and message dequeuing are single-threaded
621
+ - How to build a complete request-response system using mux
622
+
623
+ You now have a solid foundation in how mux simplifies multiplexed communication. The next step is to see how to build production-grade protocols using mux as the foundation.
624
+
625
+ ## Running the Examples
626
+
627
+ All examples in this tutorial have corresponding runnable Scala files in the `mux-examples` module. Run them in order to progressively build your understanding in practice.
628
+
629
+ ### Creating a Mux
630
+
631
+ This example creates a mux and opens your first stream, demonstrating the basic API for stream initialization and handling both success and failure cases when capacity is exceeded.
632
+
633
+ <details>
634
+ <summary>mux-examples/src/main/scala/mux/Example1CreatingAMux.scala</summary>
635
+
636
+ ```scala title="mux-examples/src/main/scala/mux/Example1CreatingAMux.scala" showLineNumbers
637
+ /*
638
+ * Copyright 2024-2026 John A. De Goes and the ZIO Contributors
639
+ *
640
+ * Licensed under the Apache License, Version 2.0 (the "License");
641
+ * you may not use this file except in compliance with the License.
642
+ * You may obtain a copy of the License at
643
+ *
644
+ * http://www.apache.org/licenses/LICENSE-2.0
645
+ *
646
+ * Unless required by applicable law or agreed to in writing, software
647
+ * distributed under the License is distributed on an "AS IS" BASIS,
648
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
649
+ * See the License for the specific language governing permissions and
650
+ * limitations under the License.
651
+ */
652
+
653
+ package mux
654
+
655
+ import zio.blocks.mux._
656
+
657
+ /**
658
+ * Title: Creating a Mux Description: Create a mux that carries String messages
659
+ * in both directions with integer stream IDs, then open a stream and handle
660
+ * both success and failure cases. Run: sbt "mux-examples/runMain
661
+ * mux.example1CreatingAMux"
662
+ */
663
+ @main def example1CreatingAMux(): Unit = {
664
+ println("=== Creating a Mux ===")
665
+
666
+ // Create a mux that carries String messages in both directions
667
+ val mux = Mux[Int, String, String](100)
668
+ println("Created mux with capacity for 100 concurrent streams")
669
+
670
+ println("\n=== Opening a Stream ===")
671
+
672
+ // Try to open stream 1
673
+ val streamOrError = mux.open(1)
674
+ streamOrError match {
675
+ case stream: MuxStream[Int, String, String] =>
676
+ println(s"✓ Opened stream with ID 1: $stream")
677
+ println(s" Stream is closed: ${stream.isClosed}")
678
+ println(s" Stream is half-closed: ${stream.isHalfClosed}")
679
+ case error: MuxError =>
680
+ println(s"✗ Failed to open stream: $error")
681
+ }
682
+ }
683
+ ```
684
+
685
+ </details>
686
+
687
+ Observe the output: the stream is successfully created and its initial state (not closed, not half-closed) is displayed.
688
+
689
+ ```bash
690
+ sbt "mux-examples/runMain mux.example1CreatingAMux"
691
+ ```
692
+
693
+ ### Understanding Streams and Message Queues
694
+
695
+ This example demonstrates the two-perspective communication model where application code uses `send()`/`receive()` while protocol code uses `takeOutbound()`/`offerInbound()`, showing how messages flow through the queue system.
696
+
697
+ <details>
698
+ <summary>mux-examples/src/main/scala/mux/Example2UnderstandingStreamsAndMessageQueues.scala</summary>
699
+
700
+ ```scala title="mux-examples/src/main/scala/mux/Example2UnderstandingStreamsAndMessageQueues.scala" showLineNumbers
701
+ /*
702
+ * Copyright 2024-2026 John A. De Goes and the ZIO Contributors
703
+ *
704
+ * Licensed under the Apache License, Version 2.0 (the "License");
705
+ * you may not use this file except in compliance with the License.
706
+ * You may obtain a copy of the License at
707
+ *
708
+ * http://www.apache.org/licenses/LICENSE-2.0
709
+ *
710
+ * Unless required by applicable law or agreed to in writing, software
711
+ * distributed under the License is distributed on an "AS IS" BASIS,
712
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
713
+ * See the License for the specific language governing permissions and
714
+ * limitations under the License.
715
+ */
716
+
717
+ package mux
718
+
719
+ import zio.blocks.mux._
720
+
721
+ /**
722
+ * Title: Understanding Streams and Message Queues
723
+ *
724
+ * Description: Demonstrate the two-way communication pattern: application uses
725
+ * send()/receive(), protocol uses takeOutbound()/offerInbound().
726
+ *
727
+ * Run: sbt "mux-examples/runMain
728
+ * mux.example2UnderstandingStreamsAndMessageQueues"
729
+ */
730
+ @main def example2UnderstandingStreamsAndMessageQueues(): Unit = {
731
+ println("=== Streams and Message Queues ===\n")
732
+
733
+ val mux = Mux[Int, String, String](100)
734
+ val stream = mux.open(1) match {
735
+ case s: MuxStream[Int, String, String] => s
736
+ case e: MuxError => throw new RuntimeException(s"Failed to open stream: $e")
737
+ }
738
+
739
+ println("--- Application sends a message ---")
740
+ // Application code: send a message
741
+ stream.send("Hello from app") match {
742
+ case () => println("✓ Message sent successfully")
743
+ case error: MuxError => println(s"✗ Send failed: $error")
744
+ }
745
+
746
+ println("\n--- Protocol extracts the message ---")
747
+ // Protocol code: extract the message to send over network
748
+ val outbound = stream.takeOutbound() match {
749
+ case Some(msg) => msg
750
+ case None => "(no message available)"
751
+ case error: MuxError => s"error: $error"
752
+ }
753
+ println(s"Protocol will send: '$outbound'")
754
+
755
+ println("\n--- Protocol delivers a response ---")
756
+ // Protocol code: deliver a response from the peer
757
+ stream.offerInbound("Hello from peer") match {
758
+ case () => println("✓ Response delivered successfully")
759
+ case error: MuxError => println(s"✗ Offer failed: $error")
760
+ }
761
+
762
+ println("\n--- Application receives the response ---")
763
+ // Application code: receive the response
764
+ val inbound = stream.receive() match {
765
+ case Some(msg) => msg
766
+ case None => "(no message available)"
767
+ case error: MuxError => s"error: $error"
768
+ }
769
+ println(s"Application received: '$inbound'")
770
+
771
+ println("\n=== Summary ===")
772
+ println("Application uses send() and receive()")
773
+ println("Protocol uses takeOutbound() and offerInbound()")
774
+ println("Messages stay in their FIFO queues per stream")
775
+ }
776
+ ```
777
+
778
+ </details>
779
+
780
+ Observe the output: messages sent by the application are extracted by the protocol, and responses delivered by the protocol are received by the application.
781
+
782
+ ```bash
783
+ sbt "mux-examples/runMain mux.example2UnderstandingStreamsAndMessageQueues"
784
+ ```
785
+
786
+ ### The Stream Lifecycle
787
+
788
+ This example shows all three ways to close a stream: graceful two-phase closure (halfClose + signalRemoteClose), immediate closure (close), and external thread-safe cancellation (mux.cancel).
789
+
790
+ <details>
791
+ <summary>mux-examples/src/main/scala/mux/Example3StreamLifecycle.scala</summary>
792
+
793
+ ```scala title="mux-examples/src/main/scala/mux/Example3StreamLifecycle.scala" showLineNumbers
794
+ /*
795
+ * Copyright 2024-2026 John A. De Goes and the ZIO Contributors
796
+ *
797
+ * Licensed under the Apache License, Version 2.0 (the "License");
798
+ * you may not use this file except in compliance with the License.
799
+ * You may obtain a copy of the License at
800
+ *
801
+ * http://www.apache.org/licenses/LICENSE-2.0
802
+ *
803
+ * Unless required by applicable law or agreed to in writing, software
804
+ * distributed under the License is distributed on an "AS IS" BASIS,
805
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
806
+ * See the License for the specific language governing permissions and
807
+ * limitations under the License.
808
+ */
809
+
810
+ package mux
811
+
812
+ import zio.blocks.mux._
813
+
814
+ /**
815
+ * Title: The Stream Lifecycle
816
+ *
817
+ * Description: Demonstrate state transitions through OPEN → HALF_CLOSED →
818
+ * CLOSED using graceful shutdown, immediate closure, and external cancellation.
819
+ *
820
+ * Run: sbt "mux-examples/runMain mux.example3StreamLifecycle"
821
+ */
822
+ @main def example3StreamLifecycle(): Unit = {
823
+ println("=== Graceful Stream Lifecycle (halfClose + signalRemoteClose) ===\n")
824
+
825
+ val mux = Mux[Int, String, String](100)
826
+ val stream1 = mux.open(1) match {
827
+ case s: MuxStream[Int, String, String] => s
828
+ case e: MuxError => throw new RuntimeException(s"Failed to open stream 1: $e")
829
+ }
830
+
831
+ // Stream starts in OPEN state
832
+ println(s"Initial state:")
833
+ println(s" isClosed: ${stream1.isClosed}")
834
+ println(s" isHalfClosed: ${stream1.isHalfClosed}")
835
+
836
+ // Application is done sending, signals half-close
837
+ println(s"\nCalling halfClose()...")
838
+ stream1.halfClose()
839
+ println(s"After halfClose():")
840
+ println(s" isClosed: ${stream1.isClosed}")
841
+ println(s" isHalfClosed: ${stream1.isHalfClosed}")
842
+
843
+ // Protocol receives a close signal from the peer
844
+ println(s"\nCalling signalRemoteClose()...")
845
+ stream1.signalRemoteClose()
846
+ println(s"After signalRemoteClose():")
847
+ println(s" isClosed: ${stream1.isClosed}")
848
+ println(s" isHalfClosed: ${stream1.isHalfClosed}")
849
+
850
+ println("\n=== Immediate Stream Closure (close) ===\n")
851
+
852
+ val stream2 = mux.open(2) match {
853
+ case s: MuxStream[Int, String, String] => s
854
+ case e: MuxError => throw new RuntimeException(s"Failed to open stream 2: $e")
855
+ }
856
+
857
+ println(s"Before close():")
858
+ println(s" isClosed: ${stream2.isClosed}")
859
+
860
+ // Application decides to close immediately (e.g., due to an error)
861
+ println(s"\nCalling close()...")
862
+ stream2.close()
863
+
864
+ println(s"After close():")
865
+ println(s" isClosed: ${stream2.isClosed}")
866
+
867
+ println("\n=== External Stream Cancellation (mux.cancel) ===\n")
868
+
869
+ val stream3 = mux.open(3) match {
870
+ case s: MuxStream[Int, String, String] => s
871
+ case e: MuxError => throw new RuntimeException(s"Failed to open stream 3: $e")
872
+ }
873
+
874
+ println(s"Before cancel():")
875
+ println(s" isClosed: ${stream3.isClosed}")
876
+
877
+ // From any thread, cancel the stream by ID (thread-safe)
878
+ println(s"\nCalling mux.cancel(3, ...)...")
879
+ mux.cancel(3, MuxError.Cancelled(3, "Cleanup: application shutting down"))
880
+
881
+ println(s"After mux.cancel():")
882
+ println(s" isClosed: ${stream3.isClosed}")
883
+
884
+ println("\n=== Summary ===")
885
+ println("Graceful shutdown: halfClose() + signalRemoteClose() (single-threaded)")
886
+ println("Immediate closure: close() (single-threaded)")
887
+ println("External cancellation: mux.cancel(id, reason) (thread-safe)")
888
+ }
889
+ ```
890
+
891
+ </details>
892
+
893
+ Observe the output: state transitions from OPEN → HALF_CLOSED → CLOSED in graceful shutdown, instant transition in immediate closure, and the final closed state after external cancellation.
894
+
895
+ ```bash
896
+ sbt "mux-examples/runMain mux.example3StreamLifecycle"
897
+ ```
898
+
899
+ ### Working with Multiple Streams
900
+
901
+ This example opens three independent streams and exchanges messages on each one, demonstrating that streams are isolated with no message crosstalk and each has its own inbound and outbound queues.
902
+
903
+ <details>
904
+ <summary>mux-examples/src/main/scala/mux/Example4WorkingWithMultipleStreams.scala</summary>
905
+
906
+ ```scala title="mux-examples/src/main/scala/mux/Example4WorkingWithMultipleStreams.scala" showLineNumbers
907
+ /*
908
+ * Copyright 2024-2026 John A. De Goes and the ZIO Contributors
909
+ *
910
+ * Licensed under the Apache License, Version 2.0 (the "License");
911
+ * you may not use this file except in compliance with the License.
912
+ * You may obtain a copy of the License at
913
+ *
914
+ * http://www.apache.org/licenses/LICENSE-2.0
915
+ *
916
+ * Unless required by applicable law or agreed to in writing, software
917
+ * distributed under the License is distributed on an "AS IS" BASIS,
918
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
919
+ * See the License for the specific language governing permissions and
920
+ * limitations under the License.
921
+ */
922
+
923
+ package mux
924
+
925
+ import zio.blocks.mux._
926
+
927
+ /**
928
+ * Title: Working with Multiple Streams Description: Demonstrate that multiple
929
+ * streams are independent: each has its own queues with no crosstalk, and
930
+ * messages stay on their originating stream. Run: sbt "mux-examples/runMain
931
+ * mux.example4WorkingWithMultipleStreams"
932
+ */
933
+ @main def example4WorkingWithMultipleStreams(): Unit = {
934
+ println("=== Working with Multiple Streams ===\n")
935
+
936
+ val mux = Mux[Int, String, String](100)
937
+
938
+ println("--- Opening three streams ---")
939
+ // Open three streams
940
+ val stream1 = mux.open(1) match {
941
+ case s: MuxStream[Int, String, String] => s
942
+ case e: MuxError => throw new RuntimeException(s"Failed to open stream 1: $e")
943
+ }
944
+ val stream2 = mux.open(2) match {
945
+ case s: MuxStream[Int, String, String] => s
946
+ case e: MuxError => throw new RuntimeException(s"Failed to open stream 2: $e")
947
+ }
948
+ val stream3 = mux.open(3) match {
949
+ case s: MuxStream[Int, String, String] => s
950
+ case e: MuxError => throw new RuntimeException(s"Failed to open stream 3: $e")
951
+ }
952
+ println("✓ Opened streams 1, 2, 3")
953
+
954
+ println("\n--- Application sends messages on each stream ---")
955
+ // Send different messages on each stream
956
+ stream1.send("Message for stream 1") match { case () => (); case e: MuxError => println(s"Stream 1 send error: $e") }
957
+ stream2.send("Message for stream 2") match { case () => (); case e: MuxError => println(s"Stream 2 send error: $e") }
958
+ stream3.send("Message for stream 3") match { case () => (); case e: MuxError => println(s"Stream 3 send error: $e") }
959
+ println("✓ Sent outbound messages on all streams")
960
+
961
+ println("\n--- Protocol extracts messages (no crosstalk) ---")
962
+ // Extract messages — they stay on their own stream
963
+ val out1 = stream1.takeOutbound() match {
964
+ case Some(msg) => msg
965
+ case None => "(empty)"
966
+ case e: MuxError => s"error: $e"
967
+ }
968
+ val out2 = stream2.takeOutbound() match {
969
+ case Some(msg) => msg
970
+ case None => "(empty)"
971
+ case e: MuxError => s"error: $e"
972
+ }
973
+ val out3 = stream3.takeOutbound() match {
974
+ case Some(msg) => msg
975
+ case None => "(empty)"
976
+ case e: MuxError => s"error: $e"
977
+ }
978
+ println(s"Stream 1 outbound: '$out1'")
979
+ println(s"Stream 2 outbound: '$out2'")
980
+ println(s"Stream 3 outbound: '$out3'")
981
+
982
+ println("\n--- Protocol delivers responses to different streams ---")
983
+ // Deliver responses to different streams
984
+ stream1.offerInbound("Response for stream 1") match {
985
+ case () => (); case e: MuxError => println(s"Stream 1 offer error: $e")
986
+ }
987
+ stream2.offerInbound("Response for stream 2") match {
988
+ case () => (); case e: MuxError => println(s"Stream 2 offer error: $e")
989
+ }
990
+ stream3.offerInbound("Response for stream 3") match {
991
+ case () => (); case e: MuxError => println(s"Stream 3 offer error: $e")
992
+ }
993
+ println("✓ Delivered inbound responses on all streams")
994
+
995
+ println("\n--- Application receives on each stream (no crosstalk) ---")
996
+ // Receive on each stream — no crosstalk
997
+ val in1 = stream1.receive() match {
998
+ case Some(msg) => msg
999
+ case None => "(empty)"
1000
+ case e: MuxError => s"error: $e"
1001
+ }
1002
+ val in2 = stream2.receive() match {
1003
+ case Some(msg) => msg
1004
+ case None => "(empty)"
1005
+ case e: MuxError => s"error: $e"
1006
+ }
1007
+ val in3 = stream3.receive() match {
1008
+ case Some(msg) => msg
1009
+ case None => "(empty)"
1010
+ case e: MuxError => s"error: $e"
1011
+ }
1012
+ println(s"Stream 1 inbound: '$in1'")
1013
+ println(s"Stream 2 inbound: '$in2'")
1014
+ println(s"Stream 3 inbound: '$in3'")
1015
+
1016
+ println("\n=== Summary ===")
1017
+ println("Each stream is independent:")
1018
+ println(" - Own outbound and inbound queues")
1019
+ println(" - No message crosstalk between streams")
1020
+ println(" - Closing one stream doesn't affect others")
1021
+ }
1022
+ ```
1023
+
1024
+ </details>
1025
+
1026
+ Observe the output: messages sent on stream 1 appear only in stream 1's outbound queue, never in streams 2 or 3, proving complete independence.
1027
+
1028
+ ```bash
1029
+ sbt "mux-examples/runMain mux.example4WorkingWithMultipleStreams"
1030
+ ```
1031
+
1032
+ ### Managing Capacity
1033
+
1034
+ This example demonstrates both mux-level capacity (controlling concurrent streams) and per-stream capacity (controlling message queue depth), showing how to handle QueueFull errors and the recovery pattern of draining to free space.
1035
+
1036
+ <details>
1037
+ <summary>mux-examples/src/main/scala/mux/Example5ManagingCapacity.scala</summary>
1038
+
1039
+ ```scala title="mux-examples/src/main/scala/mux/Example5ManagingCapacity.scala" showLineNumbers
1040
+ /*
1041
+ * Copyright 2024-2026 John A. De Goes and the ZIO Contributors
1042
+ *
1043
+ * Licensed under the Apache License, Version 2.0 (the "License");
1044
+ * you may not use this file except in compliance with the License.
1045
+ * You may obtain a copy of the License at
1046
+ *
1047
+ * http://www.apache.org/licenses/LICENSE-2.0
1048
+ *
1049
+ * Unless required by applicable law or agreed to in writing, software
1050
+ * distributed under the License is distributed on an "AS IS" BASIS,
1051
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1052
+ * See the License for the specific language governing permissions and
1053
+ * limitations under the License.
1054
+ */
1055
+
1056
+ package mux
1057
+
1058
+ import zio.blocks.mux._
1059
+
1060
+ /**
1061
+ * Title: Managing Capacity Description: Demonstrate mux-level and per-stream
1062
+ * capacity limits, handling QueueFull errors, and the recovery pattern of
1063
+ * draining outbound messages to free space. Run: sbt "mux-examples/runMain
1064
+ * mux.example5ManagingCapacity"
1065
+ */
1066
+ @main def example5ManagingCapacity(): Unit = {
1067
+ println("=== Managing Capacity ===\n")
1068
+
1069
+ println("--- Mux-level capacity (maximum concurrent streams) ---")
1070
+ // Create a tiny mux with a small capacity (for demo)
1071
+ val mux = Mux[Int, String, String](3)
1072
+ println("Created mux with capacity for 3 concurrent streams")
1073
+
1074
+ // Open streams until capacity is exceeded
1075
+ println("\nOpening streams:")
1076
+ for (i <- 1 to 5) {
1077
+ mux.open(i) match {
1078
+ case _: MuxStream[Int, String, String] =>
1079
+ println(s" ✓ Opened stream $i")
1080
+ case error: MuxError =>
1081
+ println(s" ✗ Failed to open stream $i: $error")
1082
+ }
1083
+ }
1084
+
1085
+ println("\n--- Per-stream capacity (message queue limits) ---")
1086
+ // Create a fresh mux for this demo
1087
+ val mux2 = Mux[Int, String, String](100)
1088
+ val stream = mux2.open(10) match {
1089
+ case s: MuxStream[Int, String, String] => s
1090
+ case error: MuxError =>
1091
+ println(s"Failed to open stream: $error")
1092
+ throw new RuntimeException("Cannot proceed")
1093
+ }
1094
+
1095
+ println("Sending messages until queue fills (per-stream capacity is 256):")
1096
+ var successCount = 0
1097
+ var failureCount = 0
1098
+
1099
+ for (i <- 1 to 300) {
1100
+ val result = stream.send(s"Message $i")
1101
+ result match {
1102
+ case () =>
1103
+ successCount += 1
1104
+ if (i <= 5 || i == 256) println(s" ✓ Message $i sent")
1105
+ case error: MuxError =>
1106
+ failureCount += 1
1107
+ if (failureCount <= 3) println(s" ✗ Message $i failed: $error")
1108
+ }
1109
+ }
1110
+ println(s"Summary: $successCount succeeded, $failureCount failed")
1111
+
1112
+ println("\n--- Recovery pattern: draining to free space ---")
1113
+ // Fresh mux for recovery demo
1114
+ val mux3 = Mux[Int, String, String](100)
1115
+ val stream2 = mux3.open(11) match {
1116
+ case s: MuxStream[Int, String, String] => s
1117
+ case e: MuxError => throw new RuntimeException(s"Failed to open stream 11: $e")
1118
+ }
1119
+
1120
+ // Fill the queue to capacity (256 messages per direction)
1121
+ println("Filling queue to capacity (256 messages):")
1122
+ for (i <- 1 to 256) {
1123
+ stream2.send(s"Message $i") match {
1124
+ case () => if (i == 1 || i == 256) println(s" ✓ Message $i sent")
1125
+ case e: MuxError => println(s" ✗ Unexpected failure at $i: $e")
1126
+ }
1127
+ }
1128
+
1129
+ // Queue is now full — the next send returns QueueFull
1130
+ println("\nAttempting send when queue is full:")
1131
+ stream2.send("overflow") match {
1132
+ case () => println(" (unexpected: send succeeded)")
1133
+ case e: MuxError => println(s" ✗ Expected error: $e")
1134
+ }
1135
+
1136
+ // Protocol drains one message to free space
1137
+ println("\nProtocol drains one message from outbound queue:")
1138
+ stream2.takeOutbound() match {
1139
+ case Some(msg) => println(s" Extracted: '$msg'")
1140
+ case None => println(" (queue empty)")
1141
+ case e: MuxError => println(s" Error: $e")
1142
+ }
1143
+
1144
+ // Retry after draining — now succeeds
1145
+ println("Retrying send after draining:")
1146
+ stream2.send("overflow") match {
1147
+ case () => println("✓ Send succeeded after draining (recovery pattern works)")
1148
+ case e: MuxError => println(s"✗ Still failed: $e")
1149
+ }
1150
+
1151
+ println("\n=== Summary ===")
1152
+ println("Capacity limits prevent memory exhaustion:")
1153
+ println(" - Mux-level: controls number of concurrent streams")
1154
+ println(" - Per-stream: limits messages in each queue (256 per direction)")
1155
+ println("Recovery pattern: drain outbound queue to free space")
1156
+ }
1157
+ ```
1158
+
1159
+ </details>
1160
+
1161
+ Observe the output: the mux rejects stream 4 and 5 with CapacityExceeded, messages 257+ fail with QueueFull, and after draining one message, sending resumes successfully.
1162
+
1163
+ ```bash
1164
+ sbt "mux-examples/runMain mux.example5ManagingCapacity"
1165
+ ```
1166
+
1167
+ ### Thread Safety
1168
+
1169
+ This example documents which operations are thread-safe (mux-level and send/offerInbound) versus single-threaded (state transitions and message dequeuing), with a correct usage pattern showing how threads interact safely.
1170
+
1171
+ <details>
1172
+ <summary>mux-examples/src/main/scala/mux/Example6ThreadSafety.scala</summary>
1173
+
1174
+ ```scala title="mux-examples/src/main/scala/mux/Example6ThreadSafety.scala"
1175
+ /*
1176
+ * Copyright 2024-2026 John A. De Goes and the ZIO Contributors
1177
+ *
1178
+ * Licensed under the Apache License, Version 2.0 (the "License");
1179
+ * you may not use this file except in compliance with the License.
1180
+ * You may obtain a copy of the License at
1181
+ *
1182
+ * http://www.apache.org/licenses/LICENSE-2.0
1183
+ *
1184
+ * Unless required by applicable law or agreed to in writing, software
1185
+ * distributed under the License is distributed on an "AS IS" BASIS,
1186
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1187
+ * See the License for the specific language governing permissions and
1188
+ * limitations under the License.
1189
+ */
1190
+
1191
+ package mux
1192
+
1193
+ import zio.blocks.mux._
1194
+
1195
+ /**
1196
+ * Title: Thread Safety Description: Demonstrate thread-safe vs single-threaded
1197
+ * operations, correct usage patterns, and what operations can be called from
1198
+ * which context. Run: sbt "mux-examples/runMain mux.example6ThreadSafety"
1199
+ */
1200
+ @main def example6ThreadSafety(): Unit = {
1201
+ println("=== Thread Safety: Operations and Constraints ===\n")
1202
+
1203
+ val mux = Mux[Int, String, String](100)
1204
+
1205
+ println("--- Mux-level thread-safe operations (call from any thread) ---")
1206
+ println("✓ mux.open(id) — thread-safe")
1207
+ println("✓ mux.get(id) — thread-safe")
1208
+ println("✓ mux.cancel(id, reason) — thread-safe")
1209
+ println("✓ mux.closeAll(reason) — thread-safe")
1210
+
1211
+ println("\n--- Stream-level thread-safe operations (multiple threads OK) ---")
1212
+ println("✓ stream.send(msg) — thread-safe (uses lock-free ring buffer on JVM)")
1213
+ println("✓ stream.offerInbound(msg) — thread-safe (uses lock-free ring buffer on JVM)")
1214
+
1215
+ println("\n--- Stream-level SINGLE-THREADED operations (ONE thread only) ---")
1216
+ println("✗ stream.receive() — single-threaded (corrupts if called concurrently)")
1217
+ println("✗ stream.takeOutbound() — single-threaded (corrupts if called concurrently)")
1218
+ println("✗ stream.halfClose() — single-threaded (state transition)")
1219
+ println("✗ stream.signalRemoteClose() — single-threaded (state transition)")
1220
+ println("✗ stream.close() — single-threaded (state transition)")
1221
+
1222
+ println("\n--- Correct usage pattern ---\n")
1223
+
1224
+ // Reset
1225
+ val stream2 = mux.open(2) match {
1226
+ case s: MuxStream[Int, String, String] => s
1227
+ case e: MuxError => throw new RuntimeException(s"Failed to open stream 2: $e")
1228
+ }
1229
+
1230
+ println("Thread A (Application thread): calls send() consistently")
1231
+ stream2.send("Message 1") match {
1232
+ case () => println(" ✓ Sent message 1 from thread A")
1233
+ case e: MuxError => println(s" ✗ Error: $e")
1234
+ }
1235
+ stream2.send("Message 2") match {
1236
+ case () => println(" ✓ Sent message 2 from thread A")
1237
+ case e: MuxError => println(s" ✗ Error: $e")
1238
+ }
1239
+
1240
+ println("\nThread B (Protocol/Read thread): calls takeOutbound() consistently")
1241
+ stream2.takeOutbound() match {
1242
+ case Some(msg) => println(s" ✓ Extracted from thread B: '$msg'")
1243
+ case None => println(" (queue empty)")
1244
+ case e: MuxError => println(s" ✗ Error: $e")
1245
+ }
1246
+ stream2.takeOutbound() match {
1247
+ case Some(msg) => println(s" ✓ Extracted from thread B: '$msg'")
1248
+ case None => println(" (queue empty)")
1249
+ case e: MuxError => println(s" ✗ Error: $e")
1250
+ }
1251
+
1252
+ println("\nThreads C, D, E, ... (Multiple inbound delivery threads): can safely call offerInbound()")
1253
+ for (threadId <- 1 to 3) {
1254
+ stream2.offerInbound(s"Response from thread $threadId") match {
1255
+ case () => println(s" ✓ Offered inbound from thread $threadId (thread-safe)")
1256
+ case e: MuxError => println(s" ✗ Error: $e")
1257
+ }
1258
+ }
1259
+
1260
+ println("\nThread A (back on Application thread): calls receive() consistently")
1261
+ stream2.receive() match {
1262
+ case Some(msg) => println(s" ✓ Received from thread A: '$msg'")
1263
+ case None => println(" (queue empty)")
1264
+ case e: MuxError => println(s" ✗ Error: $e")
1265
+ }
1266
+ stream2.receive() match {
1267
+ case Some(msg) => println(s" ✓ Received from thread A: '$msg'")
1268
+ case None => println(" (queue empty)")
1269
+ case e: MuxError => println(s" ✗ Error: $e")
1270
+ }
1271
+ stream2.receive() match {
1272
+ case Some(msg) => println(s" ✓ Received from thread A: '$msg'")
1273
+ case None => println(" (queue empty)")
1274
+ case e: MuxError => println(s" ✗ Error: $e")
1275
+ }
1276
+
1277
+ println("\n--- Closing streams: thread safety comparison ---\n")
1278
+
1279
+ println("Single-threaded close (from dedicated thread):")
1280
+ println(" stream.halfClose() + stream.signalRemoteClose()")
1281
+ println(" OR stream.close()")
1282
+
1283
+ println("\nThread-safe external cancel (from any thread):")
1284
+ println(" mux.cancel(id, reason)")
1285
+ println(" → Call this when closing from cleanup handler or different thread")
1286
+ mux.cancel(2, MuxError.Cancelled(2, "Example cleanup"))
1287
+ println(" ✓ Cancelled stream 2 from main thread (would work from any thread)")
1288
+
1289
+ println("\n=== Summary ===")
1290
+ println("✓ send() and offerInbound() are thread-safe (use lock-free queues)")
1291
+ println("✓ mux operations (open, cancel, closeAll) are thread-safe")
1292
+ println("✗ receive(), takeOutbound(), and state transitions must use ONE thread")
1293
+ println("✓ Use mux.cancel(id, reason) for thread-safe external cancellation")
1294
+ }
1295
+ ```
1296
+
1297
+ </details>
1298
+
1299
+ Observe the output: mux.open and mux.cancel are declared thread-safe, while receive() and takeOutbound() must use single threads, and the pattern shows multiple threads safely offering inbound messages.
1300
+
1301
+ ```bash
1302
+ sbt "mux-examples/runMain mux.example6ThreadSafety"
1303
+ ```
1304
+
1305
+ ### Putting It All Together
1306
+
1307
+ This comprehensive example demonstrates a complete request-response system that ties together all concepts: creating a mux, opening multiple streams, exchanging messages, handling errors, managing lifecycle transitions, and verifying independence.
1308
+
1309
+ <details>
1310
+ <summary>mux-examples/src/main/scala/mux/CompleteExample.scala</summary>
1311
+
1312
+ ```scala title="mux-examples/src/main/scala/mux/CompleteExample.scala" showLineNumbers
1313
+ /*
1314
+ * Copyright 2024-2026 John A. De Goes and the ZIO Contributors
1315
+ *
1316
+ * Licensed under the Apache License, Version 2.0 (the "License");
1317
+ * you may not use this file except in compliance with the License.
1318
+ * You may obtain a copy of the License at
1319
+ *
1320
+ * http://www.apache.org/licenses/LICENSE-2.0
1321
+ *
1322
+ * Unless required by applicable law or agreed to in writing, software
1323
+ * distributed under the License is distributed on an "AS IS" BASIS,
1324
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1325
+ * See the License for the specific language governing permissions and
1326
+ * limitations under the License.
1327
+ */
1328
+
1329
+ package mux
1330
+
1331
+ import zio.blocks.mux._
1332
+
1333
+ /**
1334
+ * Title: Putting It Together Description: Complete, runnable example that
1335
+ * demonstrates all mux concepts: creating mux, opening streams, exchanging
1336
+ * messages, handling errors, managing capacity, and closing gracefully. This is
1337
+ * the comprehensive end-to-end demonstration showing real request-response
1338
+ * communication. Run: sbt "mux-examples/runMain mux.completeExample"
1339
+ */
1340
+ @main def completeExample(): Unit = {
1341
+ println("=" * 70)
1342
+ println("COMPLETE MUX EXAMPLE: Request-Response Communication")
1343
+ println("=" * 70)
1344
+
1345
+ // Create a mux: stream IDs are Int, messages are String in both directions
1346
+ val mux = Mux[Int, String, String](100)
1347
+ println("\n✓ Created mux with capacity for 100 concurrent streams\n")
1348
+
1349
+ println("=" * 70)
1350
+ println("OPENING STREAMS")
1351
+ println("=" * 70)
1352
+
1353
+ // Open three streams, handling potential capacity errors
1354
+ val streams = (1 to 3).map { id =>
1355
+ mux.open(id) match {
1356
+ case stream: MuxStream[Int, String, String] =>
1357
+ println(s"✓ Opened stream $id")
1358
+ stream
1359
+ case error: MuxError =>
1360
+ println(s"✗ Failed to open stream $id: $error")
1361
+ throw new RuntimeException(s"Cannot proceed without stream $id")
1362
+ }
1363
+ }.toList
1364
+
1365
+ println("\n" + "=" * 70)
1366
+ println("APPLICATION SENDS MESSAGES")
1367
+ println("=" * 70)
1368
+
1369
+ // Application sends messages on each stream
1370
+ println()
1371
+ streams.zipWithIndex.foreach { case (stream, idx) =>
1372
+ val msg = s"Hello from stream ${idx + 1}"
1373
+ stream.send(msg) match {
1374
+ case () => println(s"✓ Stream ${idx + 1} sent: '$msg'")
1375
+ case _: MuxError => println(s"✗ Stream ${idx + 1} send failed")
1376
+ }
1377
+ }
1378
+
1379
+ println("\n" + "=" * 70)
1380
+ println("PROTOCOL EXTRACTS OUTBOUND MESSAGES")
1381
+ println("=" * 70)
1382
+
1383
+ // Protocol extracts messages to send over the network
1384
+ println()
1385
+ streams.zipWithIndex.foreach { case (stream, idx) =>
1386
+ stream.takeOutbound() match {
1387
+ case Some(msg) =>
1388
+ println(s"✓ Protocol will send on stream ${idx + 1}: '$msg'")
1389
+ case None =>
1390
+ println(s"✗ Stream ${idx + 1} has no outbound messages")
1391
+ case _: MuxError =>
1392
+ println(s"✗ Stream ${idx + 1} takeOutbound failed")
1393
+ }
1394
+ }
1395
+
1396
+ println("\n" + "=" * 70)
1397
+ println("PROTOCOL DELIVERS INBOUND MESSAGES")
1398
+ println("=" * 70)
1399
+
1400
+ // Protocol delivers responses received from the peer
1401
+ println()
1402
+ streams.zipWithIndex.foreach { case (stream, idx) =>
1403
+ val response = s"Response from peer on stream ${idx + 1}"
1404
+ stream.offerInbound(response) match {
1405
+ case () => println(s"✓ Protocol delivered on stream ${idx + 1}: '$response'")
1406
+ case _: MuxError => println(s"✗ Stream ${idx + 1} offerInbound failed")
1407
+ }
1408
+ }
1409
+
1410
+ println("\n" + "=" * 70)
1411
+ println("APPLICATION RECEIVES MESSAGES")
1412
+ println("=" * 70)
1413
+
1414
+ // Application receives the responses
1415
+ println()
1416
+ streams.zipWithIndex.foreach { case (stream, idx) =>
1417
+ stream.receive() match {
1418
+ case Some(msg) =>
1419
+ println(s"✓ Stream ${idx + 1} received: '$msg'")
1420
+ case None =>
1421
+ println(s"✗ Stream ${idx + 1} has no inbound messages")
1422
+ case _: MuxError =>
1423
+ println(s"✗ Stream ${idx + 1} receive failed")
1424
+ }
1425
+ }
1426
+
1427
+ println("\n" + "=" * 70)
1428
+ println("GRACEFUL SHUTDOWN")
1429
+ println("=" * 70)
1430
+
1431
+ // Streams close gracefully
1432
+ println()
1433
+ val stream1 = streams(0)
1434
+ println("Stream 1: signalling local half-close...")
1435
+ stream1.halfClose()
1436
+ println(s" isClosed: ${stream1.isClosed}, isHalfClosed: ${stream1.isHalfClosed}")
1437
+
1438
+ println("Stream 1: signalling remote close...")
1439
+ stream1.signalRemoteClose()
1440
+ println(s" isClosed: ${stream1.isClosed}, isHalfClosed: ${stream1.isHalfClosed}")
1441
+
1442
+ // Verify the stream is closed
1443
+ assert(stream1.isClosed, "Stream 1 should be closed")
1444
+ println("✓ Stream 1 is fully closed")
1445
+
1446
+ println("\n" + "=" * 70)
1447
+ println("VERIFY INDEPENDENCE")
1448
+ println("=" * 70)
1449
+
1450
+ // Streams 2 and 3 are unaffected by stream 1 closing
1451
+ println()
1452
+ println(s"Stream 2 is open: ${!streams(1).isClosed}")
1453
+ println(s"Stream 3 is open: ${!streams(2).isClosed}")
1454
+
1455
+ println("\n" + "=" * 70)
1456
+ println("IMMEDIATE CLOSURE")
1457
+ println("=" * 70)
1458
+
1459
+ // Demonstrate immediate closure on stream 2
1460
+ println()
1461
+ val stream2 = streams(1)
1462
+ println("Stream 2: performing immediate close()...")
1463
+ stream2.close()
1464
+ println(s" isClosed: ${stream2.isClosed}")
1465
+ println("✓ Stream 2 is immediately closed")
1466
+
1467
+ println("\n" + "=" * 70)
1468
+ println("EXTERNAL CANCELLATION")
1469
+ println("=" * 70)
1470
+
1471
+ // Demonstrate external cancellation on stream 3
1472
+ println()
1473
+ println("Stream 3: performing external mux.cancel()...")
1474
+ mux.cancel(3, MuxError.Cancelled(3, "Application shutdown"))
1475
+ val stream3 = streams(2)
1476
+ println(s" isClosed: ${stream3.isClosed}")
1477
+ println("✓ Stream 3 is cancelled")
1478
+
1479
+ println("\n" + "=" * 70)
1480
+ println("EXAMPLE COMPLETE")
1481
+ println("=" * 70)
1482
+ println("\nYou've seen:")
1483
+ println(" • Creating a mux with capacity control")
1484
+ println(" • Opening multiple independent streams")
1485
+ println(" • Application sending messages (send/receive)")
1486
+ println(" • Protocol exchanging messages (takeOutbound/offerInbound)")
1487
+ println(" • Graceful shutdown (halfClose + signalRemoteClose)")
1488
+ println(" • Immediate closure (close())")
1489
+ println(" • External cancellation (mux.cancel())")
1490
+ println(" • Stream independence and isolation")
1491
+ println("\n✓ All concepts demonstrated successfully!")
1492
+ }
1493
+ ```
1494
+
1495
+ </details>
1496
+
1497
+ Observe the output: three streams are opened, messages flow through send/receive and takeOutbound/offerInbound, graceful shutdown completes with halfClose/signalRemoteClose, and other streams remain unaffected.
1498
+
1499
+ ```bash
1500
+ sbt "mux-examples/runMain mux.completeExample"
1501
+ ```
1502
+
1503
+ ## Where to Go Next
1504
+
1505
+ - **Want to dive deeper into the API?** Read the reference page for [`Mux`](../reference/mux.mdx) for complete method signatures and advanced patterns.
1506
+ - **Ready to explore related concepts?** Check out the [Ring Buffer](../reference/ringbuffer/index.mdx) reference for understanding lock-free data structures used internally.
1507
+ - **Interested in other concurrency and stream management?** Explore other libraries in ZIO Blocks for managing complex async workflows.