@weftui/core 0.27.0 → 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.
package/README.md CHANGED
@@ -9,9 +9,11 @@ This package is renderer-agnostic — pair it with [`@weftui/dom`](https://weftu
9
9
  ## Installation
10
10
 
11
11
  ```bash
12
- npm install @weftui/core effect
12
+ npm install @weftui/core effect@beta
13
13
  ```
14
14
 
15
+ Weft tracks Effect 4's beta line. This release is built and tested against `effect@4.0.0-beta.98`; the peer range accepts newer 4.0 betas, which may contain upstream breaking changes.
16
+
15
17
  `effect` is a peer dependency. To render, add [`@weftui/dom`](https://www.npmjs.com/package/@weftui/dom).
16
18
 
17
19
  ## Key exports
@@ -29,7 +31,7 @@ npm install @weftui/core effect
29
31
 
30
32
  ```typescript
31
33
  import { h } from "@weftui/core";
32
- import { mount } from "@weftui/dom/client";
34
+ import { WeftApp } from "@weftui/dom/client";
33
35
  import { Effect, SubscriptionRef } from "effect";
34
36
 
35
37
  const Counter = () =>
@@ -43,7 +45,8 @@ const Counter = () =>
43
45
  ]);
44
46
  });
45
47
 
46
- void Effect.runPromise(mount(Counter(), document.getElementById("root")!));
48
+ const app = WeftApp.make();
49
+ void Effect.runPromise(WeftApp.mount(app, Counter(), document.getElementById("root")!));
47
50
  ```
48
51
 
49
52
  ## Documentation
@@ -1,6 +1,4 @@
1
1
  import { Cause, Effect, Option, Scope, Stream, SubscriptionRef } from "effect";
2
-
3
- //#region src/subscribable/index.d.ts
4
2
  declare namespace index_d_exports {
5
3
  export { Subscribable, TypeId, changes, get, isSubscribable, make };
6
4
  }
@@ -10491,7 +10489,7 @@ declare namespace DataType {
10491
10489
  }
10492
10490
  //#endregion
10493
10491
  //#region src/source/source.d.ts
10494
- declare const NoPropValue_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P] }>) => Cause.YieldableError & {
10492
+ declare const NoPropValue_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => Cause.YieldableError & {
10495
10493
  readonly _tag: "NoPropValue";
10496
10494
  } & Readonly<A>;
10497
10495
  /**
@@ -10543,7 +10541,7 @@ declare namespace Source {
10543
10541
  //#endregion
10544
10542
  //#region src/types/html/attributes.d.ts
10545
10543
  type HTMLAttributeSource<T> = Source.Source<T | undefined>;
10546
- type StyleProperties = { [K in keyof Properties]?: HTMLAttributeSource<Properties[K]> };
10544
+ type StyleProperties = { [K in keyof Properties]?: HTMLAttributeSource<Properties[K]>; };
10547
10545
  type StyleAttributeValue = string | StyleProperties | Source.Source<string> | Source.Source<StyleProperties>;
10548
10546
  //#endregion
10549
10547
  //#region src/types/html/aria.d.ts
package/dist/index.d.ts CHANGED
@@ -1,7 +1,6 @@
1
- import { D as Subscribable, E as Source, O as index_d_exports, T as NoPropValue, a as SVGElements, r as Renderable, s as HTMLElements, t as ElementDescriptor } from "./index-DMNuAQXj.js";
1
+ import { D as Subscribable, E as Source, O as index_d_exports, T as NoPropValue, a as SVGElements, r as Renderable, s as HTMLElements, t as ElementDescriptor } from "./index-Bsh2WLtx.js";
2
2
  import { Cause, Context, Effect, Filter, Option, Stream } from "effect";
3
3
  import { Rpc } from "effect/unstable/rpc";
4
-
5
4
  //#region src/combinator/types.d.ts
6
5
  /**
7
6
  * Widens a single prop value type so that Stream/Effect/Subscribable variants
@@ -34,18 +33,18 @@ declare namespace Node {
34
33
  type Context<N> = [N] extends [Effect.Effect<any, any, infer R>] ? R : never;
35
34
  }
36
35
  /** Extract E from a props object — Stream/Effect/Subscribable prop values and Effect-returning event handlers contribute their E channel. */
37
- type PropsE<P> = { [K in keyof P]: P[K] extends Stream.Stream<any, infer E, any> ? E : P[K] extends Effect.Effect<any, infer E, any> ? E : P[K] extends Subscribable<any, infer E, any> ? E : P[K] extends ((...args: any[]) => infer Ret) ? Ret extends Effect.Effect<any, infer E, any> ? E : never : never }[keyof P];
36
+ type PropsE<P> = { [K in keyof P]: P[K] extends Stream.Stream<any, infer E, any> ? E : P[K] extends Effect.Effect<any, infer E, any> ? E : P[K] extends Subscribable<any, infer E, any> ? E : P[K] extends ((...args: any[]) => infer Ret) ? Ret extends Effect.Effect<any, infer E, any> ? E : never : never; }[keyof P];
38
37
  /** Extract R from a props object — Stream/Effect/Subscribable prop values and Effect-returning event handlers contribute their R channel. */
39
- type PropsR<P> = { [K in keyof P]: P[K] extends Stream.Stream<any, any, infer R> ? R : P[K] extends Effect.Effect<any, any, infer R> ? R : P[K] extends Subscribable<any, any, infer R> ? R : P[K] extends ((...args: any[]) => infer Ret) ? Ret extends Effect.Effect<any, any, infer R> ? R : never : never }[keyof P];
38
+ type PropsR<P> = { [K in keyof P]: P[K] extends Stream.Stream<any, any, infer R> ? R : P[K] extends Effect.Effect<any, any, infer R> ? R : P[K] extends Subscribable<any, any, infer R> ? R : P[K] extends ((...args: any[]) => infer Ret) ? Ret extends Effect.Effect<any, any, infer R> ? R : never : never; }[keyof P];
40
39
  /** Extract E from a children array — Node (Effect) and Stream children contribute their E. */
41
- type ChildrenE<T extends readonly Renderable[]> = [T[number]] extends [never] ? never : { [K in keyof T]: T[K] extends Effect.Effect<any, infer E, any> ? E : T[K] extends Stream.Stream<any, infer E, any> ? E : never }[number];
40
+ type ChildrenE<T extends readonly Renderable[]> = [T[number]] extends [never] ? never : { [K in keyof T]: T[K] extends Effect.Effect<any, infer E, any> ? E : T[K] extends Stream.Stream<any, infer E, any> ? E : never; }[number];
42
41
  /** Extract R from a children array — Node (Effect) and Stream children contribute their R. */
43
- type ChildrenR<T extends readonly Renderable[]> = [T[number]] extends [never] ? never : { [K in keyof T]: T[K] extends Effect.Effect<any, any, infer R> ? R : T[K] extends Stream.Stream<any, any, infer R> ? R : never }[number];
42
+ type ChildrenR<T extends readonly Renderable[]> = [T[number]] extends [never] ? never : { [K in keyof T]: T[K] extends Effect.Effect<any, any, infer R> ? R : T[K] extends Stream.Stream<any, any, infer R> ? R : never; }[number];
44
43
  /**
45
44
  * Strip `children` from HTML prop types and widen all Source prop types to
46
45
  * allow any E/R — so callers can pass `Stream<T, E, R>` with real requirements.
47
46
  */
48
- type CombinatorialProps<P> = { [K in keyof Omit<P, "children">]: OpenPropSource<Omit<P, "children">[K]> };
47
+ type CombinatorialProps<P> = { [K in keyof Omit<P, "children">]: OpenPropSource<Omit<P, "children">[K]>; };
49
48
  declare namespace boundary_impl_d_exports {
50
49
  export { CatchTagE, CatchTagsE, FAILURE_BOUNDARY, FailureProps, Resource, RpcOptions, SERVER_BOUNDARY, SUSPENSE_BOUNDARY, SuspenseProps, catch_ as catch, catchCause, catchFilter, catchIf, catchTag, catchTags, rpc, suspend };
51
50
  }
@@ -142,9 +141,9 @@ declare function catchCause<C extends readonly Renderable[], FE = never, FR = ne
142
141
  * Catch errors whose `_tag` matches `props.tag`. The matched tag is removed
143
142
  * from the output `E`; unmatched errors are re-raised.
144
143
  */
145
- declare function catchTag<C extends readonly Renderable[], Tag extends (ChildrenE<C> extends {
144
+ declare function catchTag<C extends readonly Renderable[], Tag extends ChildrenE<C> extends {
146
145
  _tag: string;
147
- } ? ChildrenE<C>["_tag"] : string), FE = never, FR = never>(props: {
146
+ } ? ChildrenE<C>["_tag"] : string, FE = never, FR = never>(props: {
148
147
  readonly tag: Tag;
149
148
  readonly fallback: (e: Extract<ChildrenE<C>, {
150
149
  _tag: Tag;
@@ -159,7 +158,7 @@ declare function catchTags<C extends readonly Renderable[], Handlers extends { r
159
158
  _tag: string;
160
159
  } ? ChildrenE<C>["_tag"] : never]?: (e: Extract<ChildrenE<C>, {
161
160
  _tag: Tag;
162
- }>) => Node<any, any> }>(handlers: Handlers, children: C): Node<CatchTagsE<C, keyof Handlers & string> | { [K in keyof Handlers]: Handlers[K] extends ((e: any) => Node<infer E, any>) ? E : never }[keyof Handlers], ChildrenR<C> | { [K in keyof Handlers]: Handlers[K] extends ((e: any) => Node<any, infer R>) ? R : never }[keyof Handlers]>;
161
+ }>) => Node<any, any>; }>(handlers: Handlers, children: C): Node<CatchTagsE<C, keyof Handlers & string> | { [K in keyof Handlers]: Handlers[K] extends ((e: any) => Node<infer E, any>) ? E : never; }[keyof Handlers], ChildrenR<C> | { [K in keyof Handlers]: Handlers[K] extends ((e: any) => Node<any, infer R>) ? R : never; }[keyof Handlers]>;
163
162
  /**
164
163
  * Conditionally catch using a `Filter`. The `filter` runs on each typed
165
164
  * failure: a `Result.succeed` (pass) recovers via `fallback`, receiving the
@@ -444,13 +443,7 @@ type H = {
444
443
  * ```
445
444
  */
446
445
  fragment<const C extends readonly Renderable[]>(children: C): Node<ChildrenE<C>, ChildrenR<C>>;
447
- } & { [K in keyof HTMLElements]: ElementFn<CombinatorialProps<HTMLElements[K] & DataAttributes>> } & { [K in keyof SVGElements]: ElementFn<CombinatorialProps<SVGElements[K] & DataAttributes>> } & { [K in keyof CustomElements]: ElementFn<CustomElements[K] & DataAttributes> };
448
- /**
449
- * Builds an `h` proxy backed by the given cache. Each tag access lazily creates
450
- * an `ElementFn` and memoizes it in the cache, so repeat accesses return the
451
- * same function reference. Exposed primarily to allow tests to observe an
452
- * isolated cache; production code should use the module-level `h`.
453
- */
446
+ } & { [K in keyof HTMLElements]: ElementFn<CombinatorialProps<HTMLElements[K] & DataAttributes>>; } & { [K in keyof SVGElements]: ElementFn<CombinatorialProps<SVGElements[K] & DataAttributes>>; } & { [K in keyof CustomElements]: ElementFn<CustomElements[K] & DataAttributes>; };
454
447
  declare const h: H;
455
448
  //#endregion
456
449
  //#region src/combinator/descriptor.d.ts
@@ -1,2 +1,2 @@
1
- import { C as StyleAttributeValue, S as HTMLAttributeSource, _ as HTMLFormMethod, a as SVGElements, b as HTMLReferrerPolicy, c as HTMLRole, d as EventHandlerFn, f as HTMLAutocapitalize, g as HTMLFormEncType, h as HTMLDir, i as SVGAttributes, l as DOMAttributes, m as HTMLCrossorigin, n as ElementType, o as HTMLAttributes, p as HTMLAutocomplete, r as Renderable, s as HTMLElements, t as ElementDescriptor, u as EventHandler, v as HTMLIframeSandbox, w as StyleProperties, x as AriaAttributes, y as HTMLLinkAs } from "../index-DMNuAQXj.js";
1
+ import { C as StyleAttributeValue, S as HTMLAttributeSource, _ as HTMLFormMethod, a as SVGElements, b as HTMLReferrerPolicy, c as HTMLRole, d as EventHandlerFn, f as HTMLAutocapitalize, g as HTMLFormEncType, h as HTMLDir, i as SVGAttributes, l as DOMAttributes, m as HTMLCrossorigin, n as ElementType, o as HTMLAttributes, p as HTMLAutocomplete, r as Renderable, s as HTMLElements, t as ElementDescriptor, u as EventHandler, v as HTMLIframeSandbox, w as StyleProperties, x as AriaAttributes, y as HTMLLinkAs } from "../index-Bsh2WLtx.js";
2
2
  export { AriaAttributes, DOMAttributes, ElementDescriptor, ElementType, EventHandler, EventHandlerFn, HTMLAttributeSource, HTMLAttributes, HTMLAutocapitalize, HTMLAutocomplete, HTMLCrossorigin, HTMLDir, HTMLElements, HTMLFormEncType, HTMLFormMethod, HTMLIframeSandbox, HTMLLinkAs, HTMLReferrerPolicy, HTMLRole, Renderable, SVGAttributes, SVGElements, StyleAttributeValue, StyleProperties };
@@ -38,9 +38,9 @@ The channel algebra is the whole reason they exist: `catchTag("Foo", …)` remov
38
38
 
39
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.
40
40
 
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 the subscription fiber's failure exit is left **unobserved**. The Effect runtime itself then reports it `"Fiber terminated with an unhandled error"` because Weft raises that fiber's `FiberRef.unhandledErrorLogLevel` from the ambient default (`Debug`) to `LogLevel.Error` and annotates the log with `weft.region`, identifying 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. Interruption — the ordinary case of unmount tearing down the region's scope — is never reported; only genuine failures are.
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.
42
42
 
43
- This is deliberate: rather than a Weft-specific error-reporting config, visibility is controlled by the same knobs any Effect program uses — `Logger.withMinimumLogLevel` to filter it, `Effect.withUnhandledErrorLogLevel` to change how loudly (or quietly) unhandled fiber exits are reported elsewhere in your program. 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.
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.
44
44
 
45
45
  ## Suspense boundaries
46
46
 
@@ -147,7 +147,7 @@ When a `Stream` prop ends before emitting, the renderer raises a `NoPropValue` t
147
147
  // Handle at the mount boundary if needed. `Effect.catchTag` matches the error
148
148
  // by its string tag, so no `NoPropValue` import is required here.
149
149
  pipe(
150
- mount(App(), root),
150
+ WeftApp.mount(app, App(), root),
151
151
  Effect.catchTag("NoPropValue", (e) =>
152
152
  Effect.logWarning(`Prop stream ended before emitting: ${e.key}`),
153
153
  ),
@@ -46,9 +46,9 @@ This also fixes the update _shape_. Because the structure is fixed, an update is
46
46
  The same component tree renders on the server and the client:
47
47
 
48
48
  - 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**, `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.
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.
50
50
 
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 mount (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).
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).
52
52
 
53
53
  ## Why this matters
54
54
 
@@ -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 with `Layer.scoped`, backed by an `acquireRelease` 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
 
@@ -280,7 +280,7 @@ export const handler = RouterServer.toWebHandler(App, { document: documentShell
280
280
 
281
281
  ### `effect/unstable/httpapi` is the spine
282
282
 
283
- The tree is the authoring surface, but `effect/unstable/httpapi`'s `HttpApi` is the **single source of truth** for paths and schemas. Sealing the tree with `Router.router(...)` builds it once (`buildHttpApi`) and stamps it onto `def.httpApi`: a single `"pages"` group with one GET endpoint per leaf at its full path pattern, carrying `setPath(pathSchema)`, `setUrlParams(querySchema)`, and a `RouterNotFound → 404` error. Both sides read that one definition, so they always agree:
283
+ The tree is the authoring surface, but `effect/unstable/httpapi`'s `HttpApi` is the **single source of truth** for paths and schemas. Sealing the tree with `Router.router(...)` builds it once (`buildHttpApi`) and stamps it onto `def.httpApi`: a single `"pages"` group with one GET endpoint per leaf at its full path pattern, carrying `params: pathSchema`, `query: querySchema`, and a `RouterNotFound → 404` error. Both sides read that one definition, so they always agree:
284
284
 
285
285
  - **Server** — `RouterServer` dispatches through `HttpApiBuilder` (platform owns request→leaf matching, path/query decode, and the 404 status).
286
286
  - **Client** — `RouterLive` derives a real `HttpApiClient` from the same `def.httpApi` (exposed as `Router.httpApiClient`) for network work. SPA URL→leaf resolution stays **local** (there is no public client-side "match this URL against my `HttpApi`" utility in platform), fed from the same endpoint definitions so it never drifts from the server.
@@ -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. A plain value layer (`Layer.succeed`, `Layer.effect` with no `acquireRelease`) can be provided directly at the mount there is nothing to leak. A **scoped** layer (`Layer.scoped`, anything 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 1app 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
 
@@ -2,7 +2,7 @@
2
2
  title: "@weftui/dom"
3
3
  order: 2
4
4
  section: reference
5
- description: Full API surface for @weftui/dom — the client renderer (mount, hydrate) and the server renderer (renderToString and streaming variants).
5
+ description: Full API surface for @weftui/dom — the WeftApp client runtime (make, mount, hydrate, errors, dispose) and the server renderer (renderToString and streaming variants).
6
6
  ---
7
7
 
8
8
  # @weftui/dom API Reference
@@ -14,70 +14,191 @@ for a narrative walkthrough.
14
14
 
15
15
  ## `@weftui/dom/client`
16
16
 
17
- ### `mount`
17
+ `WeftApp` is the client entry point's app namespace (`export * as WeftApp from
18
+ "./weft-app"`). One `WeftApp` value is one lazily-built `ManagedRuntime` (the app
19
+ layer) + one root `Scope` + one unhandled-error hub. Each `WeftApp.mount` /
20
+ `WeftApp.hydrate` call creates a child **root scope** under the app scope;
21
+ layer-built services are shared **by reference** across every root mounted from the
22
+ same app (layer memoization), which is what makes cross-island reactive state work
23
+ (see [examples/shared-state-islands](https://github.com/stefvw93/weft/tree/main/examples/shared-state-islands)). The
24
+ barrel also re-exports `MountError`, `HydrateError`, `RootHandle`, `UnhandledError`,
25
+ and the `WeftApp` interface's type as `WeftAppType` (renamed on export to avoid
26
+ colliding with the `WeftApp` namespace import).
27
+
28
+ ### `WeftApp.make`
29
+
30
+ ```ts
31
+ const make: {
32
+ (): WeftApp<never, never>;
33
+ <R, E>(
34
+ layer: Layer.Layer<R, E, never>,
35
+ options?: { readonly memoMap?: Layer.MemoMap },
36
+ ): WeftApp<R, E>;
37
+ };
38
+ ```
39
+
40
+ Creates a `WeftApp` from an app layer. Synchronous and side-effect-free with
41
+ respect to the layer: the layer builds **lazily** on the first `mount` / `hydrate`
42
+ (or the first direct `app.runtime` run) — `ManagedRuntime.make` semantics. A layer
43
+ whose construction has an observable side effect shows that effect only after the
44
+ first mount, never at `make` time. `options.memoMap` shares layer memoization
45
+ across multiple `WeftApp` instances.
46
+
47
+ There is deliberately no `makeScoped`. To bind an app's lifetime to a scope,
48
+ compose it yourself:
49
+
50
+ ```ts
51
+ const acquireApp = Effect.acquireRelease(
52
+ Effect.sync(() => WeftApp.make(AppLive)),
53
+ (app) => WeftApp.dispose(app),
54
+ );
55
+ ```
56
+
57
+ ### `WeftApp.mount`
58
+
59
+ ```ts
60
+ const mount: <R, E>(
61
+ app: WeftApp<R, E>,
62
+ node: Renderable,
63
+ root: HTMLElement,
64
+ ) => Effect.Effect<RootHandle, E | MountError>;
65
+ ```
66
+
67
+ Mounts `node` into `root` as a new root of `app`. Self-contained — the returned
68
+ effect's requirement channel is `never`, so it runs with a bare `Effect.runPromise`;
69
+ services come exclusively from the app layer, and an `Effect.provide` wrapped around
70
+ this call does not reach components. Clears `root`'s existing children, renders,
71
+ appends the result. Completes after initial render; streams keep running in the
72
+ background, owned by the root's scope (a child of the app scope). The app layer
73
+ builds lazily here on first mount; its error channel `E` surfaces at that point. On
74
+ render failure the root scope is closed before the error propagates; the app
75
+ runtime and other roots are untouched. Mounting on a disposed app fails — it does
76
+ not hang.
77
+
78
+ ### `WeftApp.hydrate`
79
+
80
+ ```ts
81
+ function hydrate<A extends Renderable, R = never, E = never>(
82
+ app: WeftApp<R, E>,
83
+ node: A,
84
+ root: HTMLElement,
85
+ ): [AssertNoServerOnly<CoreNode.Context<A>>] extends [CoreNode.Context<A>]
86
+ ? Effect.Effect<RootHandle, E | HydrateError>
87
+ : ServerOnlyLeak;
88
+ ```
89
+
90
+ Continues, on the client, the DOM produced on the server by
91
+ `renderToStringHydratable` / `renderToStreamHydratable`, as a new root of `app`.
92
+ Unlike `mount`, does **not** clear `root`: it walks the node tree in lockstep with
93
+ the existing server DOM, adopting nodes in place. Error channel is `E |
94
+ HydrateError` (adds `HydrationMismatchError` on top of everything `mount` can fail
95
+ with). Preserves the compile-time `AssertNoServerOnly` → `ServerOnlyLeak` guard: a
96
+ server-only requirement left in `node`'s context degrades the return type to the
97
+ `ServerOnlyLeak` sentinel (compile error at the call site) instead of a runtime
98
+ failure. Hydration mechanics — the readiness barrier, stream-id seeding — are
99
+ otherwise unchanged from `mount`.
100
+
101
+ ### `WeftApp.errors`
18
102
 
19
103
  ```ts
20
- mount(node: Renderable, target: Element): Effect<MountHandle, RenderError, R>
104
+ const errors: <R, E>(app: WeftApp<R, E>) => Stream.Stream<UnhandledError>;
21
105
  ```
22
106
 
23
- Renders a Weft `node` into `target` for a fresh (non-SSR) page, building real DOM
24
- and starting every reactive stream. Returns a `MountHandle` whose scope owns the
25
- mounted tree; closing it tears the tree down. Use `mount` for purely client-rendered
26
- apps; use `hydrate` when the markup already exists from SSR.
107
+ The app's unhandled-error stream. While at least one subscriber exists, the default
108
+ `Effect.logError` fallback is suppressed and every `UnhandledError` is delivered to
109
+ all subscribers. With zero subscribers, each unhandled error runs the default log
110
+ (annotated with `weft.region`) instead. No replay a subscriber sees only errors
111
+ published after it subscribed; multiple concurrent subscribers each receive every
112
+ subsequent error. When the last subscriber unsubscribes, the default log resumes.
27
113
 
28
- ### `hydrate`
114
+ ### `WeftApp.dispose`
29
115
 
30
116
  ```ts
31
- hydrate(node: Renderable, target: Element): Effect<MountHandle, HydrationMismatchError | RenderError, R>
117
+ const dispose: <R, E>(app: WeftApp<R, E>) => Effect.Effect<void>;
32
118
  ```
33
119
 
34
- Adopts server-rendered DOM **in place** inside `target` and resumes reactivity
35
- without re-creating elements. The `node` must produce a tree structurally identical
36
- to what the server rendered; a divergence fails with `HydrationMismatchError`. This
37
- is the flash-free path: no second render, the existing nodes simply become live.
120
+ Disposes the app: closes every root scope (in mount order), then releases the
121
+ runtime's layers (`runtime.disposeEffect`), then shuts the error hub down.
122
+ Idempotent teardown effects run once. Subsequent `mount` / `hydrate` calls fail.
38
123
 
39
- ### `mountScoped`
124
+ ### `WeftApp<R, E>` (`WeftAppType`)
40
125
 
41
126
  ```ts
42
- mountScoped(app: Renderable, root: HTMLElement): Effect<MountHandle, UnsupportedNodeTypeError | StreamSubscriptionError | RenderError, Scope.Scope>
127
+ interface WeftApp<in R = never, out E = never> {
128
+ readonly [TypeId]: typeof TypeId;
129
+ readonly runtime: ManagedRuntime.ManagedRuntime<R, E>;
130
+ }
43
131
  ```
44
132
 
45
- Scope-aware `mount`: identical behavior, but requires an ambient `Scope.Scope` in
46
- `R` and registers `unmount` as a finalizer on it, so the mount lives until that
47
- scope closes rather than only until the mount effect resolves. Provide any scoped
48
- layer **outside** a long-lived scoped region so it outlives initial render — see
49
- [Provide Services](https://weftui.dev/docs/how-to/provide-services) for the composition and
50
- [Layer lifetime at the mount](https://weftui.dev/docs/explanation/services-and-context#layer-lifetime-at-the-mount)
51
- for why.
133
+ Re-exported from the barrel as `WeftAppType`. `runtime` is the app's
134
+ `ManagedRuntime`, for running app-level effects against the shared layer outside any
135
+ root e.g. `app.runtime.runFork(trackPageviews)` (see
136
+ `website/src/entry-client.ts`) or `app.runtime.runPromise(Router.push("/about"))`.
52
137
 
53
- ### `hydrateScoped`
138
+ ### `RootHandle`
54
139
 
55
140
  ```ts
56
- hydrateScoped(app: Renderable, root: HTMLElement): Effect<MountHandle, UnsupportedNodeTypeError | StreamSubscriptionError | RenderError | HydrationMismatchError, Scope.Scope>
141
+ interface RootHandle {
142
+ readonly element: HTMLElement;
143
+ unmount(): Effect.Effect<void>;
144
+ }
57
145
  ```
58
146
 
59
- Scope-aware `hydrate` same relationship as `mountScoped` to `mount`, with
60
- `hydrate`'s error union (`HydrationMismatchError` added) and the same client-only
61
- compile-time guard: a server-only requirement left in `app`'s `R` degrades the
62
- return type to `ServerOnlyLeak` via `AssertNoServerOnly`.
147
+ Returned by `mount` / `hydrate`. `element` is the DOM element the root was mounted
148
+ into. `unmount()` closes **this root's scope only**: it interrupts its stream
149
+ subscriptions and any scoped work forked from its event handlers. It does **not**
150
+ dispose the app runtime, touch other roots, or remove the rendered DOM nodes from
151
+ `element`. Idempotent — teardown side effects fire once.
63
152
 
64
- ### `MountHandle`
153
+ ### `UnhandledError`
65
154
 
66
- The handle returned by `mount`, `hydrate`, `mountScoped`, and `hydrateScoped`. Its
67
- `unmount()` interrupts every subscription and event handler and disposes the
68
- mount's `ManagedRuntime`; it does **not** remove the mounted DOM nodes from `root`.
69
- `unmount` is idempotent — safe to call more than once, including once
70
- automatically and once explicitly.
155
+ ```ts
156
+ interface UnhandledError {
157
+ readonly cause: Cause.Cause<unknown>;
158
+ readonly region: string;
159
+ readonly root: RootHandle;
160
+ }
161
+ ```
162
+
163
+ An error that escaped every user-level handler and reached the app's
164
+ unhandled-error hub, published on `WeftApp.errors(app)`. `region` identifies where
165
+ in the render tree the error escaped. Sources (one entry per failing occurrence):
166
+
167
+ - a rendered stream subscription failing or dying with **no enclosing `Boundary`**
168
+ (region e.g. `"attribute:class"`, `"child:stream-3"`),
169
+ - an error escaping the **outermost** `Boundary` recovery (region
170
+ `"boundary:outermost"`),
171
+ - an event-handler effect **failing or dying** (region `"event:onClick"`) — reported
172
+ in development and production alike; there is no `NODE_ENV`-gated swallow.
173
+
174
+ Interrupt-only causes are never published. Errors handled by a nested `Boundary`
175
+ never reach the hub.
176
+
177
+ ### `MountError`
178
+
179
+ ```ts
180
+ type MountError = UnsupportedNodeTypeError | StreamSubscriptionError | RenderError;
181
+ ```
182
+
183
+ Errors `mount` can fail with, beyond the app layer's own error channel `E`.
184
+
185
+ ### `HydrateError`
186
+
187
+ ```ts
188
+ type HydrateError = MountError | HydrationMismatchError;
189
+ ```
190
+
191
+ Everything `MountError` covers, plus `HydrationMismatchError` when the server DOM
192
+ and the node tree diverge.
193
+
194
+ ### `TypeId`
195
+
196
+ ```ts
197
+ const TypeId: unique symbol; // Symbol.for("@weftui/dom/WeftApp")
198
+ ```
71
199
 
72
- The runtime backing the handle lives until `unmount` runs, not until the
73
- `mount`/`hydrate` effect resolves — that effect completes right after initial
74
- render, while streams and handlers keep running in the background. If `mount` or
75
- `hydrate` runs inside a region that supplies an ambient `Scope.Scope` (e.g. under
76
- `Effect.scoped`), `unmount` is auto-registered on that scope as a finalizer, so the
77
- mount tears down when the scope closes; with no ambient scope, behavior is
78
- unchanged and `unmount` must be called explicitly. `mountScoped`/`hydrateScoped`
79
- register the same finalizer explicitly, so the typed variant does not silently
80
- depend on this auto-registration.
200
+ The unique brand for `WeftApp` values. Internal identity marker; rarely referenced
201
+ directly.
81
202
 
82
203
  ## `@weftui/dom/server`
83
204
 
@@ -136,7 +257,7 @@ Re-exports the renderer error types:
136
257
  ## See also
137
258
 
138
259
  - [Render on the Server](https://weftui.dev/docs/how-to/render-on-the-server) — a narrative walkthrough of the server/client split
139
- - [Provide Services](https://weftui.dev/docs/how-to/provide-services) — recipes for value layers, `mountScoped`, and `ManagedRuntime`
260
+ - [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
140
261
  - [The Rendering Model](https://weftui.dev/docs/explanation/rendering-model) — hydrate-in-place and why there is no virtual DOM
141
- - [Services and Context](https://weftui.dev/docs/explanation/services-and-context#layer-lifetime-at-the-mount) — why scoped layers need the mount to outlive initial render
262
+ - [Services and Context](https://weftui.dev/docs/explanation/services-and-context) — how services flow from the app layer to every root
142
263
  - [`@weftui/core` reference](https://weftui.dev/docs/reference/core) · [`@weftui/router` reference](https://weftui.dev/docs/reference/router)
@@ -203,7 +203,7 @@ RouterApp<E, R>(def: RouterDef<E, R>): Node<Exclude<E, RouterNotFound>, R | Rout
203
203
 
204
204
  The universal router root node — render this on both server and client. Wraps the nested outlet in the router's internal not-found boundary, so a `RouterNotFound` raised by a page renders the configured `notFound` page in place. Server dispatch runs through `HttpApiBuilder`: a page-raised `RouterNotFound` and a no-match surface their 404 through the platform request pipeline.
205
205
 
206
- `RouterApp` requires `Router` in its environment — provide it via `RouterLive` (client) or `RouterServer` (server), not `Effect.provide` at the node level (that would release the scoped layer immediately).
206
+ `RouterApp` requires `Router` in its environment — provide it via `RouterLive` (client) or `RouterServer` (server), not `Effect.provide` at the node level services under `WeftApp` come exclusively from the app's layer, not ambient `Effect.provide`.
207
207
 
208
208
  ### `outletNode` (a.k.a. `RouterOutlet`)
209
209
 
@@ -224,11 +224,11 @@ RouterLive(
224
224
  ): Layer.Layer<Router | AppRpcClientTag>;
225
225
  ```
226
226
 
227
- The client `Router` layer, backed by the History API. Seeds a `SubscriptionRef` from `window.location`, listens for `popstate`, installs the same-origin link-click interceptor, and derives the `HttpApiClient` exposed as `Router.httpApiClient` (over `FetchHttpClient`; `baseUrl` defaults to same-origin). Alongside `Router` it also provides the core [`AppRpcClientTag`](https://weftui.dev/docs/reference/core#apprpcclienttag) seam — a **network** flat rpc client over the app's merged `RpcGroup` (`RpcClient.make` → `POST /_eui/rpc`) — so `@weftui/dom` can resolve a [`Boundary.rpc`](https://weftui.dev/docs/reference/core#boundaryrpc) (hydrated refetch and client-first SPA mount) without depending on this package or `effect/unstable/rpc`. Pass the same merged `group` the server wires into [`RouterServer`](#routerserver). **Scoped** — it must outlive the mount, so provide it through a `ManagedRuntime`:
227
+ The client `Router` layer, backed by the History API. Seeds a `SubscriptionRef` from `window.location`, listens for `popstate`, installs the same-origin link-click interceptor, and derives the `HttpApiClient` exposed as `Router.httpApiClient` (over `FetchHttpClient`; `baseUrl` defaults to same-origin). Alongside `Router` it also provides the core [`AppRpcClientTag`](https://weftui.dev/docs/reference/core#apprpcclienttag) seam — a **network** flat rpc client over the app's merged `RpcGroup` (`RpcClient.make` → `POST /_eui/rpc`) — so `@weftui/dom` can resolve a [`Boundary.rpc`](https://weftui.dev/docs/reference/core#boundaryrpc) (hydrated refetch and client-first SPA mount) without depending on this package or `effect/unstable/rpc`. Pass the same merged `group` the server wires into [`RouterServer`](#routerserver). **Scoped** — it must outlive the mount; give it to `WeftApp.make` and the app runtime owns its lifetime (built lazily on first mount, released at `WeftApp.dispose`):
228
228
 
229
229
  ```typescript
230
- const runtime = ManagedRuntime.make(RouterLive(App, { rpc: { group: StockRpcs } }));
231
- void runtime.runPromise(hydrate(RouterApp(App), root));
230
+ const app = WeftApp.make(RouterLive(App, { rpc: { group: StockRpcs } }));
231
+ void Effect.runPromise(WeftApp.hydrate(app, RouterApp(App), root));
232
232
  ```
233
233
 
234
234
  ### Programmatic navigation
@@ -323,7 +323,7 @@ These power the runtime and are exported for tooling/tests; most apps never touc
323
323
  | Export | Description |
324
324
  | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
325
325
  | `compile(def)` | Walks a tree into flat `CompiledLeaf`s with merged path/query schemas and layout chains. |
326
- | `buildHttpApi(leaves)` | Builds the authoritative `HttpApi` (one `"pages"` group, a GET endpoint per leaf with `setPath`/`setUrlParams` + 404). Called by `Router.router`; the result is `def.httpApi`. |
326
+ | `buildHttpApi(leaves)` | Builds the authoritative `HttpApi` (one `"pages"` group, a GET endpoint per leaf with `params`/`query` schemas + 404). Called by `Router.router`; the result is `def.httpApi`. |
327
327
  | `leafRegistry` | `WeakMap<RouteNode, CompiledLeaf>` read by `href` to resolve a leaf's pattern/schemas. |
328
328
  | `match(compiled, url)` | Resolves a URL to a `RouteMatch` (`Matched` with decoded `path`/`query`, or `NotFound`). |
329
329
  | `compileMatchers(compiled)` | Precompiles per-leaf regex matchers. |
@@ -14,9 +14,11 @@ We assume you know [Effect](https://effect.website/docs/getting-started/introduc
14
14
  ## Install
15
15
 
16
16
  ```bash
17
- npm install @weftui/core @weftui/dom effect
17
+ npm install @weftui/core @weftui/dom effect@beta
18
18
  ```
19
19
 
20
+ Weft tracks Effect 4's beta line. This release is built and tested against `effect@4.0.0-beta.98`; the peer range accepts newer 4.0 betas, which may contain upstream breaking changes.
21
+
20
22
  `@weftui/core` gives you the element builders and combinators; `@weftui/dom` renders them (its `./client` entry mounts in the browser). `effect` is the peer everything is built on.
21
23
 
22
24
  ## Build a component
@@ -25,14 +27,15 @@ A **component is a plain function you call** — there is no JSX and no `<Compon
25
27
 
26
28
  ```typescript
27
29
  import { h } from "@weftui/core";
28
- import { mount } from "@weftui/dom/client";
30
+ import { WeftApp } from "@weftui/dom/client";
29
31
  import { Effect } from "effect";
30
32
 
31
33
  function App() {
32
34
  return h.div({ class: "app" }, [h.h1("Hello, Weft"), h.p("A minimal app.")]);
33
35
  }
34
36
 
35
- void Effect.runPromise(mount(App(), document.getElementById("root")!));
37
+ const app = WeftApp.make();
38
+ void Effect.runPromise(WeftApp.mount(app, App(), document.getElementById("root")!));
36
39
  ```
37
40
 
38
41
  The `h` namespace is the entry point: every property (`h.div`, `h.h1`, `h.button`, …) is a builder for that HTML tag. A builder takes optional props and children, and returns a `Node`.
@@ -40,7 +43,7 @@ The `h` namespace is the entry point: every property (`h.div`, `h.h1`, `h.button
40
43
  ## What just happened
41
44
 
42
45
  - `App()` returns a **`Node<never, never>`** — the two type parameters are the error channel (`E`) and the requirement channel (`R`), both `never` here because this component neither fails nor needs a service. As your app grows, those channels accumulate what it can fail with and what it depends on. That is the whole point of Weft's types — see [The Rendering Model](https://weftui.dev/docs/explanation/rendering-model).
43
- - `mount(node, target)` renders the node into `target`, building real DOM and starting any reactive streams. It returns an `Effect<MountHandle>`; run it with your Effect runtime (`Effect.runPromise` is fine for a script).
46
+ - `WeftApp.make()` creates a Weft app — synchronously, with no layer to build yet. `WeftApp.mount(app, node, target)` renders the node into `target`, building real DOM and starting any reactive streams. It returns `Effect<RootHandle, …>` with `R = never`, so a bare `Effect.runPromise` runs it no `Effect.provide` needed. You will give `WeftApp.make` a `Layer` once components need services — see [Services and Async](https://weftui.dev/docs/tutorial/03-services-and-async).
44
47
  - The component function runs **once**. Nothing here re-runs on a timer or a state change — because there is no state yet. That comes next.
45
48
 
46
49
  ## Next
@@ -15,7 +15,7 @@ Use Effect's `SubscriptionRef` for component-local state. `SubscriptionRef.chang
15
15
 
16
16
  ```typescript
17
17
  import { h } from "@weftui/core";
18
- import { mount } from "@weftui/dom/client";
18
+ import { WeftApp } from "@weftui/dom/client";
19
19
  import { Effect, SubscriptionRef } from "effect";
20
20
 
21
21
  const Counter = () =>
@@ -29,7 +29,8 @@ const Counter = () =>
29
29
  ]);
30
30
  });
31
31
 
32
- void Effect.runPromise(mount(Counter(), document.getElementById("root")!));
32
+ const app = WeftApp.make();
33
+ void Effect.runPromise(WeftApp.mount(app, Counter(), document.getElementById("root")!));
33
34
  ```
34
35
 
35
36
  `Effect.gen` lets you `yield*` the `SubscriptionRef` to set up state **before** building the tree. Because a `Node` is an `Effect`, the component body is an ordinary generator — no hooks, no dependency arrays.
@@ -11,12 +11,12 @@ description: Give handlers access to services from the environment, and render a
11
11
 
12
12
  ## Handlers that use services
13
13
 
14
- An event handler can **return an Effect**, and that Effect runs in the component's environment — so it can read any service you provide at the mount boundary:
14
+ An event handler can **return an Effect**, and that Effect runs in the app's environment — so it can read any service the app's layer provides:
15
15
 
16
16
  ```typescript
17
17
  import { h } from "@weftui/core";
18
- import { mount } from "@weftui/dom/client";
19
- import { Context, Effect, Layer, pipe } from "effect";
18
+ import { WeftApp } from "@weftui/dom/client";
19
+ import { Context, Effect, Layer } from "effect";
20
20
 
21
21
  class Logger extends Context.Service<Logger, { log: (message: string) => Effect.Effect<void> }>()(
22
22
  "Logger",
@@ -38,13 +38,12 @@ const LogButton = () =>
38
38
  "Log",
39
39
  );
40
40
 
41
- // Provide the layer at mount — every handler in the tree can now read Logger.
42
- void Effect.runPromise(
43
- pipe(mount(LogButton(), document.getElementById("root")!), Effect.provide(LoggerLive)),
44
- );
41
+ // Give the app the layer — every handler in every root can now read Logger.
42
+ const app = WeftApp.make(LoggerLive);
43
+ void Effect.runPromise(WeftApp.mount(app, LogButton(), document.getElementById("root")!));
45
44
  ```
46
45
 
47
- `Logger` entered the tree's requirement channel the moment `LogButton` read it, and you discharged it **once**, at `mount`, with `Effect.provide`. Provide too little and it is a compile error at the mount call. This is Weft's entire dependency-injection story — it is just Effect's. The deeper treatment is [Services and Context](https://weftui.dev/docs/explanation/services-and-context).
46
+ `Logger` entered the tree's requirement channel the moment `LogButton` read it, and you discharged it **once**, by passing `LoggerLive` to `WeftApp.make`. Provide too little and it is a compile error the type of `app` (and so of `WeftApp.mount(app, LogButton(), …)`) names exactly which service is missing. Services come exclusively from the app's layer: an `Effect.provide` wrapped around the `mount` call does **not** reach components or handlers. This is Weft's entire dependency-injection story — it is just Effect's. The deeper treatment is [Services and Context](https://weftui.dev/docs/explanation/services-and-context).
48
47
 
49
48
  ## Async loading states
50
49
 
@@ -52,7 +51,7 @@ A component can return a **`Stream<Node>`** to show different content over time.
52
51
 
53
52
  ```typescript
54
53
  import { h } from "@weftui/core";
55
- import { mount } from "@weftui/dom/client";
54
+ import { WeftApp } from "@weftui/dom/client";
56
55
  import { Effect, Stream } from "effect";
57
56
 
58
57
  const AsyncGreeting = ({ name }: { name: string }) =>
@@ -66,7 +65,10 @@ const AsyncGreeting = ({ name }: { name: string }) =>
66
65
  ),
67
66
  );
68
67
 
69
- void Effect.runPromise(mount(AsyncGreeting({ name: "World" }), document.getElementById("root")!));
68
+ const app = WeftApp.make();
69
+ void Effect.runPromise(
70
+ WeftApp.mount(app, AsyncGreeting({ name: "World" }), document.getElementById("root")!),
71
+ );
70
72
  ```
71
73
 
72
74
  The stream emits the loading node first, then the resolved node — the renderer swaps the DOM in place on the second emission. This is the raw mechanism; for coordinating _several_ async regions with a single fallback, reach for [`Boundary.suspend`](https://weftui.dev/docs/explanation/boundaries-and-suspense), which you will meet in the next step.
@@ -42,11 +42,12 @@ export const render = () => Effect.runPromise(renderToStringHydratable(App()));
42
42
 
43
43
  ```typescript
44
44
  // client entry
45
- import { hydrate } from "@weftui/dom/client";
45
+ import { WeftApp } from "@weftui/dom/client";
46
46
  import { Effect } from "effect";
47
47
  import { App } from "./app";
48
48
 
49
- void Effect.runPromise(hydrate(App(), document.getElementById("root")!));
49
+ const app = WeftApp.make();
50
+ void Effect.runPromise(WeftApp.hydrate(app, App(), document.getElementById("root")!));
50
51
  ```
51
52
 
52
53
  The same side-effect-free `App` is imported by both entries. For server-resolved data that replays into the client without a second request, `Boundary.rpc` extends this model — resolve an rpc on the server, serialize its result into the HTML, replay it on hydrate, then keep the region live for refetch.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@weftui/core",
3
- "version": "0.27.0",
3
+ "version": "0.28.0",
4
4
  "description": "Element builders and combinators for Weft — reactive UI, woven from Effect",
5
5
  "keywords": [
6
6
  "combinators",
@@ -41,15 +41,16 @@
41
41
  "access": "public"
42
42
  },
43
43
  "devDependencies": {
44
- "@types/node": "^25.9.2",
44
+ "@types/node": "^26.1.1",
45
45
  "csstype": "^3.2.3",
46
- "effect": "4.0.0-beta.93",
47
- "tsx": "^4.22.4",
48
- "typescript": "^6.0.3",
49
- "vite": "npm:@voidzero-dev/vite-plus-core@0.2.2",
50
- "vite-plus": "0.2.2"
46
+ "effect": "4.0.0-beta.98",
47
+ "tstyche": "^7.2.2",
48
+ "tsx": "^4.23.1",
49
+ "typescript": "^7.0.2",
50
+ "vite": "npm:@voidzero-dev/vite-plus-core@0.2.5",
51
+ "vite-plus": "0.2.5"
51
52
  },
52
53
  "peerDependencies": {
53
- "effect": "4.0.0-beta.93"
54
+ "effect": ">=4.0.0-beta.98 <4.0.0"
54
55
  }
55
56
  }