@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.
- package/adr/2026-07-18-data-migration.md +123 -0
- package/guides/async-getting-started.md +687 -0
- package/guides/compile-time-resource-safety-with-scope.md +21 -16
- package/guides/getting-started-with-mux.md +1395 -0
- package/guides/query-dsl-extending.md +161 -102
- package/guides/query-dsl-fluent-builder.md +217 -157
- package/guides/query-dsl-reified-optics.md +12 -10
- package/guides/query-dsl-sql.md +640 -165
- package/guides/sql-checked-interpolation.md +173 -0
- package/guides/sql-transactions.md +286 -0
- package/guides/telemetry-guide.md +1130 -0
- package/guides/zio-schema-migration.md +29 -22
- package/index.md +248 -389
- package/package.json +1 -1
- package/plans/config-follow-up-prs.md +188 -0
- package/plans/config-pr-assessment-roadmap.md +310 -0
- package/reference/MuxDataFlow.jsx +250 -0
- package/reference/async.md +1499 -0
- package/reference/chunk.md +3533 -308
- package/reference/codegen/case-class.md +436 -0
- package/reference/codegen/emitter-config.md +383 -0
- package/reference/codegen/examples.md +664 -0
- package/reference/codegen/field.md +316 -0
- package/reference/codegen/index.md +317 -0
- package/reference/codegen/scala-emitter.md +392 -0
- package/reference/codegen/scala-file.md +276 -0
- package/reference/codegen/sealed-trait.md +408 -0
- package/reference/codegen/type-definition.md +340 -0
- package/reference/codegen/type-ref.md +201 -0
- package/reference/combinators.md +347 -117
- package/reference/config/config-decoder.md +460 -0
- package/reference/config/config-source.md +489 -0
- package/reference/config/errors.md +278 -0
- package/reference/config/flags.md +369 -0
- package/reference/config/formats.md +314 -0
- package/reference/config/index.md +304 -0
- package/reference/config/rollout.md +336 -0
- package/reference/context.md +9 -52
- package/reference/data-migration.md +269 -0
- package/reference/datastar/attributes.md +302 -0
- package/reference/datastar/events.md +234 -0
- package/reference/datastar/index.md +256 -0
- package/reference/datastar/signals.md +230 -0
- package/reference/datastar/sse.md +295 -0
- package/reference/datastar.md +346 -0
- package/reference/docs.md +1461 -345
- package/reference/endpoint/auth-type.md +146 -0
- package/reference/endpoint/bulk-creation.md +96 -0
- package/reference/endpoint/endpoint.md +297 -0
- package/reference/endpoint/http-codec.md +249 -0
- package/reference/endpoint/index.md +745 -0
- package/reference/endpoint/path-codec.md +225 -0
- package/reference/endpoint/route-pattern.md +194 -0
- package/reference/endpoint/route-tree.md +111 -0
- package/reference/endpoint/segment-codec.md +199 -0
- package/reference/html.md +1424 -0
- package/reference/htmx/attribute-values.md +359 -0
- package/reference/htmx/hx-encoding.md +111 -0
- package/reference/htmx/hx-params.md +204 -0
- package/reference/htmx/hx-swap.md +276 -0
- package/reference/htmx/hx-sync.md +251 -0
- package/reference/htmx/hx-target.md +314 -0
- package/reference/htmx/hx-trigger.md +457 -0
- package/reference/htmx/hx-url-update.md +239 -0
- package/reference/htmx/index.md +807 -0
- package/reference/htmx/response-headers.md +240 -0
- package/reference/http-model/headers.md +735 -0
- package/reference/http-model/index.md +49 -0
- package/reference/http-model/model.md +1517 -0
- package/reference/http-model/schema-codecs.md +522 -0
- package/reference/http-model/schema.md +750 -0
- package/reference/http-model/server-sent-event.md +341 -0
- package/reference/jwt.md +195 -0
- package/reference/maybe.md +943 -0
- package/reference/media-type.md +2 -2
- package/reference/mux.md +254 -0
- package/reference/mux.mdx +828 -0
- package/reference/openapi.md +1351 -0
- package/reference/projection.md +654 -0
- package/reference/resource-management/defer-handle.md +1 -1
- package/reference/resource-management/resource.md +31 -98
- package/reference/resource-management/scope.md +28 -220
- package/reference/resource-management/wire.md +5 -55
- package/reference/ringbuffer/MpmcDiagram.jsx +717 -0
- package/reference/ringbuffer/MpscDiagram.jsx +618 -0
- package/reference/ringbuffer/SpmcDiagram.jsx +680 -0
- package/reference/ringbuffer/SpscDiagram.jsx +677 -0
- package/reference/ringbuffer/advanced.mdx +109 -0
- package/reference/ringbuffer/index.mdx +145 -0
- package/reference/ringbuffer/mpmc.mdx +185 -0
- package/reference/ringbuffer/mpsc.mdx +164 -0
- package/reference/ringbuffer/spmc.mdx +108 -0
- package/reference/ringbuffer/spsc.mdx +416 -0
- package/reference/{allows.md → schema/allows.md} +4 -100
- package/reference/{binding-resolver.md → schema/binding-resolver.md} +1 -1
- package/reference/{binding.md → schema/binding.md} +3 -4
- package/reference/schema/built-in-codecs/avro.md +451 -0
- package/reference/schema/built-in-codecs/bson.md +510 -0
- package/reference/schema/built-in-codecs/csv.md +564 -0
- package/reference/schema/built-in-codecs/index.md +77 -0
- package/reference/schema/built-in-codecs/json/index.md +295 -0
- package/reference/schema/built-in-codecs/json/json-config.md +217 -0
- package/reference/{json-patch.md → schema/built-in-codecs/json/json-patch.md} +5 -5
- package/reference/{json-schema.md → schema/built-in-codecs/json/json-schema.md} +14 -47
- package/reference/schema/built-in-codecs/json/json-selection.md +322 -0
- package/reference/{json.md → schema/built-in-codecs/json/json.md} +32 -64
- package/reference/schema/built-in-codecs/messagepack.md +508 -0
- package/reference/schema/built-in-codecs/thrift.md +433 -0
- package/reference/schema/built-in-codecs/toon.md +1078 -0
- package/reference/{xml.md → schema/built-in-codecs/xml.md} +13 -9
- package/reference/schema/built-in-codecs/yaml.md +552 -0
- package/reference/{codec.md → schema/codec.md} +11 -11
- package/reference/{dynamic-optic.md → schema/dynamic-optic.md} +196 -5
- package/reference/{dynamic-schema.md → schema/dynamic-schema.md} +8 -8
- package/reference/schema/format.md +92 -0
- package/reference/schema/index.md +52 -0
- package/reference/schema/migration.md +297 -0
- package/reference/{modifier.md → schema/modifier.md} +58 -7
- package/reference/{optics.md → schema/optics.md} +2 -2
- package/reference/{patch.md → schema/patch.md} +1 -1
- package/{path-interpolator.md → reference/schema/path-interpolator.md} +167 -72
- package/reference/schema/reflect-transformer.md +140 -0
- package/reference/{schema-evolution → schema/schema-evolution}/as.md +8 -8
- package/reference/{schema-evolution → schema/schema-evolution}/index.md +2 -2
- package/reference/{schema-evolution → schema/schema-evolution}/into.md +8 -8
- package/reference/{schema-expr.md → schema/schema-expr.md} +110 -175
- package/reference/schema/schema-search.md +263 -0
- package/reference/{schema.md → schema/schema.md} +22 -2
- package/reference/{structural-types.md → schema/structural-types.md} +1 -1
- package/reference/{type-class-derivation.md → schema/type-class-derivation.md} +63 -1
- package/reference/smithy.md +1032 -0
- package/reference/sql/db-codec-deriver.md +71 -0
- package/reference/sql/db-codec.md +687 -0
- package/reference/sql/db-con.md +271 -0
- package/reference/sql/db-connection.md +153 -0
- package/reference/sql/db-param-writer.md +77 -0
- package/reference/sql/db-param.md +66 -0
- package/reference/sql/db-result-reader.md +148 -0
- package/reference/sql/db-tx.md +114 -0
- package/reference/sql/db-value.md +41 -0
- package/reference/sql/ddl.md +85 -0
- package/reference/sql/frag.md +288 -0
- package/reference/sql/index.md +341 -0
- package/reference/sql/repo.md +600 -0
- package/reference/sql/sql-dialect.md +73 -0
- package/reference/sql/sql-logger.md +62 -0
- package/reference/sql/sql-name-mapper.md +70 -0
- package/reference/sql/table-metadata.md +134 -0
- package/reference/sql/table.md +448 -0
- package/reference/sql/transactor-zio.md +399 -0
- package/reference/sql/transactor.md +363 -0
- package/reference/sql-zio.md +112 -0
- package/reference/streams/core/index.md +32 -0
- package/reference/streams/core/pipeline.md +854 -0
- package/reference/streams/core/sink.md +1404 -0
- package/reference/streams/core/stream.md +3236 -0
- package/reference/streams/execution-and-compatibility/async-execution.md +822 -0
- package/reference/streams/execution-and-compatibility/index.md +35 -0
- package/reference/streams/execution-and-compatibility/platform-differences.md +297 -0
- package/reference/streams/execution-and-compatibility/scala-2-compatibility.md +88 -0
- package/reference/streams/execution-and-compatibility/zero-boxing.md +393 -0
- package/reference/streams/index.md +726 -0
- package/reference/streams/primitives/index.md +30 -0
- package/reference/streams/primitives/reader.md +1992 -0
- package/reference/streams/primitives/writer.md +1201 -0
- package/reference/telemetry/common/any-value.md +90 -0
- package/reference/telemetry/common/attribute-key.md +87 -0
- package/reference/telemetry/common/attributes.md +118 -0
- package/reference/telemetry/common/index.md +39 -0
- package/reference/telemetry/common/instrumentation-scope.md +24 -0
- package/reference/telemetry/common/resource.md +34 -0
- package/reference/telemetry/index.md +311 -0
- package/reference/telemetry/logging/index.md +197 -0
- package/reference/telemetry/logging/log-enrichment.md +72 -0
- package/reference/telemetry/logging/log-formatter.md +100 -0
- package/reference/telemetry/logging/log-record-processor.md +56 -0
- package/reference/telemetry/logging/log-record.md +44 -0
- package/reference/telemetry/logging/log-writer.md +64 -0
- package/reference/telemetry/logging/logger-provider.md +142 -0
- package/reference/telemetry/logging/logger.md +83 -0
- package/reference/telemetry/logging/severity.md +62 -0
- package/reference/telemetry/metrics/index.md +150 -0
- package/reference/telemetry/metrics/instruments.md +183 -0
- package/reference/telemetry/metrics/labeled-instruments.md +74 -0
- package/reference/telemetry/metrics/meter-provider.md +76 -0
- package/reference/telemetry/metrics/meter.md +98 -0
- package/reference/telemetry/metrics/metric-data.md +57 -0
- package/reference/telemetry/otel/custom-exporter.md +216 -0
- package/reference/telemetry/otel/index.md +212 -0
- package/reference/telemetry/tracing/index.md +155 -0
- package/reference/telemetry/tracing/sampler.md +89 -0
- package/reference/telemetry/tracing/span-builder.md +57 -0
- package/reference/telemetry/tracing/span-context.md +39 -0
- package/reference/telemetry/tracing/span-data.md +32 -0
- package/reference/telemetry/tracing/span-kind.md +55 -0
- package/reference/telemetry/tracing/span-processor.md +53 -0
- package/reference/telemetry/tracing/span-status.md +47 -0
- package/reference/telemetry/tracing/span.md +117 -0
- package/reference/telemetry/tracing/tracer-provider.md +91 -0
- package/reference/telemetry/tracing/tracer.md +52 -0
- package/reference/typeid.md +5 -83
- package/sidebars.js +376 -43
- package/undocumented-report.md +528 -270
- package/reference/formats.md +0 -694
- package/reference/http-model.md +0 -1716
- package/reference/streams.md +0 -989
- package/ringbuffer.md +0 -249
- /package/reference/{json-differ.md → schema/built-in-codecs/json/json-differ.md} +0 -0
- /package/reference/{dynamic-value.md → schema/dynamic-value.md} +0 -0
- /package/reference/{lazy.md → schema/lazy.md} +0 -0
- /package/reference/{reflect.md → schema/reflect.md} +0 -0
- /package/reference/{registers.md → schema/registers.md} +0 -0
- /package/reference/{schema-error.md → schema/schema-error.md} +0 -0
- /package/reference/{syntax.md → schema/syntax.md} +0 -0
- /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).
|
package/reference/jwt.md
ADDED
|
@@ -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).
|