@zio.dev/zio-blocks 0.0.33 → 0.0.55

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (215) hide show
  1. package/adr/2026-07-18-data-migration.md +123 -0
  2. package/guides/async-getting-started.md +687 -0
  3. package/guides/compile-time-resource-safety-with-scope.md +21 -16
  4. package/guides/getting-started-with-mux.md +1395 -0
  5. package/guides/query-dsl-extending.md +161 -102
  6. package/guides/query-dsl-fluent-builder.md +217 -157
  7. package/guides/query-dsl-reified-optics.md +12 -10
  8. package/guides/query-dsl-sql.md +640 -165
  9. package/guides/sql-checked-interpolation.md +173 -0
  10. package/guides/sql-transactions.md +286 -0
  11. package/guides/telemetry-guide.md +1130 -0
  12. package/guides/zio-schema-migration.md +29 -22
  13. package/index.md +248 -389
  14. package/package.json +1 -1
  15. package/plans/config-follow-up-prs.md +188 -0
  16. package/plans/config-pr-assessment-roadmap.md +310 -0
  17. package/reference/MuxDataFlow.jsx +250 -0
  18. package/reference/async.md +1499 -0
  19. package/reference/chunk.md +3533 -308
  20. package/reference/codegen/case-class.md +436 -0
  21. package/reference/codegen/emitter-config.md +383 -0
  22. package/reference/codegen/examples.md +664 -0
  23. package/reference/codegen/field.md +316 -0
  24. package/reference/codegen/index.md +317 -0
  25. package/reference/codegen/scala-emitter.md +392 -0
  26. package/reference/codegen/scala-file.md +276 -0
  27. package/reference/codegen/sealed-trait.md +408 -0
  28. package/reference/codegen/type-definition.md +340 -0
  29. package/reference/codegen/type-ref.md +201 -0
  30. package/reference/combinators.md +347 -117
  31. package/reference/config/config-decoder.md +460 -0
  32. package/reference/config/config-source.md +489 -0
  33. package/reference/config/errors.md +278 -0
  34. package/reference/config/flags.md +369 -0
  35. package/reference/config/formats.md +314 -0
  36. package/reference/config/index.md +304 -0
  37. package/reference/config/rollout.md +336 -0
  38. package/reference/context.md +9 -52
  39. package/reference/data-migration.md +269 -0
  40. package/reference/datastar/attributes.md +302 -0
  41. package/reference/datastar/events.md +234 -0
  42. package/reference/datastar/index.md +256 -0
  43. package/reference/datastar/signals.md +230 -0
  44. package/reference/datastar/sse.md +295 -0
  45. package/reference/datastar.md +346 -0
  46. package/reference/docs.md +1461 -345
  47. package/reference/endpoint/auth-type.md +146 -0
  48. package/reference/endpoint/bulk-creation.md +96 -0
  49. package/reference/endpoint/endpoint.md +297 -0
  50. package/reference/endpoint/http-codec.md +249 -0
  51. package/reference/endpoint/index.md +745 -0
  52. package/reference/endpoint/path-codec.md +225 -0
  53. package/reference/endpoint/route-pattern.md +194 -0
  54. package/reference/endpoint/route-tree.md +111 -0
  55. package/reference/endpoint/segment-codec.md +199 -0
  56. package/reference/html.md +1424 -0
  57. package/reference/htmx/attribute-values.md +359 -0
  58. package/reference/htmx/hx-encoding.md +111 -0
  59. package/reference/htmx/hx-params.md +204 -0
  60. package/reference/htmx/hx-swap.md +276 -0
  61. package/reference/htmx/hx-sync.md +251 -0
  62. package/reference/htmx/hx-target.md +314 -0
  63. package/reference/htmx/hx-trigger.md +457 -0
  64. package/reference/htmx/hx-url-update.md +239 -0
  65. package/reference/htmx/index.md +807 -0
  66. package/reference/htmx/response-headers.md +240 -0
  67. package/reference/http-model/headers.md +735 -0
  68. package/reference/http-model/index.md +49 -0
  69. package/reference/http-model/model.md +1517 -0
  70. package/reference/http-model/schema-codecs.md +522 -0
  71. package/reference/http-model/schema.md +750 -0
  72. package/reference/http-model/server-sent-event.md +341 -0
  73. package/reference/jwt.md +195 -0
  74. package/reference/maybe.md +943 -0
  75. package/reference/media-type.md +2 -2
  76. package/reference/mux.md +254 -0
  77. package/reference/mux.mdx +828 -0
  78. package/reference/openapi.md +1351 -0
  79. package/reference/projection.md +654 -0
  80. package/reference/resource-management/defer-handle.md +1 -1
  81. package/reference/resource-management/resource.md +31 -98
  82. package/reference/resource-management/scope.md +28 -220
  83. package/reference/resource-management/wire.md +5 -55
  84. package/reference/ringbuffer/MpmcDiagram.jsx +717 -0
  85. package/reference/ringbuffer/MpscDiagram.jsx +618 -0
  86. package/reference/ringbuffer/SpmcDiagram.jsx +680 -0
  87. package/reference/ringbuffer/SpscDiagram.jsx +677 -0
  88. package/reference/ringbuffer/advanced.mdx +109 -0
  89. package/reference/ringbuffer/index.mdx +145 -0
  90. package/reference/ringbuffer/mpmc.mdx +185 -0
  91. package/reference/ringbuffer/mpsc.mdx +164 -0
  92. package/reference/ringbuffer/spmc.mdx +108 -0
  93. package/reference/ringbuffer/spsc.mdx +416 -0
  94. package/reference/{allows.md → schema/allows.md} +4 -100
  95. package/reference/{binding-resolver.md → schema/binding-resolver.md} +1 -1
  96. package/reference/{binding.md → schema/binding.md} +3 -4
  97. package/reference/schema/built-in-codecs/avro.md +451 -0
  98. package/reference/schema/built-in-codecs/bson.md +510 -0
  99. package/reference/schema/built-in-codecs/csv.md +564 -0
  100. package/reference/schema/built-in-codecs/index.md +77 -0
  101. package/reference/schema/built-in-codecs/json/index.md +295 -0
  102. package/reference/schema/built-in-codecs/json/json-config.md +217 -0
  103. package/reference/{json-patch.md → schema/built-in-codecs/json/json-patch.md} +5 -5
  104. package/reference/{json-schema.md → schema/built-in-codecs/json/json-schema.md} +14 -47
  105. package/reference/schema/built-in-codecs/json/json-selection.md +322 -0
  106. package/reference/{json.md → schema/built-in-codecs/json/json.md} +32 -64
  107. package/reference/schema/built-in-codecs/messagepack.md +508 -0
  108. package/reference/schema/built-in-codecs/thrift.md +433 -0
  109. package/reference/schema/built-in-codecs/toon.md +1078 -0
  110. package/reference/{xml.md → schema/built-in-codecs/xml.md} +13 -9
  111. package/reference/schema/built-in-codecs/yaml.md +552 -0
  112. package/reference/{codec.md → schema/codec.md} +11 -11
  113. package/reference/{dynamic-optic.md → schema/dynamic-optic.md} +196 -5
  114. package/reference/{dynamic-schema.md → schema/dynamic-schema.md} +8 -8
  115. package/reference/schema/format.md +92 -0
  116. package/reference/schema/index.md +52 -0
  117. package/reference/schema/migration.md +297 -0
  118. package/reference/{modifier.md → schema/modifier.md} +58 -7
  119. package/reference/{optics.md → schema/optics.md} +2 -2
  120. package/reference/{patch.md → schema/patch.md} +1 -1
  121. package/{path-interpolator.md → reference/schema/path-interpolator.md} +167 -72
  122. package/reference/schema/reflect-transformer.md +140 -0
  123. package/reference/{schema-evolution → schema/schema-evolution}/as.md +8 -8
  124. package/reference/{schema-evolution → schema/schema-evolution}/index.md +2 -2
  125. package/reference/{schema-evolution → schema/schema-evolution}/into.md +8 -8
  126. package/reference/{schema-expr.md → schema/schema-expr.md} +110 -175
  127. package/reference/schema/schema-search.md +263 -0
  128. package/reference/{schema.md → schema/schema.md} +22 -2
  129. package/reference/{structural-types.md → schema/structural-types.md} +1 -1
  130. package/reference/{type-class-derivation.md → schema/type-class-derivation.md} +63 -1
  131. package/reference/smithy.md +1032 -0
  132. package/reference/sql/db-codec-deriver.md +71 -0
  133. package/reference/sql/db-codec.md +687 -0
  134. package/reference/sql/db-con.md +271 -0
  135. package/reference/sql/db-connection.md +153 -0
  136. package/reference/sql/db-param-writer.md +77 -0
  137. package/reference/sql/db-param.md +66 -0
  138. package/reference/sql/db-result-reader.md +148 -0
  139. package/reference/sql/db-tx.md +114 -0
  140. package/reference/sql/db-value.md +41 -0
  141. package/reference/sql/ddl.md +85 -0
  142. package/reference/sql/frag.md +288 -0
  143. package/reference/sql/index.md +341 -0
  144. package/reference/sql/repo.md +600 -0
  145. package/reference/sql/sql-dialect.md +73 -0
  146. package/reference/sql/sql-logger.md +62 -0
  147. package/reference/sql/sql-name-mapper.md +70 -0
  148. package/reference/sql/table-metadata.md +134 -0
  149. package/reference/sql/table.md +448 -0
  150. package/reference/sql/transactor-zio.md +399 -0
  151. package/reference/sql/transactor.md +363 -0
  152. package/reference/sql-zio.md +112 -0
  153. package/reference/streams/core/index.md +32 -0
  154. package/reference/streams/core/pipeline.md +854 -0
  155. package/reference/streams/core/sink.md +1404 -0
  156. package/reference/streams/core/stream.md +3236 -0
  157. package/reference/streams/execution-and-compatibility/async-execution.md +822 -0
  158. package/reference/streams/execution-and-compatibility/index.md +35 -0
  159. package/reference/streams/execution-and-compatibility/platform-differences.md +297 -0
  160. package/reference/streams/execution-and-compatibility/scala-2-compatibility.md +88 -0
  161. package/reference/streams/execution-and-compatibility/zero-boxing.md +393 -0
  162. package/reference/streams/index.md +726 -0
  163. package/reference/streams/primitives/index.md +30 -0
  164. package/reference/streams/primitives/reader.md +1992 -0
  165. package/reference/streams/primitives/writer.md +1201 -0
  166. package/reference/telemetry/common/any-value.md +90 -0
  167. package/reference/telemetry/common/attribute-key.md +87 -0
  168. package/reference/telemetry/common/attributes.md +118 -0
  169. package/reference/telemetry/common/index.md +39 -0
  170. package/reference/telemetry/common/instrumentation-scope.md +24 -0
  171. package/reference/telemetry/common/resource.md +34 -0
  172. package/reference/telemetry/index.md +311 -0
  173. package/reference/telemetry/logging/index.md +197 -0
  174. package/reference/telemetry/logging/log-enrichment.md +72 -0
  175. package/reference/telemetry/logging/log-formatter.md +100 -0
  176. package/reference/telemetry/logging/log-record-processor.md +56 -0
  177. package/reference/telemetry/logging/log-record.md +44 -0
  178. package/reference/telemetry/logging/log-writer.md +64 -0
  179. package/reference/telemetry/logging/logger-provider.md +142 -0
  180. package/reference/telemetry/logging/logger.md +83 -0
  181. package/reference/telemetry/logging/severity.md +62 -0
  182. package/reference/telemetry/metrics/index.md +150 -0
  183. package/reference/telemetry/metrics/instruments.md +183 -0
  184. package/reference/telemetry/metrics/labeled-instruments.md +74 -0
  185. package/reference/telemetry/metrics/meter-provider.md +76 -0
  186. package/reference/telemetry/metrics/meter.md +98 -0
  187. package/reference/telemetry/metrics/metric-data.md +57 -0
  188. package/reference/telemetry/otel/custom-exporter.md +216 -0
  189. package/reference/telemetry/otel/index.md +212 -0
  190. package/reference/telemetry/tracing/index.md +155 -0
  191. package/reference/telemetry/tracing/sampler.md +89 -0
  192. package/reference/telemetry/tracing/span-builder.md +57 -0
  193. package/reference/telemetry/tracing/span-context.md +39 -0
  194. package/reference/telemetry/tracing/span-data.md +32 -0
  195. package/reference/telemetry/tracing/span-kind.md +55 -0
  196. package/reference/telemetry/tracing/span-processor.md +53 -0
  197. package/reference/telemetry/tracing/span-status.md +47 -0
  198. package/reference/telemetry/tracing/span.md +117 -0
  199. package/reference/telemetry/tracing/tracer-provider.md +91 -0
  200. package/reference/telemetry/tracing/tracer.md +52 -0
  201. package/reference/typeid.md +5 -83
  202. package/sidebars.js +376 -43
  203. package/undocumented-report.md +528 -270
  204. package/reference/formats.md +0 -694
  205. package/reference/http-model.md +0 -1716
  206. package/reference/streams.md +0 -989
  207. package/ringbuffer.md +0 -249
  208. /package/reference/{json-differ.md → schema/built-in-codecs/json/json-differ.md} +0 -0
  209. /package/reference/{dynamic-value.md → schema/dynamic-value.md} +0 -0
  210. /package/reference/{lazy.md → schema/lazy.md} +0 -0
  211. /package/reference/{reflect.md → schema/reflect.md} +0 -0
  212. /package/reference/{registers.md → schema/registers.md} +0 -0
  213. /package/reference/{schema-error.md → schema/schema-error.md} +0 -0
  214. /package/reference/{syntax.md → schema/syntax.md} +0 -0
  215. /package/reference/{validation.md → schema/validation.md} +0 -0
@@ -0,0 +1,341 @@
1
+ ---
2
+ id: server-sent-event
3
+ title: "ServerSentEvent"
4
+ sidebar_label: "ServerSentEvent"
5
+ ---
6
+
7
+ `ServerSentEvent[A]` is an immutable envelope for one Server-Sent Event: a payload of type `A` plus the three optional SSE metadata fields. `SseDataEncoder[A]` turns the payload into the `data:` lines of the wire format. The type is covariant in `A`, and the metadata fields are `Maybe` rather than `Option` to stay allocation-free when absent:
8
+
9
+ ```scala
10
+ final class ServerSentEvent[+A] private (
11
+ val data: A,
12
+ val eventType: Maybe[String],
13
+ val eventId: Maybe[String],
14
+ val retryMillis: Maybe[Long]
15
+ )
16
+
17
+ trait SseDataEncoder[-A] {
18
+ def lines(value: A): Chunk[String]
19
+ }
20
+ ```
21
+
22
+ ## Motivation
23
+
24
+ The SSE wire format is small enough to hand-write and fiddly enough to get wrong. Fields are `name: value` lines, the event ends with a blank line, and a multi-line payload has to become several `data:` lines rather than one line containing newlines. Emit a payload with an embedded `\n` as a single `data:` line and the stream desynchronizes — the client reads the remainder as a new field, or as the end of the event.
25
+
26
+ `ServerSentEvent` owns that formatting. The envelope holds the metadata, an `SseDataEncoder` decides how the payload becomes lines, and `ServerSentEvent#render` assembles them in the order the specification requires. Splitting a multi-line string is the encoder's job, so a payload containing newlines is correct by construction rather than by remembering.
27
+
28
+ Separating the encoder from the envelope is what lets the payload be typed. `ServerSentEvent[String]` and `ServerSentEvent[Chunk[String]]` work out of the box, and any other type works as soon as it has an encoder — the envelope never needs to change.
29
+
30
+ ## Quick Showcase
31
+
32
+ An event is a payload plus optional metadata, and rendering produces the wire format:
33
+
34
+ ```scala
35
+ import zio.http.ServerSentEvent
36
+
37
+ val event = ServerSentEvent("hello", "greeting").id("42").retry(3000)
38
+ ```
39
+
40
+ Rendering emits the metadata fields, then the data lines, then the blank line that terminates the event:
41
+
42
+ ```scala
43
+ print(event.render)
44
+ // event: greeting
45
+ // id: 42
46
+ // retry: 3000
47
+ // data: hello
48
+ //
49
+ ```
50
+
51
+ ## Construction
52
+
53
+ Three entry points cover the cases: payload only, payload with an event name, and payload with any combination of metadata.
54
+
55
+ ### `ServerSentEvent.apply` — payload, optionally named
56
+
57
+ The one-argument form creates an event with no metadata at all, and the two-argument form sets the `event:` field:
58
+
59
+ ```scala
60
+ object ServerSentEvent {
61
+ def apply[A](data: A): ServerSentEvent[A]
62
+ def apply[A](data: A, event: String): ServerSentEvent[A]
63
+ }
64
+ ```
65
+
66
+ A bare event renders as a single `data:` line followed by the blank line:
67
+
68
+ ```scala
69
+ import zio.http.ServerSentEvent
70
+
71
+ val bare = ServerSentEvent("tick")
72
+ val named = ServerSentEvent("tick", "heartbeat")
73
+ ```
74
+
75
+ Naming the event adds one line above the data:
76
+
77
+ ```scala
78
+ print(bare.render)
79
+ // data: tick
80
+ //
81
+ print(named.render)
82
+ // event: heartbeat
83
+ // data: tick
84
+ //
85
+ ```
86
+
87
+ ### `ServerSentEvent.fromOptions` — all metadata at once
88
+
89
+ `ServerSentEvent.fromOptions` takes the three metadata fields as `Option`, each defaulting to `None`, which suits code that already has them in optional form:
90
+
91
+ ```scala
92
+ object ServerSentEvent {
93
+ def fromOptions[A](
94
+ data: A,
95
+ event: Option[String] = None,
96
+ id: Option[String] = None,
97
+ retry: Option[Long] = None
98
+ ): ServerSentEvent[A]
99
+ }
100
+ ```
101
+
102
+ Supplying a subset by name leaves the rest absent:
103
+
104
+ ```scala
105
+ import zio.http.ServerSentEvent
106
+
107
+ val event = ServerSentEvent.fromOptions("payload", id = Some("7"), retry = Some(1000))
108
+ ```
109
+
110
+ Only the fields you set appear on the wire:
111
+
112
+ ```scala
113
+ print(event.render)
114
+ // id: 7
115
+ // retry: 1000
116
+ // data: payload
117
+ //
118
+ ```
119
+
120
+ This is the only constructor that takes `Option`. The accessors return `Maybe`, so a round trip through `ServerSentEvent.fromOptions` converts between the two.
121
+
122
+ ## Setting Metadata
123
+
124
+ Four methods produce a modified copy. Each returns a new envelope; nothing mutates.
125
+
126
+ | Method | Effect |
127
+ | ----------------------------- | ----------------------------------------- |
128
+ | `ServerSentEvent#event` | Sets the `event:` field. |
129
+ | `ServerSentEvent#clearEvent` | Removes the `event:` field. |
130
+ | `ServerSentEvent#id` | Sets the `id:` field. |
131
+ | `ServerSentEvent#retry` | Sets the `retry:` field, in milliseconds. |
132
+ | `ServerSentEvent#clearRetry` | Removes the `retry:` field. |
133
+
134
+ They chain, since each returns a `ServerSentEvent[A]`:
135
+
136
+ ```scala
137
+ import zio.http.ServerSentEvent
138
+
139
+ val event = ServerSentEvent("payload").event("update").id("100").retry(5000)
140
+ ```
141
+
142
+ Clearing a field drops its line without disturbing the others:
143
+
144
+ ```scala
145
+ print(event.clearRetry.render)
146
+ // event: update
147
+ // id: 100
148
+ // data: payload
149
+ //
150
+ print(event.clearEvent.clearRetry.render)
151
+ // id: 100
152
+ // data: payload
153
+ //
154
+ ```
155
+
156
+ :::note[There is no `clearId`]
157
+ `ServerSentEvent#clearEvent` and `ServerSentEvent#clearRetry` exist, but the `id:` field has no clearing method. To produce an event without an id, build a fresh envelope or use `ServerSentEvent.fromOptions` with `id = None`.
158
+ :::
159
+
160
+ The accessors expose what is set, as `Maybe`:
161
+
162
+ ```scala
163
+ event.data
164
+ // res9: String = "payload"
165
+ event.eventType
166
+ // res10: Maybe[String] = "update"
167
+ event.eventId
168
+ // res11: Maybe[String] = "100"
169
+ event.retryMillis
170
+ // res12: Maybe[Long] = 5000L
171
+ ```
172
+
173
+ ## Validation
174
+
175
+ Two invariants are enforced at construction, and both throw rather than returning an error, because an invalid event cannot be rendered safely.
176
+
177
+ The `event:` and `id:` fields must not contain a carriage return or line feed. The reason is the same as for header values: a newline inside a field would terminate that field early and let the remainder be read as a new field or a new event, desynchronizing the stream.
178
+
179
+ The `retry:` value must be non-negative, since it is a reconnection delay in milliseconds.
180
+
181
+ Both checks are visible from a bare import:
182
+
183
+ ```scala
184
+ import zio.http.ServerSentEvent
185
+ ```
186
+
187
+ Both rejections are `IllegalArgumentException` with a message naming the field:
188
+
189
+ ```scala
190
+ scala.util.Try(ServerSentEvent("data", "bad\nevent")).failed.map(_.getMessage)
191
+ // res14: Try[String] = Success(
192
+ // "SSE event must not contain CR or LF characters"
193
+ // )
194
+ scala.util.Try(ServerSentEvent("data").retry(-1)).failed.map(_.getMessage)
195
+ // res15: Try[String] = Success("SSE retry must be non-negative")
196
+ ```
197
+
198
+ Validation applies to the metadata only. The **payload** is never validated, because embedded newlines in the payload are legitimate — the encoder splits them into separate `data:` lines.
199
+
200
+ ## Rendering
201
+
202
+ `ServerSentEvent#render` needs an `SseDataEncoder` for the payload type and produces the complete wire representation, terminating blank line included:
203
+
204
+ ```scala
205
+ final class ServerSentEvent[+A] {
206
+ def render(implicit encoder: SseDataEncoder[A]): String
207
+ }
208
+ ```
209
+
210
+ Fields are emitted in a fixed order — `event:`, then `id:`, then `retry:`, then the `data:` lines — with absent fields omitted entirely. A payload that produces no lines still emits one empty `data:` line, so an event is never rendered without a data field:
211
+
212
+ ```scala
213
+ import zio.http.ServerSentEvent
214
+ import zio.blocks.chunk.Chunk
215
+
216
+ val empty = ServerSentEvent(Chunk.empty[String])
217
+ ```
218
+
219
+ The result is a single valueless data line and the terminator:
220
+
221
+ ```scala
222
+ empty.render
223
+ // res17: String = """data:
224
+ //
225
+ // """
226
+ ```
227
+
228
+ ## SseDataEncoder
229
+
230
+ `SseDataEncoder[A]` maps a payload to its `data:` lines. It is contravariant in `A`, so an encoder for a supertype serves every subtype:
231
+
232
+ ```scala
233
+ trait SseDataEncoder[-A] {
234
+ def lines(value: A): Chunk[String]
235
+ }
236
+
237
+ object SseDataEncoder {
238
+ def apply[A](implicit encoder: SseDataEncoder[A]): SseDataEncoder[A]
239
+ implicit val string: SseDataEncoder[String]
240
+ implicit val stringChunk: SseDataEncoder[Chunk[String]]
241
+ }
242
+ ```
243
+
244
+ ### Built-in Instances
245
+
246
+ `SseDataEncoder.string` splits a `String` on line breaks, so a multi-line payload becomes one `data:` line per line. `\n`, `\r`, and `\r\n` all count as a single break:
247
+
248
+ ```scala
249
+ import zio.http.ServerSentEvent
250
+
251
+ val multiline = ServerSentEvent("first line\nsecond line\nthird line")
252
+ ```
253
+
254
+ Three lines in the payload become three `data:` lines, which is what the SSE specification requires:
255
+
256
+ ```scala
257
+ print(multiline.render)
258
+ // data: first line
259
+ // data: second line
260
+ // data: third line
261
+ //
262
+ ```
263
+
264
+ `SseDataEncoder.stringChunk` treats each element as a line and splits each element as well, so a chunk whose elements themselves contain newlines still flattens correctly:
265
+
266
+ ```scala
267
+ import zio.http.ServerSentEvent
268
+ import zio.blocks.chunk.Chunk
269
+
270
+ val chunked = ServerSentEvent(Chunk("alpha", "beta\ngamma"))
271
+ ```
272
+
273
+ The two elements yield three lines:
274
+
275
+ ```scala
276
+ print(chunked.render)
277
+ // data: alpha
278
+ // data: beta
279
+ // data: gamma
280
+ //
281
+ ```
282
+
283
+ An empty `Chunk` renders as one empty `data:` line rather than none, matching the single-`String` behaviour for an empty payload.
284
+
285
+ ### Custom Instances
286
+
287
+ An encoder for your own type is one method. Returning several lines is how a structured payload spans multiple `data:` fields:
288
+
289
+ ```scala
290
+ import zio.http.{ServerSentEvent, SseDataEncoder}
291
+ import zio.blocks.chunk.Chunk
292
+
293
+ final case class Progress(step: Int, total: Int, note: String)
294
+
295
+ implicit val progressEncoder: SseDataEncoder[Progress] =
296
+ new SseDataEncoder[Progress] {
297
+ def lines(value: Progress): Chunk[String] =
298
+ Chunk(s"step=${value.step}/${value.total}", s"note=${value.note}")
299
+ }
300
+ ```
301
+
302
+ With the instance in implicit scope, the payload type flows through the envelope unchanged:
303
+
304
+ ```scala
305
+ print(ServerSentEvent(Progress(3, 10, "compiling"), "progress").render)
306
+ // event: progress
307
+ // data: step=3/10
308
+ // data: note=compiling
309
+ //
310
+ ```
311
+
312
+ :::warning[Split lines yourself, or don't produce them]
313
+ `ServerSentEvent#render` prefixes each line the encoder returns with `data: ` and does not inspect it. An encoder that returns a string containing `\n` therefore emits a malformed event. Either split within `SseDataEncoder#lines`, or delegate to `SseDataEncoder.string` for the parts that might contain breaks.
314
+ :::
315
+
316
+ For a JSON payload, encode to a `String` with the JSON codec and reuse `SseDataEncoder.string`, which handles any newlines the rendered document contains.
317
+
318
+ ## Equality and Rendering as Text
319
+
320
+ `ServerSentEvent#equals` compares the payload and all three metadata fields, so two envelopes are equal when they would render identically. `ServerSentEvent#hashCode` agrees with it.
321
+
322
+ `ServerSentEvent#toString` is a diagnostic rendering, not the wire format — absent fields appear as `null` and no `data:` prefixes are added:
323
+
324
+ ```scala
325
+ import zio.http.ServerSentEvent
326
+
327
+ val event = ServerSentEvent("payload").id("9")
328
+ ```
329
+
330
+ Use `ServerSentEvent#render` for anything that goes over the wire and `ServerSentEvent#toString` only for logs:
331
+
332
+ ```scala
333
+ event.toString
334
+ // res25: String = "ServerSentEvent(data=payload, event=null, id=9, retry=null)"
335
+ ```
336
+
337
+ ## Integration Points
338
+
339
+ An SSE response is an ordinary `Response` whose body streams rendered events with a `text/event-stream` content type, so `ServerSentEvent` composes with the rest of [the HTTP model](./model.md) rather than replacing any of it. Setting that content type is a `Header.ContentType` — see [Header](./headers.md).
340
+
341
+ The type depends on `Chunk` for the line sequence and on `Maybe` for its optional fields, both from the core blocks: see [Chunk](../chunk.md) and [Maybe](../maybe.md).
@@ -0,0 +1,195 @@
1
+ # JWT
2
+
3
+ `zio-blocks-jwt` is a zero-dependency, cross-platform (JVM + Scala.js) JWT library for Scala 2.13 and Scala 3.
4
+
5
+ ## Getting Started
6
+
7
+ Add the dependency to your `build.sbt`:
8
+
9
+ ```scala
10
+ libraryDependencies += "dev.zio" %% "zio-blocks-jwt" % "<version>"
11
+ ```
12
+
13
+ Install a platform backend **once** at application startup:
14
+
15
+ ```scala
16
+ import zio.blocks.jwt._
17
+ JvmJwtCryptoBackend.install()
18
+ ```
19
+
20
+ For Scala.js (Node.js) use `JsJwtCryptoBackend.install()` instead (available on the JS platform with `jwtJS`).
21
+
22
+ ## Signing
23
+
24
+ ```scala
25
+ import zio.blocks.jwt._
26
+ val key = "0123456789ABCDEF0123456789ABCDEF".getBytes("UTF-8") // 32 bytes for HS256 (JWA minimum)
27
+ val claims = JwtClaims(sub = Some("user-123"), iss = Some("my-app"))
28
+ val token: Either[JwtError, String] = Jwt.sign(claims, key, Algorithm.HS256)
29
+ ```
30
+
31
+ ## Decoding and Verifying
32
+
33
+ ```scala
34
+ import zio.blocks.jwt._
35
+ val key = "0123456789ABCDEF0123456789ABCDEF".getBytes("UTF-8")
36
+ val claims = JwtClaims(sub = Some("user-123"), iss = Some("my-app"))
37
+ val token = Jwt.sign(claims, key, Algorithm.HS256).getOrElse("")
38
+ val result: Either[JwtError, JwtClaims] =
39
+ Jwt.decode(token, key, Algorithm.HS256, clockSkewSeconds = 30L, issuer = Some("my-app"))
40
+ ```
41
+
42
+ ## Claims
43
+
44
+ `JwtClaims` models the RFC 7519 registered claims plus arbitrary extra claims:
45
+
46
+ | Field | Type | RFC claim |
47
+ |---------|---------------------------------------|-----------|
48
+ | `iss` | `Option[String]` | Issuer |
49
+ | `sub` | `Option[String]` | Subject |
50
+ | `aud` | `Option[JwtAudience]` | Audience |
51
+ | `exp` | `Option[Long]` | Expiration (Unix seconds) |
52
+ | `nbf` | `Option[Long]` | Not Before (Unix seconds) |
53
+ | `iat` | `Option[Long]` | Issued At (Unix seconds) |
54
+ | `jti` | `Option[String]` | JWT ID |
55
+ | `extra` | `Map[String, JwtValue]` | Custom claims, preserving JSON type |
56
+
57
+ `exp`/`nbf`/`iat` are `NumericDate` per RFC 7519 §2: JSON numbers (integer, fractional, exponent) truncated toward zero to seconds, rejected if negative, non-finite, or out of `Long` range.
58
+
59
+ ### Extra claims
60
+
61
+ `JwtValue` is the public JSON ADT for `extra` (and for `JwtClaims` round-trip):
62
+
63
+ ```scala
64
+ import zio.blocks.jwt._
65
+ import zio.blocks.chunk.Chunk
66
+ val claims = JwtClaims(
67
+ sub = Some("user-123"),
68
+ extra = Map(
69
+ "role" -> JwtValue.Str("admin"),
70
+ "level" -> JwtValue.Num("3"),
71
+ "active" -> JwtValue.Bool(true),
72
+ "meta" -> JwtValue.Null,
73
+ "tags" -> JwtValue.Arr(Chunk(JwtValue.Str("a"), JwtValue.Num("1"))),
74
+ "profile"-> JwtValue.Obj(Map("age" -> JwtValue.Num("30"), "nested" -> JwtValue.Obj(Map("k" -> JwtValue.Str("v")))))
75
+ )
76
+ )
77
+ ```
78
+
79
+ Objects and arrays nest arbitrarily and are preserved exactly; no values are dropped during parsing.
80
+
81
+ ### Audience
82
+
83
+ Per RFC 7519 §4.1.3, `aud` may be a single string or an array. `None` and `JwtValue.Null` are treated as absent. Arrays must contain only strings; mixed or non-string elements are rejected with `InvalidToken`:
84
+
85
+ ```scala
86
+ import zio.blocks.jwt._
87
+ import zio.blocks.chunk.Chunk
88
+ val c1 = JwtClaims(aud = Some(JwtAudience.Single("my-service")))
89
+ val c2 = JwtClaims(aud = Some(JwtAudience.Multiple(Chunk("service-a", "service-b"))))
90
+ val c3 = JwtClaims(aud = None) // absent
91
+ ```
92
+
93
+ When `expectedAudience` is set in `JwtDecodeOptions`, `aud` is validated (must be present and contain the expected value; missing yields `MissingClaim("aud")`, mismatch yields `InvalidToken`); otherwise `aud` is only parsed.
94
+
95
+ ### Limits and Options
96
+
97
+ Resource limits are conservative and checked before allocation:
98
+
99
+ ```scala
100
+ import zio.blocks.jwt._
101
+ val limits = JwtLimits(maxTokenChars = 8192, maxSegmentChars = 4096, maxJsonChars = 8192, maxDepth = 32, maxFields = 512, maxArrayElements = 512)
102
+ val decodeOpts = JwtDecodeOptions(
103
+ issuer = Some("my-app"),
104
+ expectedAudience = Some("my-service"),
105
+ clockSkewSeconds = 30L,
106
+ limits = limits,
107
+ nowSeconds = Some(1700000000L)
108
+ )
109
+ val signOpts = JwtSignOptions(limits = limits)
110
+ val key = "0123456789ABCDEF0123456789ABCDEF".getBytes("UTF-8")
111
+ val claims = JwtClaims(sub = Some("user-123"))
112
+ val token = Jwt.sign(claims, key, Algorithm.HS256, signOpts).getOrElse("")
113
+ Jwt.sign(claims, key, Algorithm.HS256, signOpts)
114
+ Jwt.decode(token, key, Algorithm.HS256, decodeOpts)
115
+ ```
116
+
117
+ `JwtHeader` also carries `kid` and `typ` (default `"JWT"`):
118
+
119
+ ```scala
120
+ import zio.blocks.jwt._
121
+ val hdr = JwtHeader(Algorithm.HS256, typ = "JWT", kid = Some("key-1"))
122
+ val key = "0123456789ABCDEF0123456789ABCDEF".getBytes("UTF-8")
123
+ val claims = JwtClaims(sub = Some("user-123"))
124
+ Jwt.sign(claims, key, Algorithm.HS256, hdr)
125
+ ```
126
+
127
+ ## Supported Algorithms and Key Strength
128
+
129
+ | Algorithm | JVM | JS (Node.js) | Pure Scala | Key minima / curve |
130
+ |-----------|-----|--------------|-----------|--------------------|
131
+ | HS256 | ✓ | ✓ | ✓ | 32 bytes |
132
+ | HS384 | ✓ | ✓ | ✓ | 48 bytes |
133
+ | HS512 | ✓ | ✓ | ✓ | 64 bytes |
134
+ | RS256 | ✓ | ✓ | — | RSA ≥2048 bits |
135
+ | RS384 | ✓ | ✓ | — | RSA ≥2048 bits |
136
+ | RS512 | ✓ | ✓ | — | RSA ≥2048 bits |
137
+ | PS256 | ✓ | — | — | RSA ≥2048 bits, PSS |
138
+ | PS384 | ✓ | — | — | RSA ≥2048 bits, PSS |
139
+ | PS512 | ✓ | — | — | RSA ≥2048 bits, PSS |
140
+ | ES256 | ✓ | ✓ | — | P-256 (`prime256v1`) |
141
+ | ES384 | ✓ | ✓ | — | P-384 (`secp384r1`) |
142
+ | ES512 | ✓ | ✓ | — | P-521 (`secp521r1`, 66-byte components) |
143
+ | EdDSA | ✓ | — | — | Ed25519 |
144
+
145
+ Keys shorter than the HWA minima are rejected with `InvalidKey` (not `UnsupportedAlgorithm` or `InvalidToken`). RSA <2048 bits and EC curve mismatches (e.g. `ES256` with `secp384r1`) are also rejected with `InvalidKey`.
146
+
147
+ Query what a backend supports at runtime:
148
+
149
+ ```scala
150
+ import zio.blocks.jwt._
151
+ JvmJwtCryptoBackend.supportedAlgorithms // Set[Algorithm]
152
+ // res7: Set[Algorithm] = Set(
153
+ // RS256,
154
+ // PS384,
155
+ // RS512,
156
+ // PS512,
157
+ // HS384,
158
+ // ES256,
159
+ // PS256,
160
+ // EdDSA,
161
+ // ES512,
162
+ // HS512,
163
+ // RS384,
164
+ // ES384,
165
+ // HS256
166
+ // )
167
+ ```
168
+
169
+ `UnsupportedAlgorithm` is returned when the backend does not support the algorithm: `PS*`/`EdDSA` on JS (Node), any asymmetric alg when `JsCryptoCapability` reports unavailable (browser/ESM without Node `crypto`), or an unknown `alg` header (`FOO`).
170
+
171
+ Browser/ESM without Node `crypto` (detected via `JsCryptoCapability.isAvailable`) returns `UnsupportedAlgorithm` for `RS*`/`ES*`; HMAC via `SharedJwtCryptoBackend` still works.
172
+
173
+ ## Error Handling
174
+
175
+ All errors are represented as `JwtError` subtypes (no stack traces). Limits and validation map to distinct cases:
176
+
177
+ | Error | Meaning |
178
+ |----------------------|----------------------------------------------|
179
+ | `InvalidToken` | Malformed structure, bad claim type, `aud` mixed, `iss`/`sub` not string, `exp` not number, etc. |
180
+ | `ExpiredToken` | `exp` is in the past (with `clockSkewSeconds` and `nowSeconds`) |
181
+ | `NotYetValid` | `nbf` is in the future |
182
+ | `InvalidSignature` | Signature did not verify (after `alg` check, before claims parsing) |
183
+ | `UnsupportedAlgorithm` | Backend does not support the algorithm or `alg` header is `FOO` |
184
+ | `MissingClaim` | Required claim absent (`alg`, or `iss` when `issuer` expected) |
185
+ | `AlgorithmMismatch` | `alg` header differs from requested algorithm (`header.alg != alg` on `sign` or `decode`) |
186
+ | `TokenTooLarge` | Compact token > `maxTokenChars` |
187
+ | `SegmentTooLarge` | A segment > `maxSegmentChars` |
188
+ | `JsonTooLarge` | Decoded JSON > `maxJsonChars` |
189
+ | `TooDeep` | JSON depth > `maxDepth` |
190
+ | `TooManyFields` | Object fields > `maxFields` |
191
+ | `TooManyElements` | Array elements > `maxArrayElements` |
192
+
193
+ ## Base64URL Encoding
194
+
195
+ `Base64Url` is a public object (exposed for testing, but considered internal API) that encodes/decodes with no padding (`=`), per RFC 7515 §2 and RFC 4648 §5. Tokens containing `=` are rejected with `InvalidToken`; length `mod 4 == 1` and non-Base64Url chars (`+`, `/`, `!`) are rejected; trailing bits for 2- or 3-char tails must be zero (e.g. `AB` / `ABC` are rejected).