@weftui/dom 0.28.0 → 0.30.0

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 (38) hide show
  1. package/README.md +25 -14
  2. package/dist/boundary-replay-BY4GyLot.js +1 -0
  3. package/dist/client/index.d.ts +5 -5
  4. package/dist/client/index.js +1 -1
  5. package/dist/index.d.ts +195 -1
  6. package/dist/index.js +1 -1
  7. package/dist/server/index.d.ts +6 -6
  8. package/dist/server/index.js +1 -1
  9. package/dist/shared-Dz0KM9ku.js +1 -0
  10. package/docs/explanation/boundaries-and-suspense.md +39 -20
  11. package/docs/explanation/combinator-api.md +14 -12
  12. package/docs/explanation/reactive-primitives.md +14 -14
  13. package/docs/explanation/rendering-model.md +20 -18
  14. package/docs/explanation/services-and-context.md +35 -21
  15. package/docs/how-to/add-routing.md +361 -60
  16. package/docs/how-to/author-components.md +42 -30
  17. package/docs/how-to/compose-behavior-and-markup.md +144 -0
  18. package/docs/how-to/handle-forms.md +6 -6
  19. package/docs/how-to/load-async-data.md +15 -13
  20. package/docs/how-to/load-data-with-rpc.md +34 -32
  21. package/docs/how-to/provide-services.md +20 -18
  22. package/docs/how-to/render-keyed-lists.md +14 -12
  23. package/docs/how-to/render-on-the-server.md +18 -14
  24. package/docs/how-to/show-navigation-progress.md +12 -10
  25. package/docs/how-to/split-routes-lazily.md +16 -14
  26. package/docs/how-to/style-reactively.md +13 -13
  27. package/docs/how-to/use-element-refs.md +10 -8
  28. package/docs/index.md +20 -18
  29. package/docs/reference/core.md +46 -42
  30. package/docs/reference/dom.md +274 -58
  31. package/docs/reference/router.md +69 -47
  32. package/docs/tutorial/01-your-first-app.md +7 -9
  33. package/docs/tutorial/02-reactivity.md +8 -6
  34. package/docs/tutorial/03-services-and-async.md +10 -6
  35. package/docs/tutorial/04-errors-and-server.md +14 -5
  36. package/package.json +3 -3
  37. package/dist/boundary-replay-BR26_puM.js +0 -1
  38. package/dist/data-uLmMpQMV.js +0 -1
@@ -2,18 +2,20 @@
2
2
  title: Boundaries and Suspense
3
3
  order: 4
4
4
  section: explanation
5
- description: How Weft models failure, async, and server data as boundary nodes in the same tree failure-catch variants, Boundary.suspend, and Boundary.rpc, and how their E/R channels behave.
5
+ description: How Weft models failure, async, and server data as boundary nodes in the same tree. Covers failure-catch variants, Boundary.suspend, and Boundary.rpc, and how their E/R channels behave.
6
6
  ---
7
7
 
8
8
  # Boundaries and Suspense
9
9
 
10
- A **boundary** is a node that intercepts something flowing through the tree an error, a pending async child, or a server-resolved value and decides what the DOM shows in its place. Because a boundary is itself a `Node<E, R>` ([nodes are Effects](https://weftui.dev/docs/explanation/rendering-model)), it composes exactly like any other element: you nest it, and its children's channels flow through it under a transformation the boundary defines.
10
+ A **boundary** is a node that intercepts something flowing through the tree: an error, a pending async child, or a server-resolved value. It decides what the DOM shows in its place.
11
+
12
+ A boundary is itself a `Node<E, R>` ([nodes are Effects](https://weftui.dev/docs/explanation/rendering-model)), so it composes exactly like any other element. You nest it, and its children's channels flow through it under a transformation the boundary defines.
11
13
 
12
14
  The `Boundary` namespace has three kinds. This page is the conceptual map; the [core reference](https://weftui.dev/docs/reference/core#boundary-namespace) has the full signatures.
13
15
 
14
16
  ## Failure boundaries
15
17
 
16
- A component's `E` channel accumulates up the tree. A **failure boundary** is where you _discharge_ some of that `E`: it wraps children and, if one of them fails, renders a fallback instead of letting the failure propagate to the mount.
18
+ A component's `E` channel accumulates up the tree. A **failure boundary** is where you _discharge_ some of that `E`. It wraps children and, if one of them fails, renders a fallback instead of letting the failure propagate to the mount.
17
19
 
18
20
  ```typescript
19
21
  import { Boundary, h } from "@weftui/core";
@@ -32,19 +34,25 @@ There are six failure-catch variants, mirroring Effect's own error operators so
32
34
  | `catchTag` / `catchTags` | one / several tagged errors by `_tag` |
33
35
  | `catchFilter` / `catchIf` | a selected subset, by `Filter` / predicate |
34
36
 
35
- The channel algebra is the whole reason they exist: `catchTag("Foo", …)` removes `Foo` from the children's `E` and adds whatever the fallback needs so the type of the boundary node reflects exactly which failures are still live and which were handled. An unhandled failure re-raises to the **nearest enclosing** boundary; if none catches it at **mount time**, mounting fails. Boundaries nest, so an inner `catchTag` can handle a specific case while an outer `catch` sweeps the rest.
37
+ The channel algebra is the whole reason they exist. `catchTag("Foo", …)` removes `Foo` from the children's `E` and adds whatever the fallback needs. The type of the boundary node therefore reflects exactly which failures are still live and which were handled.
38
+
39
+ An unhandled failure re-raises to the **nearest enclosing** boundary; if none catches it at **mount time**, mounting fails. Boundaries nest, so an inner `catchTag` can handle a specific case while an outer `catch` sweeps the rest.
36
40
 
37
41
  ### Post-mount failures with no enclosing boundary
38
42
 
39
- The routing above describes what happens while a node is being built. Once mounted, a reactive region an attribute, child, or list stream, or a hydrated equivalent keeps running for the lifetime of its scope, and it can still fail later: a `Stream` backing a `Boundary.rpc` resource might raise `RouterNotFound` after a client-side navigation, for instance. If a `BoundaryContext` encloses the region, the failure routes to it exactly as above, and the boundary's fallback swaps in.
43
+ The routing above describes what happens while a node is being built. Once mounted, a reactive region (an attribute, child, or list stream, or a hydrated equivalent) keeps running for the lifetime of its scope. It can still fail later: a `Stream` backing a `Boundary.rpc` resource might raise `RouterNotFound` after a client-side navigation. If a `BoundaryContext` encloses the region, the failure routes to it exactly as above, and the boundary's fallback swaps in.
44
+
45
+ If no boundary encloses it, there is nothing to swap to. Weft does not synthesize one. The region's DOM keeps its last rendered content, and a watcher fiber (forked into the same scope alongside the subscription itself) observes its exit directly.
40
46
 
41
- If no boundary encloses it, there is nothing to swap to. Weft does not synthesize one: the region's DOM keeps its last rendered content, and a watcher fiber — forked into the same scope alongside the subscription itself — observes its exit directly. When that exit is a failure whose cause is not interruption-only, Weft reports it explicitly via `Effect.logError(exit.cause)`, annotated with `weft.region` to identify the failing region by kind and identity (e.g. `attribute:class`, `child:stream-3`, `list:stream-2`, `hydrate:stream-1 (/products/42)`). This fires for typed failures and defects alike, in both dev and prod, exactly once per failing region, at the `"Error"` level. Interruption the ordinary case of unmount tearing down the region's scope is never reported; only genuine failures are.
47
+ When that exit is a failure whose cause is not interruption-only, Weft reports it explicitly via `Effect.logError(exit.cause)`. The log is annotated with `weft.region` to identify the failing region by kind and identity (e.g. `attribute:class`, `child:stream-3`, `list:stream-2`, `hydrate:stream-1 (/products/42)`). This fires for typed failures and defects alike, in both dev and prod, exactly once per failing region, at the `"Error"` level. Interruption (the ordinary case of unmount tearing down the region's scope) is never reported; only genuine failures are.
42
48
 
43
- This is deliberate: rather than leaving the failure to whatever the Effect runtime would otherwise do with an unobserved fiber exit, Weft observes and logs it itself, so visibility is controlled by the same knobs any Effect program uses `References.MinimumLogLevel` (provided via `Effect.provideService`) to filter it, or a custom `Logger` to route it elsewhere. A stream that can fail and has no enclosing boundary is a stream whose failures you've chosen not to route into the UI — the log is what tells you that decision has consequences at runtime.
49
+ This is deliberate. Rather than leave the failure to whatever the Effect runtime would do with an unobserved fiber exit, Weft observes and logs it itself. Visibility is therefore controlled by the same knobs any Effect program uses: `References.MinimumLogLevel` (provided via `Effect.provideService`) to filter it, or a custom `Logger` to route it elsewhere.
50
+
51
+ A stream that can fail and has no enclosing boundary is a stream whose failures you've chosen not to route into the UI. The log is what tells you that decision has consequences at runtime.
44
52
 
45
53
  ## Suspense boundaries
46
54
 
47
- `Boundary.suspend` wraps async children and shows a `fallback` until **all** of them have emitted their first value, then swaps atomically either everything is visible or nothing is. This prevents partial flicker when sibling async regions resolve at different times.
55
+ `Boundary.suspend` wraps async children and shows a `fallback` until **all** of them have emitted their first value. Then it swaps atomically: either everything is visible or nothing is. This prevents partial flicker when sibling async regions resolve at different times.
48
56
 
49
57
  ```typescript
50
58
  import { Boundary, h } from "@weftui/core";
@@ -55,37 +63,48 @@ Boundary.suspend({ fallback: h.div({ class: "spinner" }, "Loading…") }, [
55
63
  ]);
56
64
  ```
57
65
 
58
- A suspense boundary is transparent to the type channels: its node is `Node<ChildrenE, ChildrenR>` the children's `E`/`R` pass straight through, exactly as they would for a plain `h.*` parent. It changes _timing_ (when the children become visible), not _types_.
66
+ A suspense boundary is transparent to the type channels: its node is `Node<ChildrenE, ChildrenR>`. The children's `E`/`R` pass straight through, exactly as they would for a plain `h.*` parent. It changes _timing_ (when the children become visible), not _types_.
59
67
 
60
- On the server, `renderToStreamHydratable` emits the fallback inline and appends patch scripts as children resolve; on the client, `hydrate` sees through the boundary and adopts the already-resolved DOM directly.
68
+ On the server, `renderToStreamHydratable` emits the fallback inline and appends patch scripts as children resolve. On the client, `hydrate` sees through the boundary and adopts the already-resolved DOM directly.
61
69
 
62
- > **Note.** There is no `Suspense` export the API is `Boundary.suspend(props, children)`. Reach for it for async that loads **on the client**; for data that must resolve on the **server** and hydrate without a second request, use `Boundary.rpc` (below).
70
+ > **Note.** There is no `Suspense` export; the API is `Boundary.suspend(props, children)`. Reach for it for async that loads **on the client**. For data that must resolve on the **server** and hydrate without a second request, use `Boundary.rpc` (below).
63
71
 
64
72
  ## The rpc boundary
65
73
 
66
- `Boundary.rpc` is the server-data boundary: it resolves one `Rpc` on the server, serializes the result into the HTML, replays it on the client during `hydrate` (no second request, no flash), and then keeps the region live for `refetch`. Conceptually it is the same idea as the other boundaries — a node that decides what renders in a subtree — but the thing it intercepts is a **round-trip to a server handler**, and instead of a children array it takes a `render` function that receives a reactive [`Resource`](https://weftui.dev/docs/reference/core#resourcea).
74
+ `Boundary.rpc` is the server-data boundary. It:
75
+
76
+ - resolves one `Rpc` on the server
77
+ - serializes the result into the HTML
78
+ - replays it on the client during `hydrate` (no second request, no flash)
79
+ - keeps the region live for `refetch`
80
+
81
+ Conceptually it is the same idea as the other boundaries: a node that decides what renders in a subtree. But the thing it intercepts is a **round-trip to a server handler**. Instead of a children array, it takes a `render` function that receives a reactive [`Resource`](https://weftui.dev/docs/reference/core#resourcea).
67
82
 
68
83
  ```typescript
69
- import { Boundary, h } from "@weftui/core";
84
+ import { Boundary, h, Subscribable } from "@weftui/core";
70
85
  import { Stream } from "effect";
71
86
 
72
87
  Boundary.rpc(
73
88
  GetStock,
74
89
  () => ({ id: productId }),
75
- (resource) => h.span([Stream.map(resource.value.changes, (s) => String(s.units))]),
90
+ (resource) => h.span([Stream.map(Subscribable.changes(resource.value), (s) => String(s.units))]),
76
91
  { fallback: h.p("loading…") },
77
92
  );
78
93
  ```
79
94
 
80
- Unlike the failure and suspense boundaries, `Boundary.rpc` is not self-contained: it resolves through the ambient [`AppRpcClientTag`](https://weftui.dev/docs/reference/core#apprpcclienttag) seam that `@weftui/router` provides on both sides. Its channel behavior is also distinct — the rpc's typed `error` schema joins the node's `E` (replayable through an enclosing failure boundary), while `render`'s `R` passes through untouched. The full model — the contract/handler split, the four lifecycles, typed-failure replay — is a **how-to**, not repeated here: [Load Data with RPC](https://weftui.dev/docs/how-to/load-data-with-rpc).
95
+ Unlike the failure and suspense boundaries, `Boundary.rpc` is not self-contained. It resolves through the ambient [`AppRpcClientTag`](https://weftui.dev/docs/reference/core#apprpcclienttag) seam that `@weftui/router` provides on both sides.
96
+
97
+ Its channel behavior is also distinct. The rpc's typed `error` schema joins the node's `E` (replayable through an enclosing failure boundary), while `render`'s `R` passes through untouched. The full model (the contract/handler split, the four lifecycles, typed-failure replay) is a **how-to**, not repeated here: [Load Data with RPC](https://weftui.dev/docs/how-to/load-data-with-rpc).
81
98
 
82
99
  ## One tree, three interceptors
83
100
 
84
- The unifying idea: failure, async pending state, and server data are not three separate subsystems bolted onto the renderer. They are three **boundary nodes** in the one tree, each intercepting a different thing flowing through it, each with channel behavior you can read off its type. That is why they nest freely — a `Boundary.catchTag` can wrap a `Boundary.rpc` to catch its typed failure, and a `Boundary.suspend` can wrap async siblings that themselves contain rpc boundaries.
101
+ The unifying idea: failure, async pending state, and server data are not three separate subsystems bolted onto the renderer. They are three **boundary nodes** in the one tree, each intercepting a different thing flowing through it. Each has channel behavior you can read off its type.
102
+
103
+ That is why they nest freely. A `Boundary.catchTag` can wrap a `Boundary.rpc` to catch its typed failure. A `Boundary.suspend` can wrap async siblings that themselves contain rpc boundaries.
85
104
 
86
105
  ## See also
87
106
 
88
- - [The Rendering Model](https://weftui.dev/docs/explanation/rendering-model) why a boundary is just a node in a static tree
89
- - [`Boundary` API reference](https://weftui.dev/docs/reference/core#boundary-namespace) every variant's signature and channel algebra
90
- - [Load Data with RPC](https://weftui.dev/docs/how-to/load-data-with-rpc) the full `Boundary.rpc` walkthrough and its four lifecycles
91
- - [Render on the Server](https://weftui.dev/docs/how-to/render-on-the-server) how suspense and rpc boundaries stream and hydrate
107
+ - [The Rendering Model](https://weftui.dev/docs/explanation/rendering-model): why a boundary is just a node in a static tree
108
+ - [`Boundary` API reference](https://weftui.dev/docs/reference/core#boundary-namespace): every variant's signature and channel algebra
109
+ - [Load Data with RPC](https://weftui.dev/docs/how-to/load-data-with-rpc): the full `Boundary.rpc` walkthrough and its four lifecycles
110
+ - [Render on the Server](https://weftui.dev/docs/how-to/render-on-the-server): how suspense and rpc boundaries stream and hydrate
@@ -7,7 +7,9 @@ description: How h, h.fragment, and Component.gen / Component.make work; why Nod
7
7
 
8
8
  # The Combinator API
9
9
 
10
- Weft builds UI trees by calling builder functions. Because component return types stay as generic `Effect.Effect<ElementDescriptor, E, R>`, the error channel (`E`) and requirements channel (`R`) propagate through the entire tree visible to the type checker, satisfiable at the mount boundary. JSX collapses every component's return type to an opaque `JSX.Element`, erasing both channels; the combinator API exists specifically to keep them intact.
10
+ Weft builds UI trees by calling builder functions. Because component return types stay as generic `Effect.Effect<ElementDescriptor, E, R>`, the error channel (`E`) and requirements channel (`R`) propagate through the entire tree: visible to the type checker, satisfiable at the mount boundary.
11
+
12
+ JSX collapses every component's return type to an opaque `JSX.Element`, erasing both channels. The combinator API exists specifically to keep them intact.
11
13
 
12
14
  ## Nodes are Effects
13
15
 
@@ -23,13 +25,13 @@ Nodes are first-class Effects. Everything in the Effect ecosystem works on them
23
25
  import { h } from "@weftui/core";
24
26
  import { Effect } from "effect";
25
27
 
26
- // yield* in Effect.gen R propagates into the generator's context
28
+ // yield* in Effect.gen: R propagates into the generator's context
27
29
  const node = yield * h.div({ class: "container" }, "Hello");
28
30
 
29
- // pipe chain Effect operators directly
31
+ // pipe: chain Effect operators directly
30
32
  const provided = pipe(h.div(userStream), Effect.provide(UserServiceLive));
31
33
 
32
- // Effect.flatMap sequence node creation with async logic
34
+ // Effect.flatMap: sequence node creation with async logic
33
35
  const card = pipe(
34
36
  fetchCard(id),
35
37
  Effect.flatMap((data) => h.div({ class: "card" }, data.title)),
@@ -78,7 +80,7 @@ Reactive prop values (any `Stream`, `Effect`, or `Subscribable`) contribute thei
78
80
  ```typescript
79
81
  declare const colorStream: Stream.Stream<string, never, ThemeService>;
80
82
 
81
- // Node<never, ThemeService> R comes from the stream prop
83
+ // Node<never, ThemeService>: R comes from the stream prop
82
84
  const box = h.div({ style: { color: colorStream } }, "Hello");
83
85
  ```
84
86
 
@@ -130,7 +132,7 @@ declare const labelStream: Stream.Stream<string, never, I18nService>;
130
132
  const btn = Button({ label: labelStream });
131
133
  ```
132
134
 
133
- Components also accept an optional `children` argument, either as `readonly Renderable[]` or as a `(input) => readonly Renderable[]` function (render-prop pattern). `E`/`R` from children including the array returned by a function-children call accumulate on the resulting node.
135
+ Components also accept an optional `children` argument, either as `readonly Renderable[]` or as a `(input) => readonly Renderable[]` function (render-prop pattern). `E`/`R` from children, including the array returned by a function-children call, accumulate on the resulting node.
134
136
 
135
137
  Without `Component`, a plain function's return type is fixed at definition time and does not reflect the caller's reactive prop types.
136
138
 
@@ -149,16 +151,16 @@ Boundary.suspend({ fallback: h.div({ class: "spinner" }, "Loading...") }, [
149
151
  ]);
150
152
  ```
151
153
 
152
- The fallback is replaced atomically either all children are visible or none are. This prevents partial flicker when multiple async siblings resolve at different times. The boundary's node type is `Node<ChildrenE, ChildrenR>`: the children's `E`/`R` channels accumulate onto it, exactly as they would for a plain `h.*` parent.
154
+ The fallback is replaced atomically: either all children are visible or none are. This prevents partial flicker when multiple async siblings resolve at different times. The boundary's node type is `Node<ChildrenE, ChildrenR>`: the children's `E`/`R` channels accumulate onto it, exactly as they would for a plain `h.*` parent.
153
155
 
154
156
  On the server, `renderToStreamHydratable` emits the fallback inline and appends patch scripts as children resolve. On the client, `hydrate` sees through `Boundary.suspend` boundaries and adopts the already-resolved DOM directly.
155
157
 
156
- `Boundary.suspend` is one of the boundary combinators see the [core reference](https://weftui.dev/docs/reference/core#boundarysuspend) for the full `Boundary.*` surface, including the failure-catch variants and `Boundary.rpc`.
158
+ `Boundary.suspend` is one of the boundary combinators. See the [core reference](https://weftui.dev/docs/reference/core#boundarysuspend) for the full `Boundary.*` surface, including the failure-catch variants and `Boundary.rpc`.
157
159
 
158
160
  ## See also
159
161
 
160
- - [The Rendering Model](https://weftui.dev/docs/explanation/rendering-model) why a `Node` is an `Effect` and how the tree renders
161
- - [Reactive Primitives](https://weftui.dev/docs/explanation/reactive-primitives) the `Source` vocabulary that reactive props and children accept
162
- - [Boundaries and Suspense](https://weftui.dev/docs/explanation/boundaries-and-suspense) the boundary combinators as tree nodes
163
- - [Author Components](https://weftui.dev/docs/how-to/author-components) `Component.gen` / `Component.make` in practice
162
+ - [The Rendering Model](https://weftui.dev/docs/explanation/rendering-model): why a `Node` is an `Effect` and how the tree renders
163
+ - [Reactive Primitives](https://weftui.dev/docs/explanation/reactive-primitives): the `Source` vocabulary that reactive props and children accept
164
+ - [Boundaries and Suspense](https://weftui.dev/docs/explanation/boundaries-and-suspense): the boundary combinators as tree nodes
165
+ - [Author Components](https://weftui.dev/docs/how-to/author-components): `Component.gen` / `Component.make` in practice
164
166
  - [`@weftui/core` reference](https://weftui.dev/docs/reference/core)
@@ -7,14 +7,14 @@ description: The Source<A, E, R> vocabulary; Stream, Effect, and Subscribable as
7
7
 
8
8
  # Reactive Primitives
9
9
 
10
- The unified `Source` vocabulary is what lets static values, Effects, Streams, and Subscribables be used interchangeably wherever reactivity is supported — props, children, and style values all accept the same type.
10
+ The unified `Source` vocabulary lets static values, Effects, Streams, and Subscribables be used interchangeably. Props, children, and style values all accept the same type.
11
11
 
12
12
  Weft accepts a `Source` for prop values and children. Any of these is valid wherever reactivity is supported:
13
13
 
14
14
  - A plain static value (`string`, `number`, `boolean`, ...)
15
- - An `Effect.Effect<A, E, R>` runs once and resolves to a value
16
- - A `Stream.Stream<A, E, R>` each emission replaces the previous value
17
- - A `Subscribable<A, E, R>` like a hot stream; already has a "current value"
15
+ - An `Effect.Effect<A, E, R>`: runs once and resolves to a value
16
+ - A `Stream.Stream<A, E, R>`: each emission replaces the previous value
17
+ - A `Subscribable<A, E, R>`: like a hot stream; already has a "current value"
18
18
 
19
19
  The `Source<A, E, R>` type captures this union:
20
20
 
@@ -24,7 +24,7 @@ type Source<A, E, R> = A | Effect.Effect<A, E, R> | Stream.Stream<A, E, R> | Sub
24
24
 
25
25
  ## Static values
26
26
 
27
- Static props behave exactly as you'd expect — set once and never updated:
27
+ Static props are set once and never updated:
28
28
 
29
29
  ```typescript
30
30
  h.div({ class: "container", id: "root" }, "Hello");
@@ -45,17 +45,17 @@ The `E` and `R` channels of the Effect flow into the node's own channels.
45
45
 
46
46
  ## Stream props and children
47
47
 
48
- Streams are the primary reactive primitive. Each emission replaces the previous value in the DOM no diffing, direct DOM update:
48
+ Streams are the primary reactive primitive. Each emission replaces the previous value in the DOM (no diffing, direct DOM update):
49
49
 
50
50
  ```typescript
51
51
  import { SubscriptionRef, Stream } from "effect";
52
52
 
53
53
  const count = yield * SubscriptionRef.make(0);
54
54
 
55
- // SubscriptionRef.changes(count) is a Stream<number> each new value updates the text node
55
+ // SubscriptionRef.changes(count) is a Stream<number>: each new value updates the text node
56
56
  h.span([SubscriptionRef.changes(count)]);
57
57
 
58
- // Stream as a prop each emission sets the attribute
58
+ // Stream as a prop: each emission sets the attribute
59
59
  const isDisabled = Stream.map(SubscriptionRef.changes(count), (n) => n >= 10);
60
60
  h.button({ disabled: isDisabled }, "Submit");
61
61
  ```
@@ -119,7 +119,7 @@ h.div({ style: styleObjectStream });
119
119
 
120
120
  // Combine a whole-object stream with a static property.
121
121
  // A whole-object stream replaces every property on each emit, so fold the
122
- // static value into each emitted object with Stream.map you cannot spread the
122
+ // static value into each emitted object with Stream.map. You cannot spread the
123
123
  // Stream itself into a style object (that copies the Stream's internals, not
124
124
  // its emitted style keys).
125
125
  h.div({
@@ -154,11 +154,11 @@ pipe(
154
154
  );
155
155
  ```
156
156
 
157
- In practice you only encounter `NoPropValue` if you use a finite `Stream` as a prop and it ends before emitting e.g., `Stream.empty` or `Stream.take(0, stream)`. Most usage with `SubscriptionRef.changes` or infinite streams never raises it.
157
+ In practice you only encounter `NoPropValue` when a finite `Stream` prop ends before emitting (e.g., `Stream.empty` or `Stream.take(0, stream)`). Most usage with `SubscriptionRef.changes` or infinite streams never raises it.
158
158
 
159
159
  ## See also
160
160
 
161
- - [The Rendering Model](https://weftui.dev/docs/explanation/rendering-model) streams as the weft woven through a static tree
162
- - [The Combinator API](https://weftui.dev/docs/explanation/combinator-api) how reactive props and children contribute `E`/`R`
163
- - [Style Reactively](https://weftui.dev/docs/how-to/style-reactively) and [Render Keyed Lists](https://weftui.dev/docs/how-to/render-keyed-lists) reactive props and collections in practice
164
- - [`Source` reference](https://weftui.dev/docs/reference/core#source-namespace) the `Source` type and `Source.toSubscribable`
161
+ - [The Rendering Model](https://weftui.dev/docs/explanation/rendering-model): streams as the weft woven through a static tree
162
+ - [The Combinator API](https://weftui.dev/docs/explanation/combinator-api): how reactive props and children contribute `E`/`R`
163
+ - [Style Reactively](https://weftui.dev/docs/how-to/style-reactively) and [Render Keyed Lists](https://weftui.dev/docs/how-to/render-keyed-lists): reactive props and collections in practice
164
+ - [`Source` reference](https://weftui.dev/docs/reference/core#source-namespace): the `Source` type and `Source.toSubscribable`
@@ -2,12 +2,12 @@
2
2
  title: The Rendering Model
3
3
  order: 1
4
4
  section: explanation
5
- description: Why Weft has no virtual DOM nodes are Effects, streams are the live thread woven through a static tree, and hydration adopts server DOM in place.
5
+ description: Why Weft has no virtual DOM. Nodes are Effects, streams are the live thread woven through a static tree, and hydration adopts server DOM in place.
6
6
  ---
7
7
 
8
8
  # The Rendering Model
9
9
 
10
- Weft renders UI by **weaving streams through a static tree**. There is no virtual DOM, no diff, no reconciler comparing two trees each frame. This page explains the model that makes that work — and why it falls out of one definition.
10
+ Weft renders UI by **weaving streams through a static tree**. There is no virtual DOM, no diff, no reconciler comparing two trees each frame.
11
11
 
12
12
  ## Nodes are Effects
13
13
 
@@ -17,38 +17,40 @@ The whole library rests on a single equation:
17
17
  type Node<E = never, R = never> = Effect.Effect<ElementDescriptor, E, R>;
18
18
  ```
19
19
 
20
- Every element in a Weft tree **is an Effect**. `h.div(...)`, a component's return value, a boundary each is an `Effect` that, when run, produces an element descriptor. Two consequences follow immediately, and they shape everything else:
20
+ Every element in a Weft tree **is an Effect**. `h.div(...)`, a component's return value, a boundary: each is an `Effect` that, when run, produces an element descriptor. Two consequences follow:
21
21
 
22
- 1. **The error (`E`) and requirement (`R`) channels accumulate through the tree.** A child that reads a service, or a prop backed by a failible stream, contributes its `R` and `E` to its parent, which contributes to _its_ parent, up to the mount boundary. The type of your app node is the exact union of everything it needs and everything it can fail with visible to the type checker, satisfiable exactly once, at `mount`/`hydrate`. See [The Combinator API](https://weftui.dev/docs/explanation/combinator-api) for how the accumulation works mechanically.
23
- 2. **Every Effect combinator applies to a node directly.** `Effect.provide`, `Effect.flatMap`, `Effect.gen`, `Effect.catch` none of them are special-cased for UI. A node is an ordinary Effect, so the entire Effect ecosystem composes with your view for free.
22
+ 1. **The error (`E`) and requirement (`R`) channels accumulate through the tree.** A child that reads a service, or a prop backed by a failible stream, contributes its `R` and `E` to its parent. That parent contributes to _its_ parent, up to the mount boundary. The type of your app node is the exact union of everything it needs and everything it can fail with. It is visible to the type checker, satisfiable exactly once, at `mount`/`hydrate`. See [The Combinator API](https://weftui.dev/docs/explanation/combinator-api) for how the accumulation works mechanically.
23
+ 2. **Every Effect combinator applies to a node directly.** `Effect.provide`, `Effect.flatMap`, `Effect.gen`, `Effect.catch`: none of them are special-cased for UI. A node is an ordinary Effect, so the entire Effect ecosystem composes with your view for free.
24
24
 
25
- JSX collapses every component to an opaque `JSX.Element`, erasing both channels. Weft keeps them, and that is the point of the whole design. (There is [no JSX](https://weftui.dev/docs/explanation/combinator-api) here components are plain functions you _call_.)
25
+ JSX collapses every component to an opaque `JSX.Element`, erasing both channels. Weft keeps them, and that is the point of the whole design. (There is [no JSX](https://weftui.dev/docs/explanation/combinator-api) here; components are plain functions you _call_.)
26
26
 
27
27
  ## Warp and weft
28
28
 
29
29
  The name is the metaphor. On a loom, the **warp** is the set of fixed threads held under tension; the **weft** is the live thread drawn back and forth across them to form the cloth.
30
30
 
31
- - Your **component tree is the warp** the structure, fixed for the lifetime of a mounted region.
32
- - **Streams are the weft** the live values drawn across that structure. A `Stream`, `Effect`, or `Subscribable` used as a prop value or child is a thread woven through a specific point in the tree.
31
+ - Your **component tree is the warp**: the structure, fixed for the lifetime of a mounted region.
32
+ - **Streams are the weft**: the live values drawn across that structure. A `Stream`, `Effect`, or `Subscribable` used as a prop value or child is a thread woven through a specific point in the tree.
33
33
 
34
- When a stream emits, only the DOM at _that_ point updates. Nothing above it re-runs; no sibling is touched; there is no tree to diff because the structure never changed only a value threaded through one hole in it did. This is why Weft needs no virtual DOM: **the reactivity is local by construction.** The vocabulary of stream-shaped values (and how their channels flow) is [Reactive Primitives](https://weftui.dev/docs/explanation/reactive-primitives).
34
+ When a stream emits, only the DOM at _that_ point updates. Nothing above it re-runs; no sibling is touched. There is no tree to diff because the structure never changed; only a value threaded through one hole in it did. This is why Weft needs no virtual DOM: **the reactivity is local by construction.** The vocabulary of stream-shaped values (and how their channels flow) is [Reactive Primitives](https://weftui.dev/docs/explanation/reactive-primitives).
35
35
 
36
- > **Note.** "Only that point updates" is the default, not a manual optimization. You do not memoize regions or declare dependencies a value is reactive exactly where you thread a stream, and static everywhere else.
36
+ > **Note.** "Only that point updates" is the default, not a manual optimization. You do not memoize regions or declare dependencies. A value is reactive exactly where you thread a stream, and static everywhere else.
37
37
 
38
38
  ## Streams drive all updates
39
39
 
40
- There is no `setState`, no render-triggering scheduler, no "re-render this component." A region of the DOM is live **if and only if** a stream is woven into it. To make something update, you thread a stream through it; to keep something static, you pass a plain value. The renderer subscribes to each woven stream and patches its target in place on every emission — reusing the existing DOM node, patching text and attributes rather than recreating elements (identity, focus, and typed input survive an update).
40
+ There is no `setState`, no render-triggering scheduler, no "re-render this component." A region of the DOM is live **if and only if** a stream is woven into it. To make something update, you thread a stream through it; to keep something static, you pass a plain value.
41
41
 
42
- This also fixes the update _shape_. Because the structure is fixed, an update is always "new value into a known hole," never "reconcile these two trees." Even list rendering — where the number of children genuinely varies is expressed as a keyed region ([`List.each`](https://weftui.dev/docs/how-to/render-keyed-lists)) that reconciles by key rather than by structural diff.
42
+ The renderer subscribes to each woven stream and patches its target in place on every emission. It reuses the existing DOM node and patches text and attributes rather than recreating elements, so identity, focus, and typed input survive an update.
43
+
44
+ This also fixes the update _shape_. Because the structure is fixed, an update is always "new value into a known hole," never "reconcile these two trees." Even list rendering, where the number of children genuinely varies, is expressed as a keyed region ([`List.each`](https://weftui.dev/docs/how-to/render-keyed-lists)). It reconciles by key rather than by structural diff.
43
45
 
44
46
  ## One tree, two sides, hydrate in place
45
47
 
46
48
  The same component tree renders on the server and the client:
47
49
 
48
50
  - On the **server**, the tree renders to an HTML string (or a streaming response) via `@weftui/dom/server`. The _hydratable_ renderers additionally emit the inline data each reactive region needs to resume.
49
- - On the **client**, `WeftApp.hydrate` walks that server-rendered DOM and **adopts it in place** it wires up reactivity and event handlers on the existing nodes rather than re-rendering. The first client production matches the adopted DOM exactly, so nothing is mutated and there is no flash.
51
+ - On the **client**, `WeftApp.hydrate` walks that server-rendered DOM and **adopts it in place**: it wires up reactivity and event handlers on the existing nodes rather than re-rendering. The first client production matches the adopted DOM exactly, so nothing is mutated and there is no flash.
50
52
 
51
- Because the same `Node<E, R>` describes both passes, there is nothing to keep in sync: the server output and the client's first render are the _same tree_ run in two environments. Services flow from the app layer (or the router's render-time context) through the tree to wherever a component reads them, on both sides. The mechanics of the two-sided render live in [Render on the Server](https://weftui.dev/docs/how-to/render-on-the-server); the service flow is [Services and Context](https://weftui.dev/docs/explanation/services-and-context).
53
+ Because the same `Node<E, R>` describes both passes, there is nothing to keep in sync. The server output and the client's first render are the _same tree_ run in two environments. Services flow from the app layer (or the router's render-time context) through the tree to wherever a component reads them, on both sides. The mechanics of the two-sided render live in [Render on the Server](https://weftui.dev/docs/how-to/render-on-the-server); the service flow is [Services and Context](https://weftui.dev/docs/explanation/services-and-context).
52
54
 
53
55
  ## Why this matters
54
56
 
@@ -59,7 +61,7 @@ Because the same `Node<E, R>` describes both passes, there is nothing to keep in
59
61
 
60
62
  ## See also
61
63
 
62
- - [The Combinator API](https://weftui.dev/docs/explanation/combinator-api) how `E`/`R` accumulate; why `Node` is an `Effect`; `h` and components
63
- - [Reactive Primitives](https://weftui.dev/docs/explanation/reactive-primitives) the stream-shaped values you weave through the tree
64
- - [Boundaries and Suspense](https://weftui.dev/docs/explanation/boundaries-and-suspense) how failure and async are modeled as nodes in the same tree
65
- - [Render on the Server](https://weftui.dev/docs/how-to/render-on-the-server) the server/client split and `hydrate`
64
+ - [The Combinator API](https://weftui.dev/docs/explanation/combinator-api): how `E`/`R` accumulate; why `Node` is an `Effect`; `h` and components
65
+ - [Reactive Primitives](https://weftui.dev/docs/explanation/reactive-primitives): the stream-shaped values you weave through the tree
66
+ - [Boundaries and Suspense](https://weftui.dev/docs/explanation/boundaries-and-suspense): how failure and async are modeled as nodes in the same tree
67
+ - [Render on the Server](https://weftui.dev/docs/how-to/render-on-the-server): the server/client split and `hydrate`
@@ -2,16 +2,18 @@
2
2
  title: Services and Context
3
3
  order: 5
4
4
  section: explanation
5
- description: How Effect services reach components the requirement channel, discharging R at the mount, the router's render-time context seam, and ServerTag server-only brands.
5
+ description: "How Effect services reach components: the requirement channel, discharging R at the mount, the router's render-time context seam, and ServerTag server-only brands."
6
6
  ---
7
7
 
8
8
  # Services and Context
9
9
 
10
- Weft has no separate dependency-injection system. It uses Effect's a component that needs a service reads it with `yield* Service`, and because [a node is an Effect](https://weftui.dev/docs/explanation/rendering-model), that requirement rides the node's `R` channel up the tree to a single point where you provide it. This page explains how a service travels from where you provide it to where a component reads it, and the two seams that make that work across the server/client boundary.
10
+ Weft has no separate dependency-injection system. It uses Effect's. A component that needs a service reads it with `yield* Service`. Because [a node is an Effect](https://weftui.dev/docs/explanation/rendering-model), that requirement rides the node's `R` channel up the tree to a single point where you provide it.
11
+
12
+ This page explains how a service travels from provider to reader, and the two seams that make that work across the server/client boundary.
11
13
 
12
14
  ## R accumulates, then discharges once
13
15
 
14
- When a component does `yield* ThemeService`, `ThemeService` enters that node's requirement channel. It accumulates through every parent a boundary, a layout, the app node until the whole tree's `R` is the union of everything any component needs. You satisfy it in **one** place, at the edge:
16
+ When a component does `yield* ThemeService`, `ThemeService` enters that node's requirement channel. It accumulates through every parent: a boundary, a layout, the app node. The whole tree's `R` becomes the union of everything any component needs. You satisfy it in **one** place, at the edge:
15
17
 
16
18
  ```typescript
17
19
  import { Effect } from "effect";
@@ -21,29 +23,37 @@ const app = WeftApp.make(ThemeServiceLive);
21
23
  const handle = WeftApp.mount(app, App(), document.getElementById("root")!);
22
24
  ```
23
25
 
24
- Provide too little and it is a compile error at `WeftApp.make` the type of `App()` names exactly which service is missing. This is the same discipline as any Effect program: `R` is a promise the type checker holds you to, discharged at the program's boundary, not sprinkled through the tree.
26
+ Provide too little and it is a compile error at `WeftApp.make`: the type of `App()` names exactly which service is missing. This is the same discipline as any Effect program. `R` is a promise the type checker holds you to, discharged at the program's boundary, not sprinkled through the tree.
25
27
 
26
- Services flow **down** from the app's layer to every reader, including across reactive boundaries: a stream woven into a prop carries its own `R`, and a handler that reads a service resolves it from the same context. There is no prop-drilling and no context-provider component the requirement channel _is_ the wiring.
28
+ Services flow **down** from the app's layer to every reader, including across reactive boundaries. A stream woven into a prop carries its own `R`, and a handler that reads a service resolves it from the same context. There is no prop-drilling and no context-provider component: the requirement channel _is_ the wiring.
27
29
 
28
30
  ## Layer lifetime and the app runtime
29
31
 
30
- Under the old `mount`/`hydrate` model this was a real footgun. Each call created its own implicit `ManagedRuntime`, and that runtime's effect resolved right after the tree's **initial render** not when the app stopped running — while streams, event handlers, and forked work kept running on it long after. `Effect.provide(scopedLayer)` is `acquireUseRelease` sugar: acquire, run the wrapped effect, then release **when that effect completes**. Wrapped directly around `mount`, the release ran at mount-resolve — while the mounted tree was still reading from the now-disposed service:
32
+ Under the old `mount`/`hydrate` model this was a real footgun. Each call created its own implicit `ManagedRuntime`. That runtime's effect resolved right after the tree's **initial render**, not when the app stopped running. Streams, event handlers, and forked work kept running on it long after.
33
+
34
+ `Effect.provide(scopedLayer)` is `acquireUseRelease` sugar: acquire, run the wrapped effect, then release **when that effect completes**. Wrapped directly around `mount`, the release ran at mount-resolve, while the mounted tree was still reading from the now-disposed service:
31
35
 
32
36
  ```typescript
33
37
  // ❌ (old API) the layer's finalizers ran the instant runPromise settled, while the
34
- // mounted tree kept running every subscription then read a disposed service
38
+ // mounted tree kept running: every subscription then read a disposed service
35
39
  Effect.runPromise(mount(App(), root).pipe(Effect.provide(SomeScopedLayer)));
36
40
  ```
37
41
 
38
- This is exactly what happened with the atom registry layer (`AtomRegistry.layer`, from `effect/unstable/reactivity`) in the [`effect-atom` example](https://github.com/stefvw93/weft/tree/main/examples/effect-atom) (issue #122): every atom-driven region rendered empty, with no error, because the registry the streams read from had already been disposed.
42
+ This is exactly what happened with the atom registry layer (`AtomRegistry.layer`, from `effect/unstable/reactivity`) in the [`effect-atom` example](https://github.com/stefvw93/weft/tree/main/examples/effect-atom) (issue #122). Every atom-driven region rendered empty, with no error, because the registry the streams read from had already been disposed.
43
+
44
+ `WeftApp` closes this gap structurally instead of by convention. An app owns exactly **one** lazy `ManagedRuntime`. `WeftApp.make(layer)` builds the layer on the first `mount`/`hydrate`. It releases only at `WeftApp.dispose(app)`, never when any individual mount's render effect resolves.
45
+
46
+ A scoped layer (`AtomRegistry.layer`, `RouterLive`) therefore just works passed straight to `WeftApp.make`. There is no `mountScoped`, no `Effect.never`, and no manual `ManagedRuntime` composition to reach for.
39
47
 
40
- `WeftApp` closes this gap structurally instead of by convention. An app owns exactly **one** lazy `ManagedRuntime`: `WeftApp.make(layer)` builds the layer on the first `mount`/`hydrate`, and it releases only at `WeftApp.dispose(app)` — never when any individual mount's render effect resolves. A scoped layer (`AtomRegistry.layer`, `RouterLive`) therefore just works passed straight to `WeftApp.make`, with no `mountScoped`, no `Effect.never`, and no manual `ManagedRuntime` composition to reach for. See [Provide Services](https://weftui.dev/docs/how-to/provide-services) for the recipes including `memoMap` sharing across apps and the `Effect.acquireRelease(make, dispose)` pattern for binding an app's own lifetime to an external scope (there is deliberately no `makeScoped`).
48
+ See [Provide Services](https://weftui.dev/docs/how-to/provide-services) for the recipes. They cover `memoMap` sharing across apps and the `Effect.acquireRelease(make, dispose)` pattern for binding an app's own lifetime to an external scope. There is deliberately no `makeScoped`.
41
49
 
42
50
  ## The router's render-time context seam
43
51
 
44
- A plain `WeftApp.mount`/`WeftApp.hydrate` discharges `R` once, at `WeftApp.make`. But under `@weftui/router`, the tree does not render in the context of the effect that called `render` — each request dispatches through platform's HTTP layer in its own managed context, and the reactive outlet drains in the top render context, not in any intermediate node's. Providing a service _ambiently_ around the render would be lost before it reached a route component.
52
+ A plain `WeftApp.mount`/`WeftApp.hydrate` discharges `R` once, at `WeftApp.make`. But under `@weftui/router`, the tree does not render in the context of the effect that called `render`.
45
53
 
46
- So the router exposes an explicit **`context` seam** a `Layer` threaded to the document shell and every route, layout, and leaf:
54
+ Each request dispatches through platform's HTTP layer in its own managed context. The reactive outlet drains in the top render context, not in any intermediate node's. Providing a service _ambiently_ around the render would be lost before it reached a route component.
55
+
56
+ So the router exposes an explicit **`context` seam**, a `Layer` threaded to the document shell and every route, layout, and leaf:
47
57
 
48
58
  ```typescript
49
59
  class Greeting extends Context.Service<Greeting, { text: string }>()("Greeting") {}
@@ -51,17 +61,21 @@ class Greeting extends Context.Service<Greeting, { text: string }>()("Greeting")
51
61
  // server entry
52
62
  RouterServer.render(App, { document, url, context: Layer.succeed(Greeting, { text: "hi" }) });
53
63
 
54
- // client entry same seam, so the hydrated tree reads the same services
64
+ // client entry: same seam, so the hydrated tree reads the same services
55
65
  RouterLive(App, { context: DocsLive });
56
66
  ```
57
67
 
58
- The seam is **symmetric** (same shape on both sides) and **type-tracked**: the def's aggregate residual `R` is discharged here, so a missing provide is a compile error rather than a runtime 500. The residual is `AppServices<R>` the def's `R` minus what the router itself threads (`Router`, `Router.Outlet`, `AppRpcClientTag`). An app with no app-services needs no `context`; a loosely-typed `RouterDef<any, any>` may omit it. This is how the website provides its `Docs` service to every page — see [Add Routing](https://weftui.dev/docs/how-to/add-routing).
68
+ The seam is **symmetric** (same shape on both sides) and **type-tracked**. The def's aggregate residual `R` is discharged here, so a missing provide is a compile error rather than a runtime 500. The residual is `AppServices<R>`: the def's `R` minus what the router itself threads (`Router`, `Router.Outlet`, `AppRpcClientTag`). An app with no app-services needs no `context`; a loosely-typed `RouterDef<any, any>` may omit it.
69
+
70
+ This is how the website provides its `Docs` service to every page. See [Add Routing](https://weftui.dev/docs/how-to/add-routing).
59
71
 
60
72
  ## Server-only services: `ServerTag`
61
73
 
62
- Some services must _never_ run in the browser a database handle, a private credential, an rpc handler's backing store. Declare those with [`ServerTag`](https://weftui.dev/docs/reference/core#servertag) instead of `Context.Service`. It behaves exactly like `Context.Service`, but its identifier carries a **server-only brand**.
74
+ Some services must _never_ run in the browser: a database handle, a private credential, an rpc handler's backing store. Declare those with [`ServerTag`](https://weftui.dev/docs/reference/core#servertag) instead of `Context.Service`. It behaves exactly like `Context.Service`, but its identifier carries a **server-only brand**.
75
+
76
+ The brand's job is to turn a leak into a **compile error at the `hydrate` call site**. A `Boundary.rpc` handler legitimately reads server-only services on the server, but they must not survive into client code. Since `render` only ever touches the _decoded result_ (never the service), a correctly-written boundary keeps its output `R` free of the brand.
63
77
 
64
- The brand's job is to turn a leak into a **compile error at the `hydrate` call site**. A `Boundary.rpc` handler legitimately reads server-only services on the server, but they must not survive into client code: since `render` only ever touches the _decoded result_ (never the service), a correctly-written boundary keeps its output `R` free of the brand. If a branded tag ever leaks into `render` and reaches the client requirement channel, `hydrate`'s `AssertNoServerOnly` resolves `R` to a compile-error sentinel you learn at build time, not from a runtime defect.
78
+ If a branded tag leaks into `render` and reaches the client requirement channel, `hydrate`'s `AssertNoServerOnly` resolves `R` to a compile-error sentinel. You learn at build time, not from a runtime defect.
65
79
 
66
80
  ```typescript
67
81
  import { ServerTag } from "@weftui/core";
@@ -73,15 +87,15 @@ class Db extends ServerTag("Db")<Db, { query: (sql: string) => Effect.Effect<Row
73
87
  ## The whole picture
74
88
 
75
89
  - A component reads a service with `yield* Service`; the requirement enters `R`.
76
- - `R` accumulates through the tree and is discharged **once** at `WeftApp.make`, or through the router's `context` seam.
90
+ - `R` accumulates through the tree and is discharged **once**: at `WeftApp.make`, or through the router's `context` seam.
77
91
  - The same services flow to the same components on the server and the client, because it is the same tree.
78
92
  - `ServerTag` brands the services that must stay server-side, enforced at the `hydrate` boundary.
79
93
 
80
94
  ## See also
81
95
 
82
- - [The Rendering Model](https://weftui.dev/docs/explanation/rendering-model) why services flow through the tree at all
83
- - [The Combinator API](https://weftui.dev/docs/explanation/combinator-api) how `R` accumulates from children and reactive props
84
- - [Provide Services](https://weftui.dev/docs/how-to/provide-services) recipes for app layers, scoped layers, and binding an app's lifetime to an external scope
85
- - [Add Routing](https://weftui.dev/docs/how-to/add-routing) providing app services through the router `context` seam
86
- - [Load Data with RPC](https://weftui.dev/docs/how-to/load-data-with-rpc) where `ServerTag` and the rpc handler Layer meet
96
+ - [The Rendering Model](https://weftui.dev/docs/explanation/rendering-model): why services flow through the tree at all
97
+ - [The Combinator API](https://weftui.dev/docs/explanation/combinator-api): how `R` accumulates from children and reactive props
98
+ - [Provide Services](https://weftui.dev/docs/how-to/provide-services): recipes for app layers, scoped layers, and binding an app's lifetime to an external scope
99
+ - [Add Routing](https://weftui.dev/docs/how-to/add-routing): providing app services through the router `context` seam
100
+ - [Load Data with RPC](https://weftui.dev/docs/how-to/load-data-with-rpc): where `ServerTag` and the rpc handler Layer meet
87
101
  - [`ServerTag` API reference](https://weftui.dev/docs/reference/core#servertag)