@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 +6 -3
- package/dist/{index-DMNuAQXj.d.ts → index-Bsh2WLtx.d.ts} +2 -4
- package/dist/index.d.ts +10 -17
- package/dist/types/index.d.ts +1 -1
- package/docs/explanation/boundaries-and-suspense.md +2 -2
- package/docs/explanation/reactive-primitives.md +1 -1
- package/docs/explanation/rendering-model.md +2 -2
- package/docs/explanation/services-and-context.md +13 -17
- package/docs/how-to/add-routing.md +7 -7
- package/docs/how-to/author-components.md +7 -5
- package/docs/how-to/load-data-with-rpc.md +4 -2
- package/docs/how-to/provide-services.md +52 -74
- package/docs/how-to/render-on-the-server.md +4 -3
- package/docs/index.md +2 -2
- package/docs/reference/dom.md +166 -45
- package/docs/reference/router.md +5 -5
- package/docs/tutorial/01-your-first-app.md +7 -4
- package/docs/tutorial/02-reactivity.md +3 -2
- package/docs/tutorial/03-services-and-async.md +12 -10
- package/docs/tutorial/04-errors-and-server.md +3 -2
- package/package.json +9 -8
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 {
|
|
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
|
-
|
|
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-
|
|
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
|
|
144
|
+
declare function catchTag<C extends readonly Renderable[], Tag extends ChildrenE<C> extends {
|
|
146
145
|
_tag: string;
|
|
147
|
-
} ? ChildrenE<C>["_tag"] : string
|
|
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
|
|
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
|
|
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
|
package/dist/types/index.d.ts
CHANGED
|
@@ -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-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 {
|
|
18
|
+
import { WeftApp } from "@weftui/dom/client";
|
|
19
19
|
|
|
20
|
-
const
|
|
21
|
-
|
|
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
|
|
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
|
|
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
|
|
28
|
+
## Layer lifetime and the app runtime
|
|
31
29
|
|
|
32
|
-
|
|
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
|
|
38
|
-
// mounted tree
|
|
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
|
-
|
|
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
|
|
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 `
|
|
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
|
|
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.
|
|
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 {
|
|
184
|
+
import { WeftApp } from "@weftui/dom/client";
|
|
185
185
|
import { RouterApp, RouterLive } from "@weftui/router/client";
|
|
186
|
-
import {
|
|
186
|
+
import { Effect } from "effect";
|
|
187
187
|
import { App } from "./app";
|
|
188
188
|
|
|
189
189
|
const root = document.getElementById("root")!;
|
|
190
|
-
const
|
|
191
|
-
void
|
|
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 `
|
|
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
|
|
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
|
-
|
|
238
|
+
Give the service to the app layer:
|
|
239
239
|
|
|
240
240
|
```typescript
|
|
241
|
-
|
|
242
|
-
|
|
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
|
|
95
|
-
void
|
|
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
|
|
5
|
+
description: Provide plain and scoped Layers to a WeftApp — app 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
|
|
10
|
+
**Goal:** provide a `Layer` to a `WeftApp` so its components can read services with `yield* Service`.
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
## Recipe 1 — app layers
|
|
13
13
|
|
|
14
|
-
|
|
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 {
|
|
20
|
-
import { 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
|
|
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
|
|
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
|
-
|
|
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 {
|
|
37
|
-
import {
|
|
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
|
|
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
|
-
|
|
44
|
+
`RouterLive` (from `@weftui/router/client`) is another — it 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
|
|
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
|
-
|
|
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 —
|
|
53
|
+
## Recipe 3 — sharing layer memoization with `memoMap`
|
|
82
54
|
|
|
83
|
-
|
|
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 {
|
|
87
|
-
import {
|
|
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
|
-
|
|
61
|
+
const memoMap = Layer.makeMemoMap();
|
|
95
62
|
|
|
96
|
-
|
|
97
|
-
|
|
63
|
+
const appA = WeftApp.make(SharedLive, { memoMap });
|
|
64
|
+
const appB = WeftApp.make(SharedLive, { memoMap });
|
|
98
65
|
```
|
|
99
66
|
|
|
100
|
-
|
|
67
|
+
Most apps have exactly one `WeftApp` and never need this option.
|
|
101
68
|
|
|
102
|
-
##
|
|
69
|
+
## Recipe 4 — binding an app's lifetime to a scope
|
|
103
70
|
|
|
104
|
-
|
|
71
|
+
There is deliberately no `makeScoped`. To tie an app's disposal to a `Scope` you already manage — a framework integration or a test harness that owns one — compose it yourself with `Effect.acquireRelease`:
|
|
105
72
|
|
|
106
73
|
```typescript
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
// ❌
|
|
113
|
-
//
|
|
114
|
-
|
|
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
|
-
|
|
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
|
-
- [
|
|
122
|
-
- [
|
|
123
|
-
- [
|
|
124
|
-
- [examples/
|
|
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 {
|
|
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
|
-
|
|
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 `
|
|
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
|
|
package/docs/reference/dom.md
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
104
|
+
const errors: <R, E>(app: WeftApp<R, E>) => Stream.Stream<UnhandledError>;
|
|
21
105
|
```
|
|
22
106
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
### `
|
|
114
|
+
### `WeftApp.dispose`
|
|
29
115
|
|
|
30
116
|
```ts
|
|
31
|
-
|
|
117
|
+
const dispose: <R, E>(app: WeftApp<R, E>) => Effect.Effect<void>;
|
|
32
118
|
```
|
|
33
119
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
### `
|
|
124
|
+
### `WeftApp<R, E>` (`WeftAppType`)
|
|
40
125
|
|
|
41
126
|
```ts
|
|
42
|
-
|
|
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
|
-
|
|
46
|
-
`
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
### `
|
|
138
|
+
### `RootHandle`
|
|
54
139
|
|
|
55
140
|
```ts
|
|
56
|
-
|
|
141
|
+
interface RootHandle {
|
|
142
|
+
readonly element: HTMLElement;
|
|
143
|
+
unmount(): Effect.Effect<void>;
|
|
144
|
+
}
|
|
57
145
|
```
|
|
58
146
|
|
|
59
|
-
|
|
60
|
-
`
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
### `
|
|
153
|
+
### `UnhandledError`
|
|
65
154
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
|
73
|
-
|
|
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
|
|
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
|
|
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)
|
package/docs/reference/router.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
231
|
-
void
|
|
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 `
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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 {
|
|
19
|
-
import { Context, Effect, Layer
|
|
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
|
-
//
|
|
42
|
-
|
|
43
|
-
|
|
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**,
|
|
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 {
|
|
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
|
-
|
|
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 {
|
|
45
|
+
import { WeftApp } from "@weftui/dom/client";
|
|
46
46
|
import { Effect } from "effect";
|
|
47
47
|
import { App } from "./app";
|
|
48
48
|
|
|
49
|
-
|
|
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.
|
|
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": "^
|
|
44
|
+
"@types/node": "^26.1.1",
|
|
45
45
|
"csstype": "^3.2.3",
|
|
46
|
-
"effect": "4.0.0-beta.
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"vite
|
|
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.
|
|
54
|
+
"effect": ">=4.0.0-beta.98 <4.0.0"
|
|
54
55
|
}
|
|
55
56
|
}
|