@timber-js/app 0.2.0-alpha.197 → 0.2.0-alpha.199
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/dist/_chunks/{actions-BS-m5SLv.js → actions-d1hCqnU3.js} +35 -8
- package/dist/_chunks/actions-d1hCqnU3.js.map +1 -0
- package/dist/_chunks/als-registry-C6kcfprT.js.map +1 -1
- package/dist/_chunks/{cache-api-DqzgTEqk.js → cache-api-ByagcC-J.js} +2 -2
- package/dist/_chunks/{cache-api-DqzgTEqk.js.map → cache-api-ByagcC-J.js.map} +1 -1
- package/dist/_chunks/canonicalize-CgHoscYO.js +66 -0
- package/dist/_chunks/canonicalize-CgHoscYO.js.map +1 -0
- package/dist/_chunks/{chains-CZG7E5zg.js → chains-Bpb0W4ax.js} +3 -3
- package/dist/_chunks/{chains-CZG7E5zg.js.map → chains-Bpb0W4ax.js.map} +1 -1
- package/dist/_chunks/{cli-check-bE3H5Bjr.js → cli-check-D6VolrDV.js} +3 -3
- package/dist/_chunks/{cli-check-bE3H5Bjr.js.map → cli-check-D6VolrDV.js.map} +1 -1
- package/dist/_chunks/{cli-schema-sync-DTy_-Msq.js → cli-schema-sync-D6rO-VcS.js} +2 -2
- package/dist/_chunks/{cli-schema-sync-DTy_-Msq.js.map → cli-schema-sync-D6rO-VcS.js.map} +1 -1
- package/dist/_chunks/{convention-lint-n3RJLgww.js → convention-lint-fRkwVwEH.js} +27 -4
- package/dist/_chunks/convention-lint-fRkwVwEH.js.map +1 -0
- package/dist/_chunks/error-boundary-BfPHZjm0.js +1050 -0
- package/dist/_chunks/error-boundary-BfPHZjm0.js.map +1 -0
- package/dist/_chunks/{live-graph-BXDsdzBv.js → live-graph-D_2D32Ad.js} +3 -3
- package/dist/_chunks/{live-graph-BXDsdzBv.js.map → live-graph-D_2D32Ad.js.map} +1 -1
- package/dist/_chunks/{logger-DDirEsn7.js → logger-uLBuGKDI.js} +471 -440
- package/dist/_chunks/logger-uLBuGKDI.js.map +1 -0
- package/dist/_chunks/{navigation-root-B00jjGd5.js → navigation-context-D0TU0Jog.js} +3 -101
- package/dist/_chunks/navigation-context-D0TU0Jog.js.map +1 -0
- package/dist/_chunks/navigation-root-mHSK9psY.js +126 -0
- package/dist/_chunks/{navigation-root-B00jjGd5.js.map → navigation-root-mHSK9psY.js.map} +1 -1
- package/dist/_chunks/{poison-scan-BoDLgbix.js → poison-scan-Bm9Yyqk9.js} +2 -2
- package/dist/_chunks/{poison-scan-BoDLgbix.js.map → poison-scan-Bm9Yyqk9.js.map} +1 -1
- package/dist/_chunks/{scanner-tdFPvDYi.js → scanner-AiazgH_f.js} +6 -5
- package/dist/_chunks/scanner-AiazgH_f.js.map +1 -0
- package/dist/_chunks/{segment-keys-BhqoHiLc.js → segment-keys-lqtdookO.js} +2 -65
- package/dist/_chunks/segment-keys-lqtdookO.js.map +1 -0
- package/dist/_chunks/{ssr-data-BQGhTPAK.js → ssr-data-D6T6Y3ef.js} +4 -26
- package/dist/_chunks/ssr-data-D6T6Y3ef.js.map +1 -0
- package/dist/_chunks/state-FippDgxN.js +52 -0
- package/dist/_chunks/state-FippDgxN.js.map +1 -0
- package/dist/_chunks/status-page-marker-DwQBrLBz.js +496 -0
- package/dist/_chunks/status-page-marker-DwQBrLBz.js.map +1 -0
- package/dist/_chunks/{walkers-DNX05dC0.js → walkers-B6XUtmqK.js} +2 -2
- package/dist/_chunks/{walkers-DNX05dC0.js.map → walkers-B6XUtmqK.js.map} +1 -1
- package/dist/analyze/crawl-entry.js +2 -2
- package/dist/analyze/graph-command.js +2 -2
- package/dist/cache/index.js +1 -1
- package/dist/cli.js +2 -2
- package/dist/client/browser-entry/action-dispatch.d.ts +6 -4
- package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -1
- package/dist/client/browser-entry/action-queue.d.ts +44 -0
- package/dist/client/browser-entry/action-queue.d.ts.map +1 -0
- package/dist/client/browser-entry/router-init.d.ts.map +1 -1
- package/dist/client/deny-last-resort.d.ts +29 -0
- package/dist/client/deny-last-resort.d.ts.map +1 -0
- package/dist/client/error-boundary.d.ts +48 -2
- package/dist/client/error-boundary.d.ts.map +1 -1
- package/dist/client/error-boundary.js +2 -2
- package/dist/client/history.d.ts +21 -2
- package/dist/client/history.d.ts.map +1 -1
- package/dist/client/index.js +34 -14
- package/dist/client/index.js.map +1 -1
- package/dist/client/internal.d.ts +1 -0
- package/dist/client/internal.d.ts.map +1 -1
- package/dist/client/internal.js +272 -1225
- package/dist/client/internal.js.map +1 -1
- package/dist/client/link.d.ts.map +1 -1
- package/dist/client/navigation-commit.d.ts +12 -19
- package/dist/client/navigation-commit.d.ts.map +1 -1
- package/dist/client/navigation-transition.d.ts +62 -11
- package/dist/client/navigation-transition.d.ts.map +1 -1
- package/dist/client/router-effects.d.ts +9 -8
- package/dist/client/router-effects.d.ts.map +1 -1
- package/dist/client/router-lifecycle.d.ts +60 -17
- package/dist/client/router-lifecycle.d.ts.map +1 -1
- package/dist/client/router-pipeline.d.ts +7 -4
- package/dist/client/router-pipeline.d.ts.map +1 -1
- package/dist/client/router-types.d.ts +63 -8
- package/dist/client/router-types.d.ts.map +1 -1
- package/dist/client/router.d.ts.map +1 -1
- package/dist/client/rsc-fetch.d.ts +0 -9
- package/dist/client/rsc-fetch.d.ts.map +1 -1
- package/dist/client/segment-cache.d.ts +23 -8
- package/dist/client/segment-cache.d.ts.map +1 -1
- package/dist/client/state.d.ts +16 -0
- package/dist/client/state.d.ts.map +1 -1
- package/dist/client/status-page-marker.d.ts +25 -0
- package/dist/client/status-page-marker.d.ts.map +1 -0
- package/dist/cookies/index.js +1 -1
- package/dist/dev-tools/holding-server.d.ts +4 -17
- package/dist/dev-tools/holding-server.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +74 -175
- package/dist/index.js.map +1 -1
- package/dist/plugins/dev-server.d.ts.map +1 -1
- package/dist/routing/codegen-write.d.ts.map +1 -1
- package/dist/routing/index.js +2 -2
- package/dist/routing/interception.d.ts +2 -2
- package/dist/routing/slot-placement.d.ts +2 -2
- package/dist/server/access-gate.d.ts +73 -1
- package/dist/server/access-gate.d.ts.map +1 -1
- package/dist/server/action-handler.d.ts.map +1 -1
- package/dist/server/actions.d.ts +16 -1
- package/dist/server/actions.d.ts.map +1 -1
- package/dist/server/als-registry.d.ts +3 -9
- package/dist/server/als-registry.d.ts.map +1 -1
- package/dist/server/children-interception.d.ts +1 -1
- package/dist/server/default-status-page.d.ts +2 -2
- package/dist/server/default-status-page.d.ts.map +1 -1
- package/dist/server/deny-boundary.d.ts +15 -9
- package/dist/server/deny-boundary.d.ts.map +1 -1
- package/dist/server/deny-renderer.d.ts.map +1 -1
- package/dist/server/error-boundary-wrapper.d.ts +21 -4
- package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
- package/dist/server/error-response-headers.d.ts +3 -0
- package/dist/server/error-response-headers.d.ts.map +1 -0
- package/dist/server/index.js +3 -3
- package/dist/server/index.js.map +1 -1
- package/dist/server/internal.d.ts +1 -2
- package/dist/server/internal.d.ts.map +1 -1
- package/dist/server/internal.js +2339 -2506
- package/dist/server/internal.js.map +1 -1
- package/dist/server/metadata-collector.d.ts +2 -5
- package/dist/server/metadata-collector.d.ts.map +1 -1
- package/dist/server/param-coercion.d.ts +10 -3
- package/dist/server/param-coercion.d.ts.map +1 -1
- package/dist/server/pipeline-outcome.d.ts.map +1 -1
- package/dist/server/pipeline-phases.d.ts +11 -0
- package/dist/server/pipeline-phases.d.ts.map +1 -1
- package/dist/server/port-resolution.d.ts +3 -89
- package/dist/server/port-resolution.d.ts.map +1 -1
- package/dist/server/primitives.d.ts +38 -10
- package/dist/server/primitives.d.ts.map +1 -1
- package/dist/server/response-cache-policy.d.ts +3 -0
- package/dist/server/response-cache-policy.d.ts.map +1 -0
- package/dist/server/route-element-builder.d.ts +11 -41
- package/dist/server/route-element-builder.d.ts.map +1 -1
- package/dist/server/route-element-helpers.d.ts +12 -0
- package/dist/server/route-element-helpers.d.ts.map +1 -0
- package/dist/server/route-module-loader.d.ts +37 -0
- package/dist/server/route-module-loader.d.ts.map +1 -0
- package/dist/server/rsc-cache-key-guard.d.ts.map +1 -1
- package/dist/server/rsc-entry/action-middleware-runner.d.ts.map +1 -1
- package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
- package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-payload.d.ts +22 -1
- package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-stream.d.ts +4 -11
- package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
- package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
- package/dist/server/rsc-error-envelope.d.ts +11 -0
- package/dist/server/rsc-error-envelope.d.ts.map +1 -0
- package/dist/server/skippable-prefix.d.ts +18 -15
- package/dist/server/skippable-prefix.d.ts.map +1 -1
- package/dist/server/slot-resolver.d.ts.map +1 -1
- package/dist/server/slot-subtree-contain.d.ts +54 -0
- package/dist/server/slot-subtree-contain.d.ts.map +1 -0
- package/dist/server/stream-utils.d.ts.map +1 -1
- package/dist/server/utils/element-type.d.ts +10 -0
- package/dist/server/utils/element-type.d.ts.map +1 -1
- package/dist/shared/rsc-error-envelope.d.ts +0 -9
- package/dist/shared/rsc-error-envelope.d.ts.map +1 -1
- package/dist/shared/status-reason-phrase.d.ts +26 -0
- package/dist/shared/status-reason-phrase.d.ts.map +1 -0
- package/docs/api/30-api-server.mdx +4 -2
- package/docs/api/31-api-client.mdx +5 -1
- package/docs/api/36-cli.mdx +5 -3
- package/docs/learn/12-error-handling.mdx +5 -1
- package/package.json +10 -10
- package/src/client/browser-entry/action-dispatch.ts +166 -99
- package/src/client/browser-entry/action-queue.ts +90 -0
- package/src/client/browser-entry/router-init.ts +60 -35
- package/src/client/deny-last-resort.tsx +54 -0
- package/src/client/error-boundary.tsx +144 -42
- package/src/client/history.ts +52 -3
- package/src/client/internal.ts +1 -0
- package/src/client/link.tsx +70 -35
- package/src/client/navigation-commit.ts +79 -27
- package/src/client/navigation-transition.ts +176 -127
- package/src/client/router-effects.ts +14 -17
- package/src/client/router-lifecycle.ts +181 -115
- package/src/client/router-pipeline.ts +94 -71
- package/src/client/router-types.ts +61 -7
- package/src/client/router.ts +147 -74
- package/src/client/rsc-fetch.ts +0 -13
- package/src/client/segment-cache.ts +43 -10
- package/src/client/state.ts +26 -0
- package/src/client/status-page-marker.tsx +32 -0
- package/src/dev-tools/holding-server.ts +4 -17
- package/src/index.ts +18 -34
- package/src/plugins/dev-server.ts +2 -1
- package/src/react-canary.d.ts +2 -0
- package/src/routing/codegen-write.ts +2 -0
- package/src/routing/interception.ts +2 -2
- package/src/routing/slot-placement.ts +2 -2
- package/src/server/access-gate.tsx +89 -21
- package/src/server/action-client.ts +2 -2
- package/src/server/action-handler.ts +23 -10
- package/src/server/actions.ts +81 -34
- package/src/server/als-registry.ts +3 -9
- package/src/server/children-interception.ts +1 -1
- package/src/server/default-status-page.ts +7 -47
- package/src/server/deny-boundary.ts +45 -28
- package/src/server/deny-renderer.ts +6 -2
- package/src/server/error-boundary-wrapper.ts +23 -4
- package/src/server/error-response-headers.ts +18 -0
- package/src/server/internal.ts +2 -10
- package/src/server/metadata-collector.ts +3 -18
- package/src/server/param-coercion.ts +13 -4
- package/src/server/pipeline-outcome.ts +35 -13
- package/src/server/pipeline-phases.ts +22 -16
- package/src/server/port-resolution.ts +3 -165
- package/src/server/prebuilt-builder.ts +4 -4
- package/src/server/primitives.ts +75 -11
- package/src/server/response-cache-policy.ts +45 -0
- package/src/server/route-element-builder.ts +149 -412
- package/src/server/route-element-helpers.ts +37 -0
- package/src/server/route-handler.ts +2 -2
- package/src/server/route-module-loader.ts +161 -0
- package/src/server/rsc-cache-key-guard.ts +2 -42
- package/src/server/rsc-entry/action-middleware-runner.ts +4 -4
- package/src/server/rsc-entry/api-handler.ts +5 -5
- package/src/server/rsc-entry/error-renderer.ts +3 -4
- package/src/server/rsc-entry/helpers.ts +1 -1
- package/src/server/rsc-entry/index.ts +3 -3
- package/src/server/rsc-entry/render-route.ts +4 -8
- package/src/server/rsc-entry/rsc-payload.ts +59 -42
- package/src/server/rsc-entry/rsc-stream.ts +48 -27
- package/src/server/rsc-entry/ssr-renderer.ts +6 -10
- package/src/server/rsc-error-envelope.ts +18 -0
- package/src/server/skippable-prefix.ts +105 -7
- package/src/server/slot-resolver.ts +43 -12
- package/src/server/slot-subtree-contain.ts +255 -0
- package/src/server/stream-utils.ts +12 -8
- package/src/server/utils/element-type.ts +18 -2
- package/src/shared/rsc-error-envelope.ts +0 -15
- package/src/shared/status-reason-phrase.ts +61 -0
- package/dist/_chunks/actions-BS-m5SLv.js.map +0 -1
- package/dist/_chunks/convention-lint-n3RJLgww.js.map +0 -1
- package/dist/_chunks/error-boundary-BvRCCmbN.js +0 -353
- package/dist/_chunks/error-boundary-BvRCCmbN.js.map +0 -1
- package/dist/_chunks/logger-DDirEsn7.js.map +0 -1
- package/dist/_chunks/mdx-file-CXyHGUpS.js +0 -25
- package/dist/_chunks/mdx-file-CXyHGUpS.js.map +0 -1
- package/dist/_chunks/router-ref-8gr8qsxN.js +0 -28
- package/dist/_chunks/router-ref-8gr8qsxN.js.map +0 -1
- package/dist/_chunks/rsc-error-envelope-tT5PJs4q.js +0 -40
- package/dist/_chunks/rsc-error-envelope-tT5PJs4q.js.map +0 -1
- package/dist/_chunks/scanner-tdFPvDYi.js.map +0 -1
- package/dist/_chunks/segment-keys-BhqoHiLc.js.map +0 -1
- package/dist/_chunks/ssr-data-BQGhTPAK.js.map +0 -1
- package/dist/server/tree-builder.d.ts +0 -150
- package/dist/server/tree-builder.d.ts.map +0 -1
- package/src/server/tree-builder.ts +0 -313
|
@@ -15,8 +15,16 @@ import type { RscPayloadRoot } from '../../shared/payload-root.ts';
|
|
|
15
15
|
|
|
16
16
|
import { randomUUID } from 'node:crypto';
|
|
17
17
|
|
|
18
|
+
import { matchDenyEntries, type DenyPageEntry } from '../deny-boundary.ts';
|
|
18
19
|
import { logRenderError } from '../logger.ts';
|
|
19
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
isDenySignal,
|
|
22
|
+
isRedirectSignal,
|
|
23
|
+
isRenderError,
|
|
24
|
+
DenySignal,
|
|
25
|
+
RedirectSignal,
|
|
26
|
+
RenderError,
|
|
27
|
+
} from '../primitives.ts';
|
|
20
28
|
import { checkAndWarnRscPropError } from '../rsc-prop-warnings.ts';
|
|
21
29
|
|
|
22
30
|
import {
|
|
@@ -41,7 +49,7 @@ import { isDevMode } from '../debug.ts';
|
|
|
41
49
|
export interface RenderSignals {
|
|
42
50
|
denySignal: DenySignal | null;
|
|
43
51
|
/**
|
|
44
|
-
* The first deny that no in-tree catch site could place — no `
|
|
52
|
+
* The first deny that no in-tree catch site could place — no `owners`, so
|
|
45
53
|
* no boundary may claim its error row and only the re-render fallback can
|
|
46
54
|
* produce a page for it.
|
|
47
55
|
*
|
|
@@ -49,7 +57,7 @@ export interface RenderSignals {
|
|
|
49
57
|
* deep page can hoist a 404 to a boundary that will render it while an
|
|
50
58
|
* outer layout separately throws a 403 nothing matches; whichever lands in
|
|
51
59
|
* `denySignal` first, the unplaced one still needs the fallback, and
|
|
52
|
-
* deciding from `denySignal.
|
|
60
|
+
* deciding from `denySignal.owners` alone would strand it. See TIM-1356.
|
|
53
61
|
*/
|
|
54
62
|
unplacedDeny: DenySignal | null;
|
|
55
63
|
redirectSignal: RedirectSignal | null;
|
|
@@ -66,14 +74,6 @@ export interface RenderSignals {
|
|
|
66
74
|
lastUnhandledError: unknown | null;
|
|
67
75
|
/** Callback fired when a redirect or deny signal is captured in onError. */
|
|
68
76
|
onSignal?: () => void;
|
|
69
|
-
/**
|
|
70
|
-
* Resolves when every deny-capable shell component (PageDenyBoundary +
|
|
71
|
-
* TracedLayouts with deny chains) has settled, OR immediately when any
|
|
72
|
-
* catches a DenySignal in-tree (status is known). Set by renderRoute
|
|
73
|
-
* after element-tree construction. Awaited by buildRscPayloadResponse
|
|
74
|
-
* before committing HTTP status. See TIM-1045, TIM-1208.
|
|
75
|
-
*/
|
|
76
|
-
shellSettled?: Promise<void>;
|
|
77
77
|
/** Callback fired when an unhandled error (non-signal) is captured. Used
|
|
78
78
|
* to halt Flight data injection into the inline stream. */
|
|
79
79
|
onUnhandledError?: () => void;
|
|
@@ -97,7 +97,11 @@ export interface RscStreamResult {
|
|
|
97
97
|
* fires onError during stream consumption. Signals are captured in the
|
|
98
98
|
* returned `signals` object for the caller to handle.
|
|
99
99
|
*/
|
|
100
|
-
export function renderRscStream(
|
|
100
|
+
export function renderRscStream(
|
|
101
|
+
payload: RscPayloadRoot,
|
|
102
|
+
req: Request,
|
|
103
|
+
leafDenyChain?: DenyPageEntry[]
|
|
104
|
+
): RscStreamResult {
|
|
101
105
|
const signals: RenderSignals = {
|
|
102
106
|
denySignal: null,
|
|
103
107
|
unplacedDeny: null,
|
|
@@ -121,22 +125,39 @@ export function renderRscStream(payload: RscPayloadRoot, req: Request): RscStrea
|
|
|
121
125
|
// Connection abort (user refreshed or navigated away) — suppress.
|
|
122
126
|
// Not an application error; no need to track or log.
|
|
123
127
|
if (isAbortError(error) || req.signal?.aborted) return;
|
|
124
|
-
if (error
|
|
125
|
-
|
|
126
|
-
|
|
128
|
+
if (isDenySignal(error)) {
|
|
129
|
+
// Late addressing (TIM-1450): a nested-component deny escapes
|
|
130
|
+
// all in-tree catch sites and arrives here without owners.
|
|
131
|
+
// Resolve it against the leaf deny chain — when the first match
|
|
132
|
+
// is non-MDX, stamp the full candidate list so the client can
|
|
133
|
+
// find the first reachable owner via its ancestry context.
|
|
134
|
+
if (!error.owners && leafDenyChain) {
|
|
135
|
+
const entries = matchDenyEntries(leafDenyChain, error.status);
|
|
136
|
+
if (entries.length > 0 && !entries[0].isMdx) {
|
|
137
|
+
// Only non-MDX entries are addressable — MDX boundaries hold
|
|
138
|
+
// pre-rendered fallbackElements without runtime props. TIM-1451
|
|
139
|
+
// will widen exact-code MDX; until then they stay on the
|
|
140
|
+
// re-render fallback path.
|
|
141
|
+
const nonMdx = entries.filter((e) => !e.isMdx);
|
|
142
|
+
if (nonMdx.length > 0) {
|
|
143
|
+
error.owners = nonMdx.map((e) => e.ownerKey);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// First-wins: two concurrent denies from separately serialized
|
|
148
|
+
// subtrees must commit a deterministic status. See
|
|
149
|
+
// spike-TIM-1441 §"two-concurrent-denials scenario".
|
|
150
|
+
signals.denySignal ??= error;
|
|
151
|
+
if (!error.owners) signals.unplacedDeny ??= error;
|
|
127
152
|
signals.onSignal?.();
|
|
128
|
-
// Return structured digest for client-side error boundaries.
|
|
129
|
-
// `ownerKey` is set when an in-tree catch site hoisted the deny
|
|
130
|
-
// to the segment owning the matched file — it addresses the
|
|
131
|
-
// boundary that may render it. See TIM-1356.
|
|
132
153
|
return JSON.stringify({
|
|
133
154
|
type: 'deny',
|
|
134
155
|
status: error.status,
|
|
135
156
|
data: error.data,
|
|
136
|
-
|
|
157
|
+
owners: error.owners,
|
|
137
158
|
});
|
|
138
159
|
}
|
|
139
|
-
if (error
|
|
160
|
+
if (isRedirectSignal(error)) {
|
|
140
161
|
signals.redirectSignal = error;
|
|
141
162
|
signals.onSignal?.();
|
|
142
163
|
return JSON.stringify({
|
|
@@ -145,7 +166,7 @@ export function renderRscStream(payload: RscPayloadRoot, req: Request): RscStrea
|
|
|
145
166
|
status: error.status,
|
|
146
167
|
});
|
|
147
168
|
}
|
|
148
|
-
if (error
|
|
169
|
+
if (isRenderError(error)) {
|
|
149
170
|
// Track the first render error for pre-flush handling
|
|
150
171
|
if (!signals.renderError) {
|
|
151
172
|
signals.renderError = { error, status: error.status };
|
|
@@ -238,10 +259,10 @@ export function renderRscStream(payload: RscPayloadRoot, req: Request): RscStrea
|
|
|
238
259
|
}
|
|
239
260
|
);
|
|
240
261
|
} catch (error) {
|
|
241
|
-
if (error
|
|
242
|
-
signals.denySignal
|
|
243
|
-
if (!error.
|
|
244
|
-
} else if (error
|
|
262
|
+
if (isDenySignal(error)) {
|
|
263
|
+
signals.denySignal ??= error;
|
|
264
|
+
if (!error.owners) signals.unplacedDeny ??= error;
|
|
265
|
+
} else if (isRedirectSignal(error)) {
|
|
245
266
|
signals.redirectSignal = error;
|
|
246
267
|
} else {
|
|
247
268
|
// Synchronous render error — component threw during
|
|
@@ -249,7 +270,7 @@ export function renderRscStream(payload: RscPayloadRoot, req: Request): RscStrea
|
|
|
249
270
|
// the server; the error page will be rendered below.
|
|
250
271
|
signals.renderError = {
|
|
251
272
|
error,
|
|
252
|
-
status: error
|
|
273
|
+
status: isRenderError(error) ? error.status : 500,
|
|
253
274
|
};
|
|
254
275
|
logRenderError({ method: req.method, path: new URL(req.url).pathname, error });
|
|
255
276
|
}
|
|
@@ -20,7 +20,7 @@ import { getSlotParamsForClient } from '../request-context.ts';
|
|
|
20
20
|
import type { LayoutEntry } from '../deny-renderer.ts';
|
|
21
21
|
import { renderDenyPage } from '../deny-renderer.ts';
|
|
22
22
|
import type { RouteMatch } from '../pipeline.ts';
|
|
23
|
-
import {
|
|
23
|
+
import { isSsrStreamError } from '../primitives.ts';
|
|
24
24
|
import type { LayoutComponentEntry } from '../route-element-builder.ts';
|
|
25
25
|
import type { ManifestSegmentNode } from '../route-matcher.ts';
|
|
26
26
|
import type { SlotSkipEntry } from '../slot-resolver.ts';
|
|
@@ -405,15 +405,11 @@ export async function renderSsrResponse(opts: SsrRenderOptions): Promise<Respons
|
|
|
405
405
|
// Deny pages still go through RSC → SSR (plain props), so they pass empty
|
|
406
406
|
// layouts to avoid re-executing server components in the RSC environment's
|
|
407
407
|
// stale ALS scope. See LOCAL-293.
|
|
408
|
-
//
|
|
409
|
-
//
|
|
410
|
-
//
|
|
411
|
-
//
|
|
412
|
-
|
|
413
|
-
const isSsrStreamError =
|
|
414
|
-
ssrError instanceof SsrStreamError ||
|
|
415
|
-
(ssrError instanceof Error && ssrError.name === 'SsrStreamError');
|
|
416
|
-
if (isSsrStreamError) {
|
|
408
|
+
// SsrStreamError is thrown in the SSR environment but caught here in
|
|
409
|
+
// RSC — separate Vite module graphs, so `instanceof` can never match.
|
|
410
|
+
// isSsrStreamError() checks the Symbol.for brand instead. See
|
|
411
|
+
// design/18-build-system.md §"Dev Mode: Signal Identity Across Program Reloads".
|
|
412
|
+
if (isSsrStreamError(ssrError)) {
|
|
417
413
|
// Cancel the teed inline stream — SSR failed so this response will
|
|
418
414
|
// be replaced by renderErrorPage which creates its own clean streams.
|
|
419
415
|
// Without explicit cancellation the orphaned tee branch keeps the
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { TIMBER_ERROR_HEADER } from '../shared/rsc-error-envelope.ts';
|
|
2
|
+
import { errorResponseHeaders } from './error-response-headers.ts';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Build the hard-navigate envelope for a status code.
|
|
6
|
+
*
|
|
7
|
+
* Preserve middleware's request-wide headers and cookies, but discard the
|
|
8
|
+
* abandoned body's framing, validators, and cache policy. Every producer,
|
|
9
|
+
* including errors returned normally during render, gets this cleanup here.
|
|
10
|
+
* The input header bag is left intact. The body deliberately carries no message —
|
|
11
|
+
* only the status already on the wire (design/13-security.md principle 4).
|
|
12
|
+
*/
|
|
13
|
+
export function rscErrorEnvelope(status: number, responseHeaders?: Headers): Response {
|
|
14
|
+
const headers = errorResponseHeaders(responseHeaders);
|
|
15
|
+
headers.set(TIMBER_ERROR_HEADER, '1');
|
|
16
|
+
headers.set('content-type', 'application/json; charset=utf-8');
|
|
17
|
+
return new Response(JSON.stringify({ error: true, status }), { status, headers });
|
|
18
|
+
}
|
|
@@ -1,20 +1,118 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Skippable-prefix
|
|
2
|
+
* Skippable-prefix computation.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Determines which layouts can be skipped on a partial navigation by
|
|
5
|
+
* computing a contiguous prefix of segments whose cached layouts need
|
|
6
|
+
* not re-render. Extracted from `route-element-builder.ts` to keep it
|
|
7
|
+
* under the 500-line limit (design/18-build-system.md).
|
|
8
|
+
*
|
|
9
|
+
* See design/19-client-navigation.md §"X-Timber-State-Tree Header".
|
|
8
10
|
*/
|
|
9
11
|
|
|
10
12
|
import type { ManifestSegmentNode } from './route-matcher.ts';
|
|
11
13
|
import type { RouteMatch } from './pipeline.ts';
|
|
12
|
-
import { shouldSkipSlot, type ClientStateTree } from './state-tree-diff.ts';
|
|
14
|
+
import { shouldSkipSegment, shouldSkipSlot, type ClientStateTree } from './state-tree-diff.ts';
|
|
13
15
|
import { consumedUrlParts } from './chain-url-parts.ts';
|
|
14
16
|
import { computeSlotContentKey, computeSlotKey } from '../routing/segment-keys.ts';
|
|
15
17
|
import { matchUrlParts } from './tree-match.ts';
|
|
16
18
|
import { slotUrlParts } from './slot-resolver.ts';
|
|
17
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Compute the set of segment indices whose layouts can be skipped.
|
|
22
|
+
*
|
|
23
|
+
* Three sub-rules in order:
|
|
24
|
+
* (a) Contiguous prefix via `shouldSkipSegment` — stop at the first
|
|
25
|
+
* non-skippable layout.
|
|
26
|
+
* (b) If every layout landed in the prefix, remove the innermost one
|
|
27
|
+
* as the merge point (the client needs a mounted outlet there).
|
|
28
|
+
* (c) Cross-section guard — clear the whole set unless at least one
|
|
29
|
+
* non-skipped layout's key is in `clientStateTree`.
|
|
30
|
+
*
|
|
31
|
+
* After the prefix is computed, `truncateSkippableForChangedSlots`
|
|
32
|
+
* removes any segment whose parallel slots changed content.
|
|
33
|
+
*/
|
|
34
|
+
export function computeSkippableSet(
|
|
35
|
+
segments: ManifestSegmentNode[],
|
|
36
|
+
segmentKeys: string[],
|
|
37
|
+
layoutBySegment: Map<ManifestSegmentNode, unknown>,
|
|
38
|
+
clientStateTree: ClientStateTree | null | undefined,
|
|
39
|
+
match: RouteMatch,
|
|
40
|
+
req: Request
|
|
41
|
+
): Set<number> {
|
|
42
|
+
const skippableSet = new Set<number>();
|
|
43
|
+
if (!clientStateTree) return skippableSet;
|
|
44
|
+
|
|
45
|
+
// (a) Contiguous prefix via shouldSkipSegment.
|
|
46
|
+
for (let i = 0; i < segments.length; i++) {
|
|
47
|
+
const isLeaf = i === segments.length - 1;
|
|
48
|
+
const layoutComponent = layoutBySegment.get(segments[i]);
|
|
49
|
+
|
|
50
|
+
if (
|
|
51
|
+
shouldSkipSegment(
|
|
52
|
+
segmentKeys[i],
|
|
53
|
+
layoutComponent as ((...args: unknown[]) => unknown) | undefined,
|
|
54
|
+
isLeaf,
|
|
55
|
+
clientStateTree
|
|
56
|
+
)
|
|
57
|
+
) {
|
|
58
|
+
skippableSet.add(i);
|
|
59
|
+
} else if (layoutComponent) {
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// (b) Ensure there's a merge point: if all layouts are in the prefix,
|
|
65
|
+
// keep the innermost as merge point.
|
|
66
|
+
if (skippableSet.size > 0) {
|
|
67
|
+
let mergePointIndex = -1;
|
|
68
|
+
for (let i = 0; i < segments.length; i++) {
|
|
69
|
+
if (skippableSet.has(i)) continue;
|
|
70
|
+
if (layoutBySegment.has(segments[i])) {
|
|
71
|
+
mergePointIndex = i;
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (mergePointIndex === -1) {
|
|
77
|
+
for (let i = segments.length - 1; i >= 0; i--) {
|
|
78
|
+
if (skippableSet.has(i) && layoutBySegment.has(segments[i])) {
|
|
79
|
+
skippableSet.delete(i);
|
|
80
|
+
mergePointIndex = i;
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// (c) Cross-section guard: at least one rendered layout below the
|
|
87
|
+
// prefix must exist in the client's state tree.
|
|
88
|
+
let hasClientOutlet = false;
|
|
89
|
+
for (let j = 0; j < segments.length; j++) {
|
|
90
|
+
if (skippableSet.has(j)) continue;
|
|
91
|
+
if (layoutBySegment.has(segments[j]) && clientStateTree.has(segmentKeys[j])) {
|
|
92
|
+
hasClientOutlet = true;
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (!hasClientOutlet) {
|
|
97
|
+
skippableSet.clear();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Slot-change guard (TIM-1232).
|
|
102
|
+
if (skippableSet.size > 0) {
|
|
103
|
+
truncateSkippableForChangedSlots(
|
|
104
|
+
skippableSet,
|
|
105
|
+
segments,
|
|
106
|
+
segmentKeys,
|
|
107
|
+
match,
|
|
108
|
+
clientStateTree,
|
|
109
|
+
req
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return skippableSet;
|
|
114
|
+
}
|
|
115
|
+
|
|
18
116
|
/**
|
|
19
117
|
* Truncate the skippable prefix when a skippable layout has slots whose
|
|
20
118
|
* content changed between the departing and destination URLs (TIM-1232).
|
|
@@ -28,7 +126,7 @@ import { slotUrlParts } from './slot-resolver.ts';
|
|
|
28
126
|
* Uses key-based diffing (TIM-1370): computes the destination content key
|
|
29
127
|
* per slot and checks if the client's advertised set contains it.
|
|
30
128
|
*/
|
|
31
|
-
|
|
129
|
+
function truncateSkippableForChangedSlots(
|
|
32
130
|
skippableSet: Set<number>,
|
|
33
131
|
segments: ManifestSegmentNode[],
|
|
34
132
|
segmentKeys: string[],
|
|
@@ -20,10 +20,11 @@ import { TimberErrorBoundary } from '../client/error-boundary.tsx';
|
|
|
20
20
|
import SlotErrorFallback from '../client/slot-error-fallback.tsx';
|
|
21
21
|
import { SlotAccessGate } from './access-gate.tsx';
|
|
22
22
|
import { wrapSegmentWithErrorBoundaries } from './error-boundary-wrapper.ts';
|
|
23
|
-
import { isClientReference } from './
|
|
23
|
+
import { isClientReference } from './utils/element-type.ts';
|
|
24
|
+
import { containSlotSubtree } from './slot-subtree-contain.ts';
|
|
24
25
|
import { loadModule } from './safe-load.ts';
|
|
25
26
|
import type { InterceptionContext, RouteMatch } from './pipeline.ts';
|
|
26
|
-
import {
|
|
27
|
+
import { isDenySignal, isRedirectSignal } from './primitives.ts';
|
|
27
28
|
import { logRenderError } from './logger.ts';
|
|
28
29
|
import type { ManifestSegmentNode } from './route-matcher.ts';
|
|
29
30
|
import { setSlotParams, runWithSlotSegmentParams } from './request-context.ts';
|
|
@@ -140,23 +141,30 @@ export async function resolveSlotElement(
|
|
|
140
141
|
// For server components, wrap in SafeSlotPage to catch ALL errors at the
|
|
141
142
|
// component level. This prevents errors from leaving unresolved Flight
|
|
142
143
|
// rows in the RSC stream — see TIM-524 for details.
|
|
144
|
+
//
|
|
145
|
+
// The catch below covers the page function. The tree it RETURNS is
|
|
146
|
+
// contained separately (`containSlotSubtree`): a server component
|
|
147
|
+
// nested in that tree runs as its own Flight task, outside this
|
|
148
|
+
// try/catch, and a deny there would otherwise reach Flight's global
|
|
149
|
+
// onError as a page-level 4xx. See slot-subtree-contain.ts, TIM-1450.
|
|
143
150
|
let element: React.ReactElement;
|
|
144
151
|
if (isClientReference(SlotPage)) {
|
|
145
152
|
element = h(SlotPage, {});
|
|
146
153
|
} else {
|
|
147
154
|
const SafeSlotPage = async (props: Record<string, unknown>) => {
|
|
148
155
|
try {
|
|
149
|
-
|
|
156
|
+
const rendered = await scope(() =>
|
|
150
157
|
(SlotPage as (props: Record<string, unknown>) => unknown)(props)
|
|
151
158
|
);
|
|
159
|
+
return containSlotSubtree(rendered as React.ReactNode, denyFallback);
|
|
152
160
|
} catch (error) {
|
|
153
161
|
// RedirectSignal must propagate — the pipeline handles redirects
|
|
154
162
|
// at the top level. Swallowing it here would silently return
|
|
155
163
|
// fallback content instead of redirecting. See TIM-554.
|
|
156
|
-
if (error
|
|
164
|
+
if (isRedirectSignal(error)) {
|
|
157
165
|
throw error;
|
|
158
166
|
}
|
|
159
|
-
if (error
|
|
167
|
+
if (isDenySignal(error)) {
|
|
160
168
|
return denyFallback;
|
|
161
169
|
}
|
|
162
170
|
// Log the error but don't re-throw — returning fallback ensures
|
|
@@ -176,11 +184,17 @@ export async function resolveSlotElement(
|
|
|
176
184
|
// intermediate slot segments (everything between slot root and leaf).
|
|
177
185
|
// Process innermost-first, same order as route-element-builder.ts
|
|
178
186
|
// handles main segments. The slot root (index 0) is handled below.
|
|
179
|
-
element = await wrapWithIntermediateSegments(
|
|
187
|
+
element = await wrapWithIntermediateSegments(
|
|
188
|
+
slotMatch.chain,
|
|
189
|
+
element,
|
|
190
|
+
h,
|
|
191
|
+
scope,
|
|
192
|
+
denyFallback
|
|
193
|
+
);
|
|
180
194
|
|
|
181
195
|
// Wrap with slot root's layout — INSIDE the access gate, so the layout
|
|
182
196
|
// server component never executes when access.ts denies. See TIM-1074.
|
|
183
|
-
element = await wrapWithLayout(slotNode, element, h, scope);
|
|
197
|
+
element = await wrapWithLayout(slotNode, element, h, scope, denyFallback);
|
|
184
198
|
|
|
185
199
|
// Wrap in SlotAccessGate if slot root has access.ts. The gate is
|
|
186
200
|
// OUTSIDE the layout, matching the segment model in
|
|
@@ -191,11 +205,15 @@ export async function resolveSlotElement(
|
|
|
191
205
|
// See design/04-authorization.md §"Slot-Level Auth".
|
|
192
206
|
element = await wrapWithAccessGate(slotNode, element, h, scope);
|
|
193
207
|
|
|
194
|
-
// Wrap with slot root's error boundaries (outermost)
|
|
208
|
+
// Wrap with slot root's error boundaries (outermost). Slot identity on
|
|
209
|
+
// every one of them, not only the catch-all below: a slot's own
|
|
210
|
+
// status page must not set the document status or empty the main
|
|
211
|
+
// route's segment cache.
|
|
195
212
|
element = await wrapSegmentWithErrorBoundaries(slotNode, element, {
|
|
196
213
|
loadModule,
|
|
197
214
|
createElement: h,
|
|
198
215
|
errorBoundaryComponent: TimberErrorBoundary,
|
|
216
|
+
isSlotBoundary: true,
|
|
199
217
|
});
|
|
200
218
|
|
|
201
219
|
// Catch-all error boundary: ensures slot errors NEVER propagate to the
|
|
@@ -271,7 +289,8 @@ async function wrapWithIntermediateSegments(
|
|
|
271
289
|
chain: ManifestSegmentNode[],
|
|
272
290
|
element: React.ReactElement,
|
|
273
291
|
h: CreateElementFn,
|
|
274
|
-
scope?: SlotParamScope
|
|
292
|
+
scope?: SlotParamScope,
|
|
293
|
+
denyFallback?: React.ReactNode
|
|
275
294
|
): Promise<React.ReactElement> {
|
|
276
295
|
for (let i = chain.length - 1; i > 0; i--) {
|
|
277
296
|
const seg = chain[i];
|
|
@@ -279,8 +298,9 @@ async function wrapWithIntermediateSegments(
|
|
|
279
298
|
loadModule,
|
|
280
299
|
createElement: h,
|
|
281
300
|
errorBoundaryComponent: TimberErrorBoundary,
|
|
301
|
+
isSlotBoundary: true,
|
|
282
302
|
});
|
|
283
|
-
element = await wrapWithLayout(seg, element, h, scope);
|
|
303
|
+
element = await wrapWithLayout(seg, element, h, scope, denyFallback);
|
|
284
304
|
element = await wrapWithAccessGate(seg, element, h, scope);
|
|
285
305
|
}
|
|
286
306
|
return element;
|
|
@@ -309,13 +329,22 @@ type SlotParamScope = <T>(fn: () => T) => T;
|
|
|
309
329
|
* Server component: the function-valued props are called here on the server
|
|
310
330
|
* and never reach the Flight stream, the same way `SlotAccessGate` takes
|
|
311
331
|
* `accessFn`.
|
|
332
|
+
*
|
|
333
|
+
* The layout's own chrome is part of the slot's subtree, so the tree it
|
|
334
|
+
* returns is contained like the page's: a server component the layout
|
|
335
|
+
* renders beside `children` that denies becomes the slot fallback in place,
|
|
336
|
+
* not a page-level 4xx. `children` is the element the resolver built for the
|
|
337
|
+
* level below, already contained there, and is passed to the walk as the one
|
|
338
|
+
* subtree to leave alone. See slot-subtree-contain.ts.
|
|
312
339
|
*/
|
|
313
340
|
function ScopedSlotLayout(props: {
|
|
314
341
|
layout: (props: Record<string, unknown>) => unknown;
|
|
315
342
|
scope: SlotParamScope;
|
|
343
|
+
denyFallback: React.ReactNode;
|
|
316
344
|
children: React.ReactElement;
|
|
317
345
|
}): unknown {
|
|
318
|
-
|
|
346
|
+
const rendered = props.scope(() => props.layout({ children: props.children }));
|
|
347
|
+
return containSlotSubtree(rendered as React.ReactNode, props.denyFallback, props.children);
|
|
319
348
|
}
|
|
320
349
|
|
|
321
350
|
/**
|
|
@@ -332,7 +361,8 @@ async function wrapWithLayout(
|
|
|
332
361
|
node: ManifestSegmentNode,
|
|
333
362
|
element: React.ReactElement,
|
|
334
363
|
h: CreateElementFn,
|
|
335
|
-
scope?: SlotParamScope
|
|
364
|
+
scope?: SlotParamScope,
|
|
365
|
+
denyFallback: React.ReactNode = null
|
|
336
366
|
): Promise<React.ReactElement> {
|
|
337
367
|
if (!node.layout) return element;
|
|
338
368
|
const Layout = await loadComponent(node.layout);
|
|
@@ -342,6 +372,7 @@ async function wrapWithLayout(
|
|
|
342
372
|
return h(ScopedSlotLayout, {
|
|
343
373
|
layout: Layout as (props: Record<string, unknown>) => unknown,
|
|
344
374
|
scope,
|
|
375
|
+
denyFallback,
|
|
345
376
|
children: element,
|
|
346
377
|
});
|
|
347
378
|
}
|