@weftui/core 0.28.0 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/README.md +8 -8
  2. package/dist/{index-Bsh2WLtx.d.ts → index-CX9uEejU.d.ts} +70 -51
  3. package/dist/index.d.ts +68 -119
  4. package/dist/index.js +1 -1
  5. package/dist/types/index.d.ts +1 -1
  6. package/docs/explanation/boundaries-and-suspense.md +39 -20
  7. package/docs/explanation/combinator-api.md +14 -12
  8. package/docs/explanation/reactive-primitives.md +14 -14
  9. package/docs/explanation/rendering-model.md +20 -18
  10. package/docs/explanation/services-and-context.md +35 -21
  11. package/docs/how-to/add-routing.md +361 -60
  12. package/docs/how-to/author-components.md +42 -30
  13. package/docs/how-to/compose-behavior-and-markup.md +144 -0
  14. package/docs/how-to/handle-forms.md +6 -6
  15. package/docs/how-to/load-async-data.md +15 -13
  16. package/docs/how-to/load-data-with-rpc.md +34 -32
  17. package/docs/how-to/provide-services.md +20 -18
  18. package/docs/how-to/render-keyed-lists.md +14 -12
  19. package/docs/how-to/render-on-the-server.md +18 -14
  20. package/docs/how-to/show-navigation-progress.md +12 -10
  21. package/docs/how-to/split-routes-lazily.md +16 -14
  22. package/docs/how-to/style-reactively.md +13 -13
  23. package/docs/how-to/use-element-refs.md +10 -8
  24. package/docs/index.md +20 -18
  25. package/docs/reference/core.md +46 -42
  26. package/docs/reference/dom.md +274 -58
  27. package/docs/reference/router.md +69 -47
  28. package/docs/tutorial/01-your-first-app.md +7 -9
  29. package/docs/tutorial/02-reactivity.md +8 -6
  30. package/docs/tutorial/03-services-and-async.md +10 -6
  31. package/docs/tutorial/04-errors-and-server.md +14 -5
  32. package/package.json +2 -2
package/dist/index.d.ts CHANGED
@@ -1,16 +1,16 @@
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";
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-CX9uEejU.js";
2
2
  import { Cause, Context, Effect, Filter, Option, Stream } from "effect";
3
3
  import { Rpc } from "effect/unstable/rpc";
4
4
  //#region src/combinator/types.d.ts
5
5
  /**
6
6
  * Widens a single prop value type so that Stream/Effect/Subscribable variants
7
- * accept any E and R not just `never`. Static values (string, number, etc.)
7
+ * accept any E and R, not just `never`. Static values (string, number, etc.)
8
8
  * are left unchanged. TypeScript distributes this over union types.
9
9
  */
10
10
  type OpenPropSource<T> = T extends Stream.Stream<infer A, any, any> ? Stream.Stream<A, any, any> : T extends Effect.Effect<infer A, any, any> ? Effect.Effect<A, any, any> : T extends Subscribable<infer A, any, any> ? Subscribable<A, any, any> : T;
11
11
  /**
12
12
  * A node in the combinator tree.
13
- * IS an Effect `yield*`, `Effect.gen`, and `pipe` all work natively. Resolves
13
+ * IS an Effect: `yield*`, `Effect.gen`, and `pipe` all work natively. Resolves
14
14
  * to an {@link ElementDescriptor}.
15
15
  *
16
16
  * Declared as an interface (rather than a bare alias) so it can merge with the
@@ -32,17 +32,17 @@ declare namespace Node {
32
32
  /** Extract the requirement channel `R` from a {@link Node} (a static node ⇒ `never`). */
33
33
  type Context<N> = [N] extends [Effect.Effect<any, any, infer R>] ? R : never;
34
34
  }
35
- /** Extract E from a props object Stream/Effect/Subscribable prop values and Effect-returning event handlers contribute their E channel. */
35
+ /** Extract E from a props object: Stream/Effect/Subscribable prop values and Effect-returning event handlers contribute their E channel. */
36
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];
37
- /** Extract R from a props object Stream/Effect/Subscribable prop values and Effect-returning event handlers contribute their R channel. */
37
+ /** Extract R from a props object: Stream/Effect/Subscribable prop values and Effect-returning event handlers contribute their R channel. */
38
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];
39
- /** Extract E from a children array Node (Effect) and Stream children contribute their E. */
39
+ /** Extract E from a children array: Node (Effect) and Stream children contribute their E. */
40
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];
41
- /** Extract R from a children array Node (Effect) and Stream children contribute their R. */
41
+ /** Extract R from a children array: Node (Effect) and Stream children contribute their R. */
42
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];
43
43
  /**
44
44
  * Strip `children` from HTML prop types and widen all Source prop types to
45
- * allow any E/R so callers can pass `Stream<T, E, R>` with real requirements.
45
+ * allow any E/R, so callers can pass `Stream<T, E, R>` with real requirements.
46
46
  */
47
47
  type CombinatorialProps<P> = { [K in keyof Omit<P, "children">]: OpenPropSource<Omit<P, "children">[K]>; };
48
48
  declare namespace boundary_impl_d_exports {
@@ -76,8 +76,8 @@ type CatchTagsE<C extends readonly Renderable[], Tags extends string> = Exclude<
76
76
  * variant wraps a subtree and shows a fallback in response to an event.
77
77
  *
78
78
  * - **Failure boundaries** (`catch`, `catchCause`, `catchTag`,
79
- * `catchTags`, `catchFilter`, `catchIf`) intercept rendering-path errors
80
- * construction-time errors and post-mount stream failures mirroring
79
+ * `catchTags`, `catchFilter`, `catchIf`) intercept rendering-path errors
80
+ * (construction-time errors and post-mount stream failures), mirroring
81
81
  * Effect's `catch*` combinators.
82
82
  * - **Suspense boundary** (`suspend`) shows a fallback while async children are
83
83
  * pending, then swaps to the resolved children once all have settled.
@@ -90,7 +90,7 @@ type CatchTagsE<C extends readonly Renderable[], Tags extends string> = Exclude<
90
90
  * ```ts
91
91
  * import { Boundary, h } from "@weftui/core";
92
92
  *
93
- * // Failure boundary wrapping a suspense boundary the common pairing:
93
+ * // Failure boundary wrapping a suspense boundary, the common pairing:
94
94
  * Boundary.catch({ fallback: (e) => h.div({}, e.message) }, [
95
95
  * Boundary.suspend({ fallback: h.div({}, "Loading…") }, [AsyncCard()]),
96
96
  * ])
@@ -109,7 +109,7 @@ interface FailureProps {
109
109
  readonly match: (cause: Cause.Cause<unknown>) => Node<unknown, unknown> | null;
110
110
  }
111
111
  /**
112
- * Props for the {@link suspend} boundary used by renderers to access
112
+ * Props for the {@link suspend} boundary, read by renderers to access
113
113
  * `fallback` and `children` from the node descriptor.
114
114
  */
115
115
  interface SuspenseProps {
@@ -185,7 +185,7 @@ declare function catchTags<C extends readonly Renderable[], Handlers extends { r
185
185
  */
186
186
  declare function catchFilter<C extends readonly Renderable[], EB, X, FE = never, FR = never>(filter: Filter.Filter<ChildrenE<C>, EB, X>, fallback: (matched: EB) => Node<FE, FR>, children: C): Node<X | FE, ChildrenR<C> | FR>;
187
187
  /**
188
- * Conditionally catch a predicate gates the fallback. If the predicate
188
+ * Conditionally catch: a predicate gates the fallback. If the predicate
189
189
  * returns `false`, the error is re-raised. The children's `E` is preserved
190
190
  * in the output since the boundary may not handle any given error.
191
191
  */
@@ -196,19 +196,10 @@ declare function catchIf<C extends readonly Renderable[], FE = never, FR = never
196
196
  /**
197
197
  * Creates a suspense boundary node.
198
198
  *
199
- * Shows `fallback` while async children are pending (have not yet emitted
200
- * their first value), then atomically swaps to the resolved children once
201
- * **all** pending children have settled.
202
- *
203
- * The renderer (`@weftui/dom`) identifies the boundary via its
204
- * {@link SUSPENSE_BOUNDARY} type tag.
205
- *
206
- * @example
207
- * ```ts
208
- * import { Boundary, h } from "@weftui/core";
209
- *
210
- * Boundary.suspend({ fallback: h.div({}, "Loading…") }, [AsyncCard(), AsyncSidebar()])
211
- * ```
199
+ * Shows `fallback` while async children are pending (have not yet emitted their
200
+ * first value), then atomically swaps to the resolved children once **all**
201
+ * pending children have settled. The renderer (`@weftui/dom`) identifies the
202
+ * boundary via its {@link SUSPENSE_BOUNDARY} type tag.
212
203
  */
213
204
  declare function suspend<C extends readonly Renderable[]>(props: SuspenseProps, children: C): Node<ChildrenE<C>, ChildrenR<C>>;
214
205
  /**
@@ -220,7 +211,7 @@ declare function suspend<C extends readonly Renderable[]>(props: SuspenseProps,
220
211
  *
221
212
  * On the **server** and on the **first client paint after hydrate** `value`
222
213
  * emits the SSR `data` first (await-first), so SSR HTML and the adopted DOM are
223
- * byte-identical no fallback flash.
214
+ * byte-identical, with no fallback flash.
224
215
  *
225
216
  * @typeParam A - The loaded data shape.
226
217
  */
@@ -240,7 +231,7 @@ interface Resource<A> {
240
231
  readonly pending: Subscribable<boolean>;
241
232
  /**
242
233
  * `Some` with the last refetch error, else `None`. A failed refetch leaves
243
- * the previous `value` intact (stale-on-error) it does **not** unmount the
234
+ * the previous `value` intact (stale-on-error). It does **not** unmount the
244
235
  * subtree or raise into an enclosing failure `Boundary`.
245
236
  */
246
237
  readonly error: Subscribable<Option.Option<unknown>>;
@@ -262,7 +253,7 @@ interface RpcOptions {
262
253
  *
263
254
  * The boundary is a thin consumer of one `Rpc` from the application's merged
264
255
  * `RpcGroup`. Its data source is the ambient {@link AppRpcClient}: there is no
265
- * co-located `load`, no `provide`, no per-boundary `id`/registry the rpc
256
+ * co-located `load`, no `provide`, no per-boundary `id`/registry. The rpc
266
257
  * **tag** is the stable identity and the rpc **payload schema** is the typed
267
258
  * input. The handler lives in the server-only rpc Layer, so nothing here needs a
268
259
  * bundler prune.
@@ -272,7 +263,7 @@ interface RpcOptions {
272
263
  * the rpc's `successSchema` inline as a `<script type="application/json">`
273
264
  * payload, and renders `render(seededResource)` to HTML in place.
274
265
  * - **Hydrate**: reads the inline payload at the cursor, decodes via
275
- * `successSchema`, seeds the {@link Resource}, and adopts the DOM replaying
266
+ * `successSchema`, seeds the {@link Resource}, and adopts the DOM, replaying
276
267
  * the server result, never re-calling the rpc.
277
268
  * - **Refetch**: `Resource.refetch` calls `AppRpcClient.call(tag, payload())`
278
269
  * over the network and patches the subtree in place (stale-on-error).
@@ -281,7 +272,7 @@ interface RpcOptions {
281
272
  * `render(resource)` once it resolves.
282
273
  *
283
274
  * `payload` is a thunk so a fresh payload is produced per call (SSR, refetch,
284
- * mount) its return type is the rpc's decoded payload. The renderer identifies
275
+ * mount). Its return type is the rpc's decoded payload. The renderer identifies
285
276
  * the boundary via its {@link SERVER_BOUNDARY} type tag.
286
277
  *
287
278
  * @example
@@ -308,9 +299,9 @@ declare function rpc<R extends Rpc.Any, C extends Node<any, any>>(rpc: R, payloa
308
299
  /**
309
300
  * Ambient, package-neutral seam for resolving a {@link Boundary.rpc} boundary's
310
301
  * data through the application's merged `RpcGroup`. The DOM renderer
311
- * (`@weftui/dom`) must resolve a boundary on the server (SSR), during a
312
- * client refetch, and on a client-first SPA mount — **without** importing
313
- * `effect/unstable/rpc` or `@weftui/router`. So the rpc caller is injected as a
302
+ * (`@weftui/dom`) must resolve a boundary **without** importing
303
+ * `effect/unstable/rpc` or `@weftui/router`, on the server (SSR), during a
304
+ * client refetch, and on a client-first SPA mount. So the rpc caller is injected as a
314
305
  * service: `@weftui/router` provides it (a network `RpcClient` on the browser,
315
306
  * an in-process client over the handler layer on the server), and the renderer
316
307
  * reads it from ambient context, treating `Option.none` (no router/rpc present)
@@ -334,8 +325,8 @@ interface AppRpcClient {
334
325
  }
335
326
  declare const AppRpcClientTag_base: Context.ServiceClass<AppRpcClientTag, "@weftui/core/AppRpcClient", AppRpcClient>;
336
327
  /**
337
- * Context tag for the {@link AppRpcClient} seam. Provided by `@weftui/router`
338
- * a network client (`RouterLive`, POST `/_eui/rpc`) on the client, an
328
+ * Context tag for the {@link AppRpcClient} seam. Provided by `@weftui/router`:
329
+ * a network client (`RouterLive`, POST `/_eui/rpc`) on the client, an
339
330
  * in-process client over the handler layer (`RouterServer`) on the server. Absent
340
331
  * in a router-less mount, where a {@link Boundary.rpc} resolves to a descriptive
341
332
  * "needs router/rpc" error.
@@ -345,7 +336,7 @@ declare class AppRpcClientTag extends AppRpcClientTag_base {}
345
336
  //#region src/server/brand.d.ts
346
337
  /**
347
338
  * Type-level marker stamped onto the identifier of a {@link ServerTag}. It never
348
- * exists at runtime only its `typeof` is referenced by {@link ServerOnly} so
339
+ * exists at runtime (only its `typeof` is referenced by {@link ServerOnly}), so
349
340
  * the brand is purely a compile-time discriminator for {@link AssertNoServerOnly}.
350
341
  */
351
342
  declare const ServerOnlyTypeId: unique symbol;
@@ -398,10 +389,9 @@ type AssertNoServerOnly<R> = [Extract<R, ServerOnly>] extends [never] ? R : Serv
398
389
  */
399
390
  declare function isStream(value: unknown): value is Stream.Stream<unknown, any, any>;
400
391
  /**
401
- * Normalizes a static value, `Effect`, or `Stream` into a `Stream` the
402
- * weft equivalent of Vue's `unref`. Static values become a single-element
403
- * stream, Effects become a one-shot stream, and existing Streams pass through
404
- * unchanged.
392
+ * Normalizes a static value, `Effect`, or `Stream` into a `Stream`, the weft
393
+ * equivalent of Vue's `unref`. Static values become a single-element stream,
394
+ * Effects become a one-shot stream, and existing Streams pass through unchanged.
405
395
  */
406
396
  declare function toStream<A>(value: A | Effect.Effect<A> | Stream.Stream<A>): Stream.Stream<A>;
407
397
  //#endregion
@@ -409,16 +399,15 @@ declare function toStream<A>(value: A | Effect.Effect<A> | Stream.Stream<A>): St
409
399
  /** Augmentable interface for user-defined custom element tags and props. */
410
400
  interface CustomElements {}
411
401
  /**
412
- * Callable type for an element builder (one per tag `h.div`, `h.span`, …).
413
- *
414
- * Four call shapes are supported; each preserves the caller's prop and child
415
- * `E`/`R` channels on the returned {@link Node}:
402
+ * Callable type for an element builder (one per tag: `h.div`, `h.span`, …).
403
+ * Every call shape preserves the caller's prop and child `E`/`R` channels on the
404
+ * returned {@link Node}:
416
405
  *
417
- * - `el(props, children)` props plus an array of children.
418
- * - `el(props, child)` props plus a single `string | number` child.
419
- * - `el(props)` props only, no children.
420
- * - `el(children)` children only, no props.
421
- * - `el()` no arguments; yields a `Node<never, never>`.
406
+ * - `el(props, children)`: props plus an array of children.
407
+ * - `el(props, child)`: props plus a single `string | number` child.
408
+ * - `el(props)`: props only, no children.
409
+ * - `el(children)`: children only, no props.
410
+ * - `el()`: no arguments, yielding a `Node<never, never>`.
422
411
  */
423
412
  interface ElementFn<Props> {
424
413
  <P extends Props, const C extends readonly Renderable[]>(props: P, children: C): Node<PropsE<P> | ChildrenE<C>, PropsR<P> | ChildrenR<C>>;
@@ -433,14 +422,9 @@ type DataAttributes = {
433
422
  };
434
423
  type H = {
435
424
  /**
436
- * Builds a fragment node containing the given children children are rendered
437
- * inline, with no wrapping element. Equivalent to `<>…</>` in JSX. `E`/`R`
438
- * from the children accumulate on the returned {@link Node}.
439
- *
440
- * @example
441
- * ```ts
442
- * h.fragment([h.span({}, "left"), h.span({}, "right")]);
443
- * ```
425
+ * Builds a fragment node whose children render inline, with no wrapping
426
+ * element. Equivalent to `<>…</>` in JSX. `E`/`R` from the children accumulate
427
+ * on the returned {@link Node}.
444
428
  */
445
429
  fragment<const C extends readonly Renderable[]>(children: C): Node<ChildrenE<C>, ChildrenR<C>>;
446
430
  } & { [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>; };
@@ -450,8 +434,8 @@ declare const h: H;
450
434
  /**
451
435
  * Builds a static-markup {@link Node} for a known {@link ElementDescriptor}.
452
436
  *
453
- * The result is a normal `Effect.succeed(descriptor)` fully `yield*`-able
454
- * inside `Component.gen` and indistinguishable to Effect but additionally
437
+ * The result is a normal `Effect.succeed(descriptor)` (fully `yield*`-able
438
+ * inside `Component.gen` and indistinguishable to Effect), but additionally
455
439
  * carries the descriptor on a non-enumerable property. Renderers detect this via
456
440
  * {@link getElementDescriptor} and render the descriptor directly, with no
457
441
  * `runSync` probe (the reason `h.*`, `h.fragment`, and `Boundary.*` never need to
@@ -463,8 +447,8 @@ declare function elementNode<E = never, R = never>(descriptor: ElementDescriptor
463
447
  * built with {@link elementNode}, or `undefined` if `node` is anything else
464
448
  * (a primitive, an iterable, a `Stream`, or a reactive `Effect`).
465
449
  *
466
- * Renderers call this to take the static fast path rendering the descriptor
467
- * directly before falling back to running genuinely reactive Effects.
450
+ * Renderers call this to take the static fast path (rendering the descriptor
451
+ * directly) before falling back to running genuinely reactive Effects.
468
452
  */
469
453
  declare function getElementDescriptor(node: unknown): ElementDescriptor | undefined;
470
454
  //#endregion
@@ -479,7 +463,7 @@ declare const FRAGMENT: unique symbol;
479
463
  * `FRAGMENT` and the `Boundary` symbols are detected.
480
464
  */
481
465
  declare const LIST: unique symbol;
482
- /** Element type carried by a list source the element type of the emitted `Iterable`. */
466
+ /** Element type carried by a list source: the element type of the emitted `Iterable`. */
483
467
  type ItemOf<S> = Source.Success<S> extends Iterable<infer T> ? T : never;
484
468
  /**
485
469
  * Keyed-list combinator namespace. The opt-in alternative to wholesale child
@@ -507,30 +491,20 @@ declare namespace List {
507
491
  readonly by?: (item: ItemOf<S>, index: number) => K;
508
492
  }
509
493
  /**
510
- * Declares a keyed reactive list region.
511
- *
512
- * `render` runs **once per key**; a persisted key keeps its DOM nodes and its
513
- * running subscription fibers across re-emits (it is never re-invoked). The
514
- * returned node's `E`/`R` are the union of the source channels and the
515
- * channels of the node `render` returns.
516
- *
517
- * @example
518
- * ```ts
519
- * List.each(
520
- * { of: peopleStream, by: (p) => p.id },
521
- * (person) => h.li({}, person.name),
522
- * );
523
- * ```
494
+ * Declares a keyed reactive list region. `render` runs **once per key**; a
495
+ * persisted key keeps its DOM nodes and its running subscription fibers across
496
+ * re-emits (it is never re-invoked). The returned node's `E`/`R` are the union
497
+ * of the source channels and the channels of the node `render` returns.
524
498
  */
525
499
  function each<S extends Source.Source<Iterable<any>, any, any>, CE = never, CR = never, K = ItemOf<S>>(options: Options<S, K>, render: (item: ItemOf<S>, index: number) => Node<CE, CR>): Node<Source.Error<S> | CE, Source.Context<S> | CR>;
526
500
  /**
527
- * Extract the error channel `E` from a list {@link Node} re-exported from
501
+ * Extract the error channel `E` from a list {@link Node}, re-exported from
528
502
  * {@link Node.Error}, the canonical accessor (a `Node`'s success channel is
529
503
  * fixed to `ElementDescriptor`, so only `Error`/`Context` are exposed here).
530
504
  */
531
505
  type Error<N> = Node.Error<N>;
532
506
  /**
533
- * Extract the requirement channel `R` from a list {@link Node} re-exported
507
+ * Extract the requirement channel `R` from a list {@link Node}, re-exported
534
508
  * from {@link Node.Context}, the canonical accessor.
535
509
  */
536
510
  type Context<N> = Node.Context<N>;
@@ -540,45 +514,39 @@ declare namespace List {
540
514
  /**
541
515
  * Factories for building custom components whose returned `Node`s carry the
542
516
  * caller's prop `E`/`R` channels and the component's own internal `E`/`R`.
543
- *
544
- * Two flavours are provided:
545
- *
546
- * - `Component.gen` body is a generator (use `yield*` like `Effect.gen`).
547
- * - `Component.make` — body is a plain function returning any `Effect`.
548
- *
549
- * Both accept an optional second `children` argument which may be either an
550
- * array of {@link Renderable} or a function `(input) => readonly Renderable[]` (the
551
- * render-prop / function-children pattern).
517
+ * `Component.gen` takes a generator body (use `yield*` like `Effect.gen`);
518
+ * `Component.make` takes a plain function returning any `Effect`. Both accept an
519
+ * optional second `children` argument: an array of {@link Renderable}, or a
520
+ * function `(input) => readonly Renderable[]` (the render-prop pattern).
552
521
  */
553
522
  declare namespace Component {
554
523
  /**
555
524
  * Shape of the optional `children` argument to a {@link Component}.
556
525
  *
557
- * - `readonly Renderable[]` a flat list of children, the common case.
558
- * - `(input: Input) => readonly Renderable[]` function-children: the component
526
+ * - `readonly Renderable[]`: a flat list of children, the common case.
527
+ * - `(input: Input) => readonly Renderable[]`: function-children. The component
559
528
  * supplies `input` (some scoped value) and the caller returns the children
560
529
  * array. Useful for render-prop / slot patterns.
561
530
  */
562
531
  type Children<Input = never> = readonly Renderable[] | ((input: Input) => readonly Renderable[]);
563
532
  /**
564
- * Extract the error channel `E` from the {@link Node} a component produces
533
+ * Extract the error channel `E` from the {@link Node} a component produces,
565
534
  * re-exported from {@link Node.Error}, the canonical accessor. Apply to a
566
535
  * component's return type, e.g. `Component.Error<ReturnType<typeof MyComponent>>`.
567
536
  */
568
537
  type Error<N> = Node.Error<N>;
569
538
  /**
570
539
  * Extract the requirement channel `R` from the {@link Node} a component
571
- * produces re-exported from {@link Node.Context}, the canonical accessor.
540
+ * produces, re-exported from {@link Node.Context}, the canonical accessor.
572
541
  */
573
542
  type Context<N> = Node.Context<N>;
574
543
  /**
575
544
  * The callable shape returned by {@link gen} and {@link make}. Generic over
576
545
  * the caller's specific `GenP`/`GenC` so reactive prop values and reactive
577
546
  * children contribute their `E`/`R` to the resulting `Node` at the call site.
578
- *
579
547
  * For function-children, `ChildrenE`/`ChildrenR` are extracted from the
580
- * function's `ReturnType` the array the caller would produce not from
581
- * the function itself.
548
+ * function's `ReturnType` (the array the caller would produce), not from the
549
+ * function itself.
582
550
  */
583
551
  type Component<P, C extends Children, E, R> = <GenP extends P, GenC extends C>(props: GenP, children?: GenC) => Node<PropsE<GenP> | ChildrenE<GenC extends ((...args: any[]) => any) ? ReturnType<GenC> : GenC> | E, PropsR<GenP> | ChildrenR<GenC extends ((...args: any[]) => any) ? ReturnType<GenC> : GenC> | R>;
584
552
  /**
@@ -612,30 +580,11 @@ declare namespace Component {
612
580
  function gen<Eff extends Effect.Effect<any, any, any>, BaseProps = Record<string, never>, C extends Children = readonly Renderable[]>(f: (props: BaseProps, children: C) => Generator<Eff, ElementDescriptor, never>): Component.Component<BaseProps, C, Eff extends Effect.Effect<any, infer E, any> ? E : never, Eff extends Effect.Effect<any, any, infer R> ? R : never>;
613
581
  /**
614
582
  * Defines a component whose body is a plain function returning any `Effect`
615
- * (typically a {@link Node}). Use when the implementation is a one-liner or
616
- * a pipe composition no generator overhead.
617
- *
618
- * Same `E`/`R` propagation semantics as {@link gen}: internal `E`/`R` come
619
- * from the returned effect, caller props and children contribute at the
620
- * call site, and function-children are supported via the optional second
621
- * argument.
622
- *
623
- * @example
624
- * ```ts
625
- * const Avatar = Component.make((props: { src: string }) =>
626
- * h.img({ src: props.src, alt: "" }),
627
- * );
628
- * ```
629
- *
630
- * @example With function-children
631
- * ```ts
632
- * const List = Component.make(
633
- * (
634
- * props: { items: readonly string[] },
635
- * children: (item: string) => readonly Renderable[],
636
- * ) => h.ul({}, props.items.flatMap(children)),
637
- * );
638
- * ```
583
+ * (typically a {@link Node}). Use when the implementation is a one-liner or a
584
+ * pipe composition, with no generator overhead. Same `E`/`R` propagation
585
+ * semantics as {@link gen}: internal `E`/`R` come from the returned effect,
586
+ * caller props and children contribute at the call site, and function-children
587
+ * are supported via the optional second argument.
639
588
  */
640
589
  function make<Eff extends Effect.Effect<any, any, any>, BaseProps = Record<string, never>, C extends Children = readonly Renderable[]>(f: (props: BaseProps, children: C) => Eff): Component<BaseProps, C, Eff extends Effect.Effect<any, infer E, any> ? E : never, Eff extends Effect.Effect<any, any, infer R> ? R : never>;
641
590
  }
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- import{t as e}from"./rolldown-runtime-DK3Fl9T5.js";import{Cause as t,Context as n,Data as r,Deferred as i,Effect as a,Filter as o,Option as s,Predicate as c,Result as l,Stream as u,SubscriptionRef as d,identity as f,pipe as p}from"effect";const m=Symbol.for(`@weftui/core/ElementDescriptor`);function h(e){let t=a.succeed(e);return Object.defineProperty(t,m,{value:e,enumerable:!1}),t}function g(e){return a.isEffect(e)&&m in e?e[m]:void 0}var _=e({FAILURE_BOUNDARY:()=>v,SERVER_BOUNDARY:()=>b,SUSPENSE_BOUNDARY:()=>y,catch:()=>S,catchCause:()=>C,catchFilter:()=>E,catchIf:()=>D,catchTag:()=>w,catchTags:()=>T,rpc:()=>k,suspend:()=>O});const v=Symbol.for(`weft/FAILURE_BOUNDARY`),y=Symbol.for(`weft/SUSPENSE_BOUNDARY`),b=Symbol.for(`weft/SERVER_BOUNDARY`);function x(e,t){return h({type:v,props:{match:e,children:t}})}function S(e,n){return x(n=>{let r=t.findErrorOption(n);return s.isSome(r)?e.fallback(r.value):null},n)}function C(e,t){return x(t=>e.fallback(t),t)}function w(e,n){return x(n=>{let r=t.findErrorOption(n);if(s.isNone(r))return null;let i=r.value;return i._tag===e.tag?e.fallback(i):null},n)}function T(e,n){return x(n=>{let r=t.findErrorOption(n);if(s.isNone(r))return null;let i=r.value,a=i._tag;if(a===void 0)return null;let o=e[a];return o?o(i):null},n)}function E(e,n,r){return x(r=>{let i=t.findErrorOption(r);if(s.isNone(i))return null;let a=e(i.value);return l.isSuccess(a)?n(a.success):null},r)}function D(e,n){return x(n=>{let r=t.findErrorOption(n);if(s.isNone(r))return null;let i=r.value;return e.predicate(i)?e.fallback(i):null},n)}function O(e,t){return h({type:y,props:{...e,children:t}})}function k(e,t,n,r){let i=e;return h({type:b,props:{tag:i._tag,payloadSchema:i.payloadSchema,successSchema:i.successSchema,errorSchema:i.errorSchema,payload:t,render:n,fallback:r?.fallback}})}var A=class extends n.Service()(`@weftui/core/AppRpcClient`){};const j=e=>()=>n.Service()(e);function M(e){return typeof e==`object`&&!!e&&u.TypeId in e}function N(e){return M(e)?e:a.isEffect(e)?u.fromEffect(e):u.make(e)}var P=e({TypeId:()=>F,changes:()=>R,get:()=>L,isSubscribable:()=>z,make:()=>I});const F=`~@weftui/core/Subscribable`,I=e=>({[F]:F,get:e.get,changes:e.changes}),L=e=>e.get,R=e=>e.changes,z=e=>c.hasProperty(e,F);var B=class extends r.TaggedError(`NoPropValue`){};let V;(function(e){function n(e,n){return z(e)?a.succeed(e):M(e)?a.gen(function*(){let r=yield*d.make(s.none()),c=yield*i.make(),l=yield*i.make(),m=p(u.runForEach(e,e=>p(d.set(r,s.some(e)),a.andThen(i.succeed(c,e)),a.asVoid)),a.ensuring(p(d.get(r),a.flatMap(e=>s.isNone(e)?a.asVoid(i.fail(c,new B({key:n}))):a.void))),a.onError(e=>t.hasInterruptsOnly(e)?a.void:a.asVoid(i.failCause(l,e))));yield*a.forkScoped(m);let h=p(d.get(r),a.flatMap(e=>s.isSome(e)?a.succeed(e.value):i.await(c))),g=p(d.changes(r),u.filterMap(o.fromPredicateOption(f)),u.interruptWhen(i.await(l)));return I({get:h,changes:g})}):a.isEffect(e)?a.gen(function*(){let t=yield*a.cached(e),n=u.fromEffect(t);return I({get:t,changes:n})}):a.succeed(I({get:a.succeed(e),changes:u.make(e)}))}e.toSubscribable=n})(V||={});const H=Symbol(`@weftui/core/fragment`);function U(e){return((t,n)=>{let r={},i;return Array.isArray(t)||typeof t==`string`||typeof t==`number`?i=t:t!==void 0&&(r=t,n!==void 0&&(i=n)),h({type:e,props:i===void 0?r:{...r,children:i}})})}function W(e=new Map){return new Proxy({fragment(e){return h({type:H,props:{children:e}})}},{get(t,n){return n in t?t[n]:e.get(n)??e.set(n,U(n)).get(n)}})}const G=W(new Map),K=Symbol(`@weftui/core/list`);let q;(function(e){function t(e,t){return h({type:K,props:{of:e.of,by:e.by,render:t}})}e.each=t})(q||={});let J;(function(e){function t(e){return(t,n=[])=>a.gen(function*(){return yield*e(t,n)})}e.gen=t;function n(e){return e}e.make=n})(J||={});export{A as AppRpcClientTag,_ as Boundary,J as Component,v as FAILURE_BOUNDARY,H as FRAGMENT,K as LIST,q as List,B as NoPropValue,b as SERVER_BOUNDARY,y as SUSPENSE_BOUNDARY,j as ServerTag,V as Source,P as Subscribable,h as elementNode,g as getElementDescriptor,G as h,M as isStream,N as toStream};
1
+ import{t as e}from"./rolldown-runtime-DK3Fl9T5.js";import{Cause as t,Context as n,Data as r,Deferred as i,Effect as a,Filter as o,Option as s,Predicate as c,Result as l,Stream as u,SubscriptionRef as d,identity as f,pipe as p}from"effect";const m=Symbol.for(`@weftui/core/ElementDescriptor`);function h(e){let t=a.succeed(e);return Object.defineProperty(t,m,{value:e,enumerable:!1}),t}function g(e){return a.isEffect(e)&&m in e?e[m]:void 0}var _=e({FAILURE_BOUNDARY:()=>v,SERVER_BOUNDARY:()=>b,SUSPENSE_BOUNDARY:()=>y,catch:()=>S,catchCause:()=>C,catchFilter:()=>E,catchIf:()=>D,catchTag:()=>w,catchTags:()=>T,rpc:()=>k,suspend:()=>O});const v=Symbol.for(`weft/FAILURE_BOUNDARY`),y=Symbol.for(`weft/SUSPENSE_BOUNDARY`),b=Symbol.for(`weft/SERVER_BOUNDARY`);function x(e,t){return h({type:v,props:{match:e,children:t}})}function S(e,n){return x(n=>{let r=t.findErrorOption(n);return s.isSome(r)?e.fallback(r.value):null},n)}function C(e,t){return x(t=>e.fallback(t),t)}function w(e,n){return x(n=>{let r=t.findErrorOption(n);if(s.isNone(r))return null;let i=r.value;return i._tag===e.tag?e.fallback(i):null},n)}function T(e,n){return x(n=>{let r=t.findErrorOption(n);if(s.isNone(r))return null;let i=r.value,a=i._tag;if(a===void 0)return null;let o=e[a];return o?o(i):null},n)}function E(e,n,r){return x(r=>{let i=t.findErrorOption(r);if(s.isNone(i))return null;let a=e(i.value);return l.isSuccess(a)?n(a.success):null},r)}function D(e,n){return x(n=>{let r=t.findErrorOption(n);if(s.isNone(r))return null;let i=r.value;return e.predicate(i)?e.fallback(i):null},n)}function O(e,t){return h({type:y,props:{...e,children:t}})}function k(e,t,n,r){let i=e;return h({type:b,props:{tag:i._tag,payloadSchema:i.payloadSchema,successSchema:i.successSchema,errorSchema:i.errorSchema,payload:t,render:n,fallback:r?.fallback}})}var A=class extends n.Service()(`@weftui/core/AppRpcClient`){};const j=e=>()=>n.Service()(e);function M(e){return typeof e==`object`&&!!e&&u.TypeId in e}function N(e){return M(e)?e:a.isEffect(e)?u.fromEffect(e):u.make(e)}var P=e({TypeId:()=>F,changes:()=>z,get:()=>R,isSubscribable:()=>B,make:()=>L});const F=`~@weftui/core/Subscribable`,I=e=>e,L=e=>({[F]:F,get:e.get,changes:e.changes}),R=e=>I(e).get,z=e=>I(e).changes,B=e=>c.hasProperty(e,F);var V=class extends r.TaggedError(`NoPropValue`){};let H;(function(e){function n(e,n){return B(e)?a.succeed(e):M(e)?a.gen(function*(){let r=yield*d.make(s.none()),c=yield*i.make(),l=yield*i.make(),m=p(u.runForEach(e,e=>p(d.set(r,s.some(e)),a.andThen(i.succeed(c,e)),a.asVoid)),a.ensuring(p(d.get(r),a.flatMap(e=>s.isNone(e)?a.asVoid(i.fail(c,new V({key:n}))):a.void))),a.onError(e=>t.hasInterruptsOnly(e)?a.void:a.asVoid(i.failCause(l,e))));yield*a.forkScoped(m);let h=p(d.get(r),a.flatMap(e=>s.isSome(e)?a.succeed(e.value):i.await(c))),g=p(d.changes(r),u.filterMap(o.fromPredicateOption(f)),u.interruptWhen(i.await(l)));return L({get:h,changes:g})}):a.isEffect(e)?a.gen(function*(){let t=yield*a.cached(e),n=u.fromEffect(t);return L({get:t,changes:n})}):a.succeed(L({get:a.succeed(e),changes:u.make(e)}))}e.toSubscribable=n})(H||={});const U=Symbol(`@weftui/core/fragment`);function W(e){return((t,n)=>{let r={},i;return Array.isArray(t)||typeof t==`string`||typeof t==`number`?i=t:t!==void 0&&(r=t,n!==void 0&&(i=n)),h({type:e,props:i===void 0?r:{...r,children:i}})})}function G(e=new Map){return new Proxy({fragment(e){return h({type:U,props:{children:e}})}},{get(t,n){return n in t?t[n]:e.get(n)??e.set(n,W(n)).get(n)}})}const K=G(new Map),q=Symbol(`@weftui/core/list`);let J;(function(e){function t(e,t){return h({type:q,props:{of:e.of,by:e.by,render:t}})}e.each=t})(J||={});let Y;(function(e){function t(e){return(t,n=[])=>a.gen(function*(){return yield*e(t,n)})}e.gen=t;function n(e){return e}e.make=n})(Y||={});export{A as AppRpcClientTag,_ as Boundary,Y as Component,v as FAILURE_BOUNDARY,U as FRAGMENT,q as LIST,J as List,V as NoPropValue,b as SERVER_BOUNDARY,y as SUSPENSE_BOUNDARY,j as ServerTag,H as Source,P as Subscribable,h as elementNode,g as getElementDescriptor,K as h,M as isStream,N as toStream};
@@ -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-Bsh2WLtx.js";
1
+ import { C as StyleAttributeValue, S as HTMLAttributeSource, _ as HTMLFormMethod, a as SVGElements, b as HTMLReferrerPolicy, c as HTMLRole, d as EventHandlerFn, f as HTMLAutocapitalize, g as HTMLFormEncType, h as HTMLDir, i as SVGAttributes, l as DOMAttributes, m as HTMLCrossorigin, n as ElementType, o as HTMLAttributes, p as HTMLAutocomplete, r as Renderable, s as HTMLElements, t as ElementDescriptor, u as EventHandler, v as HTMLIframeSandbox, w as StyleProperties, x as AriaAttributes, y as HTMLLinkAs } from "../index-CX9uEejU.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 };
@@ -2,18 +2,20 @@
2
2
  title: Boundaries and Suspense
3
3
  order: 4
4
4
  section: explanation
5
- description: How Weft models failure, async, and server data as boundary nodes in the same tree failure-catch variants, Boundary.suspend, and Boundary.rpc, and how their E/R channels behave.
5
+ description: How Weft models failure, async, and server data as boundary nodes in the same tree. Covers failure-catch variants, Boundary.suspend, and Boundary.rpc, and how their E/R channels behave.
6
6
  ---
7
7
 
8
8
  # Boundaries and Suspense
9
9
 
10
- A **boundary** is a node that intercepts something flowing through the tree an error, a pending async child, or a server-resolved value and decides what the DOM shows in its place. Because a boundary is itself a `Node<E, R>` ([nodes are Effects](https://weftui.dev/docs/explanation/rendering-model)), it composes exactly like any other element: you nest it, and its children's channels flow through it under a transformation the boundary defines.
10
+ A **boundary** is a node that intercepts something flowing through the tree: an error, a pending async child, or a server-resolved value. It decides what the DOM shows in its place.
11
+
12
+ A boundary is itself a `Node<E, R>` ([nodes are Effects](https://weftui.dev/docs/explanation/rendering-model)), so it composes exactly like any other element. You nest it, and its children's channels flow through it under a transformation the boundary defines.
11
13
 
12
14
  The `Boundary` namespace has three kinds. This page is the conceptual map; the [core reference](https://weftui.dev/docs/reference/core#boundary-namespace) has the full signatures.
13
15
 
14
16
  ## Failure boundaries
15
17
 
16
- A component's `E` channel accumulates up the tree. A **failure boundary** is where you _discharge_ some of that `E`: it wraps children and, if one of them fails, renders a fallback instead of letting the failure propagate to the mount.
18
+ A component's `E` channel accumulates up the tree. A **failure boundary** is where you _discharge_ some of that `E`. It wraps children and, if one of them fails, renders a fallback instead of letting the failure propagate to the mount.
17
19
 
18
20
  ```typescript
19
21
  import { Boundary, h } from "@weftui/core";
@@ -32,19 +34,25 @@ There are six failure-catch variants, mirroring Effect's own error operators so
32
34
  | `catchTag` / `catchTags` | one / several tagged errors by `_tag` |
33
35
  | `catchFilter` / `catchIf` | a selected subset, by `Filter` / predicate |
34
36
 
35
- The channel algebra is the whole reason they exist: `catchTag("Foo", …)` removes `Foo` from the children's `E` and adds whatever the fallback needs so the type of the boundary node reflects exactly which failures are still live and which were handled. An unhandled failure re-raises to the **nearest enclosing** boundary; if none catches it at **mount time**, mounting fails. Boundaries nest, so an inner `catchTag` can handle a specific case while an outer `catch` sweeps the rest.
37
+ The channel algebra is the whole reason they exist. `catchTag("Foo", …)` removes `Foo` from the children's `E` and adds whatever the fallback needs. The type of the boundary node therefore reflects exactly which failures are still live and which were handled.
38
+
39
+ An unhandled failure re-raises to the **nearest enclosing** boundary; if none catches it at **mount time**, mounting fails. Boundaries nest, so an inner `catchTag` can handle a specific case while an outer `catch` sweeps the rest.
36
40
 
37
41
  ### Post-mount failures with no enclosing boundary
38
42
 
39
- The routing above describes what happens while a node is being built. Once mounted, a reactive region an attribute, child, or list stream, or a hydrated equivalent keeps running for the lifetime of its scope, and it can still fail later: a `Stream` backing a `Boundary.rpc` resource might raise `RouterNotFound` after a client-side navigation, for instance. If a `BoundaryContext` encloses the region, the failure routes to it exactly as above, and the boundary's fallback swaps in.
43
+ The routing above describes what happens while a node is being built. Once mounted, a reactive region (an attribute, child, or list stream, or a hydrated equivalent) keeps running for the lifetime of its scope. It can still fail later: a `Stream` backing a `Boundary.rpc` resource might raise `RouterNotFound` after a client-side navigation. If a `BoundaryContext` encloses the region, the failure routes to it exactly as above, and the boundary's fallback swaps in.
44
+
45
+ If no boundary encloses it, there is nothing to swap to. Weft does not synthesize one. The region's DOM keeps its last rendered content, and a watcher fiber (forked into the same scope alongside the subscription itself) observes its exit directly.
40
46
 
41
- If no boundary encloses it, there is nothing to swap to. Weft does not synthesize one: the region's DOM keeps its last rendered content, and a watcher fiber — forked into the same scope alongside the subscription itself — observes its exit directly. When that exit is a failure whose cause is not interruption-only, Weft reports it explicitly via `Effect.logError(exit.cause)`, annotated with `weft.region` to identify the failing region by kind and identity (e.g. `attribute:class`, `child:stream-3`, `list:stream-2`, `hydrate:stream-1 (/products/42)`). This fires for typed failures and defects alike, in both dev and prod, exactly once per failing region, at the `"Error"` level. Interruption the ordinary case of unmount tearing down the region's scope is never reported; only genuine failures are.
47
+ When that exit is a failure whose cause is not interruption-only, Weft reports it explicitly via `Effect.logError(exit.cause)`. The log is annotated with `weft.region` to identify the failing region by kind and identity (e.g. `attribute:class`, `child:stream-3`, `list:stream-2`, `hydrate:stream-1 (/products/42)`). This fires for typed failures and defects alike, in both dev and prod, exactly once per failing region, at the `"Error"` level. Interruption (the ordinary case of unmount tearing down the region's scope) is never reported; only genuine failures are.
42
48
 
43
- This is deliberate: rather than leaving the failure to whatever the Effect runtime would otherwise do with an unobserved fiber exit, Weft observes and logs it itself, so visibility is controlled by the same knobs any Effect program uses `References.MinimumLogLevel` (provided via `Effect.provideService`) to filter it, or a custom `Logger` to route it elsewhere. A stream that can fail and has no enclosing boundary is a stream whose failures you've chosen not to route into the UI — the log is what tells you that decision has consequences at runtime.
49
+ This is deliberate. Rather than leave the failure to whatever the Effect runtime would do with an unobserved fiber exit, Weft observes and logs it itself. Visibility is therefore controlled by the same knobs any Effect program uses: `References.MinimumLogLevel` (provided via `Effect.provideService`) to filter it, or a custom `Logger` to route it elsewhere.
50
+
51
+ A stream that can fail and has no enclosing boundary is a stream whose failures you've chosen not to route into the UI. The log is what tells you that decision has consequences at runtime.
44
52
 
45
53
  ## Suspense boundaries
46
54
 
47
- `Boundary.suspend` wraps async children and shows a `fallback` until **all** of them have emitted their first value, then swaps atomically either everything is visible or nothing is. This prevents partial flicker when sibling async regions resolve at different times.
55
+ `Boundary.suspend` wraps async children and shows a `fallback` until **all** of them have emitted their first value. Then it swaps atomically: either everything is visible or nothing is. This prevents partial flicker when sibling async regions resolve at different times.
48
56
 
49
57
  ```typescript
50
58
  import { Boundary, h } from "@weftui/core";
@@ -55,37 +63,48 @@ Boundary.suspend({ fallback: h.div({ class: "spinner" }, "Loading…") }, [
55
63
  ]);
56
64
  ```
57
65
 
58
- A suspense boundary is transparent to the type channels: its node is `Node<ChildrenE, ChildrenR>` the children's `E`/`R` pass straight through, exactly as they would for a plain `h.*` parent. It changes _timing_ (when the children become visible), not _types_.
66
+ A suspense boundary is transparent to the type channels: its node is `Node<ChildrenE, ChildrenR>`. The children's `E`/`R` pass straight through, exactly as they would for a plain `h.*` parent. It changes _timing_ (when the children become visible), not _types_.
59
67
 
60
- On the server, `renderToStreamHydratable` emits the fallback inline and appends patch scripts as children resolve; on the client, `hydrate` sees through the boundary and adopts the already-resolved DOM directly.
68
+ On the server, `renderToStreamHydratable` emits the fallback inline and appends patch scripts as children resolve. On the client, `hydrate` sees through the boundary and adopts the already-resolved DOM directly.
61
69
 
62
- > **Note.** There is no `Suspense` export the API is `Boundary.suspend(props, children)`. Reach for it for async that loads **on the client**; for data that must resolve on the **server** and hydrate without a second request, use `Boundary.rpc` (below).
70
+ > **Note.** There is no `Suspense` export; the API is `Boundary.suspend(props, children)`. Reach for it for async that loads **on the client**. For data that must resolve on the **server** and hydrate without a second request, use `Boundary.rpc` (below).
63
71
 
64
72
  ## The rpc boundary
65
73
 
66
- `Boundary.rpc` is the server-data boundary: it resolves one `Rpc` on the server, serializes the result into the HTML, replays it on the client during `hydrate` (no second request, no flash), and then keeps the region live for `refetch`. Conceptually it is the same idea as the other boundaries — a node that decides what renders in a subtree — but the thing it intercepts is a **round-trip to a server handler**, and instead of a children array it takes a `render` function that receives a reactive [`Resource`](https://weftui.dev/docs/reference/core#resourcea).
74
+ `Boundary.rpc` is the server-data boundary. It:
75
+
76
+ - resolves one `Rpc` on the server
77
+ - serializes the result into the HTML
78
+ - replays it on the client during `hydrate` (no second request, no flash)
79
+ - keeps the region live for `refetch`
80
+
81
+ Conceptually it is the same idea as the other boundaries: a node that decides what renders in a subtree. But the thing it intercepts is a **round-trip to a server handler**. Instead of a children array, it takes a `render` function that receives a reactive [`Resource`](https://weftui.dev/docs/reference/core#resourcea).
67
82
 
68
83
  ```typescript
69
- import { Boundary, h } from "@weftui/core";
84
+ import { Boundary, h, Subscribable } from "@weftui/core";
70
85
  import { Stream } from "effect";
71
86
 
72
87
  Boundary.rpc(
73
88
  GetStock,
74
89
  () => ({ id: productId }),
75
- (resource) => h.span([Stream.map(resource.value.changes, (s) => String(s.units))]),
90
+ (resource) => h.span([Stream.map(Subscribable.changes(resource.value), (s) => String(s.units))]),
76
91
  { fallback: h.p("loading…") },
77
92
  );
78
93
  ```
79
94
 
80
- Unlike the failure and suspense boundaries, `Boundary.rpc` is not self-contained: it resolves through the ambient [`AppRpcClientTag`](https://weftui.dev/docs/reference/core#apprpcclienttag) seam that `@weftui/router` provides on both sides. Its channel behavior is also distinct — the rpc's typed `error` schema joins the node's `E` (replayable through an enclosing failure boundary), while `render`'s `R` passes through untouched. The full model — the contract/handler split, the four lifecycles, typed-failure replay — is a **how-to**, not repeated here: [Load Data with RPC](https://weftui.dev/docs/how-to/load-data-with-rpc).
95
+ Unlike the failure and suspense boundaries, `Boundary.rpc` is not self-contained. It resolves through the ambient [`AppRpcClientTag`](https://weftui.dev/docs/reference/core#apprpcclienttag) seam that `@weftui/router` provides on both sides.
96
+
97
+ Its channel behavior is also distinct. The rpc's typed `error` schema joins the node's `E` (replayable through an enclosing failure boundary), while `render`'s `R` passes through untouched. The full model (the contract/handler split, the four lifecycles, typed-failure replay) is a **how-to**, not repeated here: [Load Data with RPC](https://weftui.dev/docs/how-to/load-data-with-rpc).
81
98
 
82
99
  ## One tree, three interceptors
83
100
 
84
- The unifying idea: failure, async pending state, and server data are not three separate subsystems bolted onto the renderer. They are three **boundary nodes** in the one tree, each intercepting a different thing flowing through it, each with channel behavior you can read off its type. That is why they nest freely — a `Boundary.catchTag` can wrap a `Boundary.rpc` to catch its typed failure, and a `Boundary.suspend` can wrap async siblings that themselves contain rpc boundaries.
101
+ The unifying idea: failure, async pending state, and server data are not three separate subsystems bolted onto the renderer. They are three **boundary nodes** in the one tree, each intercepting a different thing flowing through it. Each has channel behavior you can read off its type.
102
+
103
+ That is why they nest freely. A `Boundary.catchTag` can wrap a `Boundary.rpc` to catch its typed failure. A `Boundary.suspend` can wrap async siblings that themselves contain rpc boundaries.
85
104
 
86
105
  ## See also
87
106
 
88
- - [The Rendering Model](https://weftui.dev/docs/explanation/rendering-model) why a boundary is just a node in a static tree
89
- - [`Boundary` API reference](https://weftui.dev/docs/reference/core#boundary-namespace) every variant's signature and channel algebra
90
- - [Load Data with RPC](https://weftui.dev/docs/how-to/load-data-with-rpc) the full `Boundary.rpc` walkthrough and its four lifecycles
91
- - [Render on the Server](https://weftui.dev/docs/how-to/render-on-the-server) how suspense and rpc boundaries stream and hydrate
107
+ - [The Rendering Model](https://weftui.dev/docs/explanation/rendering-model): why a boundary is just a node in a static tree
108
+ - [`Boundary` API reference](https://weftui.dev/docs/reference/core#boundary-namespace): every variant's signature and channel algebra
109
+ - [Load Data with RPC](https://weftui.dev/docs/how-to/load-data-with-rpc): the full `Boundary.rpc` walkthrough and its four lifecycles
110
+ - [Render on the Server](https://weftui.dev/docs/how-to/render-on-the-server): how suspense and rpc boundaries stream and hydrate
@@ -7,7 +7,9 @@ description: How h, h.fragment, and Component.gen / Component.make work; why Nod
7
7
 
8
8
  # The Combinator API
9
9
 
10
- Weft builds UI trees by calling builder functions. Because component return types stay as generic `Effect.Effect<ElementDescriptor, E, R>`, the error channel (`E`) and requirements channel (`R`) propagate through the entire tree visible to the type checker, satisfiable at the mount boundary. JSX collapses every component's return type to an opaque `JSX.Element`, erasing both channels; the combinator API exists specifically to keep them intact.
10
+ Weft builds UI trees by calling builder functions. Because component return types stay as generic `Effect.Effect<ElementDescriptor, E, R>`, the error channel (`E`) and requirements channel (`R`) propagate through the entire tree: visible to the type checker, satisfiable at the mount boundary.
11
+
12
+ JSX collapses every component's return type to an opaque `JSX.Element`, erasing both channels. The combinator API exists specifically to keep them intact.
11
13
 
12
14
  ## Nodes are Effects
13
15
 
@@ -23,13 +25,13 @@ Nodes are first-class Effects. Everything in the Effect ecosystem works on them
23
25
  import { h } from "@weftui/core";
24
26
  import { Effect } from "effect";
25
27
 
26
- // yield* in Effect.gen R propagates into the generator's context
28
+ // yield* in Effect.gen: R propagates into the generator's context
27
29
  const node = yield * h.div({ class: "container" }, "Hello");
28
30
 
29
- // pipe chain Effect operators directly
31
+ // pipe: chain Effect operators directly
30
32
  const provided = pipe(h.div(userStream), Effect.provide(UserServiceLive));
31
33
 
32
- // Effect.flatMap sequence node creation with async logic
34
+ // Effect.flatMap: sequence node creation with async logic
33
35
  const card = pipe(
34
36
  fetchCard(id),
35
37
  Effect.flatMap((data) => h.div({ class: "card" }, data.title)),
@@ -78,7 +80,7 @@ Reactive prop values (any `Stream`, `Effect`, or `Subscribable`) contribute thei
78
80
  ```typescript
79
81
  declare const colorStream: Stream.Stream<string, never, ThemeService>;
80
82
 
81
- // Node<never, ThemeService> R comes from the stream prop
83
+ // Node<never, ThemeService>: R comes from the stream prop
82
84
  const box = h.div({ style: { color: colorStream } }, "Hello");
83
85
  ```
84
86
 
@@ -130,7 +132,7 @@ declare const labelStream: Stream.Stream<string, never, I18nService>;
130
132
  const btn = Button({ label: labelStream });
131
133
  ```
132
134
 
133
- Components also accept an optional `children` argument, either as `readonly Renderable[]` or as a `(input) => readonly Renderable[]` function (render-prop pattern). `E`/`R` from children including the array returned by a function-children call accumulate on the resulting node.
135
+ Components also accept an optional `children` argument, either as `readonly Renderable[]` or as a `(input) => readonly Renderable[]` function (render-prop pattern). `E`/`R` from children, including the array returned by a function-children call, accumulate on the resulting node.
134
136
 
135
137
  Without `Component`, a plain function's return type is fixed at definition time and does not reflect the caller's reactive prop types.
136
138
 
@@ -149,16 +151,16 @@ Boundary.suspend({ fallback: h.div({ class: "spinner" }, "Loading...") }, [
149
151
  ]);
150
152
  ```
151
153
 
152
- The fallback is replaced atomically either all children are visible or none are. This prevents partial flicker when multiple async siblings resolve at different times. The boundary's node type is `Node<ChildrenE, ChildrenR>`: the children's `E`/`R` channels accumulate onto it, exactly as they would for a plain `h.*` parent.
154
+ The fallback is replaced atomically: either all children are visible or none are. This prevents partial flicker when multiple async siblings resolve at different times. The boundary's node type is `Node<ChildrenE, ChildrenR>`: the children's `E`/`R` channels accumulate onto it, exactly as they would for a plain `h.*` parent.
153
155
 
154
156
  On the server, `renderToStreamHydratable` emits the fallback inline and appends patch scripts as children resolve. On the client, `hydrate` sees through `Boundary.suspend` boundaries and adopts the already-resolved DOM directly.
155
157
 
156
- `Boundary.suspend` is one of the boundary combinators see the [core reference](https://weftui.dev/docs/reference/core#boundarysuspend) for the full `Boundary.*` surface, including the failure-catch variants and `Boundary.rpc`.
158
+ `Boundary.suspend` is one of the boundary combinators. See the [core reference](https://weftui.dev/docs/reference/core#boundarysuspend) for the full `Boundary.*` surface, including the failure-catch variants and `Boundary.rpc`.
157
159
 
158
160
  ## See also
159
161
 
160
- - [The Rendering Model](https://weftui.dev/docs/explanation/rendering-model) why a `Node` is an `Effect` and how the tree renders
161
- - [Reactive Primitives](https://weftui.dev/docs/explanation/reactive-primitives) the `Source` vocabulary that reactive props and children accept
162
- - [Boundaries and Suspense](https://weftui.dev/docs/explanation/boundaries-and-suspense) the boundary combinators as tree nodes
163
- - [Author Components](https://weftui.dev/docs/how-to/author-components) `Component.gen` / `Component.make` in practice
162
+ - [The Rendering Model](https://weftui.dev/docs/explanation/rendering-model): why a `Node` is an `Effect` and how the tree renders
163
+ - [Reactive Primitives](https://weftui.dev/docs/explanation/reactive-primitives): the `Source` vocabulary that reactive props and children accept
164
+ - [Boundaries and Suspense](https://weftui.dev/docs/explanation/boundaries-and-suspense): the boundary combinators as tree nodes
165
+ - [Author Components](https://weftui.dev/docs/how-to/author-components): `Component.gen` / `Component.make` in practice
164
166
  - [`@weftui/core` reference](https://weftui.dev/docs/reference/core)