@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,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.