@weftui/dom 0.27.1 → 0.28.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.
@@ -15,37 +15,33 @@ When a component does `yield* ThemeService`, `ThemeService` enters that node's r
15
15
 
16
16
  ```typescript
17
17
  import { Effect } from "effect";
18
- import { mount } from "@weftui/dom/client";
18
+ import { WeftApp } from "@weftui/dom/client";
19
19
 
20
- const handle = pipe(
21
- mount(App(), document.getElementById("root")!),
22
- Effect.provide(ThemeServiceLive),
23
- );
20
+ const app = WeftApp.make(ThemeServiceLive);
21
+ const handle = WeftApp.mount(app, App(), document.getElementById("root")!);
24
22
  ```
25
23
 
26
- Provide too little and it is a compile error at the mount call — 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.
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.
27
25
 
28
- Services flow **down** from that provide point 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.
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.
29
27
 
30
- ## Layer lifetime at the mount
28
+ ## Layer lifetime and the app runtime
31
29
 
32
- The `ThemeServiceLive` example above works because `mount`'s effect and the service's lifetime coincide by accident: `ThemeServiceLive` is a plain value layer with nothing to release, so it makes no difference whether it is "alive" for one tick or the whole session. That accident stops holding the moment the layer is **scoped** built from an `acquireRelease`-backed `Layer.effect` because `mount`'s effect resolves right after the tree's **initial render**, not when the app stops running. Streams, event handlers, and forked work all keep running on the mount's runtime long after that Effect has settled.
33
-
34
- `Effect.provide(scopedLayer)` is `acquireUseRelease` sugar: acquire, run the wrapped effect, then release **when that effect completes**. Wrap it directly around `mount`, and the release runs at mount-resolve — while the mounted tree is still reading from the now-disposed service:
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:
35
31
 
36
32
  ```typescript
37
- // ❌ the layer's finalizers run the instant runPromise settles, while the
38
- // mounted tree keeps running — every subscription now reads a disposed service
33
+ // ❌ (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
39
35
  Effect.runPromise(mount(App(), root).pipe(Effect.provide(SomeScopedLayer)));
40
36
  ```
41
37
 
42
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.
43
39
 
44
- The fix is to give the scoped layer a lifetime that matches the app, not the initial render: provide it **outside** a scoped region that stays open for as long as the app should run, and mount inside that region with `mountScoped` (which ties `unmount` to the region's scope instead of to the resolution of the mount effect). An `Effect.never` (or `Deferred.await` on a shutdown signal) keeps the region and therefore the layer alive until something explicitly closes it. See [Provide Services](https://weftui.dev/docs/how-to/provide-services) for the recipe, including the `ManagedRuntime` alternative when a scoped region isn't a good fit.
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`).
45
41
 
46
42
  ## The router's render-time context seam
47
43
 
48
- A plain `mount`/`hydrate` discharges `R` at the call site. 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.
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.
49
45
 
50
46
  So the router exposes an explicit **`context` seam** — a `Layer` threaded to the document shell and every route, layout, and leaf:
51
47
 
@@ -77,7 +73,7 @@ class Db extends ServerTag("Db")<Db, { query: (sql: string) => Effect.Effect<Row
77
73
  ## The whole picture
78
74
 
79
75
  - A component reads a service with `yield* Service`; the requirement enters `R`.
80
- - `R` accumulates through the tree and is discharged **once** — at `mount`/`hydrate`, or through the router's `context` seam.
76
+ - `R` accumulates through the tree and is discharged **once** — at `WeftApp.make`, or through the router's `context` seam.
81
77
  - The same services flow to the same components on the server and the client, because it is the same tree.
82
78
  - `ServerTag` brands the services that must stay server-side, enforced at the `hydrate` boundary.
83
79
 
@@ -85,7 +81,7 @@ class Db extends ServerTag("Db")<Db, { query: (sql: string) => Effect.Effect<Row
85
81
 
86
82
  - [The Rendering Model](https://weftui.dev/docs/explanation/rendering-model) — why services flow through the tree at all
87
83
  - [The Combinator API](https://weftui.dev/docs/explanation/combinator-api) — how `R` accumulates from children and reactive props
88
- - [Provide Services](https://weftui.dev/docs/how-to/provide-services) — recipes for value layers, scoped layers with `mountScoped`, and `ManagedRuntime`
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
89
85
  - [Add Routing](https://weftui.dev/docs/how-to/add-routing) — providing app services through the router `context` seam
90
86
  - [Load Data with RPC](https://weftui.dev/docs/how-to/load-data-with-rpc) — where `ServerTag` and the rpc handler Layer meet
91
87
  - [`ServerTag` API reference](https://weftui.dev/docs/reference/core#servertag)
@@ -177,21 +177,21 @@ Router.route("users/:id", {
177
177
 
178
178
  ## Client setup
179
179
 
180
- On the client, provide the `Router` via `RouterLive(def)` and render `RouterApp(def)`. `RouterLive` is a **scoped layer** — it owns the `popstate` listener and the same-origin link-click interceptor — so it must outlive the mount. Provide it through a long-lived `ManagedRuntime` rather than `Effect.provide` at the node level:
180
+ On the client, provide the `Router` via `RouterLive(def)` and render `RouterApp(def)`. `RouterLive` is a **scoped layer** — it owns the `popstate` listener and the same-origin link-click interceptor — so it must outlive the mount. Give it to `WeftApp.make`: the app runtime owns it for the app's lifetime, built lazily on first hydrate and released only at `WeftApp.dispose`. Do not wrap `Effect.provide` around the mount/hydrate call — services come exclusively from the app layer.
181
181
 
182
182
  ```typescript
183
183
  // entry-client.ts
184
- import { hydrate } from "@weftui/dom/client";
184
+ import { WeftApp } from "@weftui/dom/client";
185
185
  import { RouterApp, RouterLive } from "@weftui/router/client";
186
- import { ManagedRuntime } from "effect";
186
+ import { Effect } from "effect";
187
187
  import { App } from "./app";
188
188
 
189
189
  const root = document.getElementById("root")!;
190
- const runtime = ManagedRuntime.make(RouterLive(App));
191
- void runtime.runPromise(hydrate(RouterApp(App), root));
190
+ const app = WeftApp.make(RouterLive(App));
191
+ void Effect.runPromise(WeftApp.hydrate(app, RouterApp(App), root));
192
192
  ```
193
193
 
194
- For a client-only app (no SSR), swap `hydrate` for `mount` — everything else is identical.
194
+ For a client-only app (no SSR), swap `WeftApp.hydrate` for `WeftApp.mount` — everything else is identical.
195
195
 
196
196
  ### Link interception
197
197
 
@@ -56,7 +56,7 @@ The return type here is `Effect.Effect<Node, never, never>` — itself a valid `
56
56
  Every component instance is rendered under its own **instance scope** — a child of the
57
57
  mount scope created fresh for that instance. Anything bound to the instance scope lives
58
58
  exactly as long as the component is mounted and is torn down automatically when the
59
- component unmounts (or when the whole tree unmounts via the `MountHandle`). The renderer
59
+ component unmounts (or when its root unmounts via `RootHandle.unmount()`). The renderer
60
60
  provides this scope as the ambient `Scope.Scope` while it evaluates the component body,
61
61
  so it is already in context when you need it.
62
62
 
@@ -235,12 +235,14 @@ const UserAvatar = Component.gen(function* (props: { userId: string }) {
235
235
  const avatar = UserAvatar({ userId: "123" });
236
236
  ```
237
237
 
238
- Provide the service at the mount boundary:
238
+ Give the service to the app layer:
239
239
 
240
240
  ```typescript
241
- void Effect.runPromise(
242
- mount(App(), document.getElementById("root")!).pipe(Effect.provide(UserServiceLive)),
243
- );
241
+ import { WeftApp } from "@weftui/dom/client";
242
+ import { Effect } from "effect";
243
+
244
+ const app = WeftApp.make(UserServiceLive);
245
+ void Effect.runPromise(WeftApp.mount(app, App(), document.getElementById("root")!));
244
246
  ```
245
247
 
246
248
  ## Returning fragments
@@ -88,11 +88,13 @@ export const render = (url: string) =>
88
88
 
89
89
  ```typescript
90
90
  // entry-client.ts — network client posting to /_eui/rpc
91
+ import { WeftApp } from "@weftui/dom/client";
91
92
  import { RouterApp, RouterLive } from "@weftui/router/client";
93
+ import { Effect } from "effect";
92
94
  import { StockRpcs } from "./data/inventory";
93
95
 
94
- const runtime = ManagedRuntime.make(RouterLive(App, { rpc: { group: StockRpcs } }));
95
- void runtime.runPromise(hydrate(RouterApp(App), root));
96
+ const app = WeftApp.make(RouterLive(App, { rpc: { group: StockRpcs } }));
97
+ void Effect.runPromise(WeftApp.hydrate(app, RouterApp(App), root));
96
98
  ```
97
99
 
98
100
  - **Server** ([`RouterServer`](https://weftui.dev/docs/reference/router#routerserver)) mounts the handler Layer at `POST /_eui/rpc` (so a client refetch re-runs it on the server) **and** exposes an in-process client over the same handlers for SSR resolution — never a network hop.
@@ -2,123 +2,101 @@
2
2
  title: Provide Services
3
3
  order: 12
4
4
  section: how-to
5
- description: Provide plain and scoped Layers to a mounted app the direct mount for value layers, mountScoped plus a shutdown signal for scoped layers, and a ManagedRuntime as an alternative.
5
+ description: Provide plain and scoped Layers to a WeftAppapp layers for the common case, scoped layers that just work, memoMap sharing, and binding an app's lifetime to a scope.
6
6
  ---
7
7
 
8
8
  # Provide Services
9
9
 
10
- **Goal:** provide a `Layer` to the mounted app so its components can read services with `yield* Service`.
10
+ **Goal:** provide a `Layer` to a `WeftApp` so its components can read services with `yield* Service`.
11
11
 
12
- Which recipe to reach for depends on whether the layer has anything to release. Both plain and scoped layers are built with `Layer.succeed` or `Layer.effect` — the distinction is whether the layer's effect acquires releasable resources (`acquireRelease`, or a scope finalizer added directly). A plain value layer (`Layer.succeed`, or `Layer.effect` whose effect has no `acquireRelease`) can be provided directly at the mount — there is nothing to leak. A **scoped** layer (`Layer.effect` whose effect is backed by `acquireRelease`) needs the mount to outlive the effect's own resolution — see [Layer lifetime at the mount](https://weftui.dev/docs/explanation/services-and-context#layer-lifetime-at-the-mount) for why.
12
+ ## Recipe 1 app layers
13
13
 
14
- ## Recipe 1plain value layers with `mount`
15
-
16
- Provide the layer directly around `mount` and run with `runPromise`. This is the common case and needs nothing else.
14
+ Pass the layer to `WeftApp.make`. This is the common case and needs nothing else the layer builds lazily on first mount, and every component, event handler, and stream subscription in every root mounted from `app` can read it.
17
15
 
18
16
  ```typescript
19
- import { mount } from "@weftui/dom/client";
20
- import { Effect, pipe } from "effect";
17
+ import { WeftApp } from "@weftui/dom/client";
18
+ import { Effect } from "effect";
21
19
  import { App } from "./app";
22
20
  import { ThemeServiceLive } from "./theme-service";
23
21
 
24
22
  const root = document.getElementById("root")!;
25
23
 
26
- const program = pipe(mount(App(), root), Effect.provide(ThemeServiceLive));
27
-
28
- Effect.runPromise(program);
24
+ const app = WeftApp.make(ThemeServiceLive);
25
+ void Effect.runPromise(WeftApp.mount(app, App(), root));
29
26
  ```
30
27
 
31
- ## Recipe 2 — scoped layers with `mountScoped`
28
+ ## Recipe 2 — scoped layers just work
29
+
30
+ A **scoped** layer — `Layer.effect` backed by `acquireRelease`, or anything else that owns a subscription, listener, or registry — needs nothing different from Recipe 1. The app owns one lazy `ManagedRuntime`: the layer builds on first mount and releases only at `WeftApp.dispose(app)`, not when any individual mount's render effect resolves. There is no `mountScoped`, no `Effect.never`, no manual scope threading.
32
31
 
33
- Provide the scoped layer **outside** a long-lived scoped region, mount inside that region with `mountScoped`, and keep the region open with `Effect.never` or `Deferred.await` on a shutdown signal. Drive the whole thing with `runFork`, not `runPromise` the program never settles on its own.
32
+ `AtomRegistry.layer` (from `effect/unstable/reactivity`) is a real scoped layer its atom subscriptions are fibers forked for the app's whole lifetime:
34
33
 
35
34
  ```typescript
36
- import { mountScoped } from "@weftui/dom/client";
37
- import { Deferred, Effect, Fiber, pipe } from "effect";
35
+ import { WeftApp } from "@weftui/dom/client";
36
+ import { Effect } from "effect";
37
+ import { AtomRegistry } from "effect/unstable/reactivity";
38
38
  import { App } from "./app";
39
- import { AppLive } from "./app-live";
40
-
41
- const root = document.getElementById("root")!;
42
-
43
- const program = pipe(
44
- Effect.scoped(
45
- Effect.gen(function* () {
46
- yield* mountScoped(App(), root);
47
- yield* Effect.never; // keeps the region — and AppLive — alive
48
- }),
49
- ),
50
- Effect.provide(AppLive), // OUTSIDE the scoped region: outlives initial render
51
- );
52
39
 
53
- const fiber = Effect.runFork(program);
54
-
55
- // later, e.g. on a "sign out" action or test teardown:
56
- // await Effect.runPromise(Fiber.interrupt(fiber));
40
+ const app = WeftApp.make(AtomRegistry.layer);
41
+ void Effect.runPromise(WeftApp.mount(app, App(), document.getElementById("root")!));
57
42
  ```
58
43
 
59
- Interrupting `fiber` closes the inner scope first running `mountScoped`'s finalizer, which calls `unmount` and only then releases `AppLive`. Swap `Effect.never` for `Deferred.await(shutdown)` when something in the app should be able to request shutdown itself:
44
+ `RouterLive` (from `@weftui/router/client`) is anotherit owns the `popstate` listener and the same-origin link-click interceptor for as long as the app runs:
60
45
 
61
46
  ```typescript
62
- const shutdown = await Effect.runPromise(Deferred.make<void>());
63
-
64
- const program = pipe(
65
- Effect.scoped(
66
- Effect.gen(function* () {
67
- yield* mountScoped(App(), root);
68
- yield* Deferred.await(shutdown); // resolves when shutdown is signalled
69
- }),
70
- ),
71
- Effect.provide(AppLive),
72
- );
73
- Effect.runFork(program);
74
-
75
- // elsewhere, to request shutdown:
76
- // await Effect.runPromise(Deferred.succeed(shutdown, undefined));
47
+ const app = WeftApp.make(RouterLive(App, { rpc: { group: StockRpcs } }));
48
+ void Effect.runPromise(WeftApp.hydrate(app, RouterApp(App), root));
77
49
  ```
78
50
 
79
- `hydrateScoped` is the SSR counterpart same composition, swap `mountScoped` for `hydrateScoped`.
51
+ Both examples are runnable in full at [examples/effect-atom](https://github.com/stefvw93/weft/tree/main/examples/effect-atom) and [examples/router-ssr](https://github.com/stefvw93/weft/tree/main/examples/router-ssr).
80
52
 
81
- ## Recipe 3 — `ManagedRuntime` with plain `mount`
53
+ ## Recipe 3 — sharing layer memoization with `memoMap`
82
54
 
83
- Build a `ManagedRuntime` from the scoped layer and mount with plain `mount`, running through the runtime instead of `Effect.runPromise` directly. The layer lives until `runtime.dispose()` an explicit call, rather than a scope closing.
55
+ `WeftApp.make(layer, { memoMap })` accepts an explicit `Layer.MemoMap`, so multiple `WeftApp` instances can share layer construction for example, building one app per test case while reusing an expensive shared dependency's memoized build across them:
84
56
 
85
57
  ```typescript
86
- import { mount } from "@weftui/dom/client";
87
- import { ManagedRuntime } from "effect";
88
- import { App } from "./app";
89
- import { AppLive } from "./app-live";
90
-
91
- const root = document.getElementById("root")!;
92
- const runtime = ManagedRuntime.make(AppLive);
58
+ import { WeftApp } from "@weftui/dom/client";
59
+ import { Layer } from "effect";
93
60
 
94
- await runtime.runPromise(mount(App(), root));
61
+ const memoMap = Layer.makeMemoMap();
95
62
 
96
- // later:
97
- // await runtime.dispose();
63
+ const appA = WeftApp.make(SharedLive, { memoMap });
64
+ const appB = WeftApp.make(SharedLive, { memoMap });
98
65
  ```
99
66
 
100
- This reads closer to Recipe 1 at the call site and is a good fit when the surrounding app (a framework integration, a test harness) already manages a runtime's lifecycle for you.
67
+ Most apps have exactly one `WeftApp` and never need this option.
101
68
 
102
- ## Anti-patterns
69
+ ## Recipe 4 — binding an app's lifetime to a scope
103
70
 
104
- Both of these compile and both dispose the scoped layer while the app is still runningthe mounted tree keeps its subscriptions and handlers, but they now read from a released service.
71
+ There is deliberately no `makeScoped`. To tie an app's disposal to a `Scope` you already managea framework integration or a test harness that owns one compose it yourself with `Effect.acquireRelease`:
105
72
 
106
73
  ```typescript
107
- // plain mount: the layer releases the instant runPromise settles
108
- Effect.runPromise(mount(App(), root).pipe(Effect.provide(SomeScopedLayer)));
74
+ import { Effect } from "effect";
75
+ import { WeftApp } from "@weftui/dom/client";
76
+ import { AppLive } from "./app-live";
77
+
78
+ const acquireApp = Effect.acquireRelease(
79
+ Effect.sync(() => WeftApp.make(AppLive)),
80
+ (app) => WeftApp.dispose(app),
81
+ );
109
82
  ```
110
83
 
84
+ `acquireApp` yields a `WeftApp` and registers `WeftApp.dispose` as a finalizer on whatever scope the surrounding effect runs in — closing that scope tears the app down the same way `WeftApp.dispose` normally would (roots, then layers, then the error hub).
85
+
86
+ ## Anti-pattern: `Effect.provide` around the mount call
87
+
111
88
  ```typescript
112
- // ❌ mountScoped, but the scoped region closes as soon as the mount effect
113
- // resolves nothing keeps it open, so this is no better than plain mount
114
- Effect.runPromise(mountScoped(App(), root).pipe(Effect.provide(SomeScopedLayer), Effect.scoped));
89
+ // ❌ does nothing useful: WeftApp.mount's R is always `never`, and services
90
+ // come exclusively from the app layer a wrapped Effect.provide never
91
+ // reaches components, handlers, or stream subscriptions
92
+ Effect.runPromise(pipe(WeftApp.mount(app, App(), root), Effect.provide(SomeLayer)));
115
93
  ```
116
94
 
117
- In both cases the tell is the same: nothing in the composition keeps a scope open past the point where the mount Effect itself resolves. Recipe 2's `Effect.never` (or `Deferred.await`) is doing the one piece of work these anti-patterns are missing.
95
+ `WeftApp.mount`/`WeftApp.hydrate` return an effect whose requirement channel is always `never` there is no `R` left for `Effect.provide` to discharge. Any service a component needs must be in the layer passed to `WeftApp.make`.
118
96
 
119
97
  ## See also
120
98
 
121
- - [Layer lifetime at the mount](https://weftui.dev/docs/explanation/services-and-context#layer-lifetime-at-the-mount) — why the mount effect resolving early matters for scoped layers
122
- - [Services and Context](https://weftui.dev/docs/explanation/services-and-context) — how `R` accumulates and discharges at the mount
123
- - [`mountScoped` / `hydrateScoped` reference](https://weftui.dev/docs/reference/dom#mountscoped) — signatures and error unions
124
- - [examples/effect-atom](https://github.com/stefvw93/weft/tree/main/examples/effect-atom) — a real scoped layer (`AtomRegistry.layer` from `effect/unstable/reactivity`) mounted with this composition
99
+ - [Services and Context](https://weftui.dev/docs/explanation/services-and-context) — how `R` accumulates and discharges at `WeftApp.make`, and why scoped layers no longer need special handling
100
+ - [`WeftApp` reference](https://weftui.dev/docs/reference/dom) — full signatures for `make`, `mount`, `hydrate`, `dispose`
101
+ - [examples/effect-atom](https://github.com/stefvw93/weft/tree/main/examples/effect-atom) — a real scoped layer (`AtomRegistry.layer`)
102
+ - [examples/shared-state-islands](https://github.com/stefvw93/weft/tree/main/examples/shared-state-islands) — one app layer shared by reference across multiple mounted roots
@@ -12,7 +12,7 @@ Weft renders on the server and **hydrates** on the client: the server produces H
12
12
  ## The two halves
13
13
 
14
14
  - **Server** — `@weftui/dom/server` renders an app node to an HTML string (or stream). The _hydratable_ variants additionally emit the inline data each reactive region and `Boundary.rpc` needs to resume on the client.
15
- - **Client** — `@weftui/dom/client`'s `hydrate` walks the server DOM, adopts it, wires up reactivity and event handlers, and resumes from the inline data. It does **not** re-render from scratch.
15
+ - **Client** — `@weftui/dom/client`'s `WeftApp.hydrate` walks the server DOM, adopts it, wires up reactivity and event handlers, and resumes from the inline data. It does **not** re-render from scratch.
16
16
 
17
17
  ```typescript
18
18
  // server entry
@@ -25,12 +25,13 @@ export const render = (): Promise<string> => Effect.runPromise(renderToStringHyd
25
25
 
26
26
  ```typescript
27
27
  // client entry
28
- import { hydrate } from "@weftui/dom/client";
28
+ import { WeftApp } from "@weftui/dom/client";
29
29
  import { Effect } from "effect";
30
30
  import { App } from "./app";
31
31
 
32
32
  const root = document.getElementById("root")!;
33
- void Effect.runPromise(hydrate(App(), root));
33
+ const app = WeftApp.make();
34
+ void Effect.runPromise(WeftApp.hydrate(app, App(), root));
34
35
  ```
35
36
 
36
37
  The same side-effect-free `App` is imported by both entries — splice the server HTML into your template's outlet, ship it, and let the client entry hydrate it.
package/docs/index.md CHANGED
@@ -10,7 +10,7 @@ The docs follow the [Diátaxis](https://diataxis.fr) model. Pick your entry poin
10
10
 
11
11
  **[→ Tutorial](https://weftui.dev/docs/tutorial/01-your-first-app)** — a four-step guided path from a static component to a server-rendered, error-handled app. Start here if you are new to Weft:
12
12
 
13
- 1. [Your First App](https://weftui.dev/docs/tutorial/01-your-first-app) — `h` and `mount`
13
+ 1. [Your First App](https://weftui.dev/docs/tutorial/01-your-first-app) — `h` and `WeftApp`
14
14
  2. [Reactivity](https://weftui.dev/docs/tutorial/02-reactivity) — `SubscriptionRef` and streams
15
15
  3. [Services and Async](https://weftui.dev/docs/tutorial/03-services-and-async) — handlers, services, async loading
16
16
  4. [Errors and Server Rendering](https://weftui.dev/docs/tutorial/04-errors-and-server) — boundaries and SSR
@@ -31,7 +31,7 @@ New to the model itself? Read [The Rendering Model](https://weftui.dev/docs/expl
31
31
  Three published packages make up Weft's public API, plus one build-time plugin:
32
32
 
33
33
  - **`@weftui/core`** — element builders (`h`), components, sources/streams, and boundaries. Start here.
34
- - **`@weftui/dom`** — the renderer: `./client` (`mount`/`hydrate`) and `./server` (`renderToString*`).
34
+ - **`@weftui/dom`** — the renderer: `./client` (`WeftApp.mount`/`WeftApp.hydrate`) and `./server` (`renderToString*`).
35
35
  - **`@weftui/router`** — universal nested routing, `Router.lazy`, and the rpc seam.
36
36
  - **`@weftui/vite`** — a build-time Vite plugin (tooling, not a runtime API).
37
37