@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,230 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: signals
|
|
3
|
+
title: "Signal"
|
|
4
|
+
sidebar_label: "Signals"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
`Signal[A]` is a named, typed handle on a piece of state that lives in the browser. `Signal#:=` pairs it with a value to produce a `SignalUpdate[A]`, serialized through the type's JSON codec, and `Signal#ref` produces the `$name` reference form used inside Datastar expressions. Names are validated — literals during compilation. The three types:
|
|
8
|
+
|
|
9
|
+
```scala
|
|
10
|
+
final class Signal[A] private (val name: String) {
|
|
11
|
+
def :=(value: A)(implicit schema: Schema[A]): SignalUpdate[A]
|
|
12
|
+
def ref: DatastarRef
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
final class SignalUpdate[A] private (val name: String, val serialized: String)
|
|
16
|
+
|
|
17
|
+
final class DatastarRef private (val signalName: String) {
|
|
18
|
+
val value: String // "$" + signalName
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Motivation
|
|
23
|
+
|
|
24
|
+
Datastar identifies state by name, as a string, on both sides of the wire. The browser reads `data-signals="{count: 0}"`, an expression refers to `$count`, and the server patches it with `signals {"count": 42}`. Three places, one name, and nothing checks that they agree.
|
|
25
|
+
|
|
26
|
+
They disagree in ways that produce no error. A signal referenced as `$cont` is simply undefined, so the expression evaluates to nothing and the element renders blank. A patch sent for `"counter"` when the page declared `"count"` creates a second signal nobody reads. Both look like the feature is broken rather than the name being wrong.
|
|
27
|
+
|
|
28
|
+
`Signal[A]` makes the name a value declared once, so all three uses are the same Scala identifier. Declaring it also validates it — and the type parameter decides how values serialize, so a `Signal[Int]` cannot be patched with a string.
|
|
29
|
+
|
|
30
|
+
## Quick Showcase
|
|
31
|
+
|
|
32
|
+
Declare a signal, then use the same value for the attribute, the expression, and the patch:
|
|
33
|
+
|
|
34
|
+
```scala
|
|
35
|
+
import zio.blocks.html._
|
|
36
|
+
import zio.http.datastar._
|
|
37
|
+
|
|
38
|
+
val count = Signal[Int]("count")
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The three forms it produces — an initial value, a reference, and a patch:
|
|
42
|
+
|
|
43
|
+
```scala
|
|
44
|
+
(count := 0).serialized
|
|
45
|
+
// res0: String = "0"
|
|
46
|
+
count.ref.value
|
|
47
|
+
// res1: String = "$count"
|
|
48
|
+
DatastarEvent.patchSignals(count := 42).renderSSE
|
|
49
|
+
// res2: String = """event: datastar-patch-signals
|
|
50
|
+
// data: signals {"count":42}
|
|
51
|
+
//
|
|
52
|
+
// """
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Construction
|
|
56
|
+
|
|
57
|
+
Two constructors. They differ in when a *literal* name is checked; for a computed name both check at runtime.
|
|
58
|
+
|
|
59
|
+
### `Signal.apply` — checked at compile time
|
|
60
|
+
|
|
61
|
+
`Signal[A]("name")` validates a literal name during compilation, so a malformed name is a build failure rather than a silently dead expression:
|
|
62
|
+
|
|
63
|
+
```scala
|
|
64
|
+
import zio.http.datastar._
|
|
65
|
+
|
|
66
|
+
val count = Signal[Int]("count")
|
|
67
|
+
val nested = Signal[String]("user.profile.name")
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Dotted names are how Datastar expresses nested signal namespaces, and each segment is validated separately:
|
|
71
|
+
|
|
72
|
+
```scala
|
|
73
|
+
count.name
|
|
74
|
+
// res4: String = "count"
|
|
75
|
+
nested.name
|
|
76
|
+
// res5: String = "user.profile.name"
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### `Signal.dynamic` — checked at runtime
|
|
80
|
+
|
|
81
|
+
`Signal.apply` also accepts a computed name — its macro falls back to a runtime check when the argument is not a literal — so `Signal.dynamic` is not strictly required. Prefer it anyway for computed names, because it states that intent at the call site. Either way the check throws `IllegalArgumentException`:
|
|
82
|
+
|
|
83
|
+
```scala
|
|
84
|
+
object Signal {
|
|
85
|
+
def dynamic[A](name: String): Signal[A]
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Use it for names derived from data — a row id, a tenant prefix — and validate before constructing if the source is untrusted:
|
|
90
|
+
|
|
91
|
+
```scala
|
|
92
|
+
import zio.http.datastar._
|
|
93
|
+
|
|
94
|
+
def rowSignal(id: Long): Signal[Boolean] = Signal.dynamic[Boolean](s"row$id.selected")
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
A well-formed computed name behaves exactly like a literal one:
|
|
98
|
+
|
|
99
|
+
```scala
|
|
100
|
+
rowSignal(17).name
|
|
101
|
+
// res7: String = "row17.selected"
|
|
102
|
+
rowSignal(17).ref.value
|
|
103
|
+
// res8: String = "$row17.selected"
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
A malformed one fails at the point of construction rather than in the browser:
|
|
107
|
+
|
|
108
|
+
```scala
|
|
109
|
+
scala.util.Try(Signal.dynamic[Int]("has spaces")).failed.map(_.getMessage)
|
|
110
|
+
// res9: Try[String] = Success(
|
|
111
|
+
// "Invalid Datastar signal name 'has spaces'. Signal names must be dot-separated JavaScript identifiers and must not contain '__'."
|
|
112
|
+
// )
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Name Rules
|
|
116
|
+
|
|
117
|
+
A valid name is one or more dot-separated segments, each a Java identifier, and must not contain `__`:
|
|
118
|
+
|
|
119
|
+
| Name | Valid | Why |
|
|
120
|
+
| ---- | ----- | --- |
|
|
121
|
+
| `count` | yes | single identifier |
|
|
122
|
+
| `user.profile.name` | yes | dot-separated identifiers |
|
|
123
|
+
| `row17.selected` | yes | digits are identifier parts, just not the first character |
|
|
124
|
+
| `row.17.selected` | **no** | the segment `17` starts with a digit |
|
|
125
|
+
| `has spaces` | no | space is not an identifier character |
|
|
126
|
+
| `count__case` | no | `__` is reserved for Datastar modifier syntax |
|
|
127
|
+
| `` (empty) | no | at least one segment is required |
|
|
128
|
+
| `a..b` | no | empty segment |
|
|
129
|
+
|
|
130
|
+
Two of these catch people out. The `__` restriction exists because Datastar uses a double underscore to separate an attribute name from its modifiers, so a signal containing one would be parsed as a modifier suffix. And every segment must *start* with an identifier-start character, which rules out a purely numeric segment — the natural `s"row.$id.selected"` for a per-row signal is rejected, and `s"row$id.selected"` is the form that works.
|
|
131
|
+
|
|
132
|
+
## Producing Values
|
|
133
|
+
|
|
134
|
+
A signal on its own is just a name. Two methods turn it into something renderable.
|
|
135
|
+
|
|
136
|
+
### `Signal#:=` — pair a value with the name
|
|
137
|
+
|
|
138
|
+
`Signal#:=` requires a `Schema[A]` and serializes through that schema's JSON codec, producing a `SignalUpdate[A]`:
|
|
139
|
+
|
|
140
|
+
```scala
|
|
141
|
+
final class Signal[A] {
|
|
142
|
+
def :=(value: A)(implicit schema: Schema[A]): SignalUpdate[A]
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
The update holds the name and the already-serialized JSON:
|
|
147
|
+
|
|
148
|
+
```scala
|
|
149
|
+
import zio.http.datastar._
|
|
150
|
+
|
|
151
|
+
final case class Point(x: Int, y: Int)
|
|
152
|
+
|
|
153
|
+
object Point {
|
|
154
|
+
implicit val schema: zio.blocks.schema.Schema[Point] = zio.blocks.schema.Schema.derived[Point]
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
val origin = Signal[Point]("origin")
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Because serialization goes through the schema, a case class signal works without extra ceremony:
|
|
161
|
+
|
|
162
|
+
```scala
|
|
163
|
+
(origin := Point(3, 4)).serialized
|
|
164
|
+
// res11: String = "{\"x\":3,\"y\":4}"
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
:::warning[`serialized` is inserted verbatim]
|
|
168
|
+
`SignalUpdate#serialized` is spliced directly into the rendered expression or SSE payload. It is produced by the schema codec, so it is valid JSON — but if you construct a payload by other means (`DatastarEvent.patchSignalsRaw`), you are responsible for the JSON being well formed.
|
|
169
|
+
:::
|
|
170
|
+
|
|
171
|
+
### `Signal#ref` — refer to the signal in an expression
|
|
172
|
+
|
|
173
|
+
`Signal#ref` returns a `DatastarRef`, whose `value` is the name prefixed with `$` — the form Datastar expressions use to read a signal:
|
|
174
|
+
|
|
175
|
+
```scala
|
|
176
|
+
import zio.blocks.html._
|
|
177
|
+
import zio.http.datastar._
|
|
178
|
+
|
|
179
|
+
val price = Signal[Double]("price")
|
|
180
|
+
val quantity = Signal[Int]("quantity")
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
A reference renders as `$name`, and both `Signal` and `DatastarRef` have `ToJs` instances so either can be interpolated into a `js"..."` expression:
|
|
184
|
+
|
|
185
|
+
```scala
|
|
186
|
+
price.ref.value
|
|
187
|
+
// res13: String = "$price"
|
|
188
|
+
js"$price * $quantity".value
|
|
189
|
+
// res14: String = "$price * $quantity"
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Interpolating the signal itself is equivalent to interpolating its ref, which is why most code never mentions `DatastarRef` explicitly.
|
|
193
|
+
|
|
194
|
+
## SignalUpdate
|
|
195
|
+
|
|
196
|
+
`SignalUpdate[A]` carries a name and its serialized value. Beyond being consumed by `DatastarEvent.patchSignals` and the `dataSignals` attribute, it renders as a JavaScript object literal.
|
|
197
|
+
|
|
198
|
+
### `SignalUpdate.objectExpression`
|
|
199
|
+
|
|
200
|
+
`SignalUpdate.objectExpression` renders one or more updates as a single object expression, which is what the `data-signals` attribute contains:
|
|
201
|
+
|
|
202
|
+
```scala
|
|
203
|
+
object SignalUpdate {
|
|
204
|
+
def objectExpression(update: SignalUpdate[_], updates: SignalUpdate[_]*): String
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Several updates become one object, with each key quoted:
|
|
209
|
+
|
|
210
|
+
```scala
|
|
211
|
+
import zio.http.datastar._
|
|
212
|
+
|
|
213
|
+
val price = Signal[Double]("price")
|
|
214
|
+
val quantity = Signal[Int]("quantity")
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Keys are escaped, so a dotted signal name stays a single key rather than becoming a nested path:
|
|
218
|
+
|
|
219
|
+
```scala
|
|
220
|
+
SignalUpdate.objectExpression(price := 9.99, quantity := 2)
|
|
221
|
+
// res16: String = "{\"price\": 9.99, \"quantity\": 2}"
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
An implicit `ToJs[SignalUpdate[A]]` renders a single update the same way, so an update can be interpolated into an expression directly.
|
|
225
|
+
|
|
226
|
+
## Integration Points
|
|
227
|
+
|
|
228
|
+
`Signal` sits at the centre of the module: [Attributes](./attributes.md) accept signals as values and as keys (`dataBind`, `dataComputed`, `dataIndicator`), [Event Handlers](./events.md) reference them inside expressions, and [Server-Sent Events](./sse.md) patch them by name.
|
|
229
|
+
|
|
230
|
+
Outside the module, `Signal#:=` depends on `Schema[A]` and its JSON codec from [Schema](../schema/index.md), and the `ToJs` instances come from [HTML](../html.md), which is what lets a signal be interpolated into `js"..."`.
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: sse
|
|
3
|
+
title: "Server-Sent Events"
|
|
4
|
+
sidebar_label: "Server-Sent Events"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
`DatastarEvent` builds the events a server sends to patch a live page: replace elements, patch signals, execute a script, or remove elements. Each constructor returns a builder for its event kind, and `DatastarEvent#renderSSE` produces the wire format. `DatastarEvent.executeScript` is the exception noted below: it reuses the element-patch builder, so it offers two options that do not apply to it. Supporting types: `PatchElementsBuilder`, `PatchSignalsBuilder`, `RemoveElementsBuilder`, `ElementPatchMode`, `EventType`. The constructors and the one method they all end in:
|
|
8
|
+
|
|
9
|
+
```scala
|
|
10
|
+
sealed trait DatastarEvent {
|
|
11
|
+
def renderSSE: String
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
object DatastarEvent {
|
|
15
|
+
def patchElements(elements: Dom): PatchElementsBuilder
|
|
16
|
+
def patchSignals(first: SignalUpdate[_], rest: SignalUpdate[_]*): PatchSignalsBuilder
|
|
17
|
+
def patchSignalsRaw(json: String): PatchSignalsBuilder
|
|
18
|
+
def executeScript(code: Js): PatchElementsBuilder
|
|
19
|
+
def removeElements(selector: CssSelector): RemoveElementsBuilder
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Motivation
|
|
24
|
+
|
|
25
|
+
The Datastar response protocol is SSE with structure inside the `data:` field. A patch looks like this on the wire:
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
event: datastar-patch-elements
|
|
29
|
+
data: selector #results
|
|
30
|
+
data: mode append
|
|
31
|
+
data: elements <li>Widget</li>
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Three things make that easy to get wrong by hand. The event name must match the protocol exactly. The `data:` lines are a small keyed format, not free text, and the keys differ per event kind. And the terminating blank line is required — omit it and the browser waits indefinitely for an event it already has.
|
|
36
|
+
|
|
37
|
+
`DatastarEvent` removes all three concerns. Each constructor knows its event name, each builder method maps to one protocol field, and `DatastarEvent#renderSSE` delegates the SSE envelope to `ServerSentEvent`, which supplies the terminator.
|
|
38
|
+
|
|
39
|
+
## Quick Showcase
|
|
40
|
+
|
|
41
|
+
Build an event and render it:
|
|
42
|
+
|
|
43
|
+
```scala
|
|
44
|
+
import zio.blocks.html._
|
|
45
|
+
import zio.http.datastar._
|
|
46
|
+
|
|
47
|
+
val count = Signal[Int]("count")
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
A signal patch is one line of data:
|
|
51
|
+
|
|
52
|
+
```scala
|
|
53
|
+
DatastarEvent.patchSignals(count := 42).renderSSE
|
|
54
|
+
// res0: String = """event: datastar-patch-signals
|
|
55
|
+
// data: signals {"count":42}
|
|
56
|
+
//
|
|
57
|
+
// """
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
An element patch carries its target and mode:
|
|
61
|
+
|
|
62
|
+
```scala
|
|
63
|
+
DatastarEvent
|
|
64
|
+
.patchElements(li("Widget"))
|
|
65
|
+
.selector(CssSelector.id("results"))
|
|
66
|
+
.mode(ElementPatchMode.Append)
|
|
67
|
+
.renderSSE
|
|
68
|
+
// res1: String = """event: datastar-patch-elements
|
|
69
|
+
// data: selector #results
|
|
70
|
+
// data: mode append
|
|
71
|
+
// data: elements <li>Widget</li>
|
|
72
|
+
//
|
|
73
|
+
// """
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Patching Elements
|
|
77
|
+
|
|
78
|
+
`DatastarEvent.patchElements` takes a `Dom` and returns a `PatchElementsBuilder`. Its options map one-to-one onto protocol fields:
|
|
79
|
+
|
|
80
|
+
| Method | Field | Default |
|
|
81
|
+
| ------ | ----- | ------- |
|
|
82
|
+
| `selector(CssSelector)` | `selector` | omitted — patch by element id |
|
|
83
|
+
| `mode(ElementPatchMode)` | `mode` | `Outer`, which is omitted |
|
|
84
|
+
| `viewTransition` | `useViewTransition true` | omitted |
|
|
85
|
+
| `namespace(String)` | `namespace` | omitted |
|
|
86
|
+
| `eventId(String)` | SSE `id:` | omitted |
|
|
87
|
+
| `retry(Long)` | SSE `retry:` | omitted |
|
|
88
|
+
|
|
89
|
+
Every option is omitted when unset, so the minimal event is just the elements:
|
|
90
|
+
|
|
91
|
+
```scala
|
|
92
|
+
import zio.blocks.html._
|
|
93
|
+
import zio.http.datastar._
|
|
94
|
+
|
|
95
|
+
val row = li("Widget")
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
With no selector, Datastar matches on the element's own id, and `mode` is absent because `Outer` is the default:
|
|
99
|
+
|
|
100
|
+
```scala
|
|
101
|
+
DatastarEvent.patchElements(row).renderSSE
|
|
102
|
+
// res3: String = """event: datastar-patch-elements
|
|
103
|
+
// data: elements <li>Widget</li>
|
|
104
|
+
//
|
|
105
|
+
// """
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Adding options adds exactly the corresponding lines:
|
|
109
|
+
|
|
110
|
+
```scala
|
|
111
|
+
DatastarEvent.patchElements(row).selector(CssSelector.id("list")).viewTransition.eventId("evt-1").renderSSE
|
|
112
|
+
// res4: String = """event: datastar-patch-elements
|
|
113
|
+
// id: evt-1
|
|
114
|
+
// data: selector #list
|
|
115
|
+
// data: useViewTransition true
|
|
116
|
+
// data: elements <li>Widget</li>
|
|
117
|
+
//
|
|
118
|
+
// """
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
:::note[`mode` is omitted when it is `Outer`]
|
|
122
|
+
`ElementPatchMode.Outer` is the protocol default, so setting it explicitly produces no `mode` line. That is intentional and keeps the payload minimal; do not read the absence of a `mode` line as "no mode".
|
|
123
|
+
:::
|
|
124
|
+
|
|
125
|
+
### ElementPatchMode
|
|
126
|
+
|
|
127
|
+
Eight modes decide where the content lands relative to the target. Each renders as its lowercase name:
|
|
128
|
+
|
|
129
|
+
| Mode | Effect |
|
|
130
|
+
| ---- | ------ |
|
|
131
|
+
| `Outer` | Replace the target element itself *(default)* |
|
|
132
|
+
| `Inner` | Replace the target's children |
|
|
133
|
+
| `Replace` | Replace using replacement semantics |
|
|
134
|
+
| `Prepend` | Insert before the target's existing children |
|
|
135
|
+
| `Append` | Insert after the target's existing children |
|
|
136
|
+
| `Before` | Insert immediately before the target |
|
|
137
|
+
| `After` | Insert immediately after the target |
|
|
138
|
+
| `Remove` | Remove the target |
|
|
139
|
+
|
|
140
|
+
`Append` is the one to reach for when adding to a list without re-rendering it:
|
|
141
|
+
|
|
142
|
+
```scala
|
|
143
|
+
ElementPatchMode.Append.render
|
|
144
|
+
// res5: String = "append"
|
|
145
|
+
ElementPatchMode.Inner.render
|
|
146
|
+
// res6: String = "inner"
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Patching Signals
|
|
150
|
+
|
|
151
|
+
`DatastarEvent.patchSignals` takes one or more `SignalUpdate`s and renders them as a single JSON object under the `signals` key:
|
|
152
|
+
|
|
153
|
+
```scala
|
|
154
|
+
import zio.blocks.html._
|
|
155
|
+
import zio.http.datastar._
|
|
156
|
+
|
|
157
|
+
val price = Signal[Double]("price")
|
|
158
|
+
val quantity = Signal[Int]("quantity")
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Multiple updates go in one event rather than one event each:
|
|
162
|
+
|
|
163
|
+
```scala
|
|
164
|
+
DatastarEvent.patchSignals(price := 19.99, quantity := 3).renderSSE
|
|
165
|
+
// res8: String = """event: datastar-patch-signals
|
|
166
|
+
// data: signals {"price":19.99,"quantity":3}
|
|
167
|
+
//
|
|
168
|
+
// """
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
`PatchSignalsBuilder#onlyIfMissing` adds `onlyIfMissing true`, which tells Datastar to set the signal only when it is not already present — the way to supply a default without clobbering client state:
|
|
172
|
+
|
|
173
|
+
```scala
|
|
174
|
+
DatastarEvent.patchSignals(price := 0.0).onlyIfMissing.renderSSE
|
|
175
|
+
// res9: String = """event: datastar-patch-signals
|
|
176
|
+
// data: onlyIfMissing true
|
|
177
|
+
// data: signals {"price":0.0}
|
|
178
|
+
//
|
|
179
|
+
// """
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### `DatastarEvent.patchSignalsRaw` — pre-serialized JSON
|
|
183
|
+
|
|
184
|
+
When the JSON already exists — from a cache, or a shape no `Schema` describes — `DatastarEvent.patchSignalsRaw` takes it verbatim:
|
|
185
|
+
|
|
186
|
+
```scala
|
|
187
|
+
DatastarEvent.patchSignalsRaw("""{"count":7}""").renderSSE
|
|
188
|
+
// res10: String = """event: datastar-patch-signals
|
|
189
|
+
// data: signals {"count":7}
|
|
190
|
+
//
|
|
191
|
+
// """
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
:::warning[`patchSignalsRaw` does not validate]
|
|
195
|
+
The string is inserted into the payload unchanged. Malformed JSON produces an event the browser silently discards, and there is no server-side error. Prefer `DatastarEvent.patchSignals` with typed updates, which cannot produce invalid JSON.
|
|
196
|
+
:::
|
|
197
|
+
|
|
198
|
+
## Executing Scripts
|
|
199
|
+
|
|
200
|
+
`DatastarEvent.executeScript` takes a `Js` and returns a `PatchElementsBuilder` — because on the wire it *is* an element patch, appending a `<script>` element that the browser executes:
|
|
201
|
+
|
|
202
|
+
```scala
|
|
203
|
+
import zio.blocks.html._
|
|
204
|
+
import zio.http.datastar._
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
The rendered event shows the mechanism rather than hiding it:
|
|
208
|
+
|
|
209
|
+
```scala
|
|
210
|
+
DatastarEvent.executeScript(js"console.log('done')").renderSSE
|
|
211
|
+
// res12: String = """event: datastar-patch-elements
|
|
212
|
+
// data: selector body
|
|
213
|
+
// data: mode append
|
|
214
|
+
// data: elements <script data-effect="el.remove()">console.log('done')</script>
|
|
215
|
+
//
|
|
216
|
+
// """
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
The script is appended to `body` and carries `data-effect="el.remove()"`, so it deletes itself after running and leaves no residue in the DOM.
|
|
220
|
+
|
|
221
|
+
Because the return type is `PatchElementsBuilder`, the element-patch options are all available — including `selector` and `mode`, which would override that targeting and rarely make sense here. Treat the builder as offering `PatchElementsBuilder#eventId` and `PatchElementsBuilder#retry`.
|
|
222
|
+
|
|
223
|
+
## Removing Elements
|
|
224
|
+
|
|
225
|
+
`DatastarEvent.removeElements` takes a `CssSelector` and returns a `RemoveElementsBuilder` with a deliberately narrow surface — `viewTransition`, `namespace`, `RemoveElementsBuilder#eventId`, and `RemoveElementsBuilder#retry`:
|
|
226
|
+
|
|
227
|
+
```scala
|
|
228
|
+
DatastarEvent.removeElements(CssSelector.id("banner")).renderSSE
|
|
229
|
+
// res13: String = """event: datastar-patch-elements
|
|
230
|
+
// data: selector #banner
|
|
231
|
+
// data: mode remove
|
|
232
|
+
// data: elements
|
|
233
|
+
//
|
|
234
|
+
// """
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
There is no `mode` or `selector` method, because the selector is the argument and the mode is fixed to `remove` with an empty element body. That is the builder pattern doing its job: an option that would be meaningless is not offered.
|
|
238
|
+
|
|
239
|
+
## EventType
|
|
240
|
+
|
|
241
|
+
`EventType` names the protocol event in the SSE `event:` field. Two values, and you rarely name them directly since each constructor selects the right one:
|
|
242
|
+
|
|
243
|
+
| Value | Renders |
|
|
244
|
+
| ----- | ------- |
|
|
245
|
+
| `EventType.PatchElements` | `datastar-patch-elements` |
|
|
246
|
+
| `EventType.PatchSignals` | `datastar-patch-signals` |
|
|
247
|
+
|
|
248
|
+
Both render the protocol string rather than the Scala name:
|
|
249
|
+
|
|
250
|
+
```scala
|
|
251
|
+
EventType.PatchElements.render
|
|
252
|
+
// res14: String = "datastar-patch-elements"
|
|
253
|
+
EventType.PatchSignals.render
|
|
254
|
+
// res15: String = "datastar-patch-signals"
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Both element patches and script execution use `PatchElements`, and removal does too — the distinction between them is in the `data:` body, not the event name.
|
|
258
|
+
|
|
259
|
+
## Streaming Several Events
|
|
260
|
+
|
|
261
|
+
`DatastarEvent#renderSSE` produces one complete event including its terminating blank line, so a stream is a concatenation. Nothing in the module manages the stream itself; you write the strings to whatever response body your HTTP layer uses:
|
|
262
|
+
|
|
263
|
+
```scala
|
|
264
|
+
import zio.blocks.html._
|
|
265
|
+
import zio.http.datastar._
|
|
266
|
+
|
|
267
|
+
val progress = Signal[Int]("progress")
|
|
268
|
+
|
|
269
|
+
val stream: String =
|
|
270
|
+
(1 to 3).map(step => DatastarEvent.patchSignals(progress := step * 33).renderSSE).mkString
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Each event is self-delimiting, so concatenating what `DatastarEvent#renderSSE` returns is valid SSE:
|
|
274
|
+
|
|
275
|
+
```scala
|
|
276
|
+
stream
|
|
277
|
+
// res17: String = """event: datastar-patch-signals
|
|
278
|
+
// data: signals {"progress":33}
|
|
279
|
+
//
|
|
280
|
+
// event: datastar-patch-signals
|
|
281
|
+
// data: signals {"progress":66}
|
|
282
|
+
//
|
|
283
|
+
// event: datastar-patch-signals
|
|
284
|
+
// data: signals {"progress":99}
|
|
285
|
+
//
|
|
286
|
+
// """
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
Set `PatchElementsBuilder#eventId` when the client should be able to resume with `Last-Event-ID`, and `PatchElementsBuilder#retry` to control the reconnection delay — both are ordinary SSE fields handled by `ServerSentEvent`.
|
|
290
|
+
|
|
291
|
+
## Integration Points
|
|
292
|
+
|
|
293
|
+
`DatastarEvent#renderSSE` delegates to `ServerSentEvent` from `zio-http-model`, which supplies the SSE envelope, the field ordering, and the terminating blank line — see [ServerSentEvent](../http-model/server-sent-event.md). The Datastar-specific part is the event name and the keyed `data:` body.
|
|
294
|
+
|
|
295
|
+
Element patches carry `Dom` values and `CssSelector` targets from [HTML](../html.md), and signal patches carry [SignalUpdate](./signals.md) values whose JSON comes from the type's `Schema`. What the patched page does with the result is determined by the [Attributes](./attributes.md) and [Event Handlers](./events.md) rendered into it.
|