@weftui/core 0.29.0 → 0.31.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.
@@ -1,10 +1,9 @@
1
- import { Cause, Effect, Option, Scope, Stream, SubscriptionRef } from "effect";
1
+ import { Cause, Effect, Option, Scope, Stream, SubscriptionRef, Types as Types$1 } from "effect";
2
2
  declare namespace index_d_exports {
3
- export { Subscribable, TypeId, changes, get, isSubscribable, make };
3
+ export { Subscribable, TypeId, Variance, changes, get, isSubscribable, make };
4
4
  }
5
5
  /**
6
- * Unique brand identifying a {@link Subscribable}, and the key under which its
7
- * `get`/`changes` channels are held. Effect 4 dropped its own
6
+ * Unique brand identifying a {@link Subscribable}. Effect 4 dropped its own
8
7
  * `Subscribable`/`Readable` modules, so Weft carries this reactivity interface
9
8
  * locally; the string brand mirrors Effect 4's `"~effect/*"` TypeId convention
10
9
  * (e.g. `SubscriptionRef`) and backs the {@link isSubscribable} guard.
@@ -15,22 +14,27 @@ declare const TypeId = "~@weftui/core/Subscribable";
15
14
  */
16
15
  type TypeId = typeof TypeId;
17
16
  /**
18
- * A hot, await-first reactive value: `get` reads the current value as an
19
- * `Effect`, `changes` is a `Stream` of every value (including the current one).
20
- * Read them either directly or through the {@link get} / {@link changes} module
21
- * accessors, which mirror Effect 4's `SubscriptionRef.get` / `.changes` so a
17
+ * Phantom variance carrier for {@link Subscribable}; all three channels are
18
+ * covariant, matching the `Effect`/`Stream` channels the value wraps.
19
+ */
20
+ interface Variance<out A, out E, out R> {
21
+ readonly _A: Types$1.Covariant<A>;
22
+ readonly _E: Types$1.Covariant<E>;
23
+ readonly _R: Types$1.Covariant<R>;
24
+ }
25
+ /**
26
+ * A hot, await-first reactive value: a current value plus a stream of every
27
+ * value (including the current one). The interface is brand-only; read it
28
+ * through the {@link get} / {@link changes} module accessors, which mirror
29
+ * Effect 4's `SubscriptionRef.get` / `SubscriptionRef.changes` so a
22
30
  * `Subscribable` and a `SubscriptionRef` read the same way at a call site.
23
31
  *
24
- * This is Weft's local replacement for Effect 3's `Subscribable`, preserved as
25
- * public API so `Source`, `Boundary`, and the DOM renderers keep the same
26
- * reactivity surface across the Effect 4 migration.
32
+ * This is Weft's local replacement for the `Subscribable` module Effect 4
33
+ * removed, preserved as public API so `Source`, `Boundary`, and the DOM
34
+ * renderers share one reactivity surface.
27
35
  */
28
- interface Subscribable<A, E = never, R = never> {
29
- readonly [TypeId]: TypeId;
30
- /** Read the current value; also reachable via the {@link get} accessor. */
31
- readonly get: Effect.Effect<A, E, R>;
32
- /** Stream of every value; also reachable via the {@link changes} accessor. */
33
- readonly changes: Stream.Stream<A, E, R>;
36
+ interface Subscribable<out A, out E = never, out R = never> {
37
+ readonly [TypeId]: Variance<A, E, R>;
34
38
  }
35
39
  /**
36
40
  * Build a {@link Subscribable} from a `get` effect and a `changes` stream. The
@@ -43,13 +47,14 @@ declare const make: <A, E = never, R = never>(options: {
43
47
  }) => Subscribable<A, E, R>;
44
48
  /**
45
49
  * Read the current value of a {@link Subscribable} as an `Effect`. Mirrors
46
- * `SubscriptionRef.get`, so call sites read `Subscribable`s and
47
- * `SubscriptionRef`s the same way.
50
+ * `SubscriptionRef.get`; the only way to read the value, the interface exposes
51
+ * no members.
48
52
  */
49
53
  declare const get: <A, E, R>(self: Subscribable<A, E, R>) => Effect.Effect<A, E, R>;
50
54
  /**
51
55
  * The `Stream` of every value of a {@link Subscribable}, starting with the
52
- * current one. Mirrors `SubscriptionRef.changes`.
56
+ * current one. Mirrors `SubscriptionRef.changes`; the only way to observe
57
+ * changes, the interface exposes no members.
53
58
  */
54
59
  declare const changes: <A, E, R>(self: Subscribable<A, E, R>) => Stream.Stream<A, E, R>;
55
60
  /**
@@ -10521,6 +10526,19 @@ declare namespace Source {
10521
10526
  type Error<S> = S extends Stream.Stream<any, infer E, any> ? E : S extends Effect.Effect<any, infer E, any> ? E : S extends Subscribable<any, infer E, any> ? E : never;
10522
10527
  /** Extract the requirement channel `R` from any {@link Source} kind; a static value ⇒ `never`. */
10523
10528
  type Context<S> = S extends Stream.Stream<any, any, infer R> ? R : S extends Effect.Effect<any, any, infer R> ? R : S extends Subscribable<any, any, infer R> ? R : never;
10529
+ /**
10530
+ * The change stream of a `Source<A>`, without the `Subscribable` hop.
10531
+ *
10532
+ * - existing `Subscribable` → its `changes` stream;
10533
+ * - `Stream<A>` → returned as-is (identity, no wrap);
10534
+ * - `Effect<A>` → `Stream.fromEffect` (emits the resolved value once);
10535
+ * - static `A` → `Stream.make(value)` (emits once).
10536
+ *
10537
+ * Unlike {@link toSubscribable} this allocates no ref, latch, or pump fiber,
10538
+ * so consumers that only ever read changes (e.g. list reconciliation) skip
10539
+ * the extra hop entirely.
10540
+ */
10541
+ function changes<A, E = never, R = never>(source: Source.Source<A, E, R>): Stream.Stream<A, E, R>;
10524
10542
  /**
10525
10543
  * Normalize a `Source<A>` into an await-first, hot `Subscribable`.
10526
10544
  *
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
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-4cTlhojA.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-B-dPfhKZ.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
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){return B(e)?z(e):M(e)?e:a.isEffect(e)?u.fromEffect(e):u.make(e)}e.changes=n;function r(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=r})(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-4cTlhojA.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-B-dPfhKZ.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 };
@@ -40,15 +40,28 @@ An unhandled failure re-raises to the **nearest enclosing** boundary; if none ca
40
40
 
41
41
  ### Post-mount failures with no enclosing boundary
42
42
 
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.
43
+ The routing above describes what happens while a node is being built. Once mounted, a reactive region keeps running for the lifetime of its scope: an attribute, a child, a list stream, or a hydrated equivalent. It can still fail later, e.g. a `Boundary.rpc` resource's refetch stream raising after a client-side navigation.
44
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.
45
+ If a boundary encloses the region, the failure routes to it exactly as above and its fallback swaps in. If none does, 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 alongside the subscription itself) observes the exit instead.
46
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.
47
+ An exit whose cause is not interruption-only publishes an `UnhandledError` (`cause`, `region`, `root`) to the app's unhandled-error hub, exposed as `WeftApp.errors(app)`. `region` names the failing spot: `attribute:class`, `child:stream-3`, or `boundary:outermost` for a failure that escapes even the outermost boundary. Subscribe to observe every occurrence yourself:
48
48
 
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.
49
+ ```typescript
50
+ import { WeftApp } from "@weftui/dom/client";
51
+ import { Effect, Stream } from "effect";
52
+
53
+ Effect.runFork(
54
+ Stream.runForEach(WeftApp.errors(app), (error) =>
55
+ Effect.log(`unhandled in ${error.region}`, error.cause),
56
+ ),
57
+ );
58
+ ```
50
59
 
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.
60
+ With zero subscribers, each unhandled error instead runs a default `Effect.logError(cause)` annotated with `weft.region`, exactly once per occurrence, in dev and prod alike. Subscribing suppresses that fallback for as long as at least one subscriber stays attached.
61
+
62
+ This is deliberate: rather than leave an unobserved fiber exit to the runtime, Weft always surfaces it, either to your subscriber or to the log. Interruption (ordinary unmount teardown) is never published or logged; only genuine failures are.
63
+
64
+ A stream that can fail with no enclosing boundary is a stream whose failures you've chosen not to route into the UI. `WeftApp.errors` (and the log fallback) is what tells you that decision has consequences at runtime. Full contract: [`WeftApp.errors` / `UnhandledError`](https://weftui.dev/docs/reference/dom#weftapperrors).
52
65
 
53
66
  ## Suspense boundaries
54
67
 
@@ -81,13 +94,13 @@ On the server, `renderToStreamHydratable` emits the fallback inline and appends
81
94
  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).
82
95
 
83
96
  ```typescript
84
- import { Boundary, h } from "@weftui/core";
97
+ import { Boundary, h, Subscribable } from "@weftui/core";
85
98
  import { Stream } from "effect";
86
99
 
87
100
  Boundary.rpc(
88
101
  GetStock,
89
102
  () => ({ id: productId }),
90
- (resource) => h.span([Stream.map(resource.value.changes, (s) => String(s.units))]),
103
+ (resource) => h.span([Stream.map(Subscribable.changes(resource.value), (s) => String(s.units))]),
91
104
  { fallback: h.p("loading…") },
92
105
  );
93
106
  ```
@@ -100,11 +113,28 @@ Its channel behavior is also distinct. The rpc's typed `error` schema joins the
100
113
 
101
114
  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
115
 
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.
116
+ 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:
117
+
118
+ ```typescript
119
+ import { Boundary, h, Subscribable } from "@weftui/core";
120
+ import { Stream } from "effect";
121
+
122
+ Boundary.catchTag({ tag: "StockRpcError", fallback: () => h.p("Couldn't load stock.") }, [
123
+ Boundary.rpc(
124
+ GetStock,
125
+ () => ({ id: productId }),
126
+ (resource) =>
127
+ h.span([Stream.map(Subscribable.changes(resource.value), (s) => String(s.units))]),
128
+ ),
129
+ ]);
130
+ ```
131
+
132
+ Each layer only sees the channel its own kind produces: `catchTag` narrows `E`, `Boundary.rpc` widens it by the rpc's error schema. Neither cares how the other renders.
104
133
 
105
134
  ## See also
106
135
 
107
136
  - [The Rendering Model](https://weftui.dev/docs/explanation/rendering-model): why a boundary is just a node in a static tree
108
137
  - [`Boundary` API reference](https://weftui.dev/docs/reference/core#boundary-namespace): every variant's signature and channel algebra
138
+ - [`WeftApp.errors` / `UnhandledError`](https://weftui.dev/docs/reference/dom#weftapperrors): the unhandled-error hub for post-mount failures with no enclosing boundary
109
139
  - [Load Data with RPC](https://weftui.dev/docs/how-to/load-data-with-rpc): the full `Boundary.rpc` walkthrough and its four lifecycles
110
140
  - [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,14 @@ 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.
10
+ Weft builds UI trees by calling builder functions, not by writing markup. There is no JSX runtime and no `h(Component)` overload: a component is a plain function you call, and its result goes straight into the tree.
11
+
12
+ ```typescript
13
+ // No JSX, no deferred element. Header/Main/Footer already ran; these are Nodes.
14
+ const tree = h.div({ class: "app" }, [Header(), Main(), Footer()]);
15
+ ```
16
+
17
+ Because a component's return type stays a concrete `Effect.Effect<ElementDescriptor, E, R>`, its error channel (`E`) and requirement channel (`R`) propagate through the whole tree. Both are visible to the type checker and satisfiable exactly once, at the mount boundary.
11
18
 
12
19
  JSX collapses every component's return type to an opaque `JSX.Element`, erasing both channels. The combinator API exists specifically to keep them intact.
13
20
 
@@ -110,7 +117,7 @@ const TableRow = ({ user }: { user: User }) =>
110
117
 
111
118
  ## Custom components with `Component.gen` / `Component.make`
112
119
 
113
- Plain functions work fine for simple components, but the `Component` factories provide type-level wiring so the caller's reactive prop types contribute their `E`/`R` to the returned node. Pick `Component.make` for a plain-function body and `Component.gen` for a generator body (when you need `yield*` to set up local state or pull from services).
120
+ Plain functions work fine for simple components. The `Component` factories add type-level wiring: the caller's actual reactive prop types contribute their `E`/`R` to the node returned at that call site, not just the types you wrote in the signature. Pick `Component.make` for a plain-function body, `Component.gen` for a generator body (when you need `yield*` for local state or a service).
114
121
 
115
122
  ```typescript
116
123
  import { Component, h } from "@weftui/core";
@@ -132,30 +139,29 @@ declare const labelStream: Stream.Stream<string, never, I18nService>;
132
139
  const btn = Button({ label: labelStream });
133
140
  ```
134
141
 
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.
142
+ Components also accept an optional `children` argument: either `readonly Renderable[]`, or a `(input) => readonly Renderable[]` function for the render-prop pattern. Either way, the children's `E`/`R` accumulate onto the resulting node, including the array a function-children call returns.
136
143
 
137
- Without `Component`, a plain function's return type is fixed at definition time and does not reflect the caller's reactive prop types.
144
+ A plain function without `Component` has its return type fixed at definition time. It reflects only the prop types you wrote, never a specific caller's reactive prop types.
138
145
 
139
- See [component-authoring.md](https://weftui.dev/docs/how-to/author-components) for a full walkthrough.
146
+ See [Author Components](https://weftui.dev/docs/how-to/author-components) for a full walkthrough.
140
147
 
141
- ## Suspense boundaries
148
+ ## Boundaries accumulate channels too
142
149
 
143
- `Boundary.suspend` wraps async children and shows a fallback until all of them have emitted their first value:
150
+ `Boundary.suspend`, `Boundary.catch`, and the rest of the `Boundary` namespace are Nodes, not a separate concept. A boundary wraps children and its own type is `Node<ChildrenE, ChildrenR>`, so the same accumulation rules apply:
144
151
 
145
152
  ```typescript
146
153
  import { Boundary, h } from "@weftui/core";
147
154
 
155
+ // Node<ChildrenE, ChildrenR>: transparent to E/R, like a plain h.* parent
148
156
  Boundary.suspend({ fallback: h.div({ class: "spinner" }, "Loading...") }, [
149
157
  AsyncCard({ id: 1 }),
150
158
  AsyncCard({ id: 2 }),
151
159
  ]);
152
160
  ```
153
161
 
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.
155
-
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.
162
+ A boundary changes _when_ and _whether_ its children's output reaches the DOM, not what its type carries. `Boundary.catchTag` is the one exception: it removes the matched tag from `E`, since that's the failure it discharges.
157
163
 
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`.
164
+ See [Boundaries and Suspense](https://weftui.dev/docs/explanation/boundaries-and-suspense) for what each boundary variant does, and the [core reference](https://weftui.dev/docs/reference/core#boundary-namespace) for the full `Boundary.*` surface.
159
165
 
160
166
  ## See also
161
167
 
@@ -7,16 +7,14 @@ description: The Source<A, E, R> vocabulary; Stream, Effect, and Subscribable as
7
7
 
8
8
  # Reactive Primitives
9
9
 
10
- The unified `Source` vocabulary lets static values, Effects, Streams, and Subscribables be used interchangeably. Props, children, and style values all accept the same type.
10
+ Weft accepts a `Source` for any prop value, any child, and any style value: one vocabulary, four kinds, used interchangeably wherever reactivity is supported.
11
11
 
12
- Weft accepts a `Source` for prop values and children. Any of these is valid wherever reactivity is supported:
13
-
14
- - A plain static value (`string`, `number`, `boolean`, ...)
15
- - An `Effect.Effect<A, E, R>`: runs once and resolves to a value
16
- - A `Stream.Stream<A, E, R>`: each emission replaces the previous value
17
- - A `Subscribable<A, E, R>`: like a hot stream; already has a "current value"
18
-
19
- The `Source<A, E, R>` type captures this union:
12
+ | Kind | Behavior |
13
+ | --------------------------------------------------------- | ----------------------------------------- |
14
+ | a plain static value (`string`, `number`, `boolean`, ...) | set once, never updates |
15
+ | `Effect.Effect<A, E, R>` | runs once, resolves to a value |
16
+ | `Stream.Stream<A, E, R>` | each emission replaces the previous value |
17
+ | `Subscribable<A, E, R>` | a hot stream: already has a current value |
20
18
 
21
19
  ```typescript
22
20
  type Source<A, E, R> = A | Effect.Effect<A, E, R> | Stream.Stream<A, E, R> | Subscribable<A, E, R>;
@@ -139,13 +137,42 @@ h.div({
139
137
  });
140
138
  ```
141
139
 
140
+ ## Latest-value-wins conflation
141
+
142
+ Every reactive region and prop is drained by one shared commit scheduler, the
143
+ Loom (see [The Rendering Model](https://weftui.dev/docs/explanation/rendering-model#the-loom-one-scheduler-per-app-committing-asynchronously)).
144
+ When a source emits faster than the DOM commits, the Loom conflates the burst:
145
+ it keeps only the newest value per region and skips the intermediate ones.
146
+
147
+ ```typescript
148
+ const ticks = Stream.range(0, 999); // publishes far faster than the DOM commits
149
+
150
+ h.span([ticks]); // the span settles on 999; 0 through 998 may never render
151
+ ```
152
+
153
+ The final DOM state always reflects the newest value; nothing is lost
154
+ permanently. What is skipped are the values in between, by design: this is
155
+ what keeps a fast-publishing source from piling up unbounded DOM work.
156
+
157
+ Code that must observe every emission, not just the settled one (an audit
158
+ log, a counter that sums each tick), should consume the stream directly
159
+ instead of relying on what lands in the DOM:
160
+
161
+ ```typescript
162
+ yield * Stream.runForEach(ticks, (n) => Effect.sync(() => total.push(n)));
163
+ ```
164
+
142
165
  ## NoPropValue
143
166
 
144
- When a `Stream` prop ends before emitting, the renderer raises a `NoPropValue` tagged error. This carries an optional `key` field identifying which prop triggered it:
167
+ A finite `Stream` prop can end without ever emitting, e.g. `Stream.empty` or `Stream.take(0, stream)`. When it does, the renderer raises a `NoPropValue` tagged error carrying an optional `key` that identifies the prop:
168
+
169
+ ```typescript
170
+ h.span([Stream.empty]); // completes without emitting: raises NoPropValue
171
+ ```
172
+
173
+ `Effect.catchTag` matches by the string tag, so handling it at the mount boundary needs no `NoPropValue` import:
145
174
 
146
175
  ```typescript
147
- // Handle at the mount boundary if needed. `Effect.catchTag` matches the error
148
- // by its string tag, so no `NoPropValue` import is required here.
149
176
  pipe(
150
177
  WeftApp.mount(app, App(), root),
151
178
  Effect.catchTag("NoPropValue", (e) =>
@@ -154,7 +181,7 @@ pipe(
154
181
  );
155
182
  ```
156
183
 
157
- In practice you only encounter `NoPropValue` when a finite `Stream` prop ends before emitting (e.g., `Stream.empty` or `Stream.take(0, stream)`). Most usage with `SubscriptionRef.changes` or infinite streams never raises it.
184
+ `SubscriptionRef.changes` and other infinite streams always emit before completing, so most usage never raises it.
158
185
 
159
186
  ## See also
160
187
 
@@ -37,12 +37,22 @@ When a stream emits, only the DOM at _that_ point updates. Nothing above it re-r
37
37
 
38
38
  ## Streams drive all updates
39
39
 
40
- There is no `setState`, no render-triggering scheduler, no "re-render this component." A region of the DOM is live **if and only if** a stream is woven into it. To make something update, you thread a stream through it; to keep something static, you pass a plain value.
40
+ There is no `setState`, no "re-render this component," no vdom diff. A region of the DOM is live **if and only if** a stream is woven into it. To make something update, you thread a stream through it; to keep something static, you pass a plain value.
41
41
 
42
- The renderer subscribes to each woven stream and patches its target in place on every emission. It reuses the existing DOM node and patches text and attributes rather than recreating elements, so identity, focus, and typed input survive an update.
42
+ The renderer reuses the existing DOM node and patches text and attributes in place rather than recreating elements, so identity, focus, and typed input survive an update. Updates stay local: a stream woven at one point never touches an untouched sibling or re-runs a parent.
43
43
 
44
44
  This also fixes the update _shape_. Because the structure is fixed, an update is always "new value into a known hole," never "reconcile these two trees." Even list rendering, where the number of children genuinely varies, is expressed as a keyed region ([`List.each`](https://weftui.dev/docs/how-to/render-keyed-lists)). It reconciles by key rather than by structural diff.
45
45
 
46
+ ## The Loom: one scheduler per app, committing asynchronously
47
+
48
+ Every woven stream feeds one shared scheduler per `WeftApp`: the **Loom**. It keeps one latest-value slot per reactive region or prop and commits changes to the DOM in passes.
49
+
50
+ The name follows the metaphor: individual streams spin as fast as they like, like threads paid out from a bobbin. The loom only ever weaves the newest state of each thread into the fabric, one pass at a time. A region that receives several values before its next commit collapses to the last one: intermediate emissions are conflated, never committed. This bounds DOM work no matter how fast a source publishes.
51
+
52
+ Commits are asynchronous: they happen on the scheduler's own turn, not synchronously with the write. `RootHandle.awaitCommit` is the acknowledgement. It resolves once everything pending at call time has either committed or been discarded, returning the commit generation. See the [`RootHandle` reference](https://weftui.dev/docs/reference/dom#roothandle) for its exact semantics.
53
+
54
+ None of this changes what you write. You still thread a `Stream`, `Effect`, or `Subscribable` through a prop or child; the Loom is an implementation detail of how those emissions reach the DOM. Code that must observe every intermediate value, not just the settled one, should consume the stream directly instead of relying on what lands in the DOM (see [Reactive Primitives](https://weftui.dev/docs/explanation/reactive-primitives#latest-value-wins-conflation)).
55
+
46
56
  ## One tree, two sides, hydrate in place
47
57
 
48
58
  The same component tree renders on the server and the client:
@@ -56,6 +66,7 @@ Because the same `Node<E, R>` describes both passes, there is nothing to keep in
56
66
 
57
67
  - **No diff cost.** Updates are O(changed value), not O(tree). There is no reconciliation pass to pay for.
58
68
  - **Local reasoning.** A stream woven at one point cannot affect another. What is reactive is exactly what you made reactive.
69
+ - **Bounded commit work.** The Loom conflates bursts to one commit per region, so a fast-publishing source cannot outrun the DOM.
59
70
  - **Type-honest edges.** The app node's `E`/`R` is the whole app's error and dependency surface, checked at compile time and discharged once at the edge.
60
71
  - **Flash-free SSR by construction.** Hydration adopts rather than replaces, because the tree is identical on both sides.
61
72
 
@@ -65,3 +76,4 @@ Because the same `Node<E, R>` describes both passes, there is nothing to keep in
65
76
  - [Reactive Primitives](https://weftui.dev/docs/explanation/reactive-primitives): the stream-shaped values you weave through the tree
66
77
  - [Boundaries and Suspense](https://weftui.dev/docs/explanation/boundaries-and-suspense): how failure and async are modeled as nodes in the same tree
67
78
  - [Render on the Server](https://weftui.dev/docs/how-to/render-on-the-server): the server/client split and `hydrate`
79
+ - [`@weftui/dom` reference](https://weftui.dev/docs/reference/dom#roothandle): `RootHandle.awaitCommit` and `commitGeneration`