@weftui/dom 0.27.1 → 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.
- package/README.md +32 -18
- package/dist/boundary-replay-BY4GyLot.js +1 -0
- package/dist/client/index.d.ts +159 -146
- package/dist/client/index.js +1 -1
- package/dist/{data-Bk7mnjdd.d.ts → data-C88W1AwU.d.ts} +4 -5
- package/dist/index.d.ts +196 -2
- package/dist/index.js +1 -1
- package/dist/rolldown-runtime-DK3Fl9T5.js +1 -0
- package/dist/server/index.d.ts +6 -7
- package/dist/server/index.js +1 -1
- package/dist/shared-Dz0KM9ku.js +1 -0
- package/docs/explanation/boundaries-and-suspense.md +37 -18
- package/docs/explanation/combinator-api.md +14 -12
- package/docs/explanation/reactive-primitives.md +15 -15
- package/docs/explanation/rendering-model.md +20 -18
- package/docs/explanation/services-and-context.md +39 -29
- package/docs/how-to/add-routing.md +76 -52
- package/docs/how-to/author-components.md +46 -32
- package/docs/how-to/compose-behavior-and-markup.md +144 -0
- package/docs/how-to/handle-forms.md +6 -6
- package/docs/how-to/load-async-data.md +15 -13
- package/docs/how-to/load-data-with-rpc.md +34 -30
- package/docs/how-to/provide-services.md +54 -74
- package/docs/how-to/render-keyed-lists.md +10 -8
- package/docs/how-to/render-on-the-server.md +19 -14
- package/docs/how-to/show-navigation-progress.md +10 -8
- package/docs/how-to/split-routes-lazily.md +16 -14
- package/docs/how-to/style-reactively.md +13 -13
- package/docs/how-to/use-element-refs.md +10 -8
- package/docs/index.md +20 -18
- package/docs/reference/core.md +44 -40
- package/docs/reference/dom.md +395 -58
- package/docs/reference/router.md +71 -49
- package/docs/tutorial/01-your-first-app.md +10 -11
- package/docs/tutorial/02-reactivity.md +11 -8
- package/docs/tutorial/03-services-and-async.md +19 -13
- package/docs/tutorial/04-errors-and-server.md +17 -7
- package/package.json +9 -8
- package/dist/boundary-replay-BR26_puM.js +0 -1
- package/dist/data-uLmMpQMV.js +0 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,196 @@
|
|
|
1
|
-
import { i as UnsupportedNodeTypeError, n as RenderError, r as StreamSubscriptionError, t as HydrationMismatchError } from "./data-
|
|
2
|
-
|
|
1
|
+
import { i as UnsupportedNodeTypeError, n as RenderError, r as StreamSubscriptionError, t as HydrationMismatchError } from "./data-C88W1AwU.js";
|
|
2
|
+
import { Effect, Option, Stream, SubscriptionRef } from "effect";
|
|
3
|
+
import { NoPropValue, Source, Subscribable } from "@weftui/core";
|
|
4
|
+
declare namespace props_d_exports {
|
|
5
|
+
export { CxInput, CxRecord, CxResult, DomProps, Merged, cx, merge };
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* The prop-bag argument shape accepted by `h.*` element builders.
|
|
9
|
+
* Loosely constrained because {@link merge} dispatches on the key
|
|
10
|
+
* (`class`, `style`, `ref`, `on*`), not on the bag's declared type.
|
|
11
|
+
*/
|
|
12
|
+
type DomProps = object;
|
|
13
|
+
/** Any reactive prop value: the non-static arms of `Source.Source`. */
|
|
14
|
+
type ReactiveValue = Stream.Stream<any, any, any> | Effect.Effect<any, any, any> | Subscribable.Subscribable<any, any, any>;
|
|
15
|
+
/** Any function value, used to detect plain event-handler functions. */
|
|
16
|
+
type AnyFunction = (...args: ReadonlyArray<any>) => any;
|
|
17
|
+
/** The lowercase third character the renderer's `isEventHandler` check requires. */
|
|
18
|
+
type LowercaseLetter = "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j" | "k" | "l" | "m" | "n" | "o" | "p" | "q" | "r" | "s" | "t" | "u" | "v" | "w" | "x" | "y" | "z";
|
|
19
|
+
/**
|
|
20
|
+
* Type-level form of the renderer's `isEventHandler` check. Mirrors it exactly,
|
|
21
|
+
* including the lowercase third character: Weft's DOM handler props are all
|
|
22
|
+
* lowercase (`onclick`), so a camelCase `onClick` is not a handler key and must
|
|
23
|
+
* type as last-wins, matching what the runtime does with it.
|
|
24
|
+
*/
|
|
25
|
+
type EventHandlerKey = `on${LowercaseLetter}${string}`;
|
|
26
|
+
/**
|
|
27
|
+
* Error channel a handler side contributes. Covers a plain handler returning an
|
|
28
|
+
* Effect and the reactive `Stream`/`Effect`-of-handler forms core's
|
|
29
|
+
* `EventHandler` allows, so a reactive side keeps its channel even though the
|
|
30
|
+
* runtime treats it as last-wins.
|
|
31
|
+
*/
|
|
32
|
+
type HandlerError<H> = H extends ((...args: ReadonlyArray<any>) => infer Ret) ? Ret extends Effect.Effect<any, infer E, any> ? E : never : H extends Stream.Stream<any, infer E, any> ? E : H extends Effect.Effect<any, infer E, any> ? E : never;
|
|
33
|
+
/** Context channel a handler side contributes; mirrors {@link HandlerError}. */
|
|
34
|
+
type HandlerContext<H> = H extends ((...args: ReadonlyArray<any>) => infer Ret) ? Ret extends Effect.Effect<any, any, infer R> ? R : never : H extends Stream.Stream<any, any, infer R> ? R : H extends Effect.Effect<any, any, infer R> ? R : never;
|
|
35
|
+
/**
|
|
36
|
+
* A handler side with its "no handler" arms removed. Core declares handler
|
|
37
|
+
* props as `null | false | EventHandler<...>`, and leaving those in place makes
|
|
38
|
+
* the event-parameter match below fail, degrading the merged handler's event to
|
|
39
|
+
* the base `Event` (so `ev.clientX` stops compiling). Kept separate from
|
|
40
|
+
* {@link Present}, which must not strip `false` from ordinary boolean props.
|
|
41
|
+
*/
|
|
42
|
+
type HandlerSide<T> = Exclude<T, null | false | undefined>;
|
|
43
|
+
/** Event parameter of a chained handler: intersection of both sides' events. */
|
|
44
|
+
type MergedHandlerEvent<L, R> = [HandlerSide<L>] extends [(event: infer EL) => any] ? [HandlerSide<R>] extends [(event: infer ER) => any] ? EL & ER : EL : [HandlerSide<R>] extends [(event: infer ER2) => any] ? ER2 : Event;
|
|
45
|
+
/**
|
|
46
|
+
* Handler cell rule. If either side can carry a handler the merged value is a
|
|
47
|
+
* handler function whose channels union both sides'; if neither can, the cell
|
|
48
|
+
* is the nullish "no handler" type.
|
|
49
|
+
*
|
|
50
|
+
* The callable shape is deliberately coarse: core declares handler props as
|
|
51
|
+
* `null | false | EventHandler<...>`, so a rule that only matched two bare
|
|
52
|
+
* functions would miss every realistically-typed bag and drop the left side's
|
|
53
|
+
* `E`/`R` from `PropsE`/`PropsR`. Channel accuracy is the part that must not
|
|
54
|
+
* degrade, so it is computed from whatever shape each side has.
|
|
55
|
+
*/
|
|
56
|
+
type MergedHandlerValue<L, R> = [HandlerSide<L>, HandlerSide<R>] extends [never, never] // Neither side can carry a handler, so neither can the result.
|
|
57
|
+
? null | false | undefined : (event: MergedHandlerEvent<L, R>) => Effect.Effect<void, HandlerError<HandlerSide<L>> | HandlerError<HandlerSide<R>>, HandlerContext<HandlerSide<L>> | HandlerContext<HandlerSide<R>>>;
|
|
58
|
+
/**
|
|
59
|
+
* Class cell rule. A side that cannot carry a reactive value joins statically,
|
|
60
|
+
* so two such sides stay a plain `string` and the descriptor remains
|
|
61
|
+
* analyzable (AC8). If either side might be reactive the cell is exactly a
|
|
62
|
+
* `Stream`, never a union: `PropsE`/`PropsR` extract channels by matching
|
|
63
|
+
* `P[K] extends Stream<...>`, and a union would fail that match and silently
|
|
64
|
+
* drop the class's `E`/`R`. Erring toward `Stream` is therefore the safe
|
|
65
|
+
* direction, at the cost of over-reporting for a wide `Source<string>` side.
|
|
66
|
+
*/
|
|
67
|
+
type MergedClassValue<L, R> = true extends IsReactiveCxInput<L> | IsReactiveCxInput<R> ? Stream.Stream<string, CxInputError<L> | CxInputError<R> | NoPropValue, CxInputContext<L> | CxInputContext<R>> : string;
|
|
68
|
+
/**
|
|
69
|
+
* True for a per-property style object. Mirrors the runtime
|
|
70
|
+
* `isPlainStyleObject`, including its array exclusion, so the two cannot
|
|
71
|
+
* disagree about which shape takes the object-merge branch.
|
|
72
|
+
*/
|
|
73
|
+
type IsStyleObject<V> = V extends object ? V extends ReactiveValue | AnyFunction | ReadonlyArray<any> ? false : true : false;
|
|
74
|
+
/**
|
|
75
|
+
* Style cell rule: object plus object takes the key union with the right side
|
|
76
|
+
* winning per key, each surviving value passed through by reference.
|
|
77
|
+
* Any other shape on either side is last-wins.
|
|
78
|
+
*/
|
|
79
|
+
type MergedStyleValue<L, R> = [IsStyleObject<L>, IsStyleObject<R>] extends [true, true] ? { readonly [K in keyof L | keyof R]: K extends keyof R ? R[K] : K extends keyof L ? L[K] : never; } : R;
|
|
80
|
+
/**
|
|
81
|
+
* Ref cell rule: both sides concatenate into one readonly fan-out array.
|
|
82
|
+
* Typed against the same permissive element as the core `ref` array arm so the
|
|
83
|
+
* result stays assignable to any element builder. `SubscriptionRef` is
|
|
84
|
+
* invariant, so a union of the two sides' exact ref types would reject the
|
|
85
|
+
* heterogeneous fan-out this rule exists to enable (AC14a).
|
|
86
|
+
*/
|
|
87
|
+
type MergedRefValue = ReadonlyArray<SubscriptionRef.SubscriptionRef<Option.Option<any>>>;
|
|
88
|
+
/**
|
|
89
|
+
* The value a side actually carries when its key is present. An optional prop
|
|
90
|
+
* indexes to `T | undefined`; the cell rules must dispatch on `T`, or every
|
|
91
|
+
* pattern match below silently falls through to last-wins.
|
|
92
|
+
*/
|
|
93
|
+
type Present<T> = Exclude<T, undefined>;
|
|
94
|
+
/** Per-key dispatch for a key present on both sides. */
|
|
95
|
+
type MergedValue<K extends PropertyKey, L, R> = K extends "class" ? MergedClassValue<Present<L>, Present<R>> : K extends "style" ? MergedStyleValue<Present<L>, Present<R>> : K extends "ref" ? MergedRefValue : K extends EventHandlerKey ? MergedHandlerValue<Present<L>, Present<R>> : Present<R>;
|
|
96
|
+
/**
|
|
97
|
+
* The cell for a key both bags declare.
|
|
98
|
+
*
|
|
99
|
+
* When one side's key is optional it may be absent at runtime, in which case
|
|
100
|
+
* the other side's value survives unmerged. That outcome is deliberately not
|
|
101
|
+
* unioned in: each cell rule above is already coarse enough to accept it at a
|
|
102
|
+
* prop slot, and unioning the bare side back in would reintroduce the invariant
|
|
103
|
+
* `SubscriptionRef` that makes the AC14a fan-out fail to compile.
|
|
104
|
+
*/
|
|
105
|
+
type MergedSharedValue<K extends keyof L & keyof R, L, R> = MergedValue<K, L[K], R[K]>;
|
|
106
|
+
/** Flattens an intersection into one object type, preserving optional modifiers. */
|
|
107
|
+
type Simplify<T> = { [K in keyof T]: T[K]; };
|
|
108
|
+
/** Keys only the left bag has. Homomorphic over `L`, so `?` modifiers survive. */
|
|
109
|
+
type MergedLeftOnly<L, R> = { readonly [K in keyof L as K extends keyof R ? never : K]: L[K]; };
|
|
110
|
+
/** Keys only the right bag has. Homomorphic over `R`, so `?` modifiers survive. */
|
|
111
|
+
type MergedRightOnly<L, R> = { readonly [K in keyof R as K extends keyof L ? never : K]: R[K]; };
|
|
112
|
+
/**
|
|
113
|
+
* Keys both bags declare. Emitted as required (`-?`) with an undefined-free
|
|
114
|
+
* value, because `PropsE`/`PropsR` extract channels by matching `P[K]` against
|
|
115
|
+
* a function or `Stream` shape, and a `T | undefined` union fails that match
|
|
116
|
+
* and silently drops the key's `E`/`R`. Channel accuracy outranks presence
|
|
117
|
+
* precision here, so a shared key that is optional on both sides and absent at
|
|
118
|
+
* runtime is still typed as present.
|
|
119
|
+
*/
|
|
120
|
+
type MergedShared<L, R> = { readonly [K in keyof L & keyof R]-?: MergedSharedValue<K, L, R>; };
|
|
121
|
+
/**
|
|
122
|
+
* Binary merge of two bags: shared keys via {@link MergedValue}, rest pass
|
|
123
|
+
* through. Built from two homomorphic mapped types so optional keys stay
|
|
124
|
+
* optional. A single `[K in keyof L | keyof R]` map would mark every key
|
|
125
|
+
* required, claiming keys are present that the runtime never copied.
|
|
126
|
+
*/
|
|
127
|
+
type MergedPair<L, R> = Simplify<MergedLeftOnly<L, R> & MergedRightOnly<L, R> & MergedShared<L, R>>;
|
|
128
|
+
/** The empty bag: identity of the merge monoid and result of `merge()`. */
|
|
129
|
+
type EmptyBag = Readonly<Record<never, never>>;
|
|
130
|
+
/**
|
|
131
|
+
* Result of {@link merge}: a left-to-right fold of {@link MergedPair} over the
|
|
132
|
+
* bag tuple. Value types stay coarse but `E`/`R` channels stay precise, so
|
|
133
|
+
* `PropsE`/`PropsR` accumulate the full union through `h.*`.
|
|
134
|
+
*/
|
|
135
|
+
type Merged<Bags extends ReadonlyArray<DomProps>> = Bags extends readonly [] ? EmptyBag : Bags extends readonly [infer Only extends DomProps] ? Only : Bags extends readonly [infer L extends DomProps, infer R extends DomProps, ...infer Rest extends ReadonlyArray<DomProps>] ? Merged<[MergedPair<L, R>, ...Rest]> : MergedPair<Bags[number], Bags[number]>;
|
|
136
|
+
/**
|
|
137
|
+
* Merge DOM prop bags left to right. Pure: the result is a plain prop bag of
|
|
138
|
+
* ordinary Weft prop values, so `h.*` accepts it and `PropsE`/`PropsR` carry
|
|
139
|
+
* the error and context channels through. `{}` is the identity, and the fold is
|
|
140
|
+
* associative except for `style` when a non-object form takes part (see below).
|
|
141
|
+
*
|
|
142
|
+
* Rules for a key present on both sides:
|
|
143
|
+
*
|
|
144
|
+
* - `on*`: chained left to right, both always run, failures isolated and causes aggregated.
|
|
145
|
+
* - `class`: space-concatenated. Static stays a `string`, reactive derives a `Stream<string>`.
|
|
146
|
+
* - `style`: two per-property objects merge per key (right wins). Other forms are
|
|
147
|
+
* last-wins, which discards a side and so is not associative (v1 limitation).
|
|
148
|
+
* - `ref`: fan-out. Refs concatenate into an array and every ref is set.
|
|
149
|
+
* - anything else: last-wins.
|
|
150
|
+
*
|
|
151
|
+
* Keys on only one side pass through untouched.
|
|
152
|
+
*
|
|
153
|
+
* @param bags - Lowest precedence first.
|
|
154
|
+
*/
|
|
155
|
+
declare function merge<const Bags extends ReadonlyArray<DomProps>>(...bags: Bags): Merged<Bags>;
|
|
156
|
+
/** A reactive string value accepted by {@link cx} in value position. */
|
|
157
|
+
type CxReactiveValue = Stream.Stream<string, any, any> | Effect.Effect<string, any, any> | Subscribable.Subscribable<string, any, any>;
|
|
158
|
+
/** A condition in a {@link cx} record: a static or reactive boolean. */
|
|
159
|
+
type CxCondition = boolean | Stream.Stream<boolean, any, any> | Effect.Effect<boolean, any, any> | Subscribable.Subscribable<boolean, any, any>;
|
|
160
|
+
/**
|
|
161
|
+
* Record form of a {@link cx} input: each key is a class name included while
|
|
162
|
+
* its condition is truthy. Reactive conditions are the difference from clsx.
|
|
163
|
+
*/
|
|
164
|
+
interface CxRecord {
|
|
165
|
+
readonly [className: string]: CxCondition;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* One input to {@link cx}: a string, a falsy value (skipped), a reactive
|
|
169
|
+
* string, a condition record, or a nested array of inputs.
|
|
170
|
+
*/
|
|
171
|
+
type CxInput = string | false | null | undefined | CxReactiveValue | CxRecord | ReadonlyArray<CxInput>;
|
|
172
|
+
/** True when a {@link cx} input contains a reactive value at any depth. */
|
|
173
|
+
type IsReactiveCxInput<I> = I extends ReactiveValue ? true : I extends ReadonlyArray<infer Item> ? IsReactiveCxInput<Item> : I extends CxRecord ? I[keyof I] extends boolean ? false : true : false;
|
|
174
|
+
/** Union of the error channels contributed by a {@link cx} input. */
|
|
175
|
+
type CxInputError<I> = I extends ReactiveValue ? Source.Error<I> : I extends ReadonlyArray<infer Item> ? CxInputError<Item> : I extends CxRecord ? Source.Error<I[keyof I]> : never;
|
|
176
|
+
/** Union of the context channels contributed by a {@link cx} input. */
|
|
177
|
+
type CxInputContext<I> = I extends ReactiveValue ? Source.Context<I> : I extends ReadonlyArray<infer Item> ? CxInputContext<Item> : I extends CxRecord ? Source.Context<I[keyof I]> : never;
|
|
178
|
+
/**
|
|
179
|
+
* Result of {@link cx}: a plain `string` when every input is static, otherwise
|
|
180
|
+
* a `Stream<string>` unioning all reactive inputs' channels plus `NoPropValue`.
|
|
181
|
+
*/
|
|
182
|
+
type CxResult<Inputs extends ReadonlyArray<CxInput>> = true extends IsReactiveCxInput<Inputs[number]> ? Stream.Stream<string, CxInputError<Inputs[number]> | NoPropValue, CxInputContext<Inputs[number]>> : string;
|
|
183
|
+
/**
|
|
184
|
+
* Reactive clsx. Builds a class string from strings, falsy values (skipped),
|
|
185
|
+
* nested arrays, and `{ className: condition }` records, where any value or
|
|
186
|
+
* condition may be a `Stream`, `Effect`, or `Subscribable`.
|
|
187
|
+
*
|
|
188
|
+
* All-static inputs join into a plain string, so the descriptor stays
|
|
189
|
+
* analyzable. Any reactive input derives a pure `Stream<string>` description,
|
|
190
|
+
* the same engine {@link merge} uses for its `class` rule.
|
|
191
|
+
*
|
|
192
|
+
* @param inputs - Joined left to right, no dedupe.
|
|
193
|
+
*/
|
|
194
|
+
declare function cx<const Inputs extends ReadonlyArray<CxInput>>(...inputs: Inputs): CxResult<Inputs>;
|
|
195
|
+
//#endregion
|
|
196
|
+
export { HydrationMismatchError, props_d_exports as Props, RenderError, StreamSubscriptionError, UnsupportedNodeTypeError };
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{t as e}from"./rolldown-runtime-DK3Fl9T5.js";import{_ as t,b as n,h as r,i,v as a}from"./shared-Dz0KM9ku.js";import{Cause as o,Effect as s,Exit as c,Stream as l,pipe as u}from"effect";import{NoPropValue as d,Subscribable as f,isStream as p}from"@weftui/core";var m=e({cx:()=>C,merge:()=>h});function h(...e){return e.reduce((e,t)=>g(e,t),{})}function g(e,t){let n={...e};for(let[r,i]of Object.entries(t))n[r]=Object.hasOwn(e,r)?y(r,e[r],i):i;return n}function _(e){return p(e)||s.isEffect(e)||f.isSubscribable(e)}function v(e){return typeof e==`object`&&!!e&&!Array.isArray(e)&&!_(e)&&w(e)}function y(e,t,n){return e===`ref`?[...b(t),...b(n)]:e===`class`?k([t,n],`class`):e===`style`?v(t)&&v(n)?{...t,...n}:n:i(e)?typeof t==`function`&&typeof n==`function`?S(t,n):n??t:n}function b(e){return(Array.isArray(e)?e:[e]).filter(e=>e!=null)}function x(e,t){try{let n=e(t);return s.isEffect(n)?n:s.void}catch(e){return s.die(e)}}function S(e,t){return n=>{let r=x(e,n),i=x(t,n);return s.gen(function*(){let e=yield*s.exit(r),t=yield*s.exit(i);if(c.isFailure(e)&&c.isFailure(t))return yield*s.failCause(o.fromReasons([...e.cause.reasons,...t.cause.reasons]));if(c.isFailure(e))return yield*s.failCause(e.cause);if(c.isFailure(t))return yield*s.failCause(t.cause)})}}function C(...e){return k(e)}function w(e){let t=Object.getPrototypeOf(e);return t===Object.prototype||t===null}function T(e,t){for(let n of e)if(!(n===!1||n==null)){if(typeof n==`string`){n!==``&&t.push({kind:`static`,text:n});continue}if(Array.isArray(n)){T(n,t);continue}if(_(n)){t.push({kind:`value`,source:n});continue}if(w(n))for(let[e,r]of Object.entries(n))_(r)?t.push({kind:`toggle`,name:e,source:r}):r&&t.push({kind:`static`,text:e})}}function E(e,t){let n=[],r=0;for(let i of e){if(i.kind===`static`){n.push(i.text);continue}let e=t[r];r+=1,i.kind===`value`?typeof e==`string`&&e!==``&&n.push(e):e&&n.push(i.name)}return n.join(` `)}function D(e,t){return l.suspend(()=>{let n=!1;return u(e,l.tap(()=>s.sync(()=>{n=!0})),l.concat(l.suspend(()=>n?l.empty:l.fail(new d({key:t})))))})}function O(e,t){if(p(e))return D(e,t);if(s.isEffect(e))return l.fromEffect(e);let n=e;return l.concat(l.fromEffect(n.get),n.changes)}function k(e,t){let n=[];T(e,n);let r=n.filter(e=>e.kind!==`static`);if(r.length===0)return E(n,[]);let i=r.map(e=>O(e.source,t));return u(l.zipLatestAll(...i),l.map(e=>E(n,e)))}export{r as HydrationMismatchError,m as Props,t as RenderError,a as StreamSubscriptionError,n as UnsupportedNodeTypeError};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var e=Object.defineProperty,t=(t,n)=>{let r={};for(var i in t)e(r,i,{get:t[i],enumerable:!0});return n||e(r,Symbol.toStringTag,{value:`Module`}),r};export{t};
|
package/dist/server/index.d.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { Cause, Context, Effect, Option, Scope, Stream } from "effect";
|
|
2
2
|
import { AppRpcClientTag, Renderable } from "@weftui/core";
|
|
3
3
|
import { Renderable as Renderable$1 } from "@weftui/core/types";
|
|
4
|
-
|
|
5
4
|
//#region src/server/render-to-stream.d.ts
|
|
6
5
|
/**
|
|
7
6
|
* Progressively serializes an Effect-infused JSX tree (`Renderable`) into a stream
|
|
@@ -28,7 +27,7 @@ declare const renderToStreamHydratable: (node: Renderable) => Stream.Stream<stri
|
|
|
28
27
|
* The server-side counterpart to the client DOM renderer, intended to produce
|
|
29
28
|
* output isomorphic with what the client renderer creates in the browser.
|
|
30
29
|
*
|
|
31
|
-
* Suspense boundaries render their fallback directly
|
|
30
|
+
* Suspense boundaries render their fallback directly: no comment markers
|
|
32
31
|
* and no `<template>`/`<script>` patches. For streaming Suspense support use
|
|
33
32
|
* {@link renderToStreamHydratable} / {@link renderToStream} instead.
|
|
34
33
|
*
|
|
@@ -54,7 +53,7 @@ declare const renderToStringHydratable: (node: Renderable$1) => Effect.Effect<st
|
|
|
54
53
|
*/
|
|
55
54
|
interface HydratableShell {
|
|
56
55
|
/**
|
|
57
|
-
* The fully buffered main walk
|
|
56
|
+
* The fully buffered main walk, byte-identical to the `mainStream` portion
|
|
58
57
|
* of `renderToStreamHydratable` for the same tree: reactive-region markers,
|
|
59
58
|
* Suspense fallbacks inline with their markers, resolved (blocking)
|
|
60
59
|
* `Boundary.rpc` regions, and failure-boundary fallbacks with payloads.
|
|
@@ -62,7 +61,7 @@ interface HydratableShell {
|
|
|
62
61
|
readonly shell: string;
|
|
63
62
|
/**
|
|
64
63
|
* The Suspense patch queue as a stream. Never fails (resolution fibers
|
|
65
|
-
* handle their own errors
|
|
64
|
+
* handle their own errors, see {@link SuspenseFailureHandlerTag}); completes
|
|
66
65
|
* once all pending boundaries have resolved, or immediately if the tree has
|
|
67
66
|
* no `Boundary.suspend`.
|
|
68
67
|
*/
|
|
@@ -74,7 +73,7 @@ interface HydratableShell {
|
|
|
74
73
|
* an HTTP consumer can decide status/headers before flushing any bytes, then
|
|
75
74
|
* streams Suspense patches separately.
|
|
76
75
|
*
|
|
77
|
-
* Errors raised during the main walk fail this Effect
|
|
76
|
+
* Errors raised during the main walk fail this Effect. Nothing has been
|
|
78
77
|
* handed to the consumer yet, so the caller may respond with a different
|
|
79
78
|
* document and a real status (AC-SH2).
|
|
80
79
|
*
|
|
@@ -96,7 +95,7 @@ interface SuspenseFailureSubstitute {
|
|
|
96
95
|
* When `true`, the patch script also injects
|
|
97
96
|
* `<meta name="robots" content="noindex">` into `document.head` before
|
|
98
97
|
* performing the swap (the head has long been flushed; DOM injection is the
|
|
99
|
-
* only route
|
|
98
|
+
* only route: Googlebot's soft-404 pattern).
|
|
100
99
|
*/
|
|
101
100
|
readonly markNoindex: boolean;
|
|
102
101
|
/**
|
|
@@ -115,7 +114,7 @@ interface SuspenseFailureSubstitute {
|
|
|
115
114
|
* cause escapes the suspended children unhandled (no failure `Boundary`
|
|
116
115
|
* inside the children matched it). Returning `Option.some` substitutes the
|
|
117
116
|
* patch content for that boundary; `Option.none` (or an absent service)
|
|
118
|
-
* keeps the default behaviour
|
|
117
|
+
* keeps the default behaviour: the failure is swallowed, no patch is
|
|
119
118
|
* emitted, and the fallback persists. Spec: `streaming-shell.specs.md`
|
|
120
119
|
* (AC-FH1 … AC-FH6).
|
|
121
120
|
*/
|
package/dist/server/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{a as e,b as t,d as n,f as r,i,o as a,p as o,t as s,u as c}from"../shared-Dz0KM9ku.js";import{n as l,t as u}from"../boundary-replay-BY4GyLot.js";import{Cause as d,Context as f,Effect as p,Exit as m,Option as h,Queue as g,Ref as _,Schema as v,Scope as y,Stream as b}from"effect";import{AppRpcClientTag as ee,FAILURE_BOUNDARY as x,FRAGMENT as S,LIST as C,SERVER_BOUNDARY as w,SUSPENSE_BOUNDARY as T,Source as te,Subscribable as E,getElementDescriptor as D,isStream as O,toStream as k}from"@weftui/core";const A=new Set([`area`,`base`,`br`,`col`,`embed`,`hr`,`img`,`input`,`link`,`meta`,`param`,`source`,`track`,`wbr`]),j={'"':`"`,"&":`&`,"'":`'`,"<":`<`,">":`>`};function M(e){return e.replace(/["'&<>]/g,e=>j[e]??e)}const N=new Set([38,60,62,8232,8233]);function P(e){let t=JSON.stringify(e),n=``;for(let e=0;e<t.length;e++){let r=t.charCodeAt(e);N.has(r)?n+=`\\u${r.toString(16).padStart(4,`0`)}`:n+=t[e]}return n}function F(e){return e.replace(/[A-Z]/g,e=>`-${e.toLowerCase()}`)}function I(e){return p.gen(function*(){let t=``;for(let[n,r]of Object.entries(e))if(!(n===`children`||n===`ref`||i(n))){if(n===`style`){t+=yield*R(r);continue}t+=yield*ne(n,r)}return t})}function L(e){return O(e)||p.isEffect(e)?k(e).pipe(b.runHead,p.map(h.getOrElse(()=>void 0))):p.succeed(e)}function ne(e,t){return p.gen(function*(){let n=yield*L(t);return n==null?``:typeof n==`boolean`?n?` ${e}=""`:``:` ${e}="${M(String(n))}"`})}function R(e){return p.gen(function*(){let t=yield*L(e);if(t==null)return``;if(typeof t==`string`)return t===``?``:` style="${M(t)}"`;if(typeof t==`object`){let e=[];for(let[n,r]of Object.entries(t)){let t=yield*L(r);t!=null&&e.push(`${F(n)}: ${String(t)}`)}return e.length===0?``:` style="${M(e.join(`; `))}"`}return``})}var z=class extends f.Service()(`@weftui/dom/SuspenseFailureHandler`){};function B(e,t,n=!1,i){let a=o(e),c=r(e),l=n?`var m=document.createElement("meta");m.setAttribute("name","robots");m.setAttribute("content","noindex");document.head.appendChild(m);`:``,u=i!==void 0,d=u?`<script type="application/json" ${s}>`+P({error:i})+`<\/script>`+t:t,f=u?``:`p.removeChild(s);p.removeChild(e);`;return`<template id="ef-s-${e}">${d}</template><script>(function(){`+l+`var w=document.createTreeWalker(document,128),s,e;while(w.nextNode()){var d=w.currentNode.data;if(d==="${a}")s=w.currentNode;if(d==="${c}"){e=w.currentNode;break;}}if(!s||!e)return;var p=s.parentNode,c=s.nextSibling,n;while(c&&c!==e){n=c.nextSibling;p.removeChild(c);c=n;}var t=document.getElementById("ef-s-${e}");p.insertBefore(t.content,e);`+f+`t.remove();document.currentScript.remove();})();<\/script>`}function V(e,t,n){return b.unwrap(p.gen(function*(){let i=++t.idCounter.current;yield*_.update(t.pendingCount,e=>e+1);let a=e.children===void 0?null:(Array.isArray(e.children),e.children),s=p.gen(function*(){let e=yield*b.mkString(n(a));yield*g.offer(t.patchQueue,h.some(B(i,e)))}).pipe(p.catchCause(e=>d.hasInterruptsOnly(e)?p.failCause(e):p.gen(function*(){let r=yield*p.serviceOption(z);if(h.isNone(r))return;let a=r.value.handle(e);if(h.isNone(a))return;let o=yield*b.mkString(n(a.value.content));yield*g.offer(t.patchQueue,h.some(B(i,o,a.value.markNoindex,a.value.failureReplay)))})),p.ensuring(_.updateAndGet(t.pendingCount,e=>e-1).pipe(p.flatMap(e=>e<=0?g.offer(t.patchQueue,h.none()).pipe(p.asVoid):p.void))),p.ignore);yield*p.forkIn(s,t.scope);let c=`<!--${o(i)}-->`,l=`<!--${r(i)}-->`,u=e.fallback??null;return b.make(c).pipe(b.concat(n(u)),b.concat(b.make(l)))}))}function H(e,t,n){let r=e.children.length===0?null:e.children.length===1?e.children[0]:e.children;return b.unwrap(p.gen(function*(){let i=yield*b.mkString(n(r)).pipe(p.catchCause(r=>p.gen(function*(){let i=e.match(r);if(i===null)return yield*p.failCause(r);let a=yield*b.mkString(n(i));if(t===null)return a;let o=yield*_.getAndSet(t,h.none());return h.isNone(o)?a:`<script type="application/json" ${u}>${P({index:l(e.children).indexOf(o.value.owner),error:o.value.encoded})}<\/script>`+a})));return b.make(i)}))}function re(e,t,n){if(t===null)return p.failCause(n);let r=d.findErrorOption(n);return h.isNone(r)?p.failCause(n):v.encodeEffect(e.errorSchema)(r.value).pipe(p.flatMap(n=>_.set(t,h.some({owner:e,encoded:n}))),p.matchCauseEffect({onFailure:()=>p.failCause(n),onSuccess:()=>p.failCause(n)}))}function ie(e){return{value:E.make({get:p.succeed(e),changes:b.make(e)}),refetch:p.void,pending:E.make({get:p.succeed(!1),changes:b.make(!1)}),error:E.make({get:p.succeed(h.none()),changes:b.make(h.none())})}}function U(e,t,n,r){return b.unwrap(p.gen(function*(){let i=yield*(yield*ee).call(e.tag,e.payload()).pipe(p.catchCause(t=>re(e,n,t))),a=r(e.render(ie(i)));if(!t)return a;let o=`<script type="application/json">${P(yield*v.encodeEffect(e.successSchema)(i))}<\/script>`;return b.make(o).pipe(b.concat(a))}))}function W(e){return p.scoped(te.toSubscribable(e).pipe(p.flatMap(e=>e.get),p.map(e=>Array.from(e)),p.catchTag(`NoPropValue`,()=>p.succeed([]))))}function G(e,n){if(e==null||typeof e==`boolean`)return b.empty;if(typeof e==`string`||typeof e==`number`||typeof e==`bigint`)return b.make(M(String(e)));if(O(e)||p.isEffect(e)){let t=D(e);if(t!==void 0)return G(t,n);if(p.isEffect(e)){let t=p.runSyncExit(e);if(m.isSuccess(t))return G(t.value,n)}return k(e).pipe(b.runHead,p.map(h.match({onNone:()=>b.empty,onSome:e=>G(e,n)})),b.unwrap)}if(typeof e==`object`&&Symbol.iterator in e&&!(`type`in e))return b.flatMap(b.fromIterable(e),e=>G(e,n));if(typeof e==`object`&&`type`in e&&!(Symbol.iterator in e)){let{type:t,props:r}=e;if(t===S)return q(r,n);if(t===T){let e=r;return n===null?G(e.fallback??null,null):V(e,n,e=>G(e,n))}if(t===x)return H(r,null,e=>G(e,n));if(t===w)return U(r,!1,null,e=>G(e,n));if(t===C)return K(r,n);if(typeof t==`string`){let e=b.fromEffect(I(r).pipe(p.map(e=>`<${t}${e}>`)));return A.has(t)?e:e.pipe(b.concat(q(r,n)),b.concat(b.make(`</${t}>`)))}if(typeof t==`function`)return G(t(r),n)}return b.fail(new t({type:e.type,message:`Invalid Renderable type: expected string, FRAGMENT, or function, got ${typeof e.type}`}))}function K(e,t){return b.unwrap(p.gen(function*(){let n=yield*W(e.of),r=b.empty;return n.forEach((n,i)=>{r=r.pipe(b.concat(G(e.render(n,i),t)))}),r}))}function q(e,t){let n=`children`in e?e.children:void 0;if(n==null)return b.empty;let r=Array.isArray(n)?n:[n];return b.flatMap(b.fromIterable(r),e=>G(e,t))}function J(e,r,i){if(e==null||typeof e==`boolean`)return b.empty;if(typeof e==`string`||typeof e==`number`||typeof e==`bigint`)return b.make(M(String(e)));if(O(e)||p.isEffect(e)){let t=D(e);if(t!==void 0)return J(t,r,i);if(p.isEffect(e)){let t=p.runSyncExit(e);if(m.isSuccess(t))return J(t.value,r,i)}return k(e).pipe(b.runHead,p.map(e=>{let t=++r.current,a=h.match(e,{onNone:()=>b.empty,onSome:e=>J(e,r,i)});return b.make(`<!--${n(t)}-->`).pipe(b.concat(a),b.concat(b.make(`<!--${c(t)}-->`)))}),b.unwrap)}if(typeof e==`object`&&Symbol.iterator in e&&!(`type`in e))return b.flatMap(b.fromIterable(e),e=>J(e,r,i));if(typeof e==`object`&&`type`in e&&!(Symbol.iterator in e)){let{type:t,props:n}=e;if(t===S)return Y(n,r,i);if(t===T){let e=n;return i===null?J(e.fallback??null,r,null):V(e,i,e=>J(e,r,i))}if(t===x)return H(n,i?.failureCollector??null,e=>J(e,r,i));if(t===w)return U(n,!0,i?.failureCollector??null,e=>J(e,r,i));if(t===C)return ae(n,r,i);if(typeof t==`string`){let e=b.fromEffect(I(n).pipe(p.map(e=>`<${t}${e}>`)));return A.has(t)?e:e.pipe(b.concat(Y(n,r,i)),b.concat(b.make(`</${t}>`)))}if(typeof t==`function`)return J(t(n),r,i)}return b.fail(new t({type:e.type,message:`Invalid Renderable type: expected string, FRAGMENT, or function, got ${typeof e.type}`}))}function ae(t,r,i){return b.unwrap(p.gen(function*(){let o=++r.current,s=yield*W(t.of),l=b.empty;return s.forEach((n,o)=>{let s=++r.current;l=l.pipe(b.concat(b.make(`<!--${a(s)}-->`)),b.concat(J(t.render(n,o),r,i)),b.concat(b.make(`<!--${e(s)}-->`)))}),b.make(`<!--${n(o)}-->`).pipe(b.concat(l),b.concat(b.make(`<!--${c(o)}-->`)))}))}function Y(e,t,n){let r=`children`in e?e.children:void 0;if(r==null)return b.empty;let i=Array.isArray(r)?r:[r];return b.flatMap(b.fromIterable(i),e=>J(e,t,n))}const X=e=>G(e,null),oe=e=>b.unwrap(p.gen(function*(){let t=yield*g.unbounded(),n=yield*_.make(0),r=G(e,{patchQueue:t,pendingCount:n,idCounter:{current:0},scope:yield*p.scope,failureCollector:null}).pipe(b.ensuring(_.get(n).pipe(p.flatMap(e=>e===0?g.offer(t,h.none()).pipe(p.asVoid):p.void))));return b.concat(r,Z(t))})),Z=e=>b.fromQueue(e).pipe(b.takeWhile(h.isSome),b.map(e=>e.value)),Q=(e,t)=>p.gen(function*(){let n=yield*g.unbounded();yield*y.addFinalizer(t,p.asVoid(g.offer(n,h.none())));let r=yield*_.make(0);return{mainStream:J(e,{current:0},{patchQueue:n,pendingCount:r,idCounter:{current:0},scope:t,failureCollector:yield*_.make(h.none())}).pipe(b.ensuring(_.get(r).pipe(p.flatMap(e=>e===0?g.offer(n,h.none()).pipe(p.asVoid):p.void)))),patches:Z(n)}}),$=e=>b.unwrap(p.gen(function*(){let t=yield*p.scope,{mainStream:n,patches:r}=yield*Q(e,t);return b.concat(n,r)})),se=e=>X(e).pipe(b.mkString),ce=e=>$(e).pipe(b.mkString),le=e=>p.gen(function*(){let t=yield*p.scope,n=yield*y.fork(t,`sequential`),{mainStream:r,patches:i}=yield*Q(e,n);return{shell:yield*b.mkString(r).pipe(p.onError(e=>y.close(n,m.failCause(e)))),patches:i}});export{z as SuspenseFailureHandlerTag,le as renderToHydratableShell,oe as renderToStream,$ as renderToStreamHydratable,se as renderToString,ce as renderToStringHydratable};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{Context as e,Data as t}from"effect";var n=class extends t.TaggedError(`UnsupportedNodeTypeError`){},r=class extends t.TaggedError(`StreamSubscriptionError`){},i=class extends t.TaggedError(`RenderError`){},a=class extends t.TaggedError(`HydrationMismatchError`){},o=class extends e.Service()(`BoundaryContext`){},s=class extends e.Service()(`SuspenseContext`){},c=class extends e.Service()(`RenderContext`){};function l(e){return` stream-start-${e} `}function u(e){return` stream-end-${e} `}function d(e){return` suspense-start-${e} `}function f(e){return` suspense-end-${e} `}const p=`data-weft-suspense-failure`,m=/^ suspense-(start|end)-(\d+) $/;function h(e){let t=m.exec(e.data);return t===null?null:{kind:t[1],id:Number.parseInt(t[2],10)}}function g(e){return` boundary-start-${e} `}function _(e){return` boundary-end-${e} `}const v=/^ stream-(start|end)-(\d+) $/;function y(e){let t=v.exec(e.data);return t===null?null:{kind:t[1],id:Number.parseInt(t[2],10)}}function b(e){return` list-item-start-${e} `}function x(e){return` list-item-end-${e} `}const S=/^ list-item-(start|end)-(\d+) $/;function C(e){let t=S.exec(e.data);return t===null?null:{kind:t[1],id:Number.parseInt(t[2],10)}}function w(e){if(e.length<=2||!e.startsWith(`on`))return!1;let t=e[2];return t!==void 0&&t>=`a`&&t<=`z`}export{i as _,x as a,n as b,y as c,l as d,f,c as g,a as h,w as i,h as l,o as m,_ as n,b as o,d as p,g as r,C as s,p as t,u,r as v,s as y};
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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)
|
|
89
|
-
- [`Boundary` API reference](https://weftui.dev/docs/reference/core#boundary-namespace)
|
|
90
|
-
- [Load Data with RPC](https://weftui.dev/docs/how-to/load-data-with-rpc)
|
|
91
|
-
- [Render on the Server](https://weftui.dev/docs/how-to/render-on-the-server)
|
|
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
|
|
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
|
|
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
|
|
31
|
+
// pipe: chain Effect operators directly
|
|
30
32
|
const provided = pipe(h.div(userStream), Effect.provide(UserServiceLive));
|
|
31
33
|
|
|
32
|
-
// Effect.flatMap
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
161
|
-
- [Reactive Primitives](https://weftui.dev/docs/explanation/reactive-primitives)
|
|
162
|
-
- [Boundaries and Suspense](https://weftui.dev/docs/explanation/boundaries-and-suspense)
|
|
163
|
-
- [Author Components](https://weftui.dev/docs/how-to/author-components)
|
|
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)
|