@weftui/router 0.28.0 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +3 -3
  2. package/dist/client/index.d.ts +19 -19
  3. package/dist/client/index.js +15 -15
  4. package/dist/{compile-DySac7KN.d.ts → compile-Bb7AknG_.d.ts} +37 -57
  5. package/dist/{href-uvJ6b7zz.js → href-3swSSbn_.js} +7 -10
  6. package/dist/index.d.ts +2 -2
  7. package/dist/index.js +2 -2
  8. package/dist/{outlet-saKduDFz.d.ts → outlet-2AnUWKQD.d.ts} +1 -1
  9. package/dist/{outlet-C9N4a4_F.js → outlet-BgJmsO_g.js} +38 -46
  10. package/dist/server/index.d.ts +9 -9
  11. package/dist/server/index.js +10 -10
  12. package/docs/explanation/boundaries-and-suspense.md +37 -18
  13. package/docs/explanation/combinator-api.md +14 -12
  14. package/docs/explanation/reactive-primitives.md +14 -14
  15. package/docs/explanation/rendering-model.md +20 -18
  16. package/docs/explanation/services-and-context.md +35 -21
  17. package/docs/how-to/add-routing.md +72 -48
  18. package/docs/how-to/author-components.md +40 -28
  19. package/docs/how-to/compose-behavior-and-markup.md +144 -0
  20. package/docs/how-to/handle-forms.md +6 -6
  21. package/docs/how-to/load-async-data.md +15 -13
  22. package/docs/how-to/load-data-with-rpc.md +30 -28
  23. package/docs/how-to/provide-services.md +20 -18
  24. package/docs/how-to/render-keyed-lists.md +10 -8
  25. package/docs/how-to/render-on-the-server.md +16 -12
  26. package/docs/how-to/show-navigation-progress.md +10 -8
  27. package/docs/how-to/split-routes-lazily.md +16 -14
  28. package/docs/how-to/style-reactively.md +13 -13
  29. package/docs/how-to/use-element-refs.md +10 -8
  30. package/docs/index.md +20 -18
  31. package/docs/reference/core.md +44 -40
  32. package/docs/reference/dom.md +274 -58
  33. package/docs/reference/router.md +69 -47
  34. package/docs/tutorial/01-your-first-app.md +7 -9
  35. package/docs/tutorial/02-reactivity.md +8 -6
  36. package/docs/tutorial/03-services-and-async.md +10 -6
  37. package/docs/tutorial/04-errors-and-server.md +14 -5
  38. package/package.json +4 -4
@@ -27,7 +27,7 @@ const notFound = (path) => Effect.fail(new RouterNotFound({ path }));
27
27
  const isRouterNotFound = (u) => typeof u === "object" && u !== null && "_tag" in u && u._tag === "RouterNotFound";
28
28
  /**
29
29
  * Tagged error raised by `Router.params` / `Router.query` when the live match does
30
- * not satisfy the requested fields either no route is matched, or a requested
30
+ * not satisfy the requested fields: either no route is matched, or a requested
31
31
  * key is missing / fails its schema's `Type`-side validation. `source` records
32
32
  * whether the failure was on the path params or the query, and `keys` lists the
33
33
  * requested field names for diagnostics.
@@ -93,9 +93,9 @@ function longestCommonSegmentPrefix(partsList) {
93
93
  * Pass 1 walks the tree: only **routes** contribute path parts (layouts own no
94
94
  * path), so each leaf's `parts` come solely from the route segments on its branch,
95
95
  * and its ancestor `LayoutNode`s are recorded in order. Pass 2 derives one shared
96
- * {@link CompiledLayout} per distinct layout node its `patternPrefix` is the
97
- * longest common path prefix of that layout's subtree leaves then assembles each
98
- * leaf's `layoutChain` (root → parent) and merged path schema.
96
+ * {@link CompiledLayout} per distinct layout node, whose `patternPrefix` is the
97
+ * longest common path prefix of that layout's subtree leaves. It then assembles
98
+ * each leaf's `layoutChain` (root → parent) and merged path schema.
99
99
  */
100
100
  function compile(def) {
101
101
  const leafWorks = [];
@@ -156,7 +156,7 @@ function compile(def) {
156
156
  }
157
157
  /**
158
158
  * Builds the authoritative `HttpApi` for a compiled tree (S4): a single `"pages"`
159
- * group whose endpoints are GET endpoints — one per leaf at each leaf's full path
159
+ * group holding one GET endpoint per leaf, at each leaf's full path
160
160
  * pattern, carrying `params: pathSchema`, `query: querySchema`, a
161
161
  * `Schema.String` (text/HTML) success, and a `RouterNotFound → 404` error. The tree
162
162
  * (not `HttpApi`) is the authoring surface; this is the single source of truth the
@@ -237,7 +237,7 @@ function makeLayout(config, children) {
237
237
  };
238
238
  }
239
239
  /**
240
- * Brand key marking a {@link ComponentSlot} as **preloadable** a lazy slot
240
+ * Brand key marking a {@link ComponentSlot} as **preloadable**: a lazy slot
241
241
  * ({@link lazyComponent}) carries a `preload()` under this key. Internal to
242
242
  * `@weftui/router` (read by the client `navigate` to resolve a matched branch's
243
243
  * chunks before commit; see `pending-navigation.specs.md`); not public API.
@@ -255,24 +255,16 @@ function getPreload(slot) {
255
255
  }
256
256
  /**
257
257
  * Wraps a dynamic-import `load` as a lazy {@link ComponentSlot}: the route's descriptor
258
- * (`segment`, `path`/`query`) stays eager and matchable, while the component the render
259
- * body and its module's deps is split into the chunk `load` resolves. The router invokes
258
+ * (`segment`, `path`/`query`) stays eager and matchable, while the component (the render
259
+ * body and its module's deps) is split into the chunk `load` resolves. The router invokes
260
260
  * the returned slot at render time; it awaits `load` then renders the resolved component,
261
261
  * adopting the server DOM in place on hydration (flash-free) and fetching the chunk on
262
262
  * client navigation. Exposed as {@link Router.lazy}. See `lazy-component.specs.md`.
263
263
  *
264
264
  * The resolved value is a component slot (`Component.gen` / `Component.make`, or a
265
- * `() => Node` thunk) the shape `component:` already accepts so its `E`/`R` channels
265
+ * `() => Node` thunk), the shape `component:` already accepts, so its `E`/`R` channels
266
266
  * are recovered via {@link SlotNode} and propagate up the tree exactly as an eager
267
267
  * component's do.
268
- *
269
- * @example
270
- * ```ts
271
- * Router.route("docs/:category/:slug", {
272
- * path: { category: Schema.String, slug: Schema.String },
273
- * component: Router.lazy(() => import("./doc-page").then((m) => m.DocPage)),
274
- * });
275
- * ```
276
268
  */
277
269
  function lazyComponent(load) {
278
270
  let cached;
@@ -296,7 +288,7 @@ var Router = class extends Context.Service()("@weftui/router/Router") {};
296
288
  * reads it with `yield* Router.Outlet`.
297
289
  *
298
290
  * Typed **opaque** as `Node<never, never>` so splicing `[outlet]` adds nothing to
299
- * a layout's local channels the subtree's real channels are aggregated
291
+ * a layout's local channels: the subtree's real channels are aggregated
300
292
  * structurally by {@link makeLayout} / {@link makeRouter}, never inferred across
301
293
  * this DI boundary. Re-exported on the namespace as `Router.Outlet`.
302
294
  */
@@ -309,10 +301,10 @@ function pick(fields, record) {
309
301
  }
310
302
  /**
311
303
  * Reads the live match's **path params** for the requested `fields`. Snapshot
312
- * semantics reads `yield* Router` then `currentMatch.get` and returns the
304
+ * semantics (reads `yield* Router`, then `currentMatch.get`), returning the
313
305
  * already-decoded values **directly**: the matcher decoded them against the leaf's
314
- * full path schema, so no re-validation is needed (the cast is sound the picked
315
- * subset is the `Type` side of `fields`). Fails with a {@link RouterParamsError}
306
+ * full path schema, so no re-validation is needed. The cast is sound because the
307
+ * picked subset is the `Type` side of `fields`. Fails with a {@link RouterParamsError}
316
308
  * (`source: "path"`) only when no route is matched. Re-exported as `Router.params`.
317
309
  */
318
310
  function readParams(fields) {
@@ -398,7 +390,7 @@ const subscribeNavigating = Effect.map(Router, (router) => router.navigating);
398
390
  //#endregion
399
391
  //#region src/resolved-commit.ts
400
392
  /**
401
- * Internal seams for resolve-before-commit navigation the resolved-commit
393
+ * Internal seams for resolve-before-commit navigation: the resolved-commit
402
394
  * stash and the staged match view. See `resolve-before-commit.specs.md`.
403
395
  *
404
396
  * **Not public API.** This module is deliberately not re-exported from
@@ -419,10 +411,10 @@ const subscribeNavigating = Effect.map(Router, (router) => router.navigating);
419
411
  */
420
412
  const ResolvedCommit = Symbol.for("@weftui/router/resolved-commit");
421
413
  /**
422
- * Writes the stash on the (client) router instance: called by `navigate` /
423
- * popstate with the pre-run's `Exit`, immediately before the URL ref is set,
424
- * so the outlet emission triggered by that commit finds it (AC-R1/AC-R2).
425
- * Overwrites any stale entry latest-wins already guarantees only the newest
414
+ * Writes the stash on the (client) router instance. Called by `navigate` /
415
+ * popstate with the pre-run's `Exit`, immediately before the URL ref is set, so
416
+ * the outlet emission triggered by that commit finds it (AC-R1/AC-R2).
417
+ * Overwrites any stale entry: latest-wins already guarantees only the newest
426
418
  * navigation reaches the commit step (AC-R6).
427
419
  */
428
420
  function setResolvedCommit(router, entry) {
@@ -430,10 +422,10 @@ function setResolvedCommit(router, entry) {
430
422
  }
431
423
  /**
432
424
  * Consumes the stash: returns the entry when its `url` equals the emission's
433
- * `match.url` and **clears the slot** (consume-exactly-once, AC-R2), else
434
- * `undefined` a URL mismatch (stale entry) or an absent slot (server render,
435
- * hydration, non-navigation re-emission) falls through to the ordinary slot
436
- * invocation in `renderLevel`.
425
+ * `match.url` and **clears the slot** (consume-exactly-once, AC-R2). Otherwise
426
+ * returns `undefined`, so a URL mismatch (stale entry) or an absent slot (server
427
+ * render, hydration, non-navigation re-emission) falls through to the ordinary
428
+ * slot invocation in `renderLevel`.
437
429
  */
438
430
  function takeResolvedCommit(router, url) {
439
431
  const slot = router;
@@ -444,13 +436,13 @@ function takeResolvedCommit(router, url) {
444
436
  }
445
437
  /**
446
438
  * The staged `Router` view the pre-run executes under (AC-R4): identical to
447
- * `router` except `currentMatch.get` resolves to the **target** match the
448
- * URL ref has not moved yet, and one-shot reads (`Router.params`,
449
- * `Router.query`, `currentMatch.get`) inside the pre-running component body
450
- * must decode the destination, not the page being left. `currentMatch.changes`
451
- * (and `navigate` / `httpApiClient` / `navigating`) delegate to the live
452
- * service, so reactive subscriptions — which occur at render/mount time,
453
- * post-commit — observe the committed match onward.
439
+ * `router` except `currentMatch.get` resolves to the **target** match. The URL
440
+ * ref has not moved yet, and one-shot reads (`Router.params`, `Router.query`,
441
+ * `currentMatch.get`) inside the pre-running component body must decode the
442
+ * destination, not the page being left. `currentMatch.changes` (and `navigate` /
443
+ * `httpApiClient` / `navigating`) delegate to the live service, so reactive
444
+ * subscriptions, which occur at render/mount time post-commit, observe the
445
+ * committed match onward.
454
446
  */
455
447
  function stageMatch(router, target) {
456
448
  return {
@@ -462,15 +454,15 @@ function stageMatch(router, target) {
462
454
  };
463
455
  }
464
456
  /**
465
- * Pre-runs a matched leaf's component effect: invokes the slot with the
466
- * target match's handler-arg props (`{ path, query }` exactly what
467
- * `renderLevel` passes), under the {@link stageMatch | staged view}, and
468
- * captures the outcome as an `Exit` (AC-R1/AC-R7). The returned effect never
469
- * fails — failures are folded into the `Exit` for stash-and-replay but it
470
- * is **interruptible**: a superseding navigation interrupts the whole pre-run
471
- * fiber (AC-R6). Requires the caller's runtime context (the `RouterLive`
472
- * layer's — `Router`, `AppRpcClientTag`, app services), which is what makes
473
- * the pre-run possible at all (Feasibility §1).
457
+ * Pre-runs a matched leaf's component effect: invokes the slot with the target
458
+ * match's handler-arg props (`{ path, query }`, exactly what `renderLevel`
459
+ * passes), under the {@link stageMatch | staged view}, and captures the outcome
460
+ * as an `Exit` (AC-R1/AC-R7). The returned effect never fails, since failures
461
+ * are folded into the `Exit` for stash-and-replay, but it is **interruptible**:
462
+ * a superseding navigation interrupts the whole pre-run fiber (AC-R6). Requires
463
+ * the caller's runtime context (the `RouterLive` layer's: `Router`,
464
+ * `AppRpcClientTag`, app services), which is what makes the pre-run possible at
465
+ * all (Feasibility §1).
474
466
  */
475
467
  function preRunLeaf(router, target) {
476
468
  return Effect.suspend(() => {
@@ -1,4 +1,4 @@
1
- import { S as Router, i as RouterDef, l as ComponentSlot } from "../compile-DySac7KN.js";
1
+ import { S as Router, i as RouterDef, l as ComponentSlot } from "../compile-Bb7AknG_.js";
2
2
  import { Effect, Layer } from "effect";
3
3
  import { AppRpcClientTag } from "@weftui/core";
4
4
  import { RpcGroup } from "effect/unstable/rpc";
@@ -8,15 +8,15 @@ import { RpcGroup } from "effect/unstable/rpc";
8
8
  * authoritative `HttpApi` spine via `HttpApiBuilder`: platform owns request→leaf
9
9
  * matching and path/query decode, then each leaf handler builds a fixed-match
10
10
  * server `Router`, renders the universal outlet to hydratable HTML, and replies
11
- * `text/html`. Status comes from the platform pipeline a no-match (platform's
11
+ * `text/html`. Status comes from the platform pipeline: a no-match (platform's
12
12
  * `RouteNotFound`) and a page-raised `RouterNotFound` both render the configured
13
- * `notFound` page at HTTP 404 so there is no render-time status side-channel.
13
+ * `notFound` page at HTTP 404, so there is no render-time status side-channel.
14
14
  */
15
15
  declare namespace RouterServer {
16
16
  /**
17
17
  * The app's `Boundary.rpc` data foundation: the merged `RpcGroup` contract plus
18
18
  * its server-only handler `Layer`. Wired explicitly (no co-located `load`, no
19
- * registry) `toWebHandler` serves it at `POST /_eui/rpc`, and an in-process
19
+ * registry): `toWebHandler` serves it at `POST /_eui/rpc`, and an in-process
20
20
  * client over the same handlers resolves SSR boundaries in-process.
21
21
  */
22
22
  export interface RpcOptions {
@@ -27,7 +27,7 @@ declare namespace RouterServer {
27
27
  }
28
28
  /**
29
29
  * Shared server options. The document shell is a {@link ComponentSlot} that splices
30
- * the app via `yield* Router.Outlet` (the router provides it per request)
30
+ * the app via `yield* Router.Outlet` (the router provides it per request),
31
31
  * typically `<html><head>…</head><body><div id="root">{app}</div><script …></body></html>`.
32
32
  * The router provides both `Router.Outlet` (the app, per request) and `Router` (to read
33
33
  * params), so the document may use either. Mirrors the route/layout `component` slot,
@@ -37,7 +37,7 @@ declare namespace RouterServer {
37
37
  * `context` is the render-time provide seam (spec:
38
38
  * `ambient-context-propagation.specs.md`): an app-wide `Layer` provided to the
39
39
  * document shell **and** every route/layout leaf. It is required exactly when the
40
- * def carries residual app services ({@link AppServices} its aggregate `R` minus
40
+ * def carries residual app services ({@link AppServices}: its aggregate `R` minus
41
41
  * the `Router` / `Router.Outlet` / `AppRpcClientTag` the router already threads) and
42
42
  * disallowed otherwise, so a missing provide is a compile error and `rpc`-only /
43
43
  * no-service apps stay unchanged (surfaced via {@link ContextOption} at each entry).
@@ -47,7 +47,7 @@ declare namespace RouterServer {
47
47
  readonly document: ComponentSlot;
48
48
  /**
49
49
  * The app's `Boundary.rpc` foundation (contract + server handlers). Optional:
50
- * omit when the app has no `Boundary.rpc` then `POST /_eui/rpc` is not
50
+ * omit when the app has no `Boundary.rpc`. Then `POST /_eui/rpc` is not
51
51
  * served (it falls through to page dispatch) and a stray `Boundary.rpc`
52
52
  * fails with a descriptive error instead of rendering.
53
53
  */
@@ -56,12 +56,12 @@ declare namespace RouterServer {
56
56
  /**
57
57
  * The residual app services a caller must still provide through the {@link Options.context}
58
58
  * seam: a def's aggregate requirement `R` **minus** the services the router already
59
- * threads in per render `Router` and `Router.Outlet` (provided by the outlet /
59
+ * threads in per render: `Router` and `Router.Outlet` (provided by the outlet /
60
60
  * document plumbing) and `AppRpcClientTag` (provided from the `rpc` option). When this
61
61
  * resolves to `never` the app has no app-wide service to inject and `context` is disallowed.
62
62
  */
63
63
  export type AppServices<R> = Exclude<R, Router | Router.Outlet | AppRpcClientTag>;
64
- /** True only for the exact `any` type a loosely-typed `RouterDef<any, any>`. */
64
+ /** True only for the exact `any` type: a loosely-typed `RouterDef<any, any>`. */
65
65
  type IsAny<T> = 0 extends 1 & T ? true : false;
66
66
  /**
67
67
  * Conditionally shapes the `context` field at each entry point: **required** (a
@@ -1,4 +1,4 @@
1
- import { a as Router, f as isRouterNotFound, n as outletNode, u as RouterNotFound } from "../outlet-C9N4a4_F.js";
1
+ import { a as Router, f as isRouterNotFound, n as outletNode, u as RouterNotFound } from "../outlet-BgJmsO_g.js";
2
2
  import { HttpApiBuilder, HttpApiEndpoint, HttpApiGroup } from "effect/unstable/httpapi";
3
3
  import { Cause, Effect, Exit, Layer, Option, Schema, Scope, Stream } from "effect";
4
4
  import { AppRpcClientTag, Subscribable } from "@weftui/core";
@@ -36,19 +36,19 @@ let RouterServer;
36
36
  /**
37
37
  * In-process {@link AppRpcClientTag} Layer over the app's handler Layer
38
38
  * ({@link RpcTest.makeClient}, flat, no protocol/serialization). SSR
39
- * `Boundary.rpc` resolution calls `call(tag, payload())` against this the rpc
39
+ * `Boundary.rpc` resolution calls `call(tag, payload())` against this: the rpc
40
40
  * runs in-process, never over the network.
41
41
  *
42
42
  * The render path requires the tag unconditionally, so with no `rpc` configured
43
- * a stub is provided whose `call` fails descriptively a `Boundary.rpc` in an
44
- * rpc-less app surfaces the misconfiguration instead of dying opaquely.
43
+ * a stub is provided whose `call` fails descriptively. A `Boundary.rpc` in an
44
+ * rpc-less app then surfaces the misconfiguration instead of dying opaquely.
45
45
  */
46
46
  function appRpcClientLayer(rpc) {
47
47
  if (rpc === void 0) return Layer.succeed(AppRpcClientTag, AppRpcClientTag.of({ call: (tag) => Effect.fail(/* @__PURE__ */ new Error(`Boundary.rpc "${tag}" cannot resolve: no \`rpc\` option was passed to RouterServer`)) }));
48
48
  return Layer.effect(AppRpcClientTag, Effect.map(RpcTest.makeClient(rpc.group, { flatten: true }), (flat) => AppRpcClientTag.of({ call: (tag, payload) => flat(tag, payload) }))).pipe(Layer.provide(rpc.handlers));
49
49
  }
50
50
  /**
51
- * Renders the document shell with `app` spliced via `Router.Outlet` to a
51
+ * Renders the document shell (with `app` spliced via `Router.Outlet`) to a
52
52
  * hydratable HTML string. The whole tree (shell + every route/layout leaf) drains
53
53
  * in this one `renderToStringHydratable` context, so the app-wide `options.context`
54
54
  * Layer provided here reaches the leaves too (the render-time provide seam). No
@@ -60,7 +60,7 @@ let RouterServer;
60
60
  /**
61
61
  * Renders the configured `notFound` page **directly** in the shell (no nested
62
62
  * outlet, no reactive-region markers) at `status`. Mirrors the client's internal
63
- * not-found boundary fallback which replaces the whole outlet subtree so the
63
+ * not-found boundary fallback, which replaces the whole outlet subtree, so the
64
64
  * page-raised-404 HTML aligns for hydration.
65
65
  */
66
66
  function renderNotFoundDirect(def, options, url, status) {
@@ -73,8 +73,8 @@ let RouterServer;
73
73
  /**
74
74
  * Renders the no-match case: the bare {@link outletNode} with a `NotFound` match,
75
75
  * so the `notFound` page renders **inside** the level-0 reactive region (markers
76
- * present) matching what the client outlet produces for an unmatched URL — at
77
- * HTTP 404.
76
+ * present) at HTTP 404, matching what the client outlet produces for an
77
+ * unmatched URL.
78
78
  */
79
79
  function renderNoMatch(def, options, url) {
80
80
  const router = serverRouter({
@@ -142,9 +142,9 @@ let RouterServer;
142
142
  /**
143
143
  * Builds (and memoizes) the platform `(Request) => Promise<Response>` handler for
144
144
  * `def`. Dispatch runs through a **server-local** `HttpApi`: `def.httpApi`
145
- * (pristine the client and the spec read it) extended with a second `"fallback"`
145
+ * (pristine: the client and the spec read it) extended with a second `"fallback"`
146
146
  * group holding one catch-all `"*"` endpoint. Platform owns matching: a request
147
- * routes to the specific leaf endpoint, or when nothing matches to the
147
+ * routes to the specific leaf endpoint, or, when nothing matches, to the
148
148
  * catch-all, which renders the configured not-found page at 404. (Platform's own
149
149
  * unmatched path resolves a default empty 404 before any response hook can rewrite
150
150
  * it, so the catch-all is the route that keeps no-match rendering ours.)
@@ -2,18 +2,20 @@
2
2
  title: Boundaries and Suspense
3
3
  order: 4
4
4
  section: explanation
5
- description: How Weft models failure, async, and server data as boundary nodes in the same tree failure-catch variants, Boundary.suspend, and Boundary.rpc, and how their E/R channels behave.
5
+ description: How Weft models failure, async, and server data as boundary nodes in the same tree. Covers failure-catch variants, Boundary.suspend, and Boundary.rpc, and how their E/R channels behave.
6
6
  ---
7
7
 
8
8
  # Boundaries and Suspense
9
9
 
10
- A **boundary** is a node that intercepts something flowing through the tree an error, a pending async child, or a server-resolved value and decides what the DOM shows in its place. Because a boundary is itself a `Node<E, R>` ([nodes are Effects](https://weftui.dev/docs/explanation/rendering-model)), it composes exactly like any other element: you nest it, and its children's channels flow through it under a transformation the boundary defines.
10
+ A **boundary** is a node that intercepts something flowing through the tree: an error, a pending async child, or a server-resolved value. It decides what the DOM shows in its place.
11
+
12
+ A boundary is itself a `Node<E, R>` ([nodes are Effects](https://weftui.dev/docs/explanation/rendering-model)), so it composes exactly like any other element. You nest it, and its children's channels flow through it under a transformation the boundary defines.
11
13
 
12
14
  The `Boundary` namespace has three kinds. This page is the conceptual map; the [core reference](https://weftui.dev/docs/reference/core#boundary-namespace) has the full signatures.
13
15
 
14
16
  ## Failure boundaries
15
17
 
16
- A component's `E` channel accumulates up the tree. A **failure boundary** is where you _discharge_ some of that `E`: it wraps children and, if one of them fails, renders a fallback instead of letting the failure propagate to the mount.
18
+ A component's `E` channel accumulates up the tree. A **failure boundary** is where you _discharge_ some of that `E`. It wraps children and, if one of them fails, renders a fallback instead of letting the failure propagate to the mount.
17
19
 
18
20
  ```typescript
19
21
  import { Boundary, h } from "@weftui/core";
@@ -32,19 +34,25 @@ There are six failure-catch variants, mirroring Effect's own error operators so
32
34
  | `catchTag` / `catchTags` | one / several tagged errors by `_tag` |
33
35
  | `catchFilter` / `catchIf` | a selected subset, by `Filter` / predicate |
34
36
 
35
- The channel algebra is the whole reason they exist: `catchTag("Foo", …)` removes `Foo` from the children's `E` and adds whatever the fallback needs so the type of the boundary node reflects exactly which failures are still live and which were handled. An unhandled failure re-raises to the **nearest enclosing** boundary; if none catches it at **mount time**, mounting fails. Boundaries nest, so an inner `catchTag` can handle a specific case while an outer `catch` sweeps the rest.
37
+ The channel algebra is the whole reason they exist. `catchTag("Foo", …)` removes `Foo` from the children's `E` and adds whatever the fallback needs. The type of the boundary node therefore reflects exactly which failures are still live and which were handled.
38
+
39
+ An unhandled failure re-raises to the **nearest enclosing** boundary; if none catches it at **mount time**, mounting fails. Boundaries nest, so an inner `catchTag` can handle a specific case while an outer `catch` sweeps the rest.
36
40
 
37
41
  ### Post-mount failures with no enclosing boundary
38
42
 
39
- The routing above describes what happens while a node is being built. Once mounted, a reactive region an attribute, child, or list stream, or a hydrated equivalent keeps running for the lifetime of its scope, and it can still fail later: a `Stream` backing a `Boundary.rpc` resource might raise `RouterNotFound` after a client-side navigation, for instance. If a `BoundaryContext` encloses the region, the failure routes to it exactly as above, and the boundary's fallback swaps in.
43
+ The routing above describes what happens while a node is being built. Once mounted, a reactive region (an attribute, child, or list stream, or a hydrated equivalent) keeps running for the lifetime of its scope. It can still fail later: a `Stream` backing a `Boundary.rpc` resource might raise `RouterNotFound` after a client-side navigation. If a `BoundaryContext` encloses the region, the failure routes to it exactly as above, and the boundary's fallback swaps in.
44
+
45
+ If no boundary encloses it, there is nothing to swap to. Weft does not synthesize one. The region's DOM keeps its last rendered content, and a watcher fiber (forked into the same scope alongside the subscription itself) observes its exit directly.
46
+
47
+ When that exit is a failure whose cause is not interruption-only, Weft reports it explicitly via `Effect.logError(exit.cause)`. The log is annotated with `weft.region` to identify the failing region by kind and identity (e.g. `attribute:class`, `child:stream-3`, `list:stream-2`, `hydrate:stream-1 (/products/42)`). This fires for typed failures and defects alike, in both dev and prod, exactly once per failing region, at the `"Error"` level. Interruption (the ordinary case of unmount tearing down the region's scope) is never reported; only genuine failures are.
40
48
 
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.
49
+ This is deliberate. Rather than leave the failure to whatever the Effect runtime would do with an unobserved fiber exit, Weft observes and logs it itself. Visibility is therefore controlled by the same knobs any Effect program uses: `References.MinimumLogLevel` (provided via `Effect.provideService`) to filter it, or a custom `Logger` to route it elsewhere.
42
50
 
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.
51
+ A stream that can fail and has no enclosing boundary is a stream whose failures you've chosen not to route into the UI. The log is what tells you that decision has consequences at runtime.
44
52
 
45
53
  ## Suspense boundaries
46
54
 
47
- `Boundary.suspend` wraps async children and shows a `fallback` until **all** of them have emitted their first value, then swaps atomically either everything is visible or nothing is. This prevents partial flicker when sibling async regions resolve at different times.
55
+ `Boundary.suspend` wraps async children and shows a `fallback` until **all** of them have emitted their first value. Then it swaps atomically: either everything is visible or nothing is. This prevents partial flicker when sibling async regions resolve at different times.
48
56
 
49
57
  ```typescript
50
58
  import { Boundary, h } from "@weftui/core";
@@ -55,15 +63,22 @@ Boundary.suspend({ fallback: h.div({ class: "spinner" }, "Loading…") }, [
55
63
  ]);
56
64
  ```
57
65
 
58
- A suspense boundary is transparent to the type channels: its node is `Node<ChildrenE, ChildrenR>` the children's `E`/`R` pass straight through, exactly as they would for a plain `h.*` parent. It changes _timing_ (when the children become visible), not _types_.
66
+ A suspense boundary is transparent to the type channels: its node is `Node<ChildrenE, ChildrenR>`. The children's `E`/`R` pass straight through, exactly as they would for a plain `h.*` parent. It changes _timing_ (when the children become visible), not _types_.
59
67
 
60
- On the server, `renderToStreamHydratable` emits the fallback inline and appends patch scripts as children resolve; on the client, `hydrate` sees through the boundary and adopts the already-resolved DOM directly.
68
+ On the server, `renderToStreamHydratable` emits the fallback inline and appends patch scripts as children resolve. On the client, `hydrate` sees through the boundary and adopts the already-resolved DOM directly.
61
69
 
62
- > **Note.** There is no `Suspense` export the API is `Boundary.suspend(props, children)`. Reach for it for async that loads **on the client**; for data that must resolve on the **server** and hydrate without a second request, use `Boundary.rpc` (below).
70
+ > **Note.** There is no `Suspense` export; the API is `Boundary.suspend(props, children)`. Reach for it for async that loads **on the client**. For data that must resolve on the **server** and hydrate without a second request, use `Boundary.rpc` (below).
63
71
 
64
72
  ## The rpc boundary
65
73
 
66
- `Boundary.rpc` is the server-data boundary: it resolves one `Rpc` on the server, serializes the result into the HTML, replays it on the client during `hydrate` (no second request, no flash), and then keeps the region live for `refetch`. Conceptually it is the same idea as the other boundaries — a node that decides what renders in a subtree — but the thing it intercepts is a **round-trip to a server handler**, and instead of a children array it takes a `render` function that receives a reactive [`Resource`](https://weftui.dev/docs/reference/core#resourcea).
74
+ `Boundary.rpc` is the server-data boundary. It:
75
+
76
+ - resolves one `Rpc` on the server
77
+ - serializes the result into the HTML
78
+ - replays it on the client during `hydrate` (no second request, no flash)
79
+ - keeps the region live for `refetch`
80
+
81
+ Conceptually it is the same idea as the other boundaries: a node that decides what renders in a subtree. But the thing it intercepts is a **round-trip to a server handler**. Instead of a children array, it takes a `render` function that receives a reactive [`Resource`](https://weftui.dev/docs/reference/core#resourcea).
67
82
 
68
83
  ```typescript
69
84
  import { Boundary, h } from "@weftui/core";
@@ -77,15 +92,19 @@ Boundary.rpc(
77
92
  );
78
93
  ```
79
94
 
80
- Unlike the failure and suspense boundaries, `Boundary.rpc` is not self-contained: it resolves through the ambient [`AppRpcClientTag`](https://weftui.dev/docs/reference/core#apprpcclienttag) seam that `@weftui/router` provides on both sides. Its channel behavior is also distinct — the rpc's typed `error` schema joins the node's `E` (replayable through an enclosing failure boundary), while `render`'s `R` passes through untouched. The full model — the contract/handler split, the four lifecycles, typed-failure replay — is a **how-to**, not repeated here: [Load Data with RPC](https://weftui.dev/docs/how-to/load-data-with-rpc).
95
+ Unlike the failure and suspense boundaries, `Boundary.rpc` is not self-contained. It resolves through the ambient [`AppRpcClientTag`](https://weftui.dev/docs/reference/core#apprpcclienttag) seam that `@weftui/router` provides on both sides.
96
+
97
+ Its channel behavior is also distinct. The rpc's typed `error` schema joins the node's `E` (replayable through an enclosing failure boundary), while `render`'s `R` passes through untouched. The full model (the contract/handler split, the four lifecycles, typed-failure replay) is a **how-to**, not repeated here: [Load Data with RPC](https://weftui.dev/docs/how-to/load-data-with-rpc).
81
98
 
82
99
  ## One tree, three interceptors
83
100
 
84
- The unifying idea: failure, async pending state, and server data are not three separate subsystems bolted onto the renderer. They are three **boundary nodes** in the one tree, each intercepting a different thing flowing through it, each with channel behavior you can read off its type. That is why they nest freely — a `Boundary.catchTag` can wrap a `Boundary.rpc` to catch its typed failure, and a `Boundary.suspend` can wrap async siblings that themselves contain rpc boundaries.
101
+ The unifying idea: failure, async pending state, and server data are not three separate subsystems bolted onto the renderer. They are three **boundary nodes** in the one tree, each intercepting a different thing flowing through it. Each has channel behavior you can read off its type.
102
+
103
+ That is why they nest freely. A `Boundary.catchTag` can wrap a `Boundary.rpc` to catch its typed failure. A `Boundary.suspend` can wrap async siblings that themselves contain rpc boundaries.
85
104
 
86
105
  ## See also
87
106
 
88
- - [The Rendering Model](https://weftui.dev/docs/explanation/rendering-model) why a boundary is just a node in a static tree
89
- - [`Boundary` API reference](https://weftui.dev/docs/reference/core#boundary-namespace) every variant's signature and channel algebra
90
- - [Load Data with RPC](https://weftui.dev/docs/how-to/load-data-with-rpc) the full `Boundary.rpc` walkthrough and its four lifecycles
91
- - [Render on the Server](https://weftui.dev/docs/how-to/render-on-the-server) how suspense and rpc boundaries stream and hydrate
107
+ - [The Rendering Model](https://weftui.dev/docs/explanation/rendering-model): why a boundary is just a node in a static tree
108
+ - [`Boundary` API reference](https://weftui.dev/docs/reference/core#boundary-namespace): every variant's signature and channel algebra
109
+ - [Load Data with RPC](https://weftui.dev/docs/how-to/load-data-with-rpc): the full `Boundary.rpc` walkthrough and its four lifecycles
110
+ - [Render on the Server](https://weftui.dev/docs/how-to/render-on-the-server): how suspense and rpc boundaries stream and hydrate
@@ -7,7 +7,9 @@ description: How h, h.fragment, and Component.gen / Component.make work; why Nod
7
7
 
8
8
  # The Combinator API
9
9
 
10
- Weft builds UI trees by calling builder functions. Because component return types stay as generic `Effect.Effect<ElementDescriptor, E, R>`, the error channel (`E`) and requirements channel (`R`) propagate through the entire tree visible to the type checker, satisfiable at the mount boundary. JSX collapses every component's return type to an opaque `JSX.Element`, erasing both channels; the combinator API exists specifically to keep them intact.
10
+ Weft builds UI trees by calling builder functions. Because component return types stay as generic `Effect.Effect<ElementDescriptor, E, R>`, the error channel (`E`) and requirements channel (`R`) propagate through the entire tree: visible to the type checker, satisfiable at the mount boundary.
11
+
12
+ JSX collapses every component's return type to an opaque `JSX.Element`, erasing both channels. The combinator API exists specifically to keep them intact.
11
13
 
12
14
  ## Nodes are Effects
13
15
 
@@ -23,13 +25,13 @@ Nodes are first-class Effects. Everything in the Effect ecosystem works on them
23
25
  import { h } from "@weftui/core";
24
26
  import { Effect } from "effect";
25
27
 
26
- // yield* in Effect.gen R propagates into the generator's context
28
+ // yield* in Effect.gen: R propagates into the generator's context
27
29
  const node = yield * h.div({ class: "container" }, "Hello");
28
30
 
29
- // pipe chain Effect operators directly
31
+ // pipe: chain Effect operators directly
30
32
  const provided = pipe(h.div(userStream), Effect.provide(UserServiceLive));
31
33
 
32
- // Effect.flatMap sequence node creation with async logic
34
+ // Effect.flatMap: sequence node creation with async logic
33
35
  const card = pipe(
34
36
  fetchCard(id),
35
37
  Effect.flatMap((data) => h.div({ class: "card" }, data.title)),
@@ -78,7 +80,7 @@ Reactive prop values (any `Stream`, `Effect`, or `Subscribable`) contribute thei
78
80
  ```typescript
79
81
  declare const colorStream: Stream.Stream<string, never, ThemeService>;
80
82
 
81
- // Node<never, ThemeService> R comes from the stream prop
83
+ // Node<never, ThemeService>: R comes from the stream prop
82
84
  const box = h.div({ style: { color: colorStream } }, "Hello");
83
85
  ```
84
86
 
@@ -130,7 +132,7 @@ declare const labelStream: Stream.Stream<string, never, I18nService>;
130
132
  const btn = Button({ label: labelStream });
131
133
  ```
132
134
 
133
- Components also accept an optional `children` argument, either as `readonly Renderable[]` or as a `(input) => readonly Renderable[]` function (render-prop pattern). `E`/`R` from children including the array returned by a function-children call accumulate on the resulting node.
135
+ Components also accept an optional `children` argument, either as `readonly Renderable[]` or as a `(input) => readonly Renderable[]` function (render-prop pattern). `E`/`R` from children, including the array returned by a function-children call, accumulate on the resulting node.
134
136
 
135
137
  Without `Component`, a plain function's return type is fixed at definition time and does not reflect the caller's reactive prop types.
136
138
 
@@ -149,16 +151,16 @@ Boundary.suspend({ fallback: h.div({ class: "spinner" }, "Loading...") }, [
149
151
  ]);
150
152
  ```
151
153
 
152
- The fallback is replaced atomically either all children are visible or none are. This prevents partial flicker when multiple async siblings resolve at different times. The boundary's node type is `Node<ChildrenE, ChildrenR>`: the children's `E`/`R` channels accumulate onto it, exactly as they would for a plain `h.*` parent.
154
+ The fallback is replaced atomically: either all children are visible or none are. This prevents partial flicker when multiple async siblings resolve at different times. The boundary's node type is `Node<ChildrenE, ChildrenR>`: the children's `E`/`R` channels accumulate onto it, exactly as they would for a plain `h.*` parent.
153
155
 
154
156
  On the server, `renderToStreamHydratable` emits the fallback inline and appends patch scripts as children resolve. On the client, `hydrate` sees through `Boundary.suspend` boundaries and adopts the already-resolved DOM directly.
155
157
 
156
- `Boundary.suspend` is one of the boundary combinators see the [core reference](https://weftui.dev/docs/reference/core#boundarysuspend) for the full `Boundary.*` surface, including the failure-catch variants and `Boundary.rpc`.
158
+ `Boundary.suspend` is one of the boundary combinators. See the [core reference](https://weftui.dev/docs/reference/core#boundarysuspend) for the full `Boundary.*` surface, including the failure-catch variants and `Boundary.rpc`.
157
159
 
158
160
  ## See also
159
161
 
160
- - [The Rendering Model](https://weftui.dev/docs/explanation/rendering-model) why a `Node` is an `Effect` and how the tree renders
161
- - [Reactive Primitives](https://weftui.dev/docs/explanation/reactive-primitives) the `Source` vocabulary that reactive props and children accept
162
- - [Boundaries and Suspense](https://weftui.dev/docs/explanation/boundaries-and-suspense) the boundary combinators as tree nodes
163
- - [Author Components](https://weftui.dev/docs/how-to/author-components) `Component.gen` / `Component.make` in practice
162
+ - [The Rendering Model](https://weftui.dev/docs/explanation/rendering-model): why a `Node` is an `Effect` and how the tree renders
163
+ - [Reactive Primitives](https://weftui.dev/docs/explanation/reactive-primitives): the `Source` vocabulary that reactive props and children accept
164
+ - [Boundaries and Suspense](https://weftui.dev/docs/explanation/boundaries-and-suspense): the boundary combinators as tree nodes
165
+ - [Author Components](https://weftui.dev/docs/how-to/author-components): `Component.gen` / `Component.make` in practice
164
166
  - [`@weftui/core` reference](https://weftui.dev/docs/reference/core)
@@ -7,14 +7,14 @@ description: The Source<A, E, R> vocabulary; Stream, Effect, and Subscribable as
7
7
 
8
8
  # Reactive Primitives
9
9
 
10
- The unified `Source` vocabulary is what lets static values, Effects, Streams, and Subscribables be used interchangeably wherever reactivity is supported — props, children, and style values all accept the same type.
10
+ The unified `Source` vocabulary lets static values, Effects, Streams, and Subscribables be used interchangeably. Props, children, and style values all accept the same type.
11
11
 
12
12
  Weft accepts a `Source` for prop values and children. Any of these is valid wherever reactivity is supported:
13
13
 
14
14
  - A plain static value (`string`, `number`, `boolean`, ...)
15
- - An `Effect.Effect<A, E, R>` runs once and resolves to a value
16
- - A `Stream.Stream<A, E, R>` each emission replaces the previous value
17
- - A `Subscribable<A, E, R>` like a hot stream; already has a "current value"
15
+ - An `Effect.Effect<A, E, R>`: runs once and resolves to a value
16
+ - A `Stream.Stream<A, E, R>`: each emission replaces the previous value
17
+ - A `Subscribable<A, E, R>`: like a hot stream; already has a "current value"
18
18
 
19
19
  The `Source<A, E, R>` type captures this union:
20
20
 
@@ -24,7 +24,7 @@ type Source<A, E, R> = A | Effect.Effect<A, E, R> | Stream.Stream<A, E, R> | Sub
24
24
 
25
25
  ## Static values
26
26
 
27
- Static props behave exactly as you'd expect — set once and never updated:
27
+ Static props are set once and never updated:
28
28
 
29
29
  ```typescript
30
30
  h.div({ class: "container", id: "root" }, "Hello");
@@ -45,17 +45,17 @@ The `E` and `R` channels of the Effect flow into the node's own channels.
45
45
 
46
46
  ## Stream props and children
47
47
 
48
- Streams are the primary reactive primitive. Each emission replaces the previous value in the DOM no diffing, direct DOM update:
48
+ Streams are the primary reactive primitive. Each emission replaces the previous value in the DOM (no diffing, direct DOM update):
49
49
 
50
50
  ```typescript
51
51
  import { SubscriptionRef, Stream } from "effect";
52
52
 
53
53
  const count = yield * SubscriptionRef.make(0);
54
54
 
55
- // SubscriptionRef.changes(count) is a Stream<number> each new value updates the text node
55
+ // SubscriptionRef.changes(count) is a Stream<number>: each new value updates the text node
56
56
  h.span([SubscriptionRef.changes(count)]);
57
57
 
58
- // Stream as a prop each emission sets the attribute
58
+ // Stream as a prop: each emission sets the attribute
59
59
  const isDisabled = Stream.map(SubscriptionRef.changes(count), (n) => n >= 10);
60
60
  h.button({ disabled: isDisabled }, "Submit");
61
61
  ```
@@ -119,7 +119,7 @@ h.div({ style: styleObjectStream });
119
119
 
120
120
  // Combine a whole-object stream with a static property.
121
121
  // A whole-object stream replaces every property on each emit, so fold the
122
- // static value into each emitted object with Stream.map you cannot spread the
122
+ // static value into each emitted object with Stream.map. You cannot spread the
123
123
  // Stream itself into a style object (that copies the Stream's internals, not
124
124
  // its emitted style keys).
125
125
  h.div({
@@ -154,11 +154,11 @@ pipe(
154
154
  );
155
155
  ```
156
156
 
157
- In practice you only encounter `NoPropValue` if you use a finite `Stream` as a prop and it ends before emitting e.g., `Stream.empty` or `Stream.take(0, stream)`. Most usage with `SubscriptionRef.changes` or infinite streams never raises it.
157
+ In practice you only encounter `NoPropValue` when a finite `Stream` prop ends before emitting (e.g., `Stream.empty` or `Stream.take(0, stream)`). Most usage with `SubscriptionRef.changes` or infinite streams never raises it.
158
158
 
159
159
  ## See also
160
160
 
161
- - [The Rendering Model](https://weftui.dev/docs/explanation/rendering-model) streams as the weft woven through a static tree
162
- - [The Combinator API](https://weftui.dev/docs/explanation/combinator-api) how reactive props and children contribute `E`/`R`
163
- - [Style Reactively](https://weftui.dev/docs/how-to/style-reactively) and [Render Keyed Lists](https://weftui.dev/docs/how-to/render-keyed-lists) reactive props and collections in practice
164
- - [`Source` reference](https://weftui.dev/docs/reference/core#source-namespace) the `Source` type and `Source.toSubscribable`
161
+ - [The Rendering Model](https://weftui.dev/docs/explanation/rendering-model): streams as the weft woven through a static tree
162
+ - [The Combinator API](https://weftui.dev/docs/explanation/combinator-api): how reactive props and children contribute `E`/`R`
163
+ - [Style Reactively](https://weftui.dev/docs/how-to/style-reactively) and [Render Keyed Lists](https://weftui.dev/docs/how-to/render-keyed-lists): reactive props and collections in practice
164
+ - [`Source` reference](https://weftui.dev/docs/reference/core#source-namespace): the `Source` type and `Source.toSubscribable`