@zio.dev/zio-blocks 0.0.51 → 0.0.55

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/adr/2026-07-18-data-migration.md +123 -0
  2. package/guides/async-getting-started.md +687 -0
  3. package/guides/compile-time-resource-safety-with-scope.md +6 -0
  4. package/guides/getting-started-with-mux.md +0 -112
  5. package/guides/query-dsl-extending.md +1 -1
  6. package/guides/query-dsl-fluent-builder.md +1 -1
  7. package/guides/query-dsl-reified-optics.md +1 -1
  8. package/guides/query-dsl-sql.md +395 -1
  9. package/guides/sql-checked-interpolation.md +173 -0
  10. package/guides/sql-transactions.md +286 -0
  11. package/guides/telemetry-guide.md +131 -70
  12. package/guides/zio-schema-migration.md +6 -6
  13. package/index.md +200 -583
  14. package/package.json +1 -1
  15. package/reference/async.md +1379 -531
  16. package/reference/chunk.md +3 -3
  17. package/reference/codegen/index.md +1 -1
  18. package/reference/combinators.md +4 -4
  19. package/reference/config/config-decoder.md +460 -0
  20. package/reference/config/config-source.md +489 -0
  21. package/reference/config/errors.md +278 -0
  22. package/reference/config/flags.md +369 -0
  23. package/reference/config/formats.md +314 -0
  24. package/reference/config/index.md +304 -0
  25. package/reference/config/rollout.md +336 -0
  26. package/reference/context.md +6 -49
  27. package/reference/data-migration.md +269 -0
  28. package/reference/datastar/attributes.md +302 -0
  29. package/reference/datastar/events.md +234 -0
  30. package/reference/datastar/index.md +256 -0
  31. package/reference/datastar/signals.md +230 -0
  32. package/reference/datastar/sse.md +295 -0
  33. package/reference/datastar.md +2 -2
  34. package/reference/docs.md +2 -2
  35. package/reference/endpoint/bulk-creation.md +96 -0
  36. package/reference/endpoint/index.md +9 -89
  37. package/reference/endpoint/path-codec.md +12 -24
  38. package/reference/endpoint/route-pattern.md +4 -6
  39. package/reference/endpoint/segment-codec.md +19 -32
  40. package/reference/html.md +313 -9
  41. package/reference/htmx/index.md +4 -52
  42. package/reference/htmx/response-headers.md +240 -0
  43. package/reference/http-model/headers.md +735 -0
  44. package/reference/http-model/index.md +3 -1
  45. package/reference/http-model/model.md +107 -71
  46. package/reference/http-model/schema-codecs.md +522 -0
  47. package/reference/http-model/schema.md +6 -3
  48. package/reference/http-model/server-sent-event.md +341 -0
  49. package/reference/jwt.md +195 -0
  50. package/reference/maybe.md +128 -11
  51. package/reference/media-type.md +2 -2
  52. package/reference/mux.md +254 -0
  53. package/reference/mux.mdx +7 -2
  54. package/reference/openapi.md +3 -3
  55. package/reference/projection.md +654 -0
  56. package/reference/resource-management/resource.md +2 -98
  57. package/reference/resource-management/scope.md +1 -209
  58. package/reference/resource-management/wire.md +4 -50
  59. package/reference/ringbuffer/advanced.mdx +1 -1
  60. package/reference/ringbuffer/index.mdx +3 -3
  61. package/reference/ringbuffer/mpmc.mdx +38 -4
  62. package/reference/ringbuffer/mpsc.mdx +36 -4
  63. package/reference/ringbuffer/spmc.mdx +1 -1
  64. package/reference/ringbuffer/spsc.mdx +87 -15
  65. package/reference/schema/allows.md +0 -96
  66. package/reference/schema/binding.md +2 -2
  67. package/reference/schema/built-in-codecs/avro.md +2 -2
  68. package/reference/schema/built-in-codecs/bson.md +50 -20
  69. package/reference/schema/built-in-codecs/csv.md +2 -2
  70. package/reference/schema/built-in-codecs/index.md +3 -3
  71. package/reference/schema/built-in-codecs/json/index.md +2 -2
  72. package/reference/schema/built-in-codecs/messagepack.md +3 -3
  73. package/reference/schema/built-in-codecs/thrift.md +2 -2
  74. package/reference/schema/built-in-codecs/toon.md +3 -3
  75. package/reference/schema/built-in-codecs/yaml.md +2 -2
  76. package/reference/schema/codec.md +11 -11
  77. package/reference/schema/dynamic-optic.md +48 -3
  78. package/reference/schema/dynamic-schema.md +3 -3
  79. package/reference/schema/index.md +2 -0
  80. package/reference/schema/path-interpolator.md +2 -0
  81. package/reference/schema/reflect-transformer.md +140 -0
  82. package/reference/schema/schema-evolution/as.md +4 -4
  83. package/reference/schema/schema-evolution/into.md +2 -2
  84. package/reference/schema/schema-expr.md +2 -2
  85. package/reference/schema/schema-search.md +263 -0
  86. package/reference/schema/schema.md +10 -2
  87. package/reference/schema/type-class-derivation.md +1 -1
  88. package/reference/smithy.md +502 -3
  89. package/reference/sql/db-codec-deriver.md +3 -3
  90. package/reference/sql/db-codec.md +22 -22
  91. package/reference/sql/db-con.md +4 -4
  92. package/reference/sql/db-connection.md +1 -1
  93. package/reference/sql/db-param.md +1 -1
  94. package/reference/sql/db-result-reader.md +4 -2
  95. package/reference/sql/db-tx.md +46 -14
  96. package/reference/sql/ddl.md +1 -1
  97. package/reference/sql/frag.md +44 -10
  98. package/reference/sql/index.md +7 -7
  99. package/reference/sql/repo.md +15 -15
  100. package/reference/sql/sql-dialect.md +1 -1
  101. package/reference/sql/sql-logger.md +1 -1
  102. package/reference/sql/sql-name-mapper.md +3 -3
  103. package/reference/sql/table-metadata.md +3 -3
  104. package/reference/sql/table.md +10 -10
  105. package/reference/sql/transactor-zio.md +1 -1
  106. package/reference/sql/transactor.md +21 -11
  107. package/reference/sql-zio.md +1 -1
  108. package/reference/streams/core/index.md +32 -0
  109. package/reference/streams/{pipeline.md → core/pipeline.md} +210 -74
  110. package/reference/streams/{sink.md → core/sink.md} +331 -353
  111. package/reference/streams/{stream.md → core/stream.md} +919 -209
  112. package/reference/streams/execution-and-compatibility/async-execution.md +822 -0
  113. package/reference/streams/execution-and-compatibility/index.md +35 -0
  114. package/reference/streams/execution-and-compatibility/platform-differences.md +297 -0
  115. package/reference/streams/execution-and-compatibility/scala-2-compatibility.md +88 -0
  116. package/reference/streams/execution-and-compatibility/zero-boxing.md +393 -0
  117. package/reference/streams/index.md +140 -67
  118. package/reference/streams/primitives/index.md +30 -0
  119. package/reference/streams/primitives/reader.md +1992 -0
  120. package/reference/streams/{writer.md → primitives/writer.md} +254 -98
  121. package/reference/telemetry/common/any-value.md +90 -0
  122. package/reference/telemetry/common/attribute-key.md +87 -0
  123. package/reference/telemetry/common/attributes.md +118 -0
  124. package/reference/telemetry/common/index.md +39 -0
  125. package/reference/telemetry/common/instrumentation-scope.md +24 -0
  126. package/reference/telemetry/common/resource.md +34 -0
  127. package/reference/telemetry/index.md +311 -0
  128. package/reference/telemetry/logging/index.md +197 -0
  129. package/reference/telemetry/logging/log-enrichment.md +72 -0
  130. package/reference/telemetry/logging/log-formatter.md +100 -0
  131. package/reference/telemetry/logging/log-record-processor.md +56 -0
  132. package/reference/telemetry/logging/log-record.md +44 -0
  133. package/reference/telemetry/logging/log-writer.md +64 -0
  134. package/reference/telemetry/logging/logger-provider.md +142 -0
  135. package/reference/telemetry/logging/logger.md +83 -0
  136. package/reference/telemetry/logging/severity.md +62 -0
  137. package/reference/telemetry/metrics/index.md +150 -0
  138. package/reference/telemetry/metrics/instruments.md +183 -0
  139. package/reference/telemetry/metrics/labeled-instruments.md +74 -0
  140. package/reference/telemetry/metrics/meter-provider.md +76 -0
  141. package/reference/telemetry/metrics/meter.md +98 -0
  142. package/reference/telemetry/metrics/metric-data.md +57 -0
  143. package/reference/telemetry/otel/custom-exporter.md +216 -0
  144. package/reference/telemetry/otel/index.md +212 -0
  145. package/reference/telemetry/tracing/index.md +155 -0
  146. package/reference/telemetry/tracing/sampler.md +89 -0
  147. package/reference/telemetry/tracing/span-builder.md +57 -0
  148. package/reference/telemetry/tracing/span-context.md +39 -0
  149. package/reference/telemetry/tracing/span-data.md +32 -0
  150. package/reference/telemetry/tracing/span-kind.md +55 -0
  151. package/reference/telemetry/tracing/span-processor.md +53 -0
  152. package/reference/telemetry/tracing/span-status.md +47 -0
  153. package/reference/telemetry/tracing/span.md +117 -0
  154. package/reference/telemetry/tracing/tracer-provider.md +91 -0
  155. package/reference/telemetry/tracing/tracer.md +52 -0
  156. package/reference/typeid.md +0 -64
  157. package/sidebars.js +150 -12
  158. package/undocumented-report.md +528 -270
  159. package/reference/config.md +0 -158
  160. package/reference/streams/concurrent-operators.md +0 -106
  161. package/reference/streams/reader.md +0 -1284
  162. package/reference/streams/scala-2-compatibility.md +0 -55
  163. package/reference/streams/zero-boxing.md +0 -275
  164. package/reference/telemetry.md +0 -693
@@ -3,44 +3,148 @@ id: maybe
3
3
  title: "Maybe"
4
4
  ---
5
5
 
6
- `Maybe[A]` is a **low-allocation alternative to `Option[A]`** that uses `null` to represent the absence of a value. It is an opaque type alias for `A | Null`, allowing you to write nullable-like code with the safety and ergonomics of an Option-style API. Core types: `Maybe[A]`.
6
+ `Maybe[A]` is a **low-allocation alternative to `Option[A]`** that uses a top-level `Absent` sentinel object to represent the absence of a value. On Scala 3, it is an opaque type alias for `A | Absent.type | Present[A]`, where `Present[A]` is a public wrapper allocated whenever a present value would otherwise be indistinguishable from absence — nested `Maybe`s (`Maybe.present(Maybe.absent)` → `Present(Absent)`) and `null` values (`Maybe.present(null)` → `Present(null)`). On Scala 2.13, it is a sealed trait (`Present[A]` | `Absent`). Core types: `Maybe[A]`, `Present[A]`.
7
7
 
8
8
  Here's the type definition and basic construction:
9
9
 
10
10
  ```scala
11
- opaque type Maybe[+A] = A | Null
11
+ // Scala 3
12
+ final class Present[+A](val value: A) // manual companion: apply + unapply that matches both raw values and wrappers
13
+ object Absent
14
+ opaque type Maybe[+A] = A | Absent.type | Present[A]
12
15
 
13
16
  val present: Maybe[Int] = Maybe.present(42)
14
17
  val absent: Maybe[Int] = Maybe.absent
15
18
  ```
16
19
 
20
+ ## Allocation Profile
21
+
22
+ The new encoding minimizes allocation by using the raw value when possible:
23
+
24
+ | Case | Example | Allocations |
25
+ |------|---------|-------------|
26
+ | Flat present | `Maybe.present(42)` | **0** (raw `42`) |
27
+ | Flat apply | `Maybe(42)` | **0** (raw `42`) |
28
+ | Flat fromOption | `Maybe.fromOption(Some(42))` | **0** (raw `42`) |
29
+ | Absent | `Maybe.absent` | **0** (`Absent` singleton) |
30
+ | Present-of-absent | `Maybe.present(Maybe.absent)` | **1** (`Present(Absent)`) |
31
+ | Present null | `Maybe.fromOption(Some(null))` | **1** (`Present(null)`) |
32
+
33
+ The `Present[A]` wrapper is allocated **only** for present-of-absent cases: wrapping a nested `Maybe` that is itself absent (`Present(Absent)`) and wrapping a `null` value (`Present(null)`). All other flat cases store the value raw with zero allocation overhead.
34
+
35
+ ### Nesting Depth
36
+
37
+ Nested `Maybe` values reduce wrapper depth by 1 compared to `Option`:
38
+
39
+ ```scala
40
+ import zio.blocks.maybe._
41
+
42
+ // Option: Some(None) = 2 wrappers
43
+ val optNested: Option[Option[Int]] = Some(None)
44
+
45
+ // Maybe: Present(Absent) = 1 wrapper (Present) + Absent singleton
46
+ val maybeNested: Maybe[Maybe[Int]] = Maybe.present(Maybe.absent[Int])
47
+ // maybeNested matches Present(Absent), not Absent
48
+
49
+ // Flattening removes the Present wrapper
50
+ val flat: Maybe[Int] = maybeNested.flatten
51
+ // flat is absent (Absent)
52
+ ```
53
+
17
54
  ## Motivation
18
55
 
19
56
  When working with optional values, you face a choice: `Option[A]` provides type safety and functional composition but allocates a wrapper object for every value. `Maybe[A]` provides an alternative with different trade-offs depending on your Scala version.
20
57
 
21
- **On Scala 3:** `Maybe[A]` eliminates allocation overhead by leveraging union types and null semantics—every value is either the unwrapped value itself or `null`. This is ideal for performance-critical code where allocations impact throughput or latency. The type is an opaque alias for `A | Null`, giving you a dedicated API (`map`, `flatMap`, `filter`, etc.) with zero runtime wrapper overhead—just null checks.
58
+ **On Scala 3:** `Maybe[A]` eliminates allocation overhead by leveraging union types and a top-level `Absent` sentinel. The type is an opaque alias for `A | Absent.type | Present[A]`, where `Present[A]` is a public wrapper allocated whenever a present value would otherwise be indistinguishable from absence. Flat values (non-nested, non-null) are stored raw with zero allocation; only present-of-absent values allocate a `Present` wrapper (`Maybe.present(Maybe.absent)` → `Present(Absent)`, `Maybe.present(null)` → `Present(null)`). This gives you a dedicated API (`map`, `flatMap`, `filter`, etc.) with minimal runtime overhead. `case Absent` is a stable-identifier pattern that works directly.
22
59
 
23
60
  **On Scala 2.13:** `Maybe[A]` is implemented as a sealed trait (`Present[A]` | `Absent`). Present values allocate a wrapper, so the allocation savings versus `Option` are less pronounced. However, the unified API and interoperability benefits still apply.
24
61
 
25
62
  ### Why Maybe over Option?
26
63
 
27
- - **Zero allocation**: Every `Maybe` is either the value itself or `null`—no wrapper objects
64
+ - **Zero allocation (flat case)**: Non-nested `Maybe` values are either the raw value itself or the `Absent` singleton—no wrapper objects
65
+ - **Sound nesting**: Nested `Maybe[Maybe[A]]` is now sound via `Present[A]`, without requiring a compile-time guard
28
66
  - **Familiar API**: All your favorite `Option` combinators (`map`, `flatMap`, `fold`, etc.)
29
67
  - **Type safety**: The opaque type prevents accidentally mixing nullable and non-nullable values
30
68
  - **Interoperable**: Seamless conversion to/from `Option` with `toOption` and `Maybe#fromOption`
31
69
 
70
+ ## Cross-Version Parity
71
+
72
+ The API surface is consistent across Scala 2.13 and Scala 3, but the underlying encoding differs:
73
+
74
+ | Aspect | Scala 3 | Scala 2.13 |
75
+ |--------|---------|------------|
76
+ | Encoding | `opaque type Maybe[+A] = A \| Absent.type \| Present[A]` | Sealed trait `Present[A]` \| `Absent` |
77
+ | Flat allocation | **0** (raw value or `Absent` singleton) | **1** (wrapper object) |
78
+ | Nested allocation | **1** (`Present(Absent)` only) | **1** (wrapper object) |
79
+ | `Present` type | Public `final class Present[+A]` with manual companion (`apply`/`unapply`) | `MaybeValue.Present[A]` (sealed) |
80
+ | Nesting guard | **Removed** (nesting now sound via `Present`) | N/A (never existed) |
81
+
82
+ ### Behavior Differences
83
+
84
+ - **`Maybe.present(null)`**: On Scala 3, produces `Present(null)` (present-of-absent). On Scala 2.13, produces `MaybeValue.Present(null)` (also present-of-absent). Both are distinguishable from `Maybe.absent`.
85
+ - **`Maybe.fromOption(Some(null))`**: Routes through `present`, so same behavior as above.
86
+ - **Pattern matching**: On Scala 3, `Present(v)` matches both present shapes (a raw value and a `Present(...)` wrapper); absent matches `case Absent` (stable-identifier pattern, no `case _` required inside the package; external two-case matches also compile without exhaustivity warning). On Scala 2.13, `MaybeValue.Present(v)` / `MaybeValue.Absent` are the compiler-checked native patterns.
87
+
88
+ ## Pattern Matching
89
+
90
+ On Scala 3, a `Maybe[A]` value can take three runtime shapes. The `Present` companion's `unapply` collapses the two present shapes into one pattern; absent is now a real top-level singleton object:
91
+
92
+ | Shape | Pattern | Meaning |
93
+ |-------|---------|---------|
94
+ | `Absent` object | `case Absent` | absent (stable-identifier pattern) |
95
+ | `Present(v)` wrapper | `case Present(v)` | present-of-absent (a nested `Maybe`) |
96
+ | raw `v` | `case Present(v)` | present, zero allocation |
97
+
98
+ (Note: the `Present` companion's `unapply` collapses both present shapes — one `Some` allocation per present match.)
99
+
100
+ ```scala
101
+ import zio.blocks.maybe._
102
+
103
+ val maybe: Maybe[Int] = Maybe.present(42)
104
+
105
+ val description: String = maybe match {
106
+ case Present(v) => s"present ($v)"
107
+ case Absent => "absent"
108
+ }
109
+ ```
110
+
111
+ `case Absent` is a stable-identifier pattern that works because `Absent` is a plain top-level object (not a case object). The two-case match `case Present(v); case Absent` compiles without exhaustivity warning even from an external package — verified under this build's `-Xfatal-warnings` settings (`WildcardImportSpec`). Note that this is weaker than the sealed-hierarchy guarantee on Scala 2.13 below: exhaustivity here depends on the compiler decomposing the opaque union, so prefer `fold` when you need a guarantee that is independent of compiler behavior. `Present(v)` matches both a raw value and a `Present(...)` wrapper, at the cost of one `Some` allocation per present match (inherent to the `Option`-returning extractor protocol).
112
+
113
+ For production code, prefer `fold`, which is exhaustive, warning-free, and zero-allocation:
114
+
115
+ ```scala
116
+ import zio.blocks.maybe._
117
+
118
+ val maybe: Maybe[Int] = Maybe.present(42)
119
+ val description: String = maybe.fold("absent")(v => s"present ($v)")
120
+ ```
121
+
122
+ Scala 2.13 parity: on Scala 2.13, absent is the non-null case object `MaybeValue.Absent`, so it **can** be matched explicitly — the sealed `MaybeValue` trait (`Present` | `Absent`) gives compiler-checked exhaustivity:
123
+
124
+ ```scala
125
+ import zio.blocks.maybe._
126
+
127
+ val maybe: Maybe[Int] = Maybe.present(42)
128
+ val description: String = maybe match {
129
+ case MaybeValue.Present(v) => s"present ($v)"
130
+ case MaybeValue.Absent => "absent"
131
+ }
132
+ ```
133
+
134
+ So on Scala 2.13 the sealed `MaybeValue` form is the compiler-checked native idiom; on Scala 3 the opaque union with `Absent` object enables `case Absent` while preserving the zero-allocation encoding.
135
+
32
136
  ## Installation
33
137
 
34
138
  Add the `zio-blocks-maybe` module to your build:
35
139
 
36
140
  ```scala
37
- libraryDependencies += "dev.zio" %% "zio-blocks-maybe" % "0.0.51"
141
+ libraryDependencies += "dev.zio" %% "zio-blocks-maybe" % "0.0.55"
38
142
  ```
39
143
 
40
144
  For Scala.js:
41
145
 
42
146
  ```scala
43
- libraryDependencies += "dev.zio" %%% "zio-blocks-maybe" % "0.0.51"
147
+ libraryDependencies += "dev.zio" %%% "zio-blocks-maybe" % "0.0.55"
44
148
  ```
45
149
 
46
150
  Supported Scala versions: 2.13.x and 3.x
@@ -84,7 +188,7 @@ Here are key patterns for working effectively with `Maybe`:
84
188
 
85
189
  ### Present and Absent States
86
190
 
87
- Every `Maybe` is either present (holds a non-null value) or absent (is `null`). Test the state with predicates:
191
+ Every `Maybe` is either present (holds a value, possibly `null`) or absent (the `Absent` singleton). Test the state with predicates:
88
192
 
89
193
  ```scala
90
194
  import zio.blocks.maybe._
@@ -219,20 +323,28 @@ Wraps a value in `Maybe`, treating `null` as `Maybe.absent`:
219
323
  ```scala
220
324
  import zio.blocks.maybe._
221
325
 
222
- val present = Maybe(42) // Maybe[Int] containing 42
223
- val absent: Maybe[String] = Maybe(null) // Maybe[String] absent
326
+ val present: Maybe[Int] = Maybe(42) // Maybe[Int] containing 42
327
+ val absent: Maybe[String] = Maybe(null.asInstanceOf[String]) // Maybe.absent (the Absent object)
224
328
  ```
225
329
 
330
+ > **Note:** `Maybe.apply` collapses `null` to `Maybe.absent`. Use `Maybe.present` when you need to preserve present-ness even for `null` values (e.g., in nested `Maybe`s).
331
+
226
332
  #### Maybe.present
227
333
 
228
- Explicitly wraps a non-null value:
334
+ Explicitly wraps a value, preserving present-ness even for `null`:
229
335
 
230
336
  ```scala
231
337
  import zio.blocks.maybe._
232
338
 
233
339
  val value: Maybe[Int] = Maybe.present(100)
340
+
341
+ // Nested case: present of an absent Maybe produces Present(Absent), not absence
342
+ val nested: Maybe[Maybe[Int]] = Maybe.present(Maybe.absent[Int])
343
+ // nested is Present(Absent), distinguishable from Maybe.absent
234
344
  ```
235
345
 
346
+ > **Note:** Unlike `Maybe.apply`, `Maybe.present` preserves present-ness even for `null`. A non-null value is returned as-is (zero allocation). A `null` value is wrapped in `Present(null)`, which is distinguishable from `Maybe.absent` (the `Absent` object). This is what makes nested `Maybe`s sound.
347
+
236
348
  #### Maybe.absent
237
349
 
238
350
  Creates an absent value for any type:
@@ -393,9 +505,14 @@ Unwraps a nested `Maybe`:
393
505
  ```scala
394
506
  import zio.blocks.maybe._
395
507
 
396
- val nested: Maybe[Maybe[Int]] = Maybe.present(Maybe.present(42))
508
+ val nested: Maybe[Maybe[Int]] = Maybe.fromOption(Some(Maybe.fromOption(Some(42))))
397
509
  val flat: Maybe[Int] = nested.flatten
398
510
  println(flat.get) // 42
511
+
512
+ // Nested present-of-absent flattens to absent
513
+ val nestedAbsent: Maybe[Maybe[Int]] = Maybe.present(Maybe.absent[Int])
514
+ val flatAbsent: Maybe[Int] = nestedAbsent.flatten
515
+ println(flatAbsent.isAbsent) // true
399
516
  ```
400
517
 
401
518
  ### Filtering
@@ -75,13 +75,13 @@ textAny.matches(html) // true
75
75
  Add the following to your `build.sbt`:
76
76
 
77
77
  ```scala
78
- libraryDependencies += "dev.zio" %% "zio-blocks-mediatype" % "0.0.51"
78
+ libraryDependencies += "dev.zio" %% "zio-blocks-mediatype" % "0.0.55"
79
79
  ```
80
80
 
81
81
  For cross-platform projects (Scala.js):
82
82
 
83
83
  ```scala
84
- libraryDependencies += "dev.zio" %%% "zio-blocks-mediatype" % "0.0.51"
84
+ libraryDependencies += "dev.zio" %%% "zio-blocks-mediatype" % "0.0.55"
85
85
  ```
86
86
 
87
87
  Supported Scala versions: 2.13.x and 3.x.
@@ -0,0 +1,254 @@
1
+ ---
2
+ id: mux
3
+ title: "Mux"
4
+ ---
5
+
6
+ `Mux[Id, In, Out]` is a zero-dependency, cross-platform multiplexed stream coordinator. It manages many independent streams over one shared transport, where each stream is keyed by an `Id`.
7
+
8
+ Use it for:
9
+ - HTTP/2 stream multiplexing
10
+ - WebSocket subprotocols
11
+ - Any ID-keyed protocol that needs independent stream lifecycles
12
+
13
+ Key properties:
14
+ - Thread-safe registry operations and multi-producer stream writes
15
+ - Lock-free on JVM for queue operations
16
+ - Virtual-thread-friendly
17
+ - JVM uses ring buffers for stream queues
18
+
19
+ ## Overview
20
+
21
+ `Mux` owns the stream registry. Protocol code uses `offerInbound` and `takeOutbound`, while application code uses `send` and `receive`.
22
+
23
+ ```
24
+ User code → send(In) → outbound queue → Protocol reads via takeOutbound()
25
+ Protocol → offerInbound(Out) → inbound queue → User code reads via receive()
26
+ ```
27
+
28
+ Each stream has separate inbound and outbound queues, so traffic for one ID stays isolated from the others. Half-close maps cleanly to RFC 9113 stream states, which makes the API a good fit for HTTP/2-style protocols.
29
+
30
+ ## API
31
+
32
+ The `Mux` API is cross-compiled: Scala 3 uses zero-cost union return types, while Scala 2 uses `Either`.
33
+
34
+ ### Scala 3
35
+
36
+ ```scala
37
+ trait Mux[Id, In, Out] {
38
+ def open(id: Id): MuxStream[Id, In, Out] | MuxError
39
+ def get(id: Id): Option[MuxStream[Id, In, Out]]
40
+ def cancel(id: Id, reason: MuxError): Unit
41
+ def closeAll(reason: MuxError): Unit
42
+ def activeCount: Int
43
+ }
44
+
45
+ trait MuxStream[Id, In, Out] {
46
+ def id: Id
47
+ def send(msg: In): Unit | MuxError
48
+ def receive(): Option[Out] | MuxError
49
+ def offerInbound(msg: Out): Unit | MuxError
50
+ def takeOutbound(): Option[In] | MuxError
51
+ def halfClose(): Unit
52
+ def signalRemoteClose(): Unit
53
+ def isClosed: Boolean
54
+ def isHalfClosed: Boolean
55
+ def close(): Unit
56
+ }
57
+
58
+ sealed trait MuxError
59
+ ```
60
+
61
+ ### Scala 2
62
+
63
+ ```scala
64
+ trait Mux[Id, In, Out] {
65
+ def open(id: Id): Either[MuxError, MuxStream[Id, In, Out]]
66
+ def get(id: Id): Option[MuxStream[Id, In, Out]]
67
+ def cancel(id: Id, reason: MuxError): Unit
68
+ def closeAll(reason: MuxError): Unit
69
+ def activeCount: Int
70
+ }
71
+
72
+ trait MuxStream[Id, In, Out] {
73
+ def id: Id
74
+ def send(msg: In): Either[MuxError, Unit]
75
+ def receive(): Either[MuxError, Option[Out]]
76
+ def offerInbound(msg: Out): Either[MuxError, Unit]
77
+ def takeOutbound(): Either[MuxError, Option[In]]
78
+ def halfClose(): Unit
79
+ def signalRemoteClose(): Unit
80
+ def isClosed: Boolean
81
+ def isHalfClosed: Boolean
82
+ def close(): Unit
83
+ }
84
+ ```
85
+
86
+ The semantics are the same on both versions; only the surface return types differ.
87
+
88
+ ### Factory
89
+
90
+ ```scala
91
+ object Mux {
92
+ def apply[Id, In, Out](capacity: Int): Mux[Id, In, Out]
93
+ }
94
+ ```
95
+
96
+ ### Core Operations
97
+
98
+ - `open(id)` opens a new stream
99
+ - `get(id)` looks up an active stream
100
+ - `cancel(id, reason)` closes one stream with an error
101
+ - `closeAll(reason)` closes every active stream
102
+ - `activeCount` reports how many streams are open
103
+
104
+ ### Per-stream Operations
105
+
106
+ - `send(msg)` queues outbound data for the protocol layer
107
+ - `receive()` reads inbound data for user code
108
+ - `offerInbound(msg)` delivers data from the protocol layer
109
+ - `takeOutbound()` drains outbound data for the protocol layer
110
+ - `halfClose()` marks local send as finished
111
+ - `signalRemoteClose()` marks remote send as finished
112
+ - `close()` fully closes the stream immediately; buffered inbound messages can still be drained before the terminal error is observed
113
+
114
+ ### `MuxError`
115
+
116
+ Error cases:
117
+
118
+ - `MuxError.StreamClosed(id)`
119
+ - `MuxError.CapacityExceeded(limit)` — maximum concurrent streams reached
120
+ - `MuxError.QueueFull(queueCapacity)` — per-stream message queue is full (backpressure)
121
+ - `MuxError.Cancelled(id, reason)`
122
+ - `MuxError.MuxClosed`
123
+ - `MuxError.ProtocolError(message)` — e.g., null message, invalid state transition
124
+
125
+ ## Examples
126
+
127
+ ### Basic Usage
128
+
129
+ Scala 3:
130
+
131
+ ```scala
132
+ import zio.blocks.mux.*
133
+
134
+ val mux = Mux[Int, String, String](capacity = 100)
135
+
136
+ mux.open(1) match {
137
+ case stream: MuxStream[Int, String, String] =>
138
+ stream.offerInbound("hello from peer")
139
+ val msg = stream.receive() // Some("hello from peer")
140
+ stream.send("response")
141
+ val out = stream.takeOutbound() // Some("response")
142
+ case err: MuxError =>
143
+ println(s"open failed: $err")
144
+ }
145
+ ```
146
+
147
+ Scala 2 uses the same flow with `Either`:
148
+
149
+ ```scala
150
+ import zio.blocks.mux._
151
+
152
+ val mux = Mux[Int, String, String](capacity = 100)
153
+
154
+ mux.open(1) match {
155
+ case Right(stream) =>
156
+ stream.offerInbound("hello from peer")
157
+ val msg = stream.receive() // Right(Some("hello from peer"))
158
+ stream.send("response")
159
+ val out = stream.takeOutbound() // Right(Some("response"))
160
+ case Left(err) =>
161
+ println(s"open failed: $err")
162
+ }
163
+ ```
164
+
165
+ ### HTTP/2-style Multiplexing
166
+
167
+ ```scala
168
+ import zio.blocks.mux.*
169
+
170
+ final case class Request(path: String)
171
+ final case class Response(status: Int)
172
+
173
+ val mux = Mux[Int, Request, Response](capacity = 1000)
174
+
175
+ // Demuxer receives frames, routes by stream ID
176
+ def onFrame(streamId: Int, data: Response): Unit = {
177
+ mux.get(streamId).foreach(_.offerInbound(data))
178
+ }
179
+
180
+ // Application opens streams for requests
181
+ mux.open(7) match {
182
+ case stream: MuxStream[Int, Request, Response] =>
183
+ stream.send(Request("/docs"))
184
+
185
+ // ... later
186
+ val response = stream.receive()
187
+ stream.halfClose()
188
+ case err: MuxError =>
189
+ println(s"open failed: $err")
190
+ }
191
+ ```
192
+
193
+ In Scala 2, pattern match on `Right(stream)` / `Left(err)` instead.
194
+
195
+ ### Half-close Lifecycle
196
+
197
+ ```scala
198
+ import zio.blocks.mux.*
199
+
200
+ val mux = Mux[Int, String, String](capacity = 10)
201
+
202
+ mux.open(1) match {
203
+ case stream: MuxStream[Int, String, String] =>
204
+ stream.send("last message")
205
+ stream.halfClose()
206
+
207
+ // Can still receive after half-close
208
+ val msg = stream.receive()
209
+
210
+ // send() after halfClose returns an error
211
+ stream.send("nope") // MuxError.StreamClosed(1)
212
+ case err: MuxError =>
213
+ println(s"open failed: $err")
214
+ }
215
+ ```
216
+
217
+ Scala 2 returns `Left(MuxError.StreamClosed(1))` for the final `send`.
218
+
219
+ ### Graceful Shutdown
220
+
221
+ ```scala
222
+ import zio.blocks.mux.*
223
+
224
+ val mux = Mux[Int, String, String](capacity = 10)
225
+
226
+ // Cancel a single stream
227
+ mux.cancel(42, MuxError.Cancelled(42, "timeout"))
228
+
229
+ // Shut down everything
230
+ mux.closeAll(MuxError.MuxClosed)
231
+ assert(mux.activeCount == 0)
232
+ ```
233
+
234
+ ## Architecture
235
+
236
+ `Mux` keeps a registry of active streams and gives each stream its own inbound and outbound queues. The protocol layer never talks to user code directly, it only moves messages through `offerInbound` and `takeOutbound`.
237
+
238
+ Half-close models the usual protocol lifecycle:
239
+
240
+ - local side done sending
241
+ - remote side done sending
242
+ - both sides done, stream fully closed
243
+
244
+ ## Performance
245
+
246
+ - JVM: `MpscRingBuffer` for inbound and outbound, lock-free and zero-alloc for multi-producer writes
247
+ - JVM: `VarHandle` CAS for stream state transitions
248
+ - JS: `ArrayDeque` fallback
249
+ - No `synchronized`, so it stays friendly to virtual threads
250
+
251
+ ## See Also
252
+
253
+ - [Streams](streams/index.md) -- the pull-based `Stream`, `Reader`, `Sink`, and `Writer` module. `Mux` is not built on it, but the two are the same concurrency-infrastructure family, and a stream per multiplexed channel is the natural pairing when you are coordinating many keyed streams over one transport.
254
+ - [Async Execution](streams/execution-and-compatibility/async-execution.md) -- how a stream runs without blocking, which is what you want on each side of a `Mux` channel.
package/reference/mux.mdx CHANGED
@@ -47,13 +47,13 @@ Without multiplexing, protocols must open a new connection per concurrent operat
47
47
  Add the dependency to your build:
48
48
 
49
49
  ```scala
50
- libraryDependencies += "dev.zio" %% "zio-blocks-mux" % "@VERSION@"
50
+ libraryDependencies += "dev.zio" %% "zio-blocks-mux" % "0.0.55"
51
51
  ```
52
52
 
53
53
  For Scala.js:
54
54
 
55
55
  ```scala
56
- libraryDependencies += "dev.zio" %%% "zio-blocks-mux" % "@VERSION@"
56
+ libraryDependencies += "dev.zio" %%% "zio-blocks-mux" % "0.0.55"
57
57
  ```
58
58
 
59
59
  Supported Scala versions: 2.13.x and 3.x
@@ -821,3 +821,8 @@ val inMux = mux.get(1)
821
821
  - JVM: `VarHandle` CAS for stream state transitions
822
822
  - JS: `ArrayDeque` fallback
823
823
  - No `synchronized`, so it stays friendly to virtual threads
824
+
825
+ ## See Also
826
+
827
+ - [Streams](streams/index.md) -- the pull-based `Stream`, `Reader`, `Sink`, and `Writer` module. `Mux` is not built on it, but the two are the same concurrency-infrastructure family, and a stream per multiplexed channel is the natural pairing when you are coordinating many keyed streams over one transport.
828
+ - [Async Execution](streams/execution-and-compatibility/async-execution.md) -- how a stream runs without blocking, which is what you want on each side of a `Mux` channel.
@@ -27,16 +27,16 @@ The OpenAPI module bridges the gap by letting you author API specs as Scala code
27
27
  ## Installation
28
28
 
29
29
  ```scala
30
- libraryDependencies += "dev.zio" %% "zio-blocks-openapi" % "0.0.51"
30
+ libraryDependencies += "dev.zio" %% "zio-blocks-openapi" % "0.0.55"
31
31
 
32
32
  // You'll also need the schema module for Schema[A] integration:
33
- libraryDependencies += "dev.zio" %% "zio-blocks-schema" % "0.0.51"
33
+ libraryDependencies += "dev.zio" %% "zio-blocks-schema" % "0.0.55"
34
34
  ```
35
35
 
36
36
  For Scala.js:
37
37
 
38
38
  ```scala
39
- libraryDependencies += "dev.zio" %%% "zio-blocks-openapi" % "0.0.51"
39
+ libraryDependencies += "dev.zio" %%% "zio-blocks-openapi" % "0.0.55"
40
40
  ```
41
41
 
42
42
  Supported Scala versions: 2.13.x and 3.x.