@weftui/router 0.26.3 → 0.27.1
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 +3 -1
- package/dist/client/index.d.ts +4 -4
- package/dist/client/index.js +22 -19
- package/dist/{compile-CQhAXB5f.d.ts → compile-BJFIgBbE.d.ts} +26 -30
- package/dist/{href-Deoi59OI.js → href-uvJ6b7zz.js} +11 -11
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/{outlet-PoLEThsy.js → outlet-C9N4a4_F.js} +18 -13
- package/dist/{outlet-CGfLQgZS.d.ts → outlet-eoUQ-W0k.d.ts} +1 -1
- package/dist/server/index.d.ts +2 -2
- package/dist/server/index.js +19 -18
- package/docs/explanation/boundaries-and-suspense.md +10 -10
- package/docs/explanation/reactive-primitives.md +13 -11
- package/docs/explanation/rendering-model.md +1 -1
- package/docs/explanation/services-and-context.md +4 -4
- package/docs/how-to/add-routing.md +7 -5
- package/docs/how-to/author-components.md +5 -5
- package/docs/how-to/handle-forms.md +10 -10
- package/docs/how-to/load-async-data.md +4 -4
- package/docs/how-to/load-data-with-rpc.md +5 -5
- package/docs/how-to/provide-services.md +2 -2
- package/docs/how-to/use-element-refs.md +5 -5
- package/docs/reference/core.md +31 -20
- package/docs/reference/router.md +6 -6
- package/docs/tutorial/01-your-first-app.md +3 -1
- package/docs/tutorial/02-reactivity.md +6 -6
- package/docs/tutorial/03-services-and-async.md +3 -4
- package/docs/tutorial/04-errors-and-server.md +2 -2
- package/package.json +5 -7
package/README.md
CHANGED
|
@@ -9,9 +9,11 @@ Three entry points mirror `@weftui/dom`: `@weftui/router` (authoring + universal
|
|
|
9
9
|
## Installation
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
npm install @weftui/core @weftui/dom @weftui/router effect
|
|
12
|
+
npm install @weftui/core @weftui/dom @weftui/router effect@beta
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
+
Weft tracks Effect 4's beta line. This release is built and tested against `effect@4.0.0-beta.98`; the peer range accepts newer 4.0 betas, which may contain upstream breaking changes.
|
|
16
|
+
|
|
15
17
|
`effect` is a peer dependency; `@weftui/core` and `@weftui/dom` provide the tree and renderer.
|
|
16
18
|
|
|
17
19
|
## Key exports
|
package/dist/client/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { S as Router, b as NavState, i as RouterDef, m as RouteNode, u as Fields, x as NavigateOptions } from "../compile-
|
|
2
|
-
import { n as outletNode, r as HrefArgs, t as RouterApp } from "../outlet-
|
|
1
|
+
import { S as Router, b as NavState, i as RouterDef, m as RouteNode, u as Fields, x as NavigateOptions } from "../compile-BJFIgBbE.js";
|
|
2
|
+
import { n as outletNode, r as HrefArgs, t as RouterApp } from "../outlet-eoUQ-W0k.js";
|
|
3
3
|
import { Effect, Layer, Scope } from "effect";
|
|
4
4
|
import { AppRpcClientTag } from "@weftui/core";
|
|
5
|
-
import { RpcGroup } from "
|
|
5
|
+
import { RpcGroup } from "effect/unstable/rpc";
|
|
6
6
|
|
|
7
7
|
//#region src/client/router-live.d.ts
|
|
8
8
|
/**
|
|
@@ -63,7 +63,7 @@ interface RouterLiveOptions {
|
|
|
63
63
|
* **network** flat rpc client (`RpcClient.make` over `layerProtocolHttp` →
|
|
64
64
|
* `POST /_eui/rpc`) — so `@weftui/dom` can resolve a `Boundary.rpc` (hydrated
|
|
65
65
|
* refetch and client-first mount) without depending on this package or
|
|
66
|
-
*
|
|
66
|
+
* `effect/unstable/rpc`.
|
|
67
67
|
*/
|
|
68
68
|
declare function RouterLive<R>(def: RouterDef<any, R>, options?: RouterLiveOptions & ContextOption<R>): Layer.Layer<Router | AppRpcClientTag | AppServices<R>>;
|
|
69
69
|
//#endregion
|
package/dist/client/index.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { a as Router, i as setResolvedCommit, n as outletNode, o as getPreload, r as preRunLeaf, t as RouterApp } from "../outlet-
|
|
2
|
-
import { r as match, t as href } from "../href-
|
|
3
|
-
import {
|
|
4
|
-
import { Context, Effect, Exit, Fiber, Layer, Option,
|
|
5
|
-
import { AppRpcClientTag } from "@weftui/core";
|
|
1
|
+
import { a as Router, i as setResolvedCommit, n as outletNode, o as getPreload, r as preRunLeaf, t as RouterApp } from "../outlet-C9N4a4_F.js";
|
|
2
|
+
import { r as match, t as href } from "../href-uvJ6b7zz.js";
|
|
3
|
+
import { HttpApiClient } from "effect/unstable/httpapi";
|
|
4
|
+
import { Context, Effect, Exit, Fiber, Layer, Option, Schema, Scope, Stream, SubscriptionRef } from "effect";
|
|
5
|
+
import { AppRpcClientTag, Subscribable } from "@weftui/core";
|
|
6
|
+
import { FetchHttpClient } from "effect/unstable/http";
|
|
6
7
|
//#region src/client/link.ts
|
|
7
8
|
/**
|
|
8
9
|
* Installs a global, delegated click interceptor (in the `Router` layer scope)
|
|
@@ -18,7 +19,7 @@ import { AppRpcClientTag } from "@weftui/core";
|
|
|
18
19
|
*/
|
|
19
20
|
function installLinkInterceptor(def, navigate) {
|
|
20
21
|
return Effect.gen(function* () {
|
|
21
|
-
const
|
|
22
|
+
const services = yield* Effect.context();
|
|
22
23
|
const onClick = (event) => {
|
|
23
24
|
if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
|
|
24
25
|
const target = event.target;
|
|
@@ -39,7 +40,7 @@ function installLinkInterceptor(def, navigate) {
|
|
|
39
40
|
if (to === `${window.location.pathname}${window.location.search}`) return;
|
|
40
41
|
if (match(def, to)._tag !== "Matched") return;
|
|
41
42
|
event.preventDefault();
|
|
42
|
-
|
|
43
|
+
Effect.runForkWith(services)(navigate(to));
|
|
43
44
|
};
|
|
44
45
|
yield* Effect.acquireRelease(Effect.sync(() => document.addEventListener("click", onClick)), () => Effect.sync(() => document.removeEventListener("click", onClick)));
|
|
45
46
|
});
|
|
@@ -73,13 +74,13 @@ function normalizeTo(to) {
|
|
|
73
74
|
* **network** flat rpc client (`RpcClient.make` over `layerProtocolHttp` →
|
|
74
75
|
* `POST /_eui/rpc`) — so `@weftui/dom` can resolve a `Boundary.rpc` (hydrated
|
|
75
76
|
* refetch and client-first mount) without depending on this package or
|
|
76
|
-
*
|
|
77
|
+
* `effect/unstable/rpc`.
|
|
77
78
|
*/
|
|
78
79
|
function RouterLive(def, options = {}) {
|
|
79
|
-
const core = Layer.
|
|
80
|
+
const core = Layer.effectContext(Effect.gen(function* () {
|
|
80
81
|
const urlRef = yield* SubscriptionRef.make(locationUrl());
|
|
81
82
|
const navRef = yield* SubscriptionRef.make({ _tag: "Idle" });
|
|
82
|
-
const
|
|
83
|
+
const services = yield* Effect.context();
|
|
83
84
|
const httpApiClient = yield* HttpApiClient.make(def.httpApi, { baseUrl: options.baseUrl ?? window.location.origin }).pipe(Effect.provide(FetchHttpClient.layer));
|
|
84
85
|
let latest = 0;
|
|
85
86
|
let inflightPreRun;
|
|
@@ -114,11 +115,10 @@ function RouterLive(def, options = {}) {
|
|
|
114
115
|
let exit;
|
|
115
116
|
if (target._tag === "Matched") {
|
|
116
117
|
const scope = yield* Scope.make();
|
|
117
|
-
const fiber = yield* Effect.
|
|
118
|
+
const fiber = yield* Effect.forkChild(Effect.provideService(preRunLeaf(router, target), Scope.Scope, scope));
|
|
118
119
|
inflightPreRun = fiber;
|
|
119
|
-
if (!emitted) yield* Effect.
|
|
120
|
-
|
|
121
|
-
if (Option.isNone(done) && token === latest) {
|
|
120
|
+
if (!emitted) yield* Effect.forkChild(Effect.gen(function* () {
|
|
121
|
+
if (fiber.pollUnsafe() === void 0 && token === latest) {
|
|
122
122
|
emitted = true;
|
|
123
123
|
yield* SubscriptionRef.set(navRef, {
|
|
124
124
|
_tag: "Navigating",
|
|
@@ -149,28 +149,31 @@ function RouterLive(def, options = {}) {
|
|
|
149
149
|
});
|
|
150
150
|
const navigate = (to, options) => commitTo(normalizeTo(to), true, options?.replace === true);
|
|
151
151
|
const onPopState = () => {
|
|
152
|
-
|
|
152
|
+
Effect.runForkWith(services)(commitTo(locationUrl(), false, false));
|
|
153
153
|
};
|
|
154
154
|
yield* Effect.acquireRelease(Effect.sync(() => window.addEventListener("popstate", onPopState)), () => Effect.sync(() => window.removeEventListener("popstate", onPopState)));
|
|
155
155
|
yield* installLinkInterceptor(def, navigate);
|
|
156
156
|
const currentMatch = Subscribable.make({
|
|
157
157
|
get: Effect.map(SubscriptionRef.get(urlRef), (url) => match(def, url)),
|
|
158
|
-
changes: Stream.map(
|
|
158
|
+
changes: Stream.map(SubscriptionRef.changes(urlRef), (url) => match(def, url))
|
|
159
159
|
});
|
|
160
160
|
const rpc = options.rpc;
|
|
161
161
|
let appRpcClient;
|
|
162
162
|
if (rpc === void 0) appRpcClient = AppRpcClientTag.of({ call: (tag) => Effect.fail(/* @__PURE__ */ new Error(`Boundary.rpc "${tag}" cannot resolve: no \`rpc\` option was passed to RouterLive`)) });
|
|
163
163
|
else {
|
|
164
|
-
const { RpcClient, RpcSerialization } = yield* Effect.promise(() => import("
|
|
164
|
+
const { RpcClient, RpcSerialization } = yield* Effect.promise(() => import("effect/unstable/rpc"));
|
|
165
165
|
const baseUrl = String(options.baseUrl ?? window.location.origin).replace(/\/$/, "");
|
|
166
|
-
const flatClient = yield* RpcClient.make(rpc.group, { flatten: true }).pipe(Effect.provide(RpcClient.layerProtocolHttp({ url: `${baseUrl}${RPC_PATH}` }).pipe(Layer.provide(Layer.mergeAll(FetchHttpClient.layer, RpcSerialization.
|
|
166
|
+
const flatClient = yield* RpcClient.make(rpc.group, { flatten: true }).pipe(Effect.provide(RpcClient.layerProtocolHttp({ url: `${baseUrl}${RPC_PATH}` }).pipe(Layer.provide(Layer.mergeAll(FetchHttpClient.layer, RpcSerialization.layerNdjson)))));
|
|
167
167
|
appRpcClient = AppRpcClientTag.of({ call: (tag, payload) => flatClient(tag, payload) });
|
|
168
168
|
}
|
|
169
169
|
router = Router.of({
|
|
170
170
|
currentMatch,
|
|
171
171
|
navigate,
|
|
172
172
|
httpApiClient: Option.some(httpApiClient),
|
|
173
|
-
navigating:
|
|
173
|
+
navigating: Subscribable.make({
|
|
174
|
+
get: SubscriptionRef.get(navRef),
|
|
175
|
+
changes: SubscriptionRef.changes(navRef)
|
|
176
|
+
})
|
|
174
177
|
});
|
|
175
178
|
return Context.make(Router, router).pipe(Context.add(AppRpcClientTag, appRpcClient));
|
|
176
179
|
}));
|
|
@@ -1,20 +1,18 @@
|
|
|
1
|
-
import { HttpApi, HttpApiClient } from "
|
|
2
|
-
import { Context, Effect, Option, Schema
|
|
3
|
-
import { Component, Node } from "@weftui/core";
|
|
1
|
+
import { HttpApi, HttpApiClient } from "effect/unstable/httpapi";
|
|
2
|
+
import { Context, Effect, Option, Schema } from "effect";
|
|
3
|
+
import { Component, Node, Subscribable } from "@weftui/core";
|
|
4
4
|
|
|
5
5
|
//#region src/errors.d.ts
|
|
6
|
-
declare const RouterNotFound_base: Schema.
|
|
7
|
-
readonly
|
|
8
|
-
}
|
|
9
|
-
/** The path that could not be resolved, when known. */path: Schema.optional<typeof Schema.String>;
|
|
10
|
-
}>;
|
|
6
|
+
declare const RouterNotFound_base: Schema.Class<RouterNotFound, Schema.TaggedStruct<"RouterNotFound", {
|
|
7
|
+
/** The path that could not be resolved, when known. */readonly path: Schema.optional<Schema.String>;
|
|
8
|
+
}>, import("effect/Cause").YieldableError>;
|
|
11
9
|
/**
|
|
12
10
|
* Tagged error raised by {@link notFound} and caught by the router's internal
|
|
13
11
|
* not-found boundary. Exported so a user can place their own
|
|
14
12
|
* `Boundary.catchTag("RouterNotFound", …)` to override the fallback for a subtree
|
|
15
13
|
* (the router's internal boundary is outermost, so a nearer user boundary wins).
|
|
16
14
|
*
|
|
17
|
-
* Modeled as a `Schema.
|
|
15
|
+
* Modeled as a `Schema.TaggedErrorClass` so it can be encoded/decoded across the wire
|
|
18
16
|
* the same way `Boundary.rpc` replays typed failures.
|
|
19
17
|
*/
|
|
20
18
|
declare class RouterNotFound extends RouterNotFound_base {}
|
|
@@ -29,12 +27,10 @@ declare class RouterNotFound extends RouterNotFound_base {}
|
|
|
29
27
|
declare const notFound: (path?: string) => Effect.Effect<never, RouterNotFound>;
|
|
30
28
|
/** Type guard recognising a {@link RouterNotFound} value regardless of its prototype. */
|
|
31
29
|
declare const isRouterNotFound: (u: unknown) => u is RouterNotFound;
|
|
32
|
-
declare const RouterParamsError_base: Schema.
|
|
33
|
-
readonly
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
keys: Schema.Array$<typeof Schema.String>;
|
|
37
|
-
}>;
|
|
30
|
+
declare const RouterParamsError_base: Schema.Class<RouterParamsError, Schema.TaggedStruct<"RouterParamsError", {
|
|
31
|
+
/** Which side of the match failed validation. */readonly source: Schema.Literals<readonly ["path", "query"]>; /** The requested field names, for diagnostics. */
|
|
32
|
+
readonly keys: Schema.$Array<Schema.String>;
|
|
33
|
+
}>, import("effect/Cause").YieldableError>;
|
|
38
34
|
/**
|
|
39
35
|
* Tagged error raised by `Router.params` / `Router.query` when the live match does
|
|
40
36
|
* not satisfy the requested fields — either no route is matched, or a requested
|
|
@@ -45,7 +41,7 @@ declare const RouterParamsError_base: Schema.TaggedErrorClass<RouterParamsError,
|
|
|
45
41
|
* It bubbles up through the route tree's aggregate error channel, so a user may
|
|
46
42
|
* place a `Boundary.catchTag("RouterParamsError", …)` to recover within a subtree.
|
|
47
43
|
*
|
|
48
|
-
* Modeled as a `Schema.
|
|
44
|
+
* Modeled as a `Schema.TaggedErrorClass` so it can be encoded/decoded across the wire
|
|
49
45
|
* the same way `RouterNotFound` and `Boundary.rpc` replay typed failures.
|
|
50
46
|
*/
|
|
51
47
|
declare class RouterParamsError extends RouterParamsError_base {}
|
|
@@ -63,16 +59,16 @@ type RouteMatch = {
|
|
|
63
59
|
readonly url: string;
|
|
64
60
|
};
|
|
65
61
|
/** A string-encodeable schema as carried by an HttpApi endpoint's path/urlParams slot. */
|
|
66
|
-
type ParamSchema = Schema.
|
|
62
|
+
type ParamSchema = Schema.Codec<Record<string, unknown>, unknown, never>;
|
|
67
63
|
/** A precompiled regex + decode schemas for one leaf, sourced from its HttpApi endpoint. */
|
|
68
64
|
interface MatcherEntry {
|
|
69
65
|
/** The compiled leaf (render/nesting metadata), resolved from the endpoint id. */
|
|
70
66
|
readonly leaf: CompiledLeaf;
|
|
71
67
|
readonly regex: RegExp;
|
|
72
68
|
readonly paramNames: readonly string[];
|
|
73
|
-
/** Path-param schema, read from the endpoint's `
|
|
69
|
+
/** Path-param schema, read from the endpoint's `params` slot. */
|
|
74
70
|
readonly pathSchema: ParamSchema;
|
|
75
|
-
/** Query schema, read from the endpoint's `
|
|
71
|
+
/** Query schema, read from the endpoint's `query` slot. */
|
|
76
72
|
readonly querySchema: ParamSchema;
|
|
77
73
|
}
|
|
78
74
|
/**
|
|
@@ -115,7 +111,7 @@ declare function match(def: RouterDef, url: string): RouteMatch;
|
|
|
115
111
|
*/
|
|
116
112
|
/**
|
|
117
113
|
* The platform `HttpApiClient` derived from a router's `HttpApi` spine. Typed
|
|
118
|
-
* opaquely (`Client<any, …>`) because the spine is `HttpApi.
|
|
114
|
+
* opaquely (`Client<any, …>`) because the spine is `HttpApi.Top` — its
|
|
119
115
|
* group/endpoint shapes are assembled in a runtime loop by `buildHttpApi`, so a
|
|
120
116
|
* precise client type is not recoverable. Present (`Option.some`) on the client
|
|
121
117
|
* (`RouterLive`), absent (`Option.none`) on the server, which is itself the origin.
|
|
@@ -145,7 +141,7 @@ interface NavigateOptions {
|
|
|
145
141
|
*/
|
|
146
142
|
readonly replace?: boolean;
|
|
147
143
|
}
|
|
148
|
-
declare const Router_base: Context.
|
|
144
|
+
declare const Router_base: Context.ServiceClass<Router, "@weftui/router/Router", {
|
|
149
145
|
/** The current match as a hot `Subscribable`; drives the outlet. */readonly currentMatch: Subscribable.Subscribable<RouteMatch>;
|
|
150
146
|
/**
|
|
151
147
|
* Navigates to `to` (a path, optionally with a query). On the client this
|
|
@@ -170,7 +166,7 @@ declare const Router_base: Context.TagClass<Router, "@weftui/router/Router", {
|
|
|
170
166
|
readonly navigating: Subscribable.Subscribable<NavState>;
|
|
171
167
|
}>;
|
|
172
168
|
declare class Router extends Router_base {}
|
|
173
|
-
declare const OutletTag_base: Context.
|
|
169
|
+
declare const OutletTag_base: Context.ServiceClass<OutletTag, "@weftui/router/Outlet", Node<never, never>>;
|
|
174
170
|
/**
|
|
175
171
|
* The injected outlet: the node a layout (or the server document shell) splices
|
|
176
172
|
* to place the next level down. Provided per render by the router
|
|
@@ -456,16 +452,16 @@ interface CompiledLeaf {
|
|
|
456
452
|
/**
|
|
457
453
|
* Path-param schema. Its **encoded** side is typed string-encodeable
|
|
458
454
|
* (`Record<string, string | undefined>`) so it satisfies platform's
|
|
459
|
-
* `HttpApiEndpoint
|
|
455
|
+
* `HttpApiEndpoint` `params` constraint without an `as any` cast — param schemas
|
|
460
456
|
* round-trip strings, so the `Schema.Struct` value is asserted to this shape.
|
|
461
457
|
*/
|
|
462
|
-
readonly pathSchema: Schema.
|
|
458
|
+
readonly pathSchema: Schema.Codec<Record<string, unknown>, Readonly<Record<string, string | undefined>>>;
|
|
463
459
|
/**
|
|
464
460
|
* Query schema. Its **encoded** side is typed string-encodeable
|
|
465
461
|
* (`Record<string, string | ReadonlyArray<string> | undefined>`) so it satisfies
|
|
466
|
-
* platform's `HttpApiEndpoint
|
|
462
|
+
* platform's `HttpApiEndpoint` `query` constraint without a cast.
|
|
467
463
|
*/
|
|
468
|
-
readonly querySchema: Schema.
|
|
464
|
+
readonly querySchema: Schema.Codec<Record<string, unknown>, Readonly<Record<string, string | ReadonlyArray<string> | undefined>>>;
|
|
469
465
|
/** The page's component slot; invoked per render, reads params via `Router.params` / `Router.query`. */
|
|
470
466
|
readonly component: ComponentSlot;
|
|
471
467
|
/** Ancestor layouts (root → parent) wrapping this leaf. */
|
|
@@ -493,7 +489,7 @@ interface RouterDef<E = any, R = any> {
|
|
|
493
489
|
* nesting/render metadata platform's flat API can't represent. Built by
|
|
494
490
|
* {@link buildHttpApi} during {@link makeRouter}.
|
|
495
491
|
*/
|
|
496
|
-
readonly httpApi: HttpApi.
|
|
492
|
+
readonly httpApi: HttpApi.Top;
|
|
497
493
|
/**
|
|
498
494
|
* Phantom marker for the tree's aggregate error channel. Covariant (stores `E`
|
|
499
495
|
* directly) so a fully-static `RouterDef<never, never>` stays assignable to the
|
|
@@ -531,20 +527,20 @@ declare function compile(def: {
|
|
|
531
527
|
/**
|
|
532
528
|
* Builds the authoritative `HttpApi` for a compiled tree (S4): a single `"pages"`
|
|
533
529
|
* group whose endpoints are GET endpoints — one per leaf — at each leaf's full path
|
|
534
|
-
* pattern, carrying `
|
|
530
|
+
* pattern, carrying `params: pathSchema`, `query: querySchema`, a
|
|
535
531
|
* `Schema.String` (text/HTML) success, and a `RouterNotFound → 404` error. The tree
|
|
536
532
|
* (not `HttpApi`) is the authoring surface; this is the single source of truth the
|
|
537
533
|
* server dispatch (`HttpApiBuilder`) and the client matcher / derived `HttpApiClient`
|
|
538
534
|
* read from, so both sides agree on paths and schemas.
|
|
539
535
|
*
|
|
540
536
|
* Each leaf's `pathSchema`/`querySchema` are typed string-encodeable (see
|
|
541
|
-
* {@link CompiledLeaf}), so `
|
|
537
|
+
* {@link CompiledLeaf}), so the `params`/`query` options need no `as any` casts.
|
|
542
538
|
*
|
|
543
539
|
* `Boundary.rpc` data no longer rides this spine: it resolves through the app's
|
|
544
540
|
* merged `RpcGroup` over the ambient `AppRpcClient` (`POST /_eui/rpc`), wired
|
|
545
541
|
* explicitly into `RouterServer`/`RouterLive`. The matcher reads only `"pages"`.
|
|
546
542
|
*/
|
|
547
|
-
declare function buildHttpApi(leaves: readonly CompiledLeaf[]): HttpApi.
|
|
543
|
+
declare function buildHttpApi(leaves: readonly CompiledLeaf[]): HttpApi.Top;
|
|
548
544
|
/**
|
|
549
545
|
* Seals a route tree into a {@link RouterDef}, compiling it eagerly (so leaf
|
|
550
546
|
* references are stamped for `href`), building its authoritative {@link buildHttpApi}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { l as leafRegistry } from "./outlet-
|
|
2
|
-
import {
|
|
1
|
+
import { l as leafRegistry } from "./outlet-C9N4a4_F.js";
|
|
2
|
+
import { Result, Schema } from "effect";
|
|
3
3
|
//#region src/matcher.ts
|
|
4
4
|
/** Escapes a literal path segment for inclusion in a `RegExp`. */
|
|
5
5
|
function escapeRegex(literal) {
|
|
@@ -50,14 +50,14 @@ function compileMatchers(def) {
|
|
|
50
50
|
const endpoints = def.httpApi.groups["pages"]?.endpoints ?? {};
|
|
51
51
|
const entries = [];
|
|
52
52
|
for (const endpoint of Object.values(endpoints)) {
|
|
53
|
-
const leaf = leafById.get(endpoint.
|
|
53
|
+
const leaf = leafById.get(endpoint.identifier);
|
|
54
54
|
if (leaf === void 0) continue;
|
|
55
55
|
entries.push({
|
|
56
56
|
leaf,
|
|
57
57
|
regex: patternToRegex(endpoint.path),
|
|
58
58
|
paramNames: paramNamesOf(endpoint.path),
|
|
59
|
-
pathSchema:
|
|
60
|
-
querySchema:
|
|
59
|
+
pathSchema: endpoint.params ?? emptySchema,
|
|
60
|
+
querySchema: endpoint.query ?? emptySchema
|
|
61
61
|
});
|
|
62
62
|
}
|
|
63
63
|
entries.sort((a, b) => {
|
|
@@ -108,15 +108,15 @@ function match(def, url) {
|
|
|
108
108
|
const raw = m[i + 1];
|
|
109
109
|
if (raw !== void 0) rawParams[name] = decodeURIComponent(raw);
|
|
110
110
|
});
|
|
111
|
-
const decodedPath = Schema.
|
|
112
|
-
if (
|
|
113
|
-
const decodedQuery = Schema.
|
|
114
|
-
if (
|
|
111
|
+
const decodedPath = Schema.decodeUnknownResult(entry.pathSchema)(rawParams);
|
|
112
|
+
if (Result.isFailure(decodedPath)) continue;
|
|
113
|
+
const decodedQuery = Schema.decodeUnknownResult(entry.querySchema)(parseQuery(search));
|
|
114
|
+
if (Result.isFailure(decodedQuery)) continue;
|
|
115
115
|
return {
|
|
116
116
|
_tag: "Matched",
|
|
117
117
|
leaf: entry.leaf,
|
|
118
|
-
path: decodedPath.
|
|
119
|
-
query: decodedQuery.
|
|
118
|
+
path: decodedPath.success,
|
|
119
|
+
query: decodedQuery.success,
|
|
120
120
|
url: normalizedUrl
|
|
121
121
|
};
|
|
122
122
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { A as notFound, C as RouterHttpApiClient, D as RouterNotFound, E as match, O as RouterParamsError, S as Router, T as compileMatchers, _ as TreeE, a as RouterOptions, b as NavState, c as leafRegistry, d as FieldsType, f as LayoutNode, g as SubtreeR, h as SubtreeE, i as RouterDef, k as isRouterNotFound, l as ComponentSlot, m as RouteNode, n as CompiledLayout, o as buildHttpApi, p as RouteHandlerProps, r as CompiledLeaf, s as compile, t as Compiled, u as Fields, v as TreeNode, w as RouteMatch, x as NavigateOptions, y as TreeR } from "./compile-
|
|
2
|
-
import { i as href, n as outletNode, r as HrefArgs, t as RouterApp } from "./outlet-
|
|
1
|
+
import { A as notFound, C as RouterHttpApiClient, D as RouterNotFound, E as match, O as RouterParamsError, S as Router, T as compileMatchers, _ as TreeE, a as RouterOptions, b as NavState, c as leafRegistry, d as FieldsType, f as LayoutNode, g as SubtreeR, h as SubtreeE, i as RouterDef, k as isRouterNotFound, l as ComponentSlot, m as RouteNode, n as CompiledLayout, o as buildHttpApi, p as RouteHandlerProps, r as CompiledLeaf, s as compile, t as Compiled, u as Fields, v as TreeNode, w as RouteMatch, x as NavigateOptions, y as TreeR } from "./compile-BJFIgBbE.js";
|
|
2
|
+
import { i as href, n as outletNode, r as HrefArgs, t as RouterApp } from "./outlet-eoUQ-W0k.js";
|
|
3
3
|
export { type Compiled, type CompiledLayout, type CompiledLeaf, type ComponentSlot, type Fields, type FieldsType, type HrefArgs, type LayoutNode, type NavState, type NavigateOptions, type RouteHandlerProps, type RouteMatch, type RouteNode, Router, RouterApp, type RouterDef, type RouterHttpApiClient, RouterNotFound, type RouterOptions, RouterParamsError, type SubtreeE, type SubtreeR, type TreeE, type TreeNode, type TreeR, buildHttpApi, compile, compileMatchers, href, isRouterNotFound, leafRegistry, match, notFound, outletNode };
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { a as Router, c as compile, d as RouterParamsError, f as isRouterNotFound, l as leafRegistry, n as outletNode, p as notFound, s as buildHttpApi, t as RouterApp, u as RouterNotFound } from "./outlet-
|
|
2
|
-
import { n as compileMatchers, r as match, t as href } from "./href-
|
|
1
|
+
import { a as Router, c as compile, d as RouterParamsError, f as isRouterNotFound, l as leafRegistry, n as outletNode, p as notFound, s as buildHttpApi, t as RouterApp, u as RouterNotFound } from "./outlet-C9N4a4_F.js";
|
|
2
|
+
import { n as compileMatchers, r as match, t as href } from "./href-uvJ6b7zz.js";
|
|
3
3
|
export { Router, RouterApp, RouterNotFound, RouterParamsError, buildHttpApi, compile, compileMatchers, href, isRouterNotFound, leafRegistry, match, notFound, outletNode };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { HttpApi, HttpApiEndpoint, HttpApiGroup } from "
|
|
2
|
-
import { Context, Effect, Exit, Schema, Stream,
|
|
3
|
-
import { Boundary, getElementDescriptor, h } from "@weftui/core";
|
|
1
|
+
import { HttpApi, HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "effect/unstable/httpapi";
|
|
2
|
+
import { Context, Effect, Exit, Schema, Stream, pipe } from "effect";
|
|
3
|
+
import { Boundary, Subscribable, getElementDescriptor, h } from "@weftui/core";
|
|
4
4
|
//#region src/errors.ts
|
|
5
5
|
/**
|
|
6
6
|
* Tagged error raised by {@link notFound} and caught by the router's internal
|
|
@@ -8,10 +8,10 @@ import { Boundary, getElementDescriptor, h } from "@weftui/core";
|
|
|
8
8
|
* `Boundary.catchTag("RouterNotFound", …)` to override the fallback for a subtree
|
|
9
9
|
* (the router's internal boundary is outermost, so a nearer user boundary wins).
|
|
10
10
|
*
|
|
11
|
-
* Modeled as a `Schema.
|
|
11
|
+
* Modeled as a `Schema.TaggedErrorClass` so it can be encoded/decoded across the wire
|
|
12
12
|
* the same way `Boundary.rpc` replays typed failures.
|
|
13
13
|
*/
|
|
14
|
-
var RouterNotFound = class extends Schema.
|
|
14
|
+
var RouterNotFound = class extends Schema.TaggedErrorClass()("RouterNotFound", {
|
|
15
15
|
/** The path that could not be resolved, when known. */
|
|
16
16
|
path: Schema.optional(Schema.String) }) {};
|
|
17
17
|
/**
|
|
@@ -35,12 +35,12 @@ const isRouterNotFound = (u) => typeof u === "object" && u !== null && "_tag" in
|
|
|
35
35
|
* It bubbles up through the route tree's aggregate error channel, so a user may
|
|
36
36
|
* place a `Boundary.catchTag("RouterParamsError", …)` to recover within a subtree.
|
|
37
37
|
*
|
|
38
|
-
* Modeled as a `Schema.
|
|
38
|
+
* Modeled as a `Schema.TaggedErrorClass` so it can be encoded/decoded across the wire
|
|
39
39
|
* the same way `RouterNotFound` and `Boundary.rpc` replay typed failures.
|
|
40
40
|
*/
|
|
41
|
-
var RouterParamsError = class extends Schema.
|
|
41
|
+
var RouterParamsError = class extends Schema.TaggedErrorClass()("RouterParamsError", {
|
|
42
42
|
/** Which side of the match failed validation. */
|
|
43
|
-
source: Schema.
|
|
43
|
+
source: Schema.Literals(["path", "query"]),
|
|
44
44
|
/** The requested field names, for diagnostics. */
|
|
45
45
|
keys: Schema.Array(Schema.String)
|
|
46
46
|
}) {};
|
|
@@ -157,21 +157,26 @@ function compile(def) {
|
|
|
157
157
|
/**
|
|
158
158
|
* Builds the authoritative `HttpApi` for a compiled tree (S4): a single `"pages"`
|
|
159
159
|
* group whose endpoints are GET endpoints — one per leaf — at each leaf's full path
|
|
160
|
-
* pattern, carrying `
|
|
160
|
+
* pattern, carrying `params: pathSchema`, `query: querySchema`, a
|
|
161
161
|
* `Schema.String` (text/HTML) success, and a `RouterNotFound → 404` error. The tree
|
|
162
162
|
* (not `HttpApi`) is the authoring surface; this is the single source of truth the
|
|
163
163
|
* server dispatch (`HttpApiBuilder`) and the client matcher / derived `HttpApiClient`
|
|
164
164
|
* read from, so both sides agree on paths and schemas.
|
|
165
165
|
*
|
|
166
166
|
* Each leaf's `pathSchema`/`querySchema` are typed string-encodeable (see
|
|
167
|
-
* {@link CompiledLeaf}), so `
|
|
167
|
+
* {@link CompiledLeaf}), so the `params`/`query` options need no `as any` casts.
|
|
168
168
|
*
|
|
169
169
|
* `Boundary.rpc` data no longer rides this spine: it resolves through the app's
|
|
170
170
|
* merged `RpcGroup` over the ambient `AppRpcClient` (`POST /_eui/rpc`), wired
|
|
171
171
|
* explicitly into `RouterServer`/`RouterLive`. The matcher reads only `"pages"`.
|
|
172
172
|
*/
|
|
173
173
|
function buildHttpApi(leaves) {
|
|
174
|
-
const group = leaves.reduce((g, leaf) => g.add(HttpApiEndpoint.get(leaf.id, leaf.fullPathPattern
|
|
174
|
+
const group = leaves.reduce((g, leaf) => g.add(HttpApiEndpoint.get(leaf.id, leaf.fullPathPattern, {
|
|
175
|
+
params: leaf.pathSchema,
|
|
176
|
+
query: leaf.querySchema,
|
|
177
|
+
success: Schema.String,
|
|
178
|
+
error: RouterNotFound.pipe(HttpApiSchema.status(404))
|
|
179
|
+
})), HttpApiGroup.make("pages"));
|
|
175
180
|
return HttpApi.make("router").add(group);
|
|
176
181
|
}
|
|
177
182
|
/**
|
|
@@ -283,7 +288,7 @@ function lazyComponent(load) {
|
|
|
283
288
|
}
|
|
284
289
|
//#endregion
|
|
285
290
|
//#region src/router-service.ts
|
|
286
|
-
var Router = class extends Context.
|
|
291
|
+
var Router = class extends Context.Service()("@weftui/router/Router") {};
|
|
287
292
|
/**
|
|
288
293
|
* The injected outlet: the node a layout (or the server document shell) splices
|
|
289
294
|
* to place the next level down. Provided per render by the router
|
|
@@ -295,7 +300,7 @@ var Router = class extends Context.Tag("@weftui/router/Router")() {};
|
|
|
295
300
|
* structurally by {@link makeLayout} / {@link makeRouter}, never inferred across
|
|
296
301
|
* this DI boundary. Re-exported on the namespace as `Router.Outlet`.
|
|
297
302
|
*/
|
|
298
|
-
var OutletTag = class extends Context.
|
|
303
|
+
var OutletTag = class extends Context.Service()("@weftui/router/Outlet") {};
|
|
299
304
|
/** Picks the requested `fields` keys out of a decoded match record. */
|
|
300
305
|
function pick(fields, record) {
|
|
301
306
|
const subset = {};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { D as RouterNotFound, S as Router, d as FieldsType, i as RouterDef, m as RouteNode, u as Fields } from "./compile-
|
|
1
|
+
import { D as RouterNotFound, S as Router, d as FieldsType, i as RouterDef, m as RouteNode, u as Fields } from "./compile-BJFIgBbE.js";
|
|
2
2
|
import { Node } from "@weftui/core";
|
|
3
3
|
|
|
4
4
|
//#region src/href.d.ts
|
package/dist/server/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { S as Router, i as RouterDef, l as ComponentSlot } from "../compile-
|
|
1
|
+
import { S as Router, i as RouterDef, l as ComponentSlot } from "../compile-BJFIgBbE.js";
|
|
2
2
|
import { Effect, Layer } from "effect";
|
|
3
3
|
import { AppRpcClientTag } from "@weftui/core";
|
|
4
|
-
import { RpcGroup } from "
|
|
4
|
+
import { RpcGroup } from "effect/unstable/rpc";
|
|
5
5
|
|
|
6
6
|
//#region src/server/router-server.d.ts
|
|
7
7
|
/**
|
package/dist/server/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { a as Router, f as isRouterNotFound, n as outletNode, u as RouterNotFound } from "../outlet-
|
|
2
|
-
import { HttpApiBuilder, HttpApiEndpoint, HttpApiGroup
|
|
3
|
-
import { Cause, Effect, Exit, Layer, Option, Schema, Scope, Stream
|
|
4
|
-
import { AppRpcClientTag } from "@weftui/core";
|
|
1
|
+
import { a as Router, f as isRouterNotFound, n as outletNode, u as RouterNotFound } from "../outlet-C9N4a4_F.js";
|
|
2
|
+
import { HttpApiBuilder, HttpApiEndpoint, HttpApiGroup } from "effect/unstable/httpapi";
|
|
3
|
+
import { Cause, Effect, Exit, Layer, Option, Schema, Scope, Stream } from "effect";
|
|
4
|
+
import { AppRpcClientTag, Subscribable } from "@weftui/core";
|
|
5
|
+
import { HttpRouter, HttpServer, HttpServerResponse } from "effect/unstable/http";
|
|
5
6
|
import { SuspenseFailureHandlerTag, renderToHydratableShell, renderToStringHydratable } from "@weftui/dom/server";
|
|
6
|
-
import { RpcSerialization, RpcServer, RpcTest } from "
|
|
7
|
+
import { RpcSerialization, RpcServer, RpcTest } from "effect/unstable/rpc";
|
|
7
8
|
//#region src/server/router-server.ts
|
|
8
9
|
let RouterServer;
|
|
9
10
|
(function(_RouterServer) {
|
|
@@ -44,7 +45,7 @@ let RouterServer;
|
|
|
44
45
|
*/
|
|
45
46
|
function appRpcClientLayer(rpc) {
|
|
46
47
|
if (rpc === void 0) return Layer.succeed(AppRpcClientTag, AppRpcClientTag.of({ call: (tag) => Effect.fail(/* @__PURE__ */ new Error(`Boundary.rpc "${tag}" cannot resolve: no \`rpc\` option was passed to RouterServer`)) }));
|
|
47
|
-
return Layer.
|
|
48
|
+
return Layer.effect(AppRpcClientTag, Effect.map(RpcTest.makeClient(rpc.group, { flatten: true }), (flat) => AppRpcClientTag.of({ call: (tag, payload) => flat(tag, payload) }))).pipe(Layer.provide(rpc.handlers));
|
|
48
49
|
}
|
|
49
50
|
/**
|
|
50
51
|
* Renders the document shell — with `app` spliced via `Router.Outlet` — to a
|
|
@@ -106,7 +107,7 @@ let RouterServer;
|
|
|
106
107
|
*/
|
|
107
108
|
function notFoundSuspenseHandler(def) {
|
|
108
109
|
return { handle: (cause) => {
|
|
109
|
-
const failure = Cause.
|
|
110
|
+
const failure = Cause.findErrorOption(cause);
|
|
110
111
|
return Option.isSome(failure) && isRouterNotFound(failure.value) ? Option.some({
|
|
111
112
|
content: def.compiled.notFound(),
|
|
112
113
|
markNoindex: true,
|
|
@@ -126,7 +127,7 @@ let RouterServer;
|
|
|
126
127
|
const app = outletNode(def);
|
|
127
128
|
return Effect.gen(function* () {
|
|
128
129
|
const scope = yield* Scope.make();
|
|
129
|
-
const { shell, patches } = yield* renderToHydratableShell(Effect.provideService(options.document({}), Router.Outlet, app)).pipe(Effect.provideService(Router, router), Effect.provideService(SuspenseFailureHandlerTag, notFoundSuspenseHandler(def)), Effect.provide(appRpcClientLayer(options.rpc)), Effect.provide(options.context ?? Layer.empty), Scope.
|
|
130
|
+
const { shell, patches } = yield* renderToHydratableShell(Effect.provideService(options.document({}), Router.Outlet, app)).pipe(Effect.provideService(Router, router), Effect.provideService(SuspenseFailureHandlerTag, notFoundSuspenseHandler(def)), Effect.provide(appRpcClientLayer(options.rpc)), Effect.provide(options.context ?? Layer.empty), Scope.provide(scope), Effect.onError((cause) => Scope.close(scope, Exit.failCause(cause))));
|
|
130
131
|
const body = Stream.make(`<!DOCTYPE html>\n${shell}`).pipe(Stream.concat(patches), Stream.ensuring(Scope.close(scope, Exit.void)), Stream.encodeText);
|
|
131
132
|
return HttpServerResponse.stream(body, {
|
|
132
133
|
status: 200,
|
|
@@ -155,24 +156,24 @@ let RouterServer;
|
|
|
155
156
|
if (cached !== void 0) return cached;
|
|
156
157
|
const leaves = def.compiled.leaves;
|
|
157
158
|
const builder = HttpApiBuilder;
|
|
158
|
-
const fallbackGroup = HttpApiGroup.make("fallback").add(HttpApiEndpoint.get("catchAll", "*"
|
|
159
|
+
const fallbackGroup = HttpApiGroup.make("fallback").add(HttpApiEndpoint.get("catchAll", "*", { success: Schema.String }));
|
|
159
160
|
const api = def.httpApi.add(fallbackGroup);
|
|
160
161
|
const pagesLayer = builder.group(api, "pages", (handlers) => leaves.reduce((h, leaf) => h.handle(leaf.id, (request) => leafRenderer(def, options, {
|
|
161
162
|
_tag: "Matched",
|
|
162
163
|
leaf,
|
|
163
|
-
path: request.
|
|
164
|
-
query: request.
|
|
164
|
+
path: request.params,
|
|
165
|
+
query: request.query,
|
|
165
166
|
url: request.request.url
|
|
166
167
|
})), handlers));
|
|
167
168
|
const fallbackLayer = builder.group(api, "fallback", (handlers) => handlers.handle("catchAll", (request) => renderNoMatch(def, options, request.request.url)));
|
|
168
|
-
const
|
|
169
|
-
const { handler: pageHandler } = HttpApiBuilder.toWebHandler(Layer.mergeAll(apiLayer, HttpServer.layerContext));
|
|
169
|
+
const apiRoutes = HttpApiBuilder.layer(api).pipe(Layer.provide(Layer.mergeAll(pagesLayer, fallbackLayer)));
|
|
170
170
|
const rpc = options.rpc;
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
}
|
|
171
|
+
const rpcRoutes = rpc !== void 0 ? RpcServer.layerHttp({
|
|
172
|
+
group: rpc.group,
|
|
173
|
+
path: RPC_PATH,
|
|
174
|
+
protocol: "http"
|
|
175
|
+
}).pipe(Layer.provide(Layer.mergeAll(rpc.handlers, RpcSerialization.layerNdjson))) : Layer.empty;
|
|
176
|
+
const { handler } = HttpRouter.toWebHandler(Layer.mergeAll(apiRoutes, rpcRoutes, HttpServer.layerServices));
|
|
176
177
|
perDef.set(options.document, handler);
|
|
177
178
|
return handler;
|
|
178
179
|
}
|
|
@@ -18,29 +18,29 @@ A component's `E` channel accumulates up the tree. A **failure boundary** is whe
|
|
|
18
18
|
```typescript
|
|
19
19
|
import { Boundary, h } from "@weftui/core";
|
|
20
20
|
|
|
21
|
-
Boundary.
|
|
21
|
+
Boundary.catch({ fallback: (e) => h.div({ class: "error" }, `Failed: ${e.message}`) }, [
|
|
22
22
|
RiskyWidget(),
|
|
23
23
|
]);
|
|
24
24
|
```
|
|
25
25
|
|
|
26
26
|
There are six failure-catch variants, mirroring Effect's own error operators so the mental model transfers directly:
|
|
27
27
|
|
|
28
|
-
| Variant
|
|
29
|
-
|
|
|
30
|
-
| `
|
|
31
|
-
| `
|
|
32
|
-
| `catchTag` / `catchTags`
|
|
33
|
-
| `
|
|
28
|
+
| Variant | Catches |
|
|
29
|
+
| ------------------------- | ------------------------------------------ |
|
|
30
|
+
| `catch` | every failure in `E` |
|
|
31
|
+
| `catchCause` | the full `Cause` (defects included) |
|
|
32
|
+
| `catchTag` / `catchTags` | one / several tagged errors by `_tag` |
|
|
33
|
+
| `catchFilter` / `catchIf` | a selected subset, by `Filter` / predicate |
|
|
34
34
|
|
|
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 `
|
|
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.
|
|
36
36
|
|
|
37
37
|
### Post-mount failures with no enclosing boundary
|
|
38
38
|
|
|
39
39
|
The routing above describes what happens while a node is being built. Once mounted, a reactive region — an attribute, child, or list stream, or a hydrated equivalent — keeps running for the lifetime of its scope, and it can still fail later: a `Stream` backing a `Boundary.rpc` resource might raise `RouterNotFound` after a client-side navigation, for instance. If a `BoundaryContext` encloses the region, the failure routes to it exactly as above, and the boundary's fallback swaps in.
|
|
40
40
|
|
|
41
|
-
If no boundary encloses it, there is nothing to swap to. Weft does not synthesize one: the region's DOM keeps its last rendered content, and
|
|
41
|
+
If no boundary encloses it, there is nothing to swap to. Weft does not synthesize one: the region's DOM keeps its last rendered content, and a watcher fiber — forked into the same scope alongside the subscription itself — observes its exit directly. When that exit is a failure whose cause is not interruption-only, Weft reports it explicitly via `Effect.logError(exit.cause)`, annotated with `weft.region` to identify the failing region by kind and identity (e.g. `attribute:class`, `child:stream-3`, `list:stream-2`, `hydrate:stream-1 (/products/42)`). This fires for typed failures and defects alike, in both dev and prod, exactly once per failing region, at the `"Error"` level. Interruption — the ordinary case of unmount tearing down the region's scope — is never reported; only genuine failures are.
|
|
42
42
|
|
|
43
|
-
This is deliberate: rather than
|
|
43
|
+
This is deliberate: rather than leaving the failure to whatever the Effect runtime would otherwise do with an unobserved fiber exit, Weft observes and logs it itself, so visibility is controlled by the same knobs any Effect program uses — `References.MinimumLogLevel` (provided via `Effect.provideService`) to filter it, or a custom `Logger` to route it elsewhere. A stream that can fail and has no enclosing boundary is a stream whose failures you've chosen not to route into the UI — the log is what tells you that decision has consequences at runtime.
|
|
44
44
|
|
|
45
45
|
## Suspense boundaries
|
|
46
46
|
|