@timber-js/app 0.2.0-alpha.204 → 0.2.0-alpha.205

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/dist/_chunks/{actions-HUdJADAD.js → actions-jNrXdzgs.js} +4 -3
  2. package/dist/_chunks/{actions-HUdJADAD.js.map → actions-jNrXdzgs.js.map} +1 -1
  3. package/dist/_chunks/als-registry-C6kcfprT.js.map +1 -1
  4. package/dist/_chunks/{build-manifest-DWppEdLB.js → build-manifest-DTmSGLRz.js} +51 -2
  5. package/dist/_chunks/build-manifest-DTmSGLRz.js.map +1 -0
  6. package/dist/_chunks/{cache-api-CAPbZTga.js → cache-api-CUD9Ezaq.js} +3 -2
  7. package/dist/_chunks/{cache-api-CAPbZTga.js.map → cache-api-CUD9Ezaq.js.map} +1 -1
  8. package/dist/_chunks/{chains-C9Y7Oyno.js → chains-BjGKq9Je.js} +2 -2
  9. package/dist/_chunks/{chains-C9Y7Oyno.js.map → chains-BjGKq9Je.js.map} +1 -1
  10. package/dist/_chunks/{cli-check-BzMGuIH6.js → cli-check-3WE_GyoL.js} +3 -3
  11. package/dist/_chunks/{cli-check-BzMGuIH6.js.map → cli-check-3WE_GyoL.js.map} +1 -1
  12. package/dist/_chunks/{cli-schema-sync-DnXqcIIj.js → cli-schema-sync-D3iIZjku.js} +2 -2
  13. package/dist/_chunks/{cli-schema-sync-DnXqcIIj.js.map → cli-schema-sync-D3iIZjku.js.map} +1 -1
  14. package/dist/_chunks/{cloudflare-DxX1SU0g.js → cloudflare-BFb__LYG.js} +3 -3
  15. package/dist/_chunks/{cloudflare-DxX1SU0g.js.map → cloudflare-BFb__LYG.js.map} +1 -1
  16. package/dist/_chunks/{convention-lint-DHOFvX5s.js → convention-lint-BnO5TyHD.js} +2 -2
  17. package/dist/_chunks/{convention-lint-DHOFvX5s.js.map → convention-lint-BnO5TyHD.js.map} +1 -1
  18. package/dist/_chunks/{dev-server-BV01iyUs.js → dev-server-Dbdgx2dD.js} +8 -195
  19. package/dist/_chunks/dev-server-Dbdgx2dD.js.map +1 -0
  20. package/dist/_chunks/{error-boundary-BQKxl6EX.js → error-boundary-BndF-3Td.js} +2 -2
  21. package/dist/_chunks/{error-boundary-BQKxl6EX.js.map → error-boundary-BndF-3Td.js.map} +1 -1
  22. package/dist/_chunks/{graph-cache-Cv3njEH8.js → graph-cache-CP4GEmf9.js} +2 -2
  23. package/dist/_chunks/{graph-cache-Cv3njEH8.js.map → graph-cache-CP4GEmf9.js.map} +1 -1
  24. package/dist/_chunks/{live-graph-DCLQzV6m.js → live-graph-BM2UbbNx.js} +3 -3
  25. package/dist/_chunks/{live-graph-DCLQzV6m.js.map → live-graph-BM2UbbNx.js.map} +1 -1
  26. package/dist/_chunks/logger-k2DQ4EUf.js +506 -0
  27. package/dist/_chunks/logger-k2DQ4EUf.js.map +1 -0
  28. package/dist/_chunks/{poison-scan-CX_NaNDi.js → poison-scan-g83aAXhs.js} +2 -2
  29. package/dist/_chunks/{poison-scan-CX_NaNDi.js.map → poison-scan-g83aAXhs.js.map} +1 -1
  30. package/dist/_chunks/{logger-DqJ2VoAY.js → primitives-DLAnvsrk.js} +17 -518
  31. package/dist/_chunks/primitives-DLAnvsrk.js.map +1 -0
  32. package/dist/_chunks/{scanner-B_tnqFcF.js → scanner-BhNruzv0.js} +3 -3
  33. package/dist/_chunks/{scanner-B_tnqFcF.js.map → scanner-BhNruzv0.js.map} +1 -1
  34. package/dist/_chunks/{walkers-Cm3PC5JT.js → walkers-BNsswm-h.js} +2 -2
  35. package/dist/_chunks/{walkers-Cm3PC5JT.js.map → walkers-BNsswm-h.js.map} +1 -1
  36. package/dist/adapters/cloudflare-dev.js +1 -1
  37. package/dist/adapters/cloudflare-kv-cache.js +1 -1
  38. package/dist/adapters/cloudflare.js +1 -1
  39. package/dist/analyze/crawl-entry.js +2 -2
  40. package/dist/analyze/graph-command.js +3 -3
  41. package/dist/cache/index.js +1 -1
  42. package/dist/cli.js +3 -3
  43. package/dist/client/browser-entry/rsc-stream.d.ts.map +1 -1
  44. package/dist/client/error-boundary.js +1 -1
  45. package/dist/client/internal.js +1 -1
  46. package/dist/dev-tools/index.d.ts +1 -1
  47. package/dist/dev-tools/index.d.ts.map +1 -1
  48. package/dist/dev-tools/overlay.d.ts +1 -21
  49. package/dist/dev-tools/overlay.d.ts.map +1 -1
  50. package/dist/index.js +6 -7
  51. package/dist/index.js.map +1 -1
  52. package/dist/plugins/dev-server.d.ts.map +1 -1
  53. package/dist/routing/index.js +2 -2
  54. package/dist/server/als-registry.d.ts +0 -7
  55. package/dist/server/als-registry.d.ts.map +1 -1
  56. package/dist/server/deny-renderer.d.ts +2 -6
  57. package/dist/server/deny-renderer.d.ts.map +1 -1
  58. package/dist/server/index.js +3 -2
  59. package/dist/server/index.js.map +1 -1
  60. package/dist/server/internal.js +180 -7
  61. package/dist/server/internal.js.map +1 -1
  62. package/dist/server/pipeline.d.ts +1 -13
  63. package/dist/server/pipeline.d.ts.map +1 -1
  64. package/dist/server/rsc-entry/deny-fallback.d.ts.map +1 -1
  65. package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
  66. package/dist/server/rsc-entry/helpers.d.ts +0 -38
  67. package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
  68. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  69. package/dist/server/rsc-entry/render-route.d.ts +0 -1
  70. package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
  71. package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
  72. package/dist/server/rsc-entry/rsc-stream.d.ts +1 -7
  73. package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
  74. package/dist/server/rsc-entry/ssr-renderer.d.ts +0 -1
  75. package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
  76. package/dist/server/slot-resolver.d.ts.map +1 -1
  77. package/dist/server/slot-subtree-contain.d.ts +1 -1
  78. package/dist/server/slot-subtree-contain.d.ts.map +1 -1
  79. package/package.json +1 -1
  80. package/src/client/browser-entry/rsc-stream.ts +1 -82
  81. package/src/dev-tools/index.ts +0 -2
  82. package/src/dev-tools/overlay.ts +1 -65
  83. package/src/plugins/dev-server.ts +4 -45
  84. package/src/server/als-registry.ts +0 -8
  85. package/src/server/deny-renderer.ts +0 -7
  86. package/src/server/pipeline.ts +1 -14
  87. package/src/server/rsc-entry/deny-fallback.ts +1 -10
  88. package/src/server/rsc-entry/error-renderer.ts +1 -11
  89. package/src/server/rsc-entry/helpers.ts +0 -92
  90. package/src/server/rsc-entry/index.ts +1 -14
  91. package/src/server/rsc-entry/render-route.ts +2 -23
  92. package/src/server/rsc-entry/rsc-payload.ts +1 -2
  93. package/src/server/rsc-entry/rsc-stream.ts +3 -29
  94. package/src/server/rsc-entry/ssr-renderer.ts +2 -15
  95. package/src/server/slot-resolver.ts +7 -29
  96. package/src/server/slot-subtree-contain.ts +22 -14
  97. package/dist/_chunks/build-manifest-DWppEdLB.js.map +0 -1
  98. package/dist/_chunks/csp-nonce-hOGniaG4.js +0 -227
  99. package/dist/_chunks/csp-nonce-hOGniaG4.js.map +0 -1
  100. package/dist/_chunks/dev-server-BV01iyUs.js.map +0 -1
  101. package/dist/_chunks/logger-DqJ2VoAY.js.map +0 -1
  102. package/dist/dev-tools/debug-channel.d.ts +0 -55
  103. package/dist/dev-tools/debug-channel.d.ts.map +0 -1
  104. package/src/dev-tools/debug-channel.ts +0 -151
@@ -1 +1 @@
1
- {"version":3,"file":"error-boundary-BQKxl6EX.js","names":[],"sources":["../../src/shared/payload-root.ts","../../src/client/rsc-fetch.ts","../../src/client/unload-guard.ts","../../src/client/stale-client.ts","../../src/client/router-skew.ts","../../src/client/router-effects.ts","../../src/client/error-boundary.tsx"],"sourcesContent":["/**\n * The shape of an RSC payload's root row.\n *\n * Every tree the server sends the browser is serialized as\n * `{ tree, params, slotParams }` — the route's element tree, plus the coerced\n * params it rendered with, as *siblings* rather than as a wrapper around the\n * tree. See design/41-global-params.md §\"Transport\".\n *\n * Two reasons it is a sibling field rather than a provider inside the payload\n * (TIM-1297, which is TIM-1294's mechanism corrected):\n *\n * 1. **A provider inside the payload cannot serve a partial navigation.** The\n * client keeps the previous tree and splices the new payload in at the\n * first updated `SegmentOutlet`, so the payload's own provider lands\n * *below* the retained region — and the retained region's root is the\n * *departing* route's provider, which therefore wins for every reader in a\n * skipped layout. Wrapping something above it does not help: context\n * resolves to the nearest provider, and the departing one is nearer.\n * Params belong to the *navigation*, so the provider has to be owned by the\n * client, above the merge point, and there must be exactly one.\n *\n * 2. **Element props are not readable at the instant the root resolves.**\n * Flight outlines a React element's props and fills the record in after the\n * root row resolves, so a reader that unblocks on the root sees `{}` — and\n * since the object is filled *in place*, its identity never changes and any\n * memo keyed on it caches the empty snapshot permanently. A plain model\n * field has no such window: the root chunk stays blocked until everything\n * it references has resolved.\n *\n * This module is the single declaration of that shape. The server builds roots\n * through `server/publish-params.ts` and the client reads them through the\n * helpers here — producer and consumer do not each describe the payload.\n */\n\nimport type { CoercedParams } from './param-value.ts';\nimport type { SlotParamsRecord } from './slot-params.ts';\n\n/** The params published beside a tree. */\nexport interface PublishedParams {\n /** The main route's coerced params. */\n params: CoercedParams;\n /** Per-slot params, or null when the route rendered no slots with params. */\n slotParams: SlotParamsRecord | null;\n}\n\n/**\n * Params ready to publish: settled, or a promise the root provider suspends on.\n *\n * Both forms exist on purpose. The hydration path cannot await — `hydrateRoot`\n * has to be called before the inline Flight stream has necessarily produced\n * the root row — so it hands over the promise and suspends exactly where React\n * was already going to suspend on the tree. Every other path settles it inside\n * the navigation transition first, which is what keeps a decode failure inside\n * the app's own error boundaries instead of above them (TIM-1297).\n */\nexport type ParamsSource = PublishedParams | Promise<PublishedParams>;\n\n/** The root row of an RSC payload. */\nexport interface RscPayloadRoot extends PublishedParams {\n /** The route's element tree. */\n tree: unknown;\n}\n\n/**\n * What a reader gets when the value it was handed is not a payload root.\n *\n * In the browser this is unreachable for a real response: all four producers\n * of a route payload go through `withPublishedParams`, and a client talking to\n * a different build is answered with 204 at Stage 1c\n * (`server/pipeline-phases.ts`) before any payload exists. It is reachable in\n * the router's test/fallback path, where `decodeRsc` is absent and the\n * \"payload\" is the raw response text.\n */\nexport const NO_PUBLISHED_PARAMS: PublishedParams = { params: {}, slotParams: null };\n\n/** True when `value` carries published params — a root, or a read of one. */\nfunction hasPublishedParams(value: unknown): value is PublishedParams {\n return (\n typeof value === 'object' &&\n value !== null &&\n 'params' in value &&\n typeof (value as PublishedParams).params === 'object' &&\n (value as PublishedParams).params !== null\n );\n}\n\n/**\n * Read the params published beside a tree.\n *\n * Accepts a payload root *or* a `PublishedParams` already split off one, so\n * the client can pre-resolve on the navigation path and still hand the same\n * value to the same provider. Never throws.\n */\nexport function readPublishedParams(source: unknown): PublishedParams {\n if (!hasPublishedParams(source)) return NO_PUBLISHED_PARAMS;\n return { params: source.params, slotParams: source.slotParams ?? null };\n}\n\n/**\n * Read the renderable tree out of a payload root.\n *\n * Returns the value unchanged when it is not a root — the router's fallback\n * path stores raw response text under the same name, and a test asserting on\n * that text should see the text.\n */\nexport function readPayloadTree(root: unknown): unknown {\n if (typeof root === 'object' && root !== null && 'tree' in root && hasPublishedParams(root)) {\n return (root as RscPayloadRoot).tree;\n }\n return root;\n}\n\n/** A decoded payload, split into the two things the client does with it. */\nexport interface SplitPayload {\n /** The renderable tree — an element, or a thenable React suspends on. */\n tree: unknown;\n /**\n * The params published beside it. Settled when the root already is, so the\n * hydration path can render without an extra await.\n *\n * **Never rejects.** A decode failure has to surface where React renders\n * `tree`, so the error boundary *around the tree* catches it. Rejecting here\n * too would take down the provider above every boundary in the app, turning\n * a contained failure into a blank page (TIM-1297).\n */\n params: ParamsSource;\n}\n\n/**\n * Split a decoded payload root into its tree and its params.\n *\n * Accepts a settled root or a thenable of one, and preserves which it was: a\n * settled root splits synchronously, so no code path gains a suspend point it\n * did not already have.\n */\nexport function splitPayloadRoot(root: unknown): SplitPayload {\n if (\n typeof root !== 'object' ||\n root === null ||\n typeof (root as PromiseLike<unknown>).then !== 'function'\n ) {\n return { tree: readPayloadTree(root), params: readPublishedParams(root) };\n }\n const settled = root as PromiseLike<unknown>;\n const tree = Promise.resolve(settled).then(readPayloadTree);\n // The rejection still reaches React through its own read of `tree`; this\n // only marks it handled for the case where nothing renders it (a partial\n // navigation whose segment update finds no matching outlet).\n tree.catch(() => {});\n const params = Promise.resolve(settled).then(readPublishedParams, () => NO_PUBLISHED_PARAMS);\n return { tree, params };\n}\n","/**\n * RSC Fetch — handles fetching and parsing RSC Flight payloads.\n *\n * Extracted from router.ts to keep both files under the 500-line limit.\n * This module handles:\n * - Cache-busting URL generation for RSC requests\n * - Building RSC request headers (Accept, X-Timber-State-Tree)\n * - Extracting metadata from RSC response headers\n * - Fetching and decoding RSC payloads\n *\n * See design/19-client-navigation.md §\"RSC Payload Handling\"\n */\n\nimport {\n rscCacheKey,\n recordLookup,\n randomRscCacheKey,\n RSC_KEY_PARAM,\n} from '../shared/rsc-cache-key.ts';\nimport type { RscManifest, RscManifestEntry } from '../shared/rsc-manifest.ts';\nimport {\n splitPayloadRoot,\n readPublishedParams,\n type PublishedParams,\n} from '../shared/payload-root.ts';\nimport { normalizeBasePath, stripBasePath, withBasePath } from '../shared/base-path.ts';\nimport { rscPayloadPath } from '../shared/rsc-payload-path.ts';\nimport type { SegmentInfo, StateTree } from '../shared/segment-info.ts';\nimport { TIMBER_ERROR_HEADER } from '../shared/rsc-error-envelope.ts';\nimport { RSC_CONTENT_TYPE, isRscContentType } from '../shared/rsc-media-type.ts';\nimport type { RouterDeps } from './router-types.ts';\n\n// ─── Types ───────────────────────────────────────────────────────\n\n/** Result of fetching an RSC payload — includes segment metadata. */\nexport interface FetchResult {\n /** The renderable tree, split off the payload root. */\n payload: unknown;\n /**\n * The params the same payload published beside that tree.\n *\n * Not a separate channel: both fields come out of one `splitPayloadRoot`\n * call on one decoded root, so a response cannot deliver a tree and another\n * route's params. They used to arrive on their own header, which is how a\n * navigation could render one route and publish another's (TIM-1285,\n * TIM-1294).\n */\n params: PublishedParams | Promise<PublishedParams>;\n /**\n * Promise that settles when the RSC decode completes or fails.\n * The payload thenable is NOT awaited before returning (for streaming),\n * so callers must monitor this to catch async decode errors\n * (truncated streams, Flight parse failures) that would otherwise\n * become unhandled rejections.\n */\n decodePromise: Promise<void> | null;\n /** Segment metadata from X-Timber-Segments header for populating the segment cache. */\n segmentInfo: SegmentInfo[] | null;\n /** Segment paths that were skipped by the server (for client-side merging). */\n skippedSegments: string[] | null;\n}\n\n// ─── URL Helpers ─────────────────────────────────────────────────\n\n/**\n * Append a `_rsc=<key>` query parameter to the URL (TIM-1268).\n *\n * The key is a deterministic hash of the RSC request headers, so identical\n * requests share a URL and the payload becomes cacheable. It also keeps the\n * document and the payload on different URLs, so a shared cache cannot\n * return HTML for an RSC request (or vice versa) even where `Vary` is\n * opt-in configuration rather than default behaviour — Cloudflare and\n * CloudFront both. See GHSA-wfc6-r584-vfw7, design/13-security.md.\n *\n * The origin recomputes this key from the received headers and refuses to\n * let a mismatched response be shared-cached, so a caller cannot claim\n * another client's key while sending its own headers.\n *\n * Falls back to a random value when no key can be derived — `crypto.subtle`\n * is absent in non-secure browsing contexts (plain `http://` on a LAN\n * address). Dropping the parameter instead would put the payload back on\n * the document URL, where a query-keyed cache that ignores `Vary` could\n * store a Flight response under `/about` and serve it to an HTML\n * navigation. The random parameter this feature replaced did separate those\n * representations, and the fallback must not be worse than what it\n * replaced. The origin cannot verify a random key, so it refuses to\n * share-cache the response — which is exactly the old behaviour, where a\n * unique URL was never a cache hit anyway.\n *\n * Strips any #fragment before appending — fragments are client-only and\n * fetch() discards them, so _rsc would land inside the hash and be lost.\n */\nasync function appendRscParam(url: string, headers: Record<string, string>): Promise<string> {\n const hashIndex = url.indexOf('#');\n const urlWithoutHash = hashIndex === -1 ? url : url.slice(0, hashIndex);\n const key = (await rscCacheKey(recordLookup(headers))) ?? randomRscCacheKey();\n const separator = urlWithoutHash.includes('?') ? '&' : '?';\n return `${urlWithoutHash}${separator}${RSC_KEY_PARAM}=${key}`;\n}\n\n// ─── Deployment ID ───────────────────────────────────────────────\n\n/**\n * The client's deployment ID, set at bootstrap from the runtime config.\n * Sent with every RSC/action request for version skew detection.\n * Null in dev mode. See TIM-446.\n */\nlet clientDeploymentId: string | null = null;\n\n/** Set the client deployment ID. Called once at bootstrap. */\nexport function setClientDeploymentId(id: string | null): void {\n clientDeploymentId = id;\n}\n\n/** Get the client deployment ID. */\nexport function getClientDeploymentId(): string | null {\n return clientDeploymentId;\n}\n\n// ─── Base Path ───────────────────────────────────────────────────\n\n/**\n * The deployment base path (Vite's resolved `base`), normalized.\n * Set at bootstrap from `virtual:timber-config`; `'/'` unless the app is\n * deployed under a sub-path. See design/11-platform.md, TIM-1261.\n *\n * **The base is applied in exactly one place: at fetch time**\n * (`toStaticRscUrl`). The static build writes the RSC manifest with\n * root-relative keys *and* root-relative URLs, so the generated site stays\n * relocatable and there is no second copy of the base to drift from this one.\n */\nlet basePath = '/';\n\n/** Set the deployment base path. Called once at bootstrap. */\nexport function setBasePath(base: string | undefined): void {\n basePath = normalizeBasePath(base);\n}\n\n/** Get the normalized deployment base path. @internal Exported for testing. */\nexport function getBasePath(): string {\n return basePath;\n}\n\n// ─── Static Mode ────────────────────────────────────────────────\n\n/**\n * When true, RSC fetches use _rsc/*.rsc file URLs instead of\n * the route URL with Accept headers. Static hosts ignore Accept\n * headers, so the client must fetch the pre-generated .rsc files\n * directly. Set at bootstrap from virtual:timber-config output mode.\n */\nlet staticMode = false;\n\nexport function setStaticMode(enabled: boolean): void {\n staticMode = enabled;\n}\n\nexport function isStaticMode(): boolean {\n return staticMode;\n}\n\n/**\n * RSC manifest mapping unhashed → content-hashed `.rsc` URLs. Populated from\n * `window.__TIMBER_RSC_MANIFEST__` (injected into HTML during static\n * generation). Shape shared with the static build that writes it — see\n * `shared/rsc-manifest.ts`. See TIM-1254.\n */\nlet rscManifest: RscManifest | null = null;\n\nfunction getRscManifest(): RscManifest | null {\n if (rscManifest) return rscManifest;\n if (typeof window !== 'undefined' && (window as any).__TIMBER_RSC_MANIFEST__) {\n rscManifest = (window as any).__TIMBER_RSC_MANIFEST__;\n }\n return rscManifest;\n}\n\n/** @internal Exported for testing. */\nexport function setRscManifest(manifest: RscManifest | null): void {\n rscManifest = manifest;\n}\n\n/** A resolved static payload target: the URL to fetch, and how it was resolved. */\ninterface StaticRscTarget {\n /** Fetchable URL — base-prefixed. */\n url: string;\n /**\n * True when the RSC manifest supplied a content-hashed filename. The hash\n * guarantees freshness, so no `_rsc` cache key is appended (TIM-1254).\n */\n hashed: boolean;\n}\n\n/**\n * Resolve a route URL to the `_rsc/*.rsc` file to fetch. The naming rule is\n * `shared/rsc-payload-path.ts`, shared with the build that writes the files.\n *\n * When an RSC manifest is available (hashed filenames from TIM-1254),\n * the manifest is consulted to resolve to the hashed path.\n *\n * / → /_rsc/index.rsc (or /_rsc/index-B7YxEKdN.rsc with manifest)\n * /about → /_rsc/about/index.rsc (or /_rsc/about/index-C8ZzFLfO.rsc)\n * /blog/hello → /_rsc/blog/hello/index.rsc\n *\n * The manifest is written and read in route space (no base prefix), so the\n * base is applied here — once, on the way out. See client/base-path.ts.\n */\nfunction toStaticRscUrl(url: string): StaticRscTarget {\n const unhashed = toUnhashedRscUrl(url);\n const hashedUrl = manifestLookup(unhashed);\n return { url: withBasePath(basePath, hashedUrl ?? unhashed), hashed: hashedUrl !== null };\n}\n\n/**\n * Compute the unhashed `_rsc/*.rsc` path for a route URL.\n *\n * Returns a **route-space** path: the deployment base is stripped from the\n * incoming URL and is NOT re-applied, because this value doubles as the RSC\n * manifest key, and the manifest is base-less. `toStaticRscUrl` applies the\n * base to whatever is actually fetched.\n *\n * @internal Exported for testing.\n */\nexport function toUnhashedRscUrl(url: string): string {\n const hashIndex = url.indexOf('#');\n const queryIndex = url.indexOf('?');\n const hashEnd = hashIndex === -1 ? url.length : hashIndex;\n const queryEnd = queryIndex === -1 ? url.length : queryIndex;\n const end = Math.min(hashEnd, queryEnd);\n let pathname = stripBasePath(basePath, url.slice(0, end));\n // Strip trailing slash (unless root) to match static build output naming\n if (pathname.length > 1 && pathname.endsWith('/')) {\n pathname = pathname.slice(0, -1);\n }\n return rscPayloadPath(pathname);\n}\n\n/**\n * Look up a key in the RSC manifest, falling back to percent-decoded\n * lookup for encoded browser URLs. Returns the entry or null.\n */\nfunction manifestEntry(key: string): RscManifestEntry | null {\n const manifest = getRscManifest();\n if (!manifest) return null;\n if (manifest[key]) return manifest[key];\n try {\n const decoded = decodeURIComponent(key);\n if (decoded !== key && manifest[decoded]) return manifest[decoded];\n } catch {\n // Malformed percent sequence (e.g. /100%.rsc) — skip decoded lookup\n }\n return null;\n}\n\n/**\n * Look up the hashed URL for an unhashed RSC path.\n */\nfunction manifestLookup(key: string): string | null {\n return manifestEntry(key)?.url ?? null;\n}\n\n// The manifest used to inline each route's params, because a generated\n// `.rsc` file carries no response headers and the header channel therefore\n// did not exist on the static path (TIM-1255, TIM-1285). The `.rsc` file now\n// carries them itself — they are props inside the payload — so the static\n// path needs no special case at all.\n\n// ─── Reload Signal ───────────────────────────────────────────────\n\n/** Header name used by the server to signal a version skew reload. */\nexport const RELOAD_HEADER = 'X-Timber-Reload';\n\n/** Header name for the client's deployment ID. */\nexport const DEPLOYMENT_ID_HEADER = 'X-Timber-Deployment-Id';\n\n/**\n * Check if a response signals a version skew reload.\n * Triggers a full page reload if the server indicates the client is stale.\n */\nexport function checkReloadSignal(response: Response): boolean {\n return response.headers.get(RELOAD_HEADER) === '1';\n}\n\n// ─── Header Builder ──────────────────────────────────────────────\n\n/**\n * Build the headers for an RSC payload request.\n *\n * **Every header added here is hashed into the payload URL** as the `_rsc`\n * cache key (see `rscCacheKey`), and URLs are logged by proxies, CDNs, and\n * origin access logs. So: no credentials, no session identifiers, no\n * user-supplied content. `fnv1aHash` is a cache-key hash, not a KDF — a\n * low-entropy secret would be recoverable from the URL by brute force.\n * Authentication already travels on cookies, which are not part of this\n * object and never enter the key.\n */\nexport function buildRscHeaders(\n stateTree: StateTree | undefined,\n currentUrl?: string\n): Record<string, string> {\n const headers: Record<string, string> = {\n Accept: RSC_CONTENT_TYPE,\n };\n if (stateTree) {\n headers['X-Timber-State-Tree'] = JSON.stringify(stateTree);\n }\n // Send current URL for intercepting route resolution.\n // The server uses this to determine if an intercepting route should\n // render instead of the actual target route (modal pattern).\n // See design/07-routing.md §\"Intercepting Routes\"\n if (currentUrl) {\n headers['X-Timber-URL'] = currentUrl;\n }\n // Send deployment ID for version skew detection (TIM-446).\n // The server compares this against the current build's ID.\n // On mismatch, the server signals a reload instead of returning\n // an RSC payload with mismatched module references.\n if (clientDeploymentId) {\n headers[DEPLOYMENT_ID_HEADER] = clientDeploymentId;\n }\n return headers;\n}\n\n// ─── Response Header Extraction ──────────────────────────────────\n\n/** Dev-only warning for malformed framework headers. Tree-shaken in production. */\nfunction warnMalformedHeader(headerName: string, raw: string): void {\n if (process.env.NODE_ENV !== 'production') {\n const preview = raw.length > 200 ? raw.slice(0, 200) + '…' : raw;\n console.warn(\n `[timber] Malformed ${headerName} header \\u2014 JSON.parse failed. ` +\n `This indicates a framework bug or header corruption. Raw (first 200 chars): ${preview}`\n );\n }\n}\n\n/**\n * Extract segment metadata from the X-Timber-Segments response header.\n * Returns null if the header is missing or malformed.\n *\n * Format: JSON array of {path, isRequestDependent} objects describing the rendered\n * segment chain from root to leaf. Used to populate the client-side\n * segment cache for state tree diffing on subsequent navigations.\n */\nexport function extractSegmentInfo(response: Response): SegmentInfo[] | null {\n const header = response.headers.get('X-Timber-Segments');\n if (!header) return null;\n try {\n return JSON.parse(header);\n } catch {\n warnMalformedHeader('X-Timber-Segments', header);\n return null;\n }\n}\n\n/**\n * Extract skipped segment paths from the X-Timber-Skipped-Segments header.\n * Returns null if the header is missing or malformed.\n *\n * When the server skips sync layouts the client already has cached,\n * it sends this header listing the skipped segment paths (outermost first).\n * The client uses this to merge the partial payload with cached segments.\n */\nexport function extractSkippedSegments(response: Response): string[] | null {\n const header = response.headers.get('X-Timber-Skipped-Segments');\n if (!header) return null;\n try {\n const parsed = JSON.parse(header);\n return Array.isArray(parsed) ? parsed : null;\n } catch {\n warnMalformedHeader('X-Timber-Skipped-Segments', header);\n return null;\n }\n}\n\n// ─── Redirect Error ──────────────────────────────────────────────\n\n/**\n * Thrown when an RSC payload response contains X-Timber-Redirect header.\n * Caught in navigate() to trigger a soft router navigation to the redirect target.\n */\nexport class RedirectError extends Error {\n readonly redirectUrl: string;\n constructor(url: string) {\n super(`Server redirect to ${url}`);\n this.redirectUrl = url;\n }\n}\n\n/**\n * Thrown when the server signals a version skew (X-Timber-Reload header).\n * Caught in navigate() to trigger a full page reload.\n * See TIM-446.\n */\nexport class VersionSkewError extends Error {\n constructor() {\n super('Version skew detected — server has been redeployed');\n }\n}\n\n/**\n * Thrown when the server returns an error for an RSC payload request.\n * The server sends X-Timber-Error header and a JSON body instead of a\n * broken RSC stream for any RenderError (4xx or 5xx). Caught in\n * navigate() to trigger a hard navigation so the server can render\n * the error page as HTML.\n *\n * See design/10-error-handling.md §\"Error Page Rendering for Client Navigation\"\n */\nexport class ServerErrorResponse extends Error {\n readonly status: number;\n readonly url: string;\n constructor(status: number, url: string) {\n super(`Server error ${status} during navigation to ${url}`);\n this.status = status;\n this.url = url;\n }\n}\n\n/**\n * Thrown when the RSC fetch response has a Content-Type that is not\n * text/x-component — e.g., a static asset (image, CSS, JS) served\n * for a same-origin URL that isn't a route. The response body is\n * cancelled immediately (headers-only cost). Caught in navigate()\n * to trigger a hard navigation; caught in prefetch() to store a\n * negative cache entry so click hard-navigates without a second fetch.\n *\n * See TIM-1231.\n */\nexport class NonRscResponse extends Error {\n readonly url: string;\n constructor(url: string) {\n super(`Non-RSC response for ${url}`);\n this.url = url;\n }\n}\n\n// ─── Stream Completion Tracking ───────────────────────────────────\n\n/**\n * Wrap a response body stream to track when it's fully consumed.\n * Returns a new body that passes all chunks through unchanged, plus\n * a `done` promise that resolves when the last chunk is read (or\n * rejects if the stream errors).\n *\n * createFromFetch's thenable resolves on shell arrival, but callers need to\n * know when the stream is fully decoded: that is what `navigateTransition`'s\n * returned promise means, and what the router's pending store, the Navigation\n * API deferred and `<Link>`'s `isPending` are all timed against.\n */\nfunction trackStreamCompletion(body: ReadableStream<Uint8Array>): {\n body: ReadableStream<Uint8Array>;\n done: Promise<void>;\n} {\n let resolveDone!: () => void;\n let rejectDone!: (e: unknown) => void;\n const done = new Promise<void>((res, rej) => {\n resolveDone = res;\n rejectDone = rej;\n });\n\n const reader = body.getReader();\n const tracked = new ReadableStream<Uint8Array>({\n async pull(controller) {\n try {\n const result = await reader.read();\n if (result.done) {\n controller.close();\n resolveDone();\n } else {\n controller.enqueue(result.value);\n }\n } catch (error) {\n controller.error(error);\n rejectDone(error);\n }\n },\n cancel(reason) {\n reader.cancel(reason);\n resolveDone();\n },\n });\n\n return { body: tracked, done };\n}\n\n// ─── Fetch ───────────────────────────────────────────────────────\n\n/**\n * Fetch an RSC payload from the server. If a decodeRsc function is provided,\n * the response is decoded into a React element tree via createFromFetch.\n * Otherwise, the raw response text is returned (test mode).\n */\nexport async function fetchRscPayload(\n url: string,\n deps: RouterDeps,\n stateTree?: { segments: string[] },\n currentUrl?: string,\n signal?: AbortSignal\n): Promise<FetchResult> {\n // In static mode, fetch the pre-generated _rsc/*.rsc file directly\n // instead of the route URL with Accept headers. Static hosts ignore\n // Accept headers, so the route URL would return HTML.\n const staticTarget = staticMode ? toStaticRscUrl(url) : null;\n const fetchTarget = staticTarget ? staticTarget.url : url;\n // Static hosts resolve neither partial payloads nor intercepting routes,\n // so neither the state tree nor the source URL is sent — and neither may\n // fragment the cache key of a file that does not vary on them.\n const headers = staticMode\n ? buildRscHeaders(undefined, undefined)\n : buildRscHeaders(stateTree, currentUrl);\n // Skip the cache key when the manifest supplies a content-hashed\n // URL — the hash itself guarantees freshness, and the param would defeat\n // the immutable cache headers on /_rsc/* (TIM-1254).\n const rscUrl = staticTarget?.hashed ? fetchTarget : await appendRscParam(fetchTarget, headers);\n // Deriving the cache key is async (SubtleCrypto), so the fetch is no\n // longer issued in the same tick as the call. A navigation superseded\n // during that window would otherwise still hit the network with an\n // already-aborted signal.\n signal?.throwIfAborted();\n if (deps.decodeRsc) {\n // Production path: use createFromFetch for streaming RSC decoding.\n // createFromFetch takes a Promise<Response> and progressively parses\n // the RSC Flight stream as chunks arrive.\n //\n // Intercept the response to read segment metadata before createFromFetch\n // consumes the body. Reading headers does NOT consume the body stream.\n const fetchPromise = deps.fetch(rscUrl, { headers, redirect: 'manual', signal });\n let segmentInfo: SegmentInfo[] | null = null;\n let skippedSegments: string[] | null = null;\n // Track when the full RSC body stream is consumed (not just shell).\n // Initialized to resolved for bodyless responses; overwritten when\n // the response has a body.\n let streamDone: Promise<void> = Promise.resolve();\n\n const wrappedPromise = fetchPromise.then((response) => {\n // Version skew detection (TIM-446): if the server signals a reload,\n // throw VersionSkewError so the caller (router navigate) can trigger\n // a full page reload.\n if (checkReloadSignal(response)) {\n throw new VersionSkewError();\n }\n // Detect server-side redirects. The server returns 204 + X-Timber-Redirect\n // for RSC payload requests instead of a raw 302, because fetch with\n // redirect: \"manual\" turns 302s into opaque redirects (status 0, null body)\n // which crashes createFromFetch when it tries to read the body stream.\n const redirectLocation =\n response.headers.get('X-Timber-Redirect') ||\n (response.status >= 300 && response.status < 400 ? response.headers.get('Location') : null);\n if (redirectLocation) {\n throw new RedirectError(redirectLocation);\n }\n // Detect server error responses. The server returns X-Timber-Error header\n // with a JSON body instead of a broken RSC stream for any RenderError\n // (4xx or 5xx). Hard-navigate so the server renders the error page as HTML.\n // See design/10-error-handling.md §\"Error Page Rendering for Client Navigation\"\n if (response.headers.get(TIMBER_ERROR_HEADER) === '1') {\n throw new ServerErrorResponse(response.status, url);\n }\n // Content-Type guard: reject non-RSC responses before createFromFetch\n // tries to parse the body as Flight data.\n // In static mode, accept octet-stream/text/plain/absent content-type\n // (static hosts serve .rsc files with these), but still reject 404s\n // and text/html (missing .rsc file → host returns 404 page or SPA\n // HTML fallback). See TIM-1231, TIM-1243, TIM-1247.\n if (staticMode) {\n const contentType = response.headers.get('content-type');\n if (\n !response.ok ||\n (contentType && contentType.split(';')[0].trim().toLowerCase() === 'text/html')\n ) {\n response.body?.cancel();\n throw new NonRscResponse(url);\n }\n } else {\n if (!isRscContentType(response.headers.get('content-type'))) {\n response.body?.cancel();\n throw new NonRscResponse(url);\n }\n }\n // Metadata (<title>/<meta>/<link>) now rides the RSC Flight payload\n // as React elements — React 19 Float handles them. See TIM-1151.\n segmentInfo = extractSegmentInfo(response);\n skippedSegments = extractSkippedSegments(response);\n\n // Wrap the body to track full stream consumption. createFromFetch's\n // thenable resolves when the root model (shell) arrives, but we need\n // to know when ALL chunks are read — that is what the navigation\n // promise resolving means, and what keeps the pending store set.\n if (response.body) {\n const tracked = trackStreamCompletion(response.body);\n streamDone = tracked.done;\n streamDone.catch(() => {}); // prevent unhandled rejection\n return new Response(tracked.body, {\n headers: response.headers,\n status: response.status,\n });\n }\n return response;\n });\n // Await headers so segmentInfo is populated.\n await wrappedPromise;\n // Start decoding but do NOT await — return the in-progress thenable.\n // React can render a Flight thenable directly: it suspends on unresolved\n // parts and progressively renders as chunks arrive, spreading work across\n // frames instead of blocking the main thread in one burst.\n const root = deps.decodeRsc(wrappedPromise);\n // One split, one root: the tree and the params it was rendered with come\n // apart here and nowhere else, so nothing downstream has to keep them\n // paired by hand.\n const { tree: payload, params } = splitPayloadRoot(root);\n // Combine stream completion with payload error propagation.\n // streamDone marks the end of the RSC stream, which is what the\n // navigation promise resolves on. payloadError propagates\n // decode failures (stale client references, Flight parse errors)\n // so the router's catch block can trigger recovery (stale reload).\n const payloadError = new Promise<void>((_, reject) => {\n Promise.resolve(root).then(() => {}, reject);\n });\n payloadError.catch(() => {});\n const decodePromise = Promise.race([streamDone, payloadError]);\n // Mark handled, like payloadError above. The same rejection reaches the\n // router through `payload`, and a navigation that recovers by hard\n // navigating (version skew, non-RSC response) hangs deliberately and never\n // awaits this one — leaving it to surface as an unhandled rejection.\n decodePromise.catch(() => {});\n return {\n payload,\n params,\n decodePromise,\n segmentInfo,\n skippedSegments,\n };\n }\n // Test/fallback path: return raw text\n const response = await deps.fetch(rscUrl, { headers, redirect: 'manual', signal });\n // Check for redirect in test path too\n if (response.status >= 300 && response.status < 400) {\n const location = response.headers.get('Location');\n if (location) {\n throw new RedirectError(location);\n }\n }\n // Server error guard (same as production path above).\n if (response.headers.get(TIMBER_ERROR_HEADER) === '1') {\n throw new ServerErrorResponse(response.status, url);\n }\n // Content-Type guard (same as production path above). See TIM-1231, TIM-1243, TIM-1247.\n if (staticMode) {\n const fallbackContentType = response.headers.get('content-type');\n if (\n !response.ok ||\n (fallbackContentType &&\n fallbackContentType.split(';')[0].trim().toLowerCase() === 'text/html')\n ) {\n response.body?.cancel();\n throw new NonRscResponse(url);\n }\n } else {\n if (!isRscContentType(response.headers.get('content-type'))) {\n response.body?.cancel();\n throw new NonRscResponse(url);\n }\n }\n // Test/fallback path: raw text carries no payload root, so no params.\n return {\n payload: await response.text(),\n params: readPublishedParams(undefined),\n decodePromise: null,\n segmentInfo: extractSegmentInfo(response),\n skippedSegments: extractSkippedSegments(response),\n };\n}\n","/**\n * Page unload detection — suppresses spurious errors during page refresh/navigation.\n *\n * When the user refreshes the page or navigates away while React is still\n * streaming Suspense content, the aborted connection causes streaming errors.\n * These are not application errors — they're a side effect of the browser\n * tearing down the connection. This module tracks whether the page is being\n * unloaded so error boundaries and error handlers can suppress abort-related\n * errors during the unload window.\n *\n * This is the single owner of \"is this document going away\". Anything that\n * needs the answer imports `isPageUnloading()` — an unload flag maintained a\n * second time somewhere else is a flag that will drift out of agreement with\n * this one (TIM-1260).\n *\n * Mutable state is delegated to client/state.ts for singleton guarantees.\n * See design/18-build-system.md §\"Singleton State Registry\"\n *\n * See design/10-error-handling.md §\"Known limitation: deny() inside Suspense and hydration\"\n */\n\nimport { unloading, _setUnloading } from './state.ts';\n\n/**\n * Departure is *committed*: `pagehide` has fired, or this document has called\n * reload() on itself. `beforeunload` alone doesn't qualify — it is\n * speculative, and the user can still cancel out of it.\n */\nlet committed = false;\n\nif (typeof window !== 'undefined') {\n window.addEventListener('beforeunload', () => {\n _setUnloading(true);\n\n // An app with an unsaved-changes prompt can have this cancelled. Nothing\n // fires to say so, so the only signal is that the page is still running\n // afterwards — and timers are blocked while the modal is up, so this task\n // cannot run until the user has chosen. If they left, `pagehide` has\n // already committed and this is a no-op.\n setTimeout(() => {\n if (!committed) _setUnloading(false);\n }, 0);\n });\n\n // pagehide is the committed-departure event, and fires for both navigations\n // and bfcache entry. Safari in particular may skip beforeunload entirely.\n window.addEventListener('pagehide', () => {\n committed = true;\n _setUnloading(true);\n });\n\n window.addEventListener('pageshow', (event) => {\n // A bfcache restore un-does the teardown `pagehide` announced: the same\n // document is alive again, with this module's state intact. Without the\n // reset, every consumer would go on suppressing errors on a live page.\n if (event.persisted) {\n committed = false;\n _setUnloading(false);\n }\n });\n}\n\n/**\n * Returns true if the page is currently being unloaded (user refreshed\n * or navigated away). Error boundaries should suppress errors in this state.\n */\nexport function isPageUnloading(): boolean {\n return unloading;\n}\n","/**\n * Stale Client — knowing when this bundle has been superseded.\n *\n * Layer 1 of version skew handling (see design/33-version-skew.md) is\n * proactive: the server compares `X-Timber-Deployment-Id` against the current\n * build and answers a stale client with `X-Timber-Reload: 1`. That covers\n * every RSC navigation and every server action — as long as there is a server\n * in the request path.\n *\n * This module is the reactive half, plus the state both halves feed. It\n * recognizes the two error shapes a stale client produces on its own:\n *\n * 1. **Stale client references** — React's Flight client throws\n * \"Could not find the module …\" when an RSC payload names a client module\n * ID that isn't in this bundle's manifest.\n * 2. **Chunk load failures** — a content-hashed chunk that no longer exists on\n * the CDN fails to `import()`. Every engine words this differently.\n *\n * It matters most for `output: 'static'`, where there is no server to send the\n * reload header and this is the *only* skew detection that can run.\n *\n * **Nothing here reloads the page.** A stale client is recorded, not acted on;\n * the router turns the next user-initiated navigation into a full document\n * load. See design/33-version-skew.md §\"Recovery on the next navigation\" for\n * why — a self-issued `location.reload()` cannot tell a recovery from a bounce\n * between two live deployments, and mid-rollout it produces exactly that loop.\n *\n * See TIM-1260, TIM-1275.\n */\n\n// The only import: the shared unload state. Kept minimal because this module\n// is reached from the error boundary, a `'use client'` component evaluated in\n// the SSR graph too — a dependency on the client fetch layer would drag that\n// graph along with it.\nimport { isPageUnloading } from './unload-guard.ts';\n\n// ─── Error Shapes ────────────────────────────────────────────────\n\n/**\n * Substrings that identify a failed dynamic import, lowercased for\n * case-insensitive matching. Engines and bundlers each word this\n * differently and none of them expose a stable error code.\n */\nconst CHUNK_LOAD_PATTERNS = [\n // Chromium\n 'failed to fetch dynamically imported module',\n // Firefox\n 'error loading dynamically imported module',\n // Safari\n 'importing a module script failed',\n // Vite's preload helper, for the stylesheet of a lazily-imported chunk\n 'unable to preload css for',\n];\n\n/** Substring React's Flight client uses when a client module ID is unknown. */\nconst STALE_REFERENCE_PATTERN = 'could not find the module';\n\n/**\n * Pull a comparable message out of an unknown thrown value. Rejections cross\n * enough boundaries (event handlers, `unhandledrejection`, Vite's preload\n * event) that the value is not reliably an `Error`.\n */\nfunction messageOf(error: unknown): string {\n if (typeof error === 'string') return error.toLowerCase();\n if (error instanceof Error) return error.message.toLowerCase();\n if (error && typeof error === 'object' && 'message' in error) {\n const message = (error as { message: unknown }).message;\n if (typeof message === 'string') return message.toLowerCase();\n }\n return '';\n}\n\n/**\n * True when the error is React's \"client reference not in this manifest\"\n * failure — the payload was produced by a build this bundle doesn't match.\n */\nexport function isStaleClientReference(error: unknown): boolean {\n return messageOf(error).includes(STALE_REFERENCE_PATTERN);\n}\n\n/**\n * True when the error is a failed dynamic import or CSS preload — the\n * content-hashed chunk this bundle asks for is gone from the CDN.\n */\nexport function isChunkLoadError(error: unknown): boolean {\n if (error instanceof Error && error.name === 'ChunkLoadError') return true;\n const message = messageOf(error);\n return CHUNK_LOAD_PATTERNS.some((pattern) => message.includes(pattern));\n}\n\n// ─── Staleness ───────────────────────────────────────────────────\n\n/**\n * True once this client is known to be running against a superseded\n * deployment. One-way: a document that has gone stale cannot come back, since\n * the only way to get fresh code is to load a new document.\n *\n * In-memory on purpose. Persisting it would be wrong — the next document gets\n * whatever build the server hands it, and if that one is stale too it will\n * find out the same way.\n */\nlet stale = false;\n\n/**\n * True once {@link initStaleClient} has seen a deployment ID — i.e. this is a\n * production build. False in dev, where nothing is ever stale: HMR owns code\n * updates, and a failed dynamic import is a real error the developer needs to\n * see rather than something a page load fixes.\n */\nlet deployed = false;\n\n/** Whether this client has been superseded by a newer deployment. */\nexport function isClientStale(): boolean {\n return stale;\n}\n\n/**\n * Record that the origin has told us this client is stale — the `X-Timber-Reload`\n * header on an RSC or action response. Authoritative, so it is not gated on\n * anything: a dev server never sends the header in the first place.\n */\nexport function markClientStale(): void {\n stale = true;\n}\n\n/**\n * Record staleness if `error` is one of the shapes a superseded bundle\n * produces. Returns whether it did, so a caller mid-navigation can decide to\n * hard-navigate instead of surfacing the error.\n *\n * Inference, unlike {@link markClientStale}, so it is gated on being a\n * production build. This is the single place that gate lives — call sites\n * cannot see each other, and \"remember to gate the next one too\" is not a\n * guarantee.\n */\nexport function markStaleFromError(error: unknown): boolean {\n if (!deployed) return false;\n\n // A document being torn down aborts its in-flight dynamic imports, and an\n // aborted import reads exactly like a chunk the CDN has dropped. That is not\n // evidence of staleness, and the bit outlives the teardown that produced it:\n // a document entering the back-forward cache is frozen, not destroyed, so a\n // false positive recorded on the way out would cost the *restored* page its\n // client-side navigation for good. `unload-guard.ts` clears its own flag on\n // a persisted `pageshow`; this one is one-way by design, so it has to not be\n // set in the first place.\n if (isPageUnloading()) return false;\n\n if (!isStaleClientReference(error) && !isChunkLoadError(error)) return false;\n stale = true;\n return true;\n}\n\n// ─── Initialization ──────────────────────────────────────────────\n\n/**\n * The underlying error Vite attaches to a `vite:preloadError` event.\n *\n * Narrowed at runtime rather than cast: the property is Vite's, not ours, and\n * a shape change should fall back to matching the event itself rather than\n * silently reading `undefined` forever.\n */\nfunction preloadErrorPayload(event: Event): unknown {\n return 'payload' in event ? (event as Event & { payload: unknown }).payload : event;\n}\n\n/**\n * Arm the module, and register the listeners that catch stale-client errors\n * never passing through the router: a lazily imported client component, a\n * preloaded stylesheet, anything that rejects outside a navigation.\n *\n * No-op when `deploymentId` is null — that is dev. Called from the browser\n * entry before hydration, so it precedes any render or navigation that could\n * throw.\n */\nexport function initStaleClient(deploymentId: string | null): void {\n if (!deploymentId) return;\n deployed = true;\n\n // Vite dispatches this for failed chunk/CSS preloads. Note what is *not*\n // here: `preventDefault()`. Recording staleness is not recovering from this\n // error — the page is still broken until the user navigates — so Vite must\n // go on rethrowing it rather than have it silently swallowed.\n window.addEventListener('vite:preloadError', (event) => {\n markStaleFromError(preloadErrorPayload(event));\n });\n\n window.addEventListener('unhandledrejection', (event: PromiseRejectionEvent) => {\n markStaleFromError(event.reason);\n });\n}\n","/**\n * Router Skew Recording — turning a rejection into \"this bundle is superseded\".\n *\n * Detection lives in `stale-client.ts`; this is the one place the router (and\n * anything else holding an RSC rejection) turns an error into that verdict.\n * Recovery is not here: every path that gives up on client-side navigation —\n * skew or not — leaves through `createSpaExits()` in `router-effects.ts`.\n *\n * See design/33-version-skew.md §\"Recovery on the next navigation\"\n */\n\nimport { VersionSkewError } from './rsc-fetch.ts';\nimport { markClientStale, markStaleFromError } from './stale-client.ts';\n\n/**\n * Record a version skew if `error` is one, and report whether it was.\n *\n * `VersionSkewError` is the origin saying so outright; the reactive shapes are\n * inferred from the error text and are a no-op in dev.\n *\n * Closes over nothing — anywhere an RSC payload can fail needs it, including\n * places that have no router instance to hand.\n */\nexport function recordSkew(error: unknown): boolean {\n if (error instanceof VersionSkewError) {\n markClientStale();\n return true;\n }\n return markStaleFromError(error);\n}\n","/**\n * Router Effects — full-page navigation and post-paint scroll.\n *\n * The two things the router does *to the document* rather than to its own\n * state: leaving the SPA entirely, and moving the scroll position once React\n * has committed. Extracted from `router.ts` to keep that file focused on\n * navigation state (see design/18-build-system.md §\"No file >500 lines\").\n *\n * See design/19-client-navigation.md §\"Scroll Restoration\"\n */\n\nimport { setHardNavigating } from './navigation-root.tsx';\nimport type { RenderOwner } from './navigation-transition.ts';\nimport { RedirectError, ServerErrorResponse, NonRscResponse } from './rsc-fetch.ts';\nimport { recordSkew } from './router-skew.ts';\n\n/** The subset of `RouterDeps` the post-paint scroll effects need. */\nexport interface ScrollEffectDeps {\n scrollTo: (x: number, y: number) => void;\n afterPaint?: (callback: () => void) => void;\n scrollToHash?: (hash: string) => boolean;\n}\n\nexport interface ScrollEffects {\n /**\n * Schedule scroll restoration after the next paint and fire the\n * scroll-restored event. Used by navigate, popstate, and refresh.\n */\n restoreScrollAfterPaint: (scrollY: number) => void;\n /**\n * Scroll to the element matching the URL #fragment after paint, falling\n * back to scroll-to-top when no element matches (matching full-page-load\n * behavior for an unknown fragment). Used by forward navigation to a\n * hash-bearing URL (TIM-1035).\n */\n scrollToHashAfterPaint: (hash: string) => void;\n}\n\nexport function createScrollEffects(deps: ScrollEffectDeps): ScrollEffects {\n /** Run a callback after the next paint (after React commit). */\n function afterPaint(callback: () => void): void {\n if (deps.afterPaint) {\n deps.afterPaint(callback);\n } else {\n callback();\n }\n }\n\n function restoreScrollAfterPaint(scrollY: number): void {\n afterPaint(() => {\n deps.scrollTo(0, scrollY);\n window.dispatchEvent(new Event('timber:scroll-restored'));\n });\n }\n\n function scrollToHashAfterPaint(hash: string): void {\n afterPaint(() => {\n if (deps.scrollToHash?.(hash) !== true) {\n deps.scrollTo(0, 0);\n }\n window.dispatchEvent(new Event('timber:scroll-restored'));\n });\n }\n\n return { restoreScrollAfterPaint, scrollToHashAfterPaint };\n}\n\n/**\n * Leave the SPA and never come back.\n *\n * Every router path that gives up on client-side navigation does the same\n * three things: flag the hard navigation so the Navigation API and React stop\n * acting on a dying document, perform the document load, then block forever so\n * the caller cannot carry on rendering into it. The returned promise is\n * deliberately unresolvable — `location` assignment does not stop this turn of\n * the event loop.\n *\n * Callers: server error, non-RSC response, and every version skew path\n * ([33-version-skew.md](../../../../design/33-version-skew.md)).\n */\nexport function leaveSpa(url: string, fromUrl: string): Promise<never> {\n setHardNavigating(true);\n hardNavigate(url, fromUrl);\n return new Promise<never>(() => {});\n}\n\nexport interface SpaExitDeps {\n /** The owner that holds the router right now, or null when idle. */\n currentOwner: () => RenderOwner | null;\n /** Supersede whatever is in flight and take ownership. */\n supersede: () => void;\n}\n\nexport interface SpaExits {\n /**\n * Leave the SPA — but only if `owner` still owns the router. Every\n * failure a navigation can end on (a server error, a non-RSC response, a\n * version skew) can surface long after the user has clicked something else:\n * a fetch that was never cancellable, a Flight thenable that rejects late.\n * Hard navigating to the abandoned URL would undo that click, so a\n * navigation that no longer owns the router does nothing and lets the one\n * that does drive. Never resolves when it does navigate.\n *\n * Must be called from inside the `runNavigation()` callback. That function's\n * `finally` clears the current controller, so an ownership check downstream\n * of it compares against `null` and never leaves at all.\n */\n leaveSpaIfOwned: (owner: RenderOwner, url: string, fromUrl: string) => Promise<void>;\n\n /**\n * Leave the SPA from a path that never entered `runNavigation()` — the\n * stale-client short circuits, which decide before any fetch is attempted.\n *\n * Supersedes first. Nothing else on those paths takes ownership, so without\n * it an in-flight navigation keeps the router: when its fetch or decode\n * fails a moment later it still passes its own ownership check and hard\n * navigates to *its* URL, overwriting the document load started here.\n */\n leaveSpaSuperseding: (url: string, fromUrl: string) => Promise<never>;\n}\n\n/**\n * The two ownership-aware ways out of the SPA.\n *\n * Both wrap {@link leaveSpa} with the question \"is this navigation still the\n * one the user is waiting for?\" — asked before leaving on a failure, answered\n * by force on a path that decided before any navigation began. They live\n * together because every caller of `leaveSpa()` needs one or the other, and\n * an unguarded call is the bug (TIM-1275, TIM-1276).\n */\nexport function createSpaExits({ currentOwner, supersede }: SpaExitDeps): SpaExits {\n async function leaveSpaIfOwned(owner: RenderOwner, url: string, fromUrl: string): Promise<void> {\n if (currentOwner() !== owner) return;\n await leaveSpa(url, fromUrl);\n }\n\n function leaveSpaSuperseding(url: string, fromUrl: string): Promise<never> {\n supersede();\n return leaveSpa(url, fromUrl);\n }\n\n return { leaveSpaIfOwned, leaveSpaSuperseding };\n}\n\nexport interface NavigationRecoveryDeps {\n /** The owner that holds the router right now, or null when idle. */\n currentOwner: () => RenderOwner | null;\n leaveSpaIfOwned: SpaExits['leaveSpaIfOwned'];\n /** Replace the current entry with `url` — the router's own `navigate()`. */\n navigate: (url: string) => Promise<void>;\n}\n\n/**\n * Answer the failures an RSC fetch can end on, or report that this one is not\n * ours to answer.\n *\n * Returns `true` when the error was handled; the caller rethrows on `false`.\n * Handling a failure by leaving the SPA never resolves at all — the document\n * is going away and the caller must not carry on rendering into it.\n *\n * There is exactly one family of these and one response to it, so it lives in\n * one function rather than once per fetch path. `navigate()`, `refresh()` and\n * an uncached traversal all reach it: the latter two used to rethrow instead,\n * and since nobody awaits a traversal that meant pressing Back on a 500 left\n * the user on the old document with an unhandled rejection (TIM-1277).\n *\n * Order matters in exactly one way: `recordSkew()` ends in error-*text*\n * matching, so it goes last and can never pre-empt a framework control-flow\n * signal whose message happens to read like a stale bundle. The class checks\n * above it are mutually exclusive, so their order is free.\n *\n * Must be called from inside the `runNavigation()` callback — see\n * {@link SpaExits.leaveSpaIfOwned}.\n */\nexport function createNavigationRecovery({\n currentOwner,\n leaveSpaIfOwned,\n navigate,\n}: NavigationRecoveryDeps) {\n return async function recoverFromNavigationError(\n error: unknown,\n owner: RenderOwner,\n url: string,\n fromUrl: string\n ): Promise<boolean> {\n if (error instanceof RedirectError) {\n // Same ownership rule as leaving the SPA: a superseded navigation must\n // not steer the document to the destination it was abandoned for.\n if (currentOwner() !== owner) return true;\n await navigate(error.redirectUrl);\n return true;\n }\n // A server error, a non-RSC response and a version skew all end the same\n // way: hand `url` back to the server as a document load, so it can render\n // the outcome as HTML (design/10-error-handling.md §\"Error Page Rendering\n // for Client Navigation\").\n if (\n error instanceof ServerErrorResponse ||\n error instanceof NonRscResponse ||\n recordSkew(error)\n ) {\n await leaveSpaIfOwned(owner, url, fromUrl);\n return true;\n }\n return false;\n };\n}\n\n/**\n * Perform the full document load.\n *\n * `fromUrl` is the URL the navigation departed from, passed explicitly\n * because the address bar may have already been updated.\n *\n * When the target differs from the departure point only by #fragment,\n * assigning `href` is a hash change, not a load — so assign first (to update\n * the address bar) and then reload (TIM-1235).\n */\nexport function hardNavigate(url: string, fromUrl: string): void {\n const current = new URL(fromUrl, window.location.origin);\n const target = new URL(url, window.location.origin);\n if (target.pathname === current.pathname && target.search === current.search) {\n window.location.href = url;\n window.location.reload();\n } else {\n window.location.href = url;\n }\n}\n","'use client';\n\n/**\n * Framework-injected React error boundary.\n *\n * Catches errors thrown by children and renders a fallback component\n * with the appropriate props based on error type:\n * - DenySignal (4xx) → { status, data }\n * - RenderError (5xx) → { error, digest, reset }\n * - Unhandled error → { error, digest: null, reset }\n *\n * The `status` prop controls which errors this boundary catches:\n * - Specific code (e.g. 403) → only that status\n * - Category (400) → any 4xx\n * - Category (500) → any 5xx\n * - Omitted → catches everything (error.tsx behavior)\n *\n * See design/10-error-handling.md §\"Status-Code Files\"\n */\n\nimport { Component, createContext, createElement, useContext, type ReactNode } from 'react';\nimport { getSsrData } from './ssr-data.ts';\nimport { getRouterOrNull } from './router-ref.ts';\nimport { markStaleFromError } from './stale-client.ts';\nimport { StatusPageMarker } from './status-page-marker.tsx';\nimport { leaveSpa } from './router-effects.ts';\n// Page unload detection: when the page is being torn down, error boundaries\n// suppress activation — the error is from the aborted connection, not the\n// application. unload-guard.ts is the single owner of that state; this module\n// used to keep its own copy, which drifted (TIM-1260).\nimport { isPageUnloading } from './unload-guard.ts';\n\n// ─── Ancestry Context ────────────────────────────────────────────────────────\n\n/**\n * Tracks the chain of segment keys from root to the current boundary.\n * Each keyed `TimberErrorBoundary` appends its `segmentKey`; unkeyed\n * slot boundaries pass the parent value through. The claim rule uses\n * this to find the first *reachable* owner from the `owners` list,\n * eliminating the owner-below-throw-site escape. See TIM-1450.\n */\nconst DenyAncestryContext = createContext<readonly string[]>([]);\n\nexport { DenyAncestryContext };\n\n// ─── Digest Types ────────────────────────────────────────────────────────────\n\n/** Structured digest returned by RSC onError for DenySignal. */\ninterface DenyDigest {\n type: 'deny';\n status: number;\n data: unknown;\n /**\n * Ordered candidate owners — segment keys of segments with matching deny\n * pages, best match first. Present = addressed (a boundary should render\n * it). In-tree hoists produce a single-element list; late addressing\n * (TIM-1450) produces the full candidate list so the boundary can find the\n * first *reachable* owner via its ancestry context.\n */\n owners?: string[];\n}\n\n/** Structured digest returned by RSC onError for RenderError. */\ninterface RenderErrorDigest {\n type: 'render-error';\n code: string;\n data: unknown;\n status: number;\n}\n\n/** Structured digest returned by RSC onError for RedirectSignal. */\ninterface RedirectDigest {\n type: 'redirect';\n location: string;\n status: number;\n}\n\ntype ParsedDigest = DenyDigest | RenderErrorDigest | RedirectDigest;\n\n// ─── Props & State ───────────────────────────────────────────────────────────\n\nexport interface TimberErrorBoundaryProps {\n /** The component to render when an error is caught. */\n fallbackComponent?: (...args: unknown[]) => ReactNode;\n /**\n * Status code filter. If set, only catches errors matching this status.\n * 400 = any 4xx, 500 = any 5xx, specific number = exact match.\n */\n status?: number;\n /**\n * When true, this boundary wraps a parallel slot. Slot denials are\n * graceful degradation — they must NOT change the HTTP status code.\n */\n isSlotBoundary?: boolean;\n /**\n * Segment key of the segment that declared this boundary. Only meaningful\n * against a deny digest carrying `owners`. See the decline rule in\n * `render()` and TIM-1356.\n */\n segmentKey?: string;\n /**\n * Catch denies only; re-throw everything else. Set on the copy of a\n * segment's boundaries placed outside its layout, which gives a deny thrown\n * from that layout's own chrome a catcher keyed to the segment — without\n * changing where a 5xx from the same chrome goes. See TIM-1450.\n */\n denyOnly?: boolean;\n /**\n * The deny chain's terminal: the outermost framework boundary, which claims\n * any deny that streamed past every segment boundary and renders the\n * framework default page. Implies `denyOnly`. Only a deny thrown from a\n * layout's chrome with no status file at or above that layout gets here —\n * the segment copies (`denyOnly`) claim every other shape. See TIM-1450.\n */\n lastResort?: boolean;\n children: ReactNode;\n}\n\ninterface TimberErrorBoundaryState {\n hasError: boolean;\n error: Error | null;\n}\n\n// Module-level guard: the first boundary to catch a given redirect digest\n// schedules the navigation and marks it handled here. Subsequent renders\n// (React retries the error-boundary render path multiple times in dev, and\n// outer boundaries that also see the error) become no-ops, preventing\n// duplicate router.navigate() calls.\nconst _handledRedirects = new WeakSet<object>();\n\n// The same guard for the terminal's exit: React may render the fallback\n// path more than once for one error, and the document load must start once.\nconst _handledExits = new WeakSet<object>();\n\n// Companion guard keyed by location: client-render redirect() (the\n// next/navigation shim) throws a FRESH error instance on every render\n// attempt, and React replays the errored render — so the WeakSet alone\n// would schedule the navigation twice. While a navigation to a location\n// is pending, further redirect errors for the same location are no-ops.\nlet _pendingRedirectLocation: string | null = null;\n\n// ─── Component ───────────────────────────────────────────────────────────────\n\nexport class TimberErrorBoundary extends Component<\n TimberErrorBoundaryProps,\n TimberErrorBoundaryState\n> {\n constructor(props: TimberErrorBoundaryProps) {\n super(props);\n this.state = { hasError: false, error: null };\n }\n\n static getDerivedStateFromError(error: Error): TimberErrorBoundaryState {\n // Suppress error boundaries during page unload (refresh/navigate away).\n // The aborted connection causes React's streaming hydration to error,\n // but the page is about to be replaced — showing an error boundary\n // would be a jarring flash for the user.\n if (isPageUnloading()) {\n return { hasError: false, error: null };\n }\n\n return { hasError: true, error };\n }\n\n componentDidCatch(error: Error): void {\n // A stale bundle most often surfaces here rather than as a rejected\n // navigation: a client reference missing from this build's manifest, or\n // a lazily-imported chunk the CDN no longer has, thrown during render.\n //\n // Recording it is all that happens — this boundary still renders its\n // fallback, and the recovery is the user's next navigation, which the\n // router turns into a full document load. Reloading from here would be a\n // page load the user did not ask for, on a page that may be mid-rollout\n // and would come back just as stale (TIM-1275).\n markStaleFromError(error);\n }\n\n componentDidUpdate(prevProps: TimberErrorBoundaryProps): void {\n // Reset error state when children change (e.g. client-side navigation).\n // Without this, navigating from one error page to another keeps the\n // stale error — getDerivedStateFromError doesn't re-fire for new children.\n if (this.state.hasError && prevProps.children !== this.props.children) {\n this.setState({ hasError: false, error: null });\n }\n }\n\n /** Reset the error state so children re-render. */\n private reset = () => {\n this.setState({ hasError: false, error: null });\n };\n\n static override contextType = DenyAncestryContext;\n declare context: readonly string[];\n\n private get childAncestry(): readonly string[] {\n const parent = this.context ?? [];\n const key = this.props.segmentKey;\n // A segment has two keyed boundary sets (outside and inside its layout);\n // the inner one sees its own key already present and adds nothing.\n return key && !parent.includes(key) ? [...parent, key] : parent;\n }\n\n render(): ReactNode {\n if (!this.state.hasError || !this.state.error) {\n return createElement(\n DenyAncestryContext.Provider,\n { value: this.childAncestry },\n this.props.children\n );\n }\n\n const error = this.state.error;\n const parsed = parseDigest(error);\n\n // RedirectSignal handling splits by environment:\n //\n // - SSR (no window): re-throw so the Fizz shell fails and the server\n // pipeline catch block can produce a proper HTTP 3xx response. See\n // design/04-authorization.md.\n //\n // - Client (window exists): a RedirectSignal that leaked into the RSC\n // Flight payload during client navigation would otherwise bubble past\n // every error boundary to `window.onerror`, which in dev triggers the\n // Vite error overlay (false positive — navigation itself works).\n // Instead, ask the router to perform an SPA navigation to the target\n // and render nothing while it takes over. The router's own catch path\n // handles cross-origin / hard navigations. See TIM-838.\n if (parsed?.type === 'redirect') {\n if (typeof window === 'undefined') {\n throw error;\n }\n // Dedupe: React may render the error-boundary fallback path more\n // than once for a single caught error (dev-mode double render, and\n // additional boundaries above us in the tree). Only schedule the\n // navigation on the first render that sees this error instance.\n const alreadyHandled =\n _handledRedirects.has(error) || _pendingRedirectLocation === parsed.location;\n if (!alreadyHandled) {\n _handledRedirects.add(error);\n const router = getRouterOrNull();\n if (router) {\n _pendingRedirectLocation = parsed.location;\n // Schedule the navigation outside the render phase to avoid\n // setState-during-render warnings from router state updates.\n queueMicrotask(() => {\n router\n .navigate(parsed.location, { replace: true })\n .catch(() => {\n // Fall back to a hard navigation if the soft navigation fails.\n window.location.href = parsed.location;\n })\n .finally(() => {\n _pendingRedirectLocation = null;\n });\n });\n } else {\n // No router available (shouldn't happen post-bootstrap) — fall\n // back to a hard navigation so the user still ends up at the\n // target.\n window.location.href = parsed.location;\n }\n }\n // Nothing to show, but still a subtree replaced: a layout that\n // redirected after I/O never mounted its children, and the commit that\n // renders this empty spot must not teach the cache otherwise — or the\n // redirect's own navigation would advertise those layouts as mounted\n // and the server would skip into an outlet the client does not have.\n return this.markStatusPage(null);\n }\n\n // Owner-addressed denies: the digest carries an ordered `owners` list\n // of segment keys with matching deny pages. This boundary claims the\n // deny only when it is the first *reachable* owner — i.e. its\n // segmentKey appears in `owners` AND in its ancestry (the chain from\n // root to here). Unreachable owners (below the throw site) are skipped,\n // and the first ancestor that IS reachable claims. This eliminates the\n // owner-below-throw-site escape (TIM-1450).\n //\n // Unkeyed boundaries (slots, or boundaries without deny chains) never\n // claim an addressed deny. See TIM-1356.\n if (parsed?.type === 'deny' && Array.isArray(parsed.owners) && parsed.owners.length > 0) {\n // Slot boundaries always claim addressed denies — graceful degradation\n // within the slot is correct regardless of which segment the owner\n // chain resolved to. Without this, the deny escapes the slot and\n // becomes a page-level 4xx. See TIM-1450.\n if (!this.props.lastResort && !this.props.isSlotBoundary) {\n const reachable = parsed.owners.find((k: string) => this.childAncestry.includes(k));\n if (!this.props.segmentKey || reachable !== this.props.segmentKey) {\n throw error;\n }\n }\n }\n // Deny-only boundaries (the outer segment copies) re-throw everything\n // else so a crash in a layout's chrome still reaches the documented 5xx\n // flow instead of a status page for an unrelated error.\n if (this.props.denyOnly && !this.props.lastResort && parsed?.type !== 'deny') {\n throw error;\n }\n // The terminal is where that flow ends on the client. A non-deny row\n // nothing claimed — a RenderError or a plain crash on a route with no\n // matching `error.tsx` — has no page to render in-SPA; React would\n // unmount the root. So the terminal does what the router does for an\n // error envelope (design/10 §\"Error Page Rendering for Client\n // Navigation\"): leave the SPA, and let the document request render the\n // real error page with the real status. On the server it re-throws so\n // the Fizz shell fails and the pipeline serves that page directly. The\n // envelope still covers errors that land inside Flight's opening window;\n // this covers the ones that land after it, which used to be held for\n // (TIM-1495, TIM-1497).\n if (this.props.lastResort && parsed?.type !== 'deny') {\n if (typeof window === 'undefined') {\n throw error;\n }\n if (!_handledExits.has(error)) {\n _handledExits.add(error);\n const router = getRouterOrNull();\n const current = window.location.pathname + window.location.search;\n // The row arrived for the navigation in flight, whose URL is not on\n // the address bar yet; fall back to the current document.\n const target = router?.getPendingUrl() ?? current;\n queueMicrotask(() => {\n void leaveSpa(target, current);\n });\n }\n return null;\n }\n\n // If this boundary has a status filter, check whether the error matches.\n // Non-matching errors re-throw so an outer boundary can catch them.\n if (this.props.status != null) {\n const errorStatus = getErrorStatus(parsed, error);\n if (errorStatus == null || !statusMatches(this.props.status, errorStatus)) {\n // Re-throw: this boundary doesn't handle this error.\n throw error;\n }\n }\n\n // Report DenySignal handling so the pipeline skips the redundant\n // renderDenyPage() re-render.\n //\n // For segment boundaries: also set the HTTP status code from the\n // DenySignal so the Response has the correct 4xx status. This runs\n // synchronously during Fizz rendering, BEFORE onShellReady.\n //\n // For slot boundaries: set _denyHandledByBoundary but do NOT change\n // statusCode — slot denials are graceful degradation.\n //\n // See TIM-664, LOCAL-298.\n if (parsed?.type === 'deny') {\n const ssrData = getSsrData();\n if (ssrData?._navContext) {\n ssrData._navContext._denyHandledByBoundary = true;\n if (!this.props.isSlotBoundary) {\n ssrData._navContext.statusCode = parsed.status;\n }\n }\n }\n\n return this.markStatusPage(this.renderFallback(parsed, error));\n }\n\n /**\n * The status page this boundary shows for a row it claimed. Its shape is\n * the file's documented contract: TSX status files get `{ status,\n * data }`, `error.tsx` gets `{ error, digest, reset }` (plus\n * the deny extras for dual-shape implementations — without the error prop,\n * an error.tsx reading error.message crashes on deny fall-through, TIM-1081).\n */\n private renderFallback(parsed: ParsedDigest | null, error: Error): ReactNode {\n if (parsed?.type === 'deny') {\n if (this.props.status != null) {\n return createElement(this.props.fallbackComponent as never, {\n status: parsed.status,\n data: parsed.data,\n });\n }\n return createElement(this.props.fallbackComponent as never, {\n error: denyError(parsed.status, error),\n digest: null,\n reset: this.reset,\n status: parsed.status,\n data: parsed.data,\n });\n }\n\n // 5xx / RenderError / unhandled error\n const digest =\n parsed?.type === 'render-error' ? { code: parsed.code, data: parsed.data } : null;\n\n return createElement(this.props.fallbackComponent as never, {\n error,\n digest,\n reset: this.reset,\n });\n }\n\n /**\n * A status page (or, for a redirect row, nothing at all) replaced part of\n * the main route, and the segment cache must not learn a chain that never\n * mounted; the marker tells the navigation that commits this tree. A boundary that declined in `render()` re-threw\n * before getting here, so only the boundary that put a page on screen\n * speaks. A slot degrading leaves the page and its chain intact, so slot\n * boundaries stay silent. See `status-page-marker.tsx` (TIM-1495).\n */\n private markStatusPage(page: ReactNode): ReactNode {\n if (this.props.isSlotBoundary) return page;\n return createElement(StatusPageMarker, null, page);\n }\n}\n\n// ─── Helpers ─────────────────────────────────────────────────────────────────\n\n/**\n * Parse the structured digest from the error.\n * React sets `error.digest` from the string returned by RSC's onError.\n */\nfunction parseDigest(error: Error): ParsedDigest | null {\n const raw = (error as { digest?: string }).digest;\n if (typeof raw !== 'string') return null;\n try {\n const parsed = JSON.parse(raw);\n if (parsed && typeof parsed === 'object' && typeof parsed.type === 'string') {\n return parsed as ParsedDigest;\n }\n } catch (e) {\n console.debug('[timber] error digest is not valid JSON:', e instanceof Error ? e.message : e);\n }\n return null;\n}\n\n/**\n * The Error an `error.tsx` serving a deny should receive.\n *\n * The error that actually arrives here is React's, not the framework's: a\n * server-thrown error crossing Flight has its message and name replaced with\n * a generic redacted string in production, by design. An `error.tsx` written\n * per the docs branches on `error.message` — so handing it the redacted error\n * gives it nothing to branch on, while the in-tree path hands the same file a\n * `DenySignal` named error reading `Access denied with status <N>` (see\n * `renderDenyEntry`). This reconstructs that shape from the digest, which is\n * where the status already is.\n *\n * Derived from the status alone — no stack, no server data — so it is safe on\n * the client for the same reason the digest is. See design/13-security.md\n * §\"Errors don't leak\", TIM-1081.\n */\nfunction denyError(status: number, original: Error): Error {\n // A dev build may have preserved something more useful than the redaction.\n if (original.name === 'DenySignal') return original;\n const error = new Error(`Access denied with status ${status}`);\n error.name = 'DenySignal';\n return error;\n}\n\n/**\n * Extract the HTTP status code from a parsed digest or error message.\n * Falls back to message pattern matching for errors without a digest.\n */\nfunction getErrorStatus(parsed: ParsedDigest | null, error: Error): number | null {\n if (parsed?.type === 'deny') return parsed.status;\n if (parsed?.type === 'render-error') return parsed.status;\n if (parsed?.type === 'redirect') return parsed.status;\n\n // Fallback: parse DenySignal message pattern for errors that lost their digest\n const match = error.message.match(/^Access denied with status (\\d+)$/);\n if (match) return parseInt(match[1], 10);\n\n // Unhandled errors are implicitly 500\n return 500;\n}\n\n/**\n * Check whether an error's status matches the boundary's status filter.\n * Category markers (400, 500) match any status in that range.\n */\nfunction statusMatches(boundaryStatus: number, errorStatus: number): boolean {\n // Category catch-all: 400 matches any 4xx, 500 matches any 5xx\n if (boundaryStatus === 400) return errorStatus >= 400 && errorStatus <= 499;\n if (boundaryStatus === 500) return errorStatus >= 500 && errorStatus <= 599;\n // Exact match\n return boundaryStatus === errorStatus;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAyEA,IAAa,sBAAuC;CAAE,QAAQ,CAAC;CAAG,YAAY;AAAK;;AAGnF,SAAS,mBAAmB,OAA0C;CACpE,OACE,OAAO,UAAU,YACjB,UAAU,QACV,YAAY,SACZ,OAAQ,MAA0B,WAAW,YAC5C,MAA0B,WAAW;AAE1C;;;;;;;;AASA,SAAgB,oBAAoB,QAAkC;CACpE,IAAI,CAAC,mBAAmB,MAAM,GAAG,OAAO;CACxC,OAAO;EAAE,QAAQ,OAAO;EAAQ,YAAY,OAAO,cAAc;CAAK;AACxE;;;;;;;;AASA,SAAgB,gBAAgB,MAAwB;CACtD,IAAI,OAAO,SAAS,YAAY,SAAS,QAAQ,UAAU,QAAQ,mBAAmB,IAAI,GACxF,OAAQ,KAAwB;CAElC,OAAO;AACT;;;;;;;;AAyBA,SAAgB,iBAAiB,MAA6B;CAC5D,IACE,OAAO,SAAS,YAChB,SAAS,QACT,OAAQ,KAA8B,SAAS,YAE/C,OAAO;EAAE,MAAM,gBAAgB,IAAI;EAAG,QAAQ,oBAAoB,IAAI;CAAE;CAE1E,MAAM,UAAU;CAChB,MAAM,OAAO,QAAQ,QAAQ,OAAO,CAAC,CAAC,KAAK,eAAe;CAI1D,KAAK,YAAY,CAAC,CAAC;CAEnB,OAAO;EAAE;EAAM,QADA,QAAQ,QAAQ,OAAO,CAAC,CAAC,KAAK,2BAA2B,mBACzD;CAAO;AACxB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AC3DA,eAAe,eAAe,KAAa,SAAkD;CAC3F,MAAM,YAAY,IAAI,QAAQ,GAAG;CACjC,MAAM,iBAAiB,cAAc,KAAK,MAAM,IAAI,MAAM,GAAG,SAAS;CACtE,MAAM,MAAO,MAAM,YAAY,aAAa,OAAO,CAAC,KAAM,kBAAkB;CAE5E,OAAO,GAAG,iBADQ,eAAe,SAAS,GAAG,IAAI,MAAM,MAChB,cAAc,GAAG;AAC1D;;;;;;AASA,IAAI,qBAAoC;;;;;;;;;;;AAwBxC,IAAI,WAAW;;;;;;;AAoBf,IAAI,aAAa;;;;;;;AAgBjB,IAAI,cAAkC;AAEtC,SAAS,iBAAqC;CAC5C,IAAI,aAAa,OAAO;CACxB,IAAI,OAAO,WAAW,eAAgB,OAAe,yBACnD,cAAe,OAAe;CAEhC,OAAO;AACT;;;;;;;;;;;;;;;AAgCA,SAAS,eAAe,KAA8B;CACpD,MAAM,WAAW,iBAAiB,GAAG;CACrC,MAAM,YAAY,eAAe,QAAQ;CACzC,OAAO;EAAE,KAAK,aAAa,UAAU,aAAa,QAAQ;EAAG,QAAQ,cAAc;CAAK;AAC1F;;;;;;;;;;;AAYA,SAAgB,iBAAiB,KAAqB;CACpD,MAAM,YAAY,IAAI,QAAQ,GAAG;CACjC,MAAM,aAAa,IAAI,QAAQ,GAAG;CAClC,MAAM,UAAU,cAAc,KAAK,IAAI,SAAS;CAChD,MAAM,WAAW,eAAe,KAAK,IAAI,SAAS;CAClD,MAAM,MAAM,KAAK,IAAI,SAAS,QAAQ;CACtC,IAAI,WAAW,cAAc,UAAU,IAAI,MAAM,GAAG,GAAG,CAAC;CAExD,IAAI,SAAS,SAAS,KAAK,SAAS,SAAS,GAAG,GAC9C,WAAW,SAAS,MAAM,GAAG,EAAE;CAEjC,OAAO,eAAe,QAAQ;AAChC;;;;;AAMA,SAAS,cAAc,KAAsC;CAC3D,MAAM,WAAW,eAAe;CAChC,IAAI,CAAC,UAAU,OAAO;CACtB,IAAI,SAAS,MAAM,OAAO,SAAS;CACnC,IAAI;EACF,MAAM,UAAU,mBAAmB,GAAG;EACtC,IAAI,YAAY,OAAO,SAAS,UAAU,OAAO,SAAS;CAC5D,QAAQ,CAER;CACA,OAAO;AACT;;;;AAKA,SAAS,eAAe,KAA4B;CAClD,OAAO,cAAc,GAAG,CAAC,EAAE,OAAO;AACpC;;AAWA,IAAa,gBAAgB;;AAG7B,IAAa,uBAAuB;;;;;AAMpC,SAAgB,kBAAkB,UAA6B;CAC7D,OAAO,SAAS,QAAQ,IAAI,aAAa,MAAM;AACjD;;;;;;;;;;;;AAeA,SAAgB,gBACd,WACA,YACwB;CACxB,MAAM,UAAkC,EACtC,QAAQ,iBACV;CACA,IAAI,WACF,QAAQ,yBAAyB,KAAK,UAAU,SAAS;CAM3D,IAAI,YACF,QAAQ,kBAAkB;CAM5B,IAAI,oBACF,QAAQ,wBAAwB;CAElC,OAAO;AACT;;AAKA,SAAS,oBAAoB,YAAoB,KAAmB;CAClE,IAAA,QAAA,IAAA,aAA6B,cAAc;EACzC,MAAM,UAAU,IAAI,SAAS,MAAM,IAAI,MAAM,GAAG,GAAG,IAAI,MAAM;EAC7D,QAAQ,KACN,sBAAsB,WAAW,gHACgD,SACnF;CACF;AACF;;;;;;;;;AAUA,SAAgB,mBAAmB,UAA0C;CAC3E,MAAM,SAAS,SAAS,QAAQ,IAAI,mBAAmB;CACvD,IAAI,CAAC,QAAQ,OAAO;CACpB,IAAI;EACF,OAAO,KAAK,MAAM,MAAM;CAC1B,QAAQ;EACN,oBAAoB,qBAAqB,MAAM;EAC/C,OAAO;CACT;AACF;;;;;;;;;AAUA,SAAgB,uBAAuB,UAAqC;CAC1E,MAAM,SAAS,SAAS,QAAQ,IAAI,2BAA2B;CAC/D,IAAI,CAAC,QAAQ,OAAO;CACpB,IAAI;EACF,MAAM,SAAS,KAAK,MAAM,MAAM;EAChC,OAAO,MAAM,QAAQ,MAAM,IAAI,SAAS;CAC1C,QAAQ;EACN,oBAAoB,6BAA6B,MAAM;EACvD,OAAO;CACT;AACF;;;;;AAQA,IAAa,gBAAb,cAAmC,MAAM;CACvC;CACA,YAAY,KAAa;EACvB,MAAM,sBAAsB,KAAK;EACjC,KAAK,cAAc;CACrB;AACF;;;;;;AAOA,IAAa,mBAAb,cAAsC,MAAM;CAC1C,cAAc;EACZ,MAAM,oDAAoD;CAC5D;AACF;;;;;;;;;;AAWA,IAAa,sBAAb,cAAyC,MAAM;CAC7C;CACA;CACA,YAAY,QAAgB,KAAa;EACvC,MAAM,gBAAgB,OAAO,wBAAwB,KAAK;EAC1D,KAAK,SAAS;EACd,KAAK,MAAM;CACb;AACF;;;;;;;;;;;AAYA,IAAa,iBAAb,cAAoC,MAAM;CACxC;CACA,YAAY,KAAa;EACvB,MAAM,wBAAwB,KAAK;EACnC,KAAK,MAAM;CACb;AACF;;;;;;;;;;;;AAeA,SAAS,sBAAsB,MAG7B;CACA,IAAI;CACJ,IAAI;CACJ,MAAM,OAAO,IAAI,SAAe,KAAK,QAAQ;EAC3C,cAAc;EACd,aAAa;CACf,CAAC;CAED,MAAM,SAAS,KAAK,UAAU;CAsB9B,OAAO;EAAE,MAAM,IArBK,eAA2B;GAC7C,MAAM,KAAK,YAAY;IACrB,IAAI;KACF,MAAM,SAAS,MAAM,OAAO,KAAK;KACjC,IAAI,OAAO,MAAM;MACf,WAAW,MAAM;MACjB,YAAY;KACd,OACE,WAAW,QAAQ,OAAO,KAAK;IAEnC,SAAS,OAAO;KACd,WAAW,MAAM,KAAK;KACtB,WAAW,KAAK;IAClB;GACF;GACA,OAAO,QAAQ;IACb,OAAO,OAAO,MAAM;IACpB,YAAY;GACd;EACF,CAEe;EAAS;CAAK;AAC/B;;;;;;AASA,eAAsB,gBACpB,KACA,MACA,WACA,YACA,QACsB;CAItB,MAAM,eAAe,aAAa,eAAe,GAAG,IAAI;CACxD,MAAM,cAAc,eAAe,aAAa,MAAM;CAItD,MAAM,UAAU,aACZ,gBAAgB,KAAA,GAAW,KAAA,CAAS,IACpC,gBAAgB,WAAW,UAAU;CAIzC,MAAM,SAAS,cAAc,SAAS,cAAc,MAAM,eAAe,aAAa,OAAO;CAK7F,QAAQ,eAAe;CACvB,IAAI,KAAK,WAAW;EAOlB,MAAM,eAAe,KAAK,MAAM,QAAQ;GAAE;GAAS,UAAU;GAAU;EAAO,CAAC;EAC/E,IAAI,cAAoC;EACxC,IAAI,kBAAmC;EAIvC,IAAI,aAA4B,QAAQ,QAAQ;EAEhD,MAAM,iBAAiB,aAAa,MAAM,aAAa;GAIrD,IAAI,kBAAkB,QAAQ,GAC5B,MAAM,IAAI,iBAAiB;GAM7B,MAAM,mBACJ,SAAS,QAAQ,IAAI,mBAAmB,MACvC,SAAS,UAAU,OAAO,SAAS,SAAS,MAAM,SAAS,QAAQ,IAAI,UAAU,IAAI;GACxF,IAAI,kBACF,MAAM,IAAI,cAAc,gBAAgB;GAM1C,IAAI,SAAS,QAAQ,IAAA,gBAAuB,MAAM,KAChD,MAAM,IAAI,oBAAoB,SAAS,QAAQ,GAAG;GAQpD,IAAI,YAAY;IACd,MAAM,cAAc,SAAS,QAAQ,IAAI,cAAc;IACvD,IACE,CAAC,SAAS,MACT,eAAe,YAAY,MAAM,GAAG,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,YAAY,MAAM,aACnE;KACA,SAAS,MAAM,OAAO;KACtB,MAAM,IAAI,eAAe,GAAG;IAC9B;GACF,OACE,IAAI,CAAC,iBAAiB,SAAS,QAAQ,IAAI,cAAc,CAAC,GAAG;IAC3D,SAAS,MAAM,OAAO;IACtB,MAAM,IAAI,eAAe,GAAG;GAC9B;GAIF,cAAc,mBAAmB,QAAQ;GACzC,kBAAkB,uBAAuB,QAAQ;GAMjD,IAAI,SAAS,MAAM;IACjB,MAAM,UAAU,sBAAsB,SAAS,IAAI;IACnD,aAAa,QAAQ;IACrB,WAAW,YAAY,CAAC,CAAC;IACzB,OAAO,IAAI,SAAS,QAAQ,MAAM;KAChC,SAAS,SAAS;KAClB,QAAQ,SAAS;IACnB,CAAC;GACH;GACA,OAAO;EACT,CAAC;EAED,MAAM;EAKN,MAAM,OAAO,KAAK,UAAU,cAAc;EAI1C,MAAM,EAAE,MAAM,SAAS,WAAW,iBAAiB,IAAI;EAMvD,MAAM,eAAe,IAAI,SAAe,GAAG,WAAW;GACpD,QAAQ,QAAQ,IAAI,CAAC,CAAC,WAAW,CAAC,GAAG,MAAM;EAC7C,CAAC;EACD,aAAa,YAAY,CAAC,CAAC;EAC3B,MAAM,gBAAgB,QAAQ,KAAK,CAAC,YAAY,YAAY,CAAC;EAK7D,cAAc,YAAY,CAAC,CAAC;EAC5B,OAAO;GACL;GACA;GACA;GACA;GACA;EACF;CACF;CAEA,MAAM,WAAW,MAAM,KAAK,MAAM,QAAQ;EAAE;EAAS,UAAU;EAAU;CAAO,CAAC;CAEjF,IAAI,SAAS,UAAU,OAAO,SAAS,SAAS,KAAK;EACnD,MAAM,WAAW,SAAS,QAAQ,IAAI,UAAU;EAChD,IAAI,UACF,MAAM,IAAI,cAAc,QAAQ;CAEpC;CAEA,IAAI,SAAS,QAAQ,IAAA,gBAAuB,MAAM,KAChD,MAAM,IAAI,oBAAoB,SAAS,QAAQ,GAAG;CAGpD,IAAI,YAAY;EACd,MAAM,sBAAsB,SAAS,QAAQ,IAAI,cAAc;EAC/D,IACE,CAAC,SAAS,MACT,uBACC,oBAAoB,MAAM,GAAG,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,YAAY,MAAM,aAC7D;GACA,SAAS,MAAM,OAAO;GACtB,MAAM,IAAI,eAAe,GAAG;EAC9B;CACF,OACE,IAAI,CAAC,iBAAiB,SAAS,QAAQ,IAAI,cAAc,CAAC,GAAG;EAC3D,SAAS,MAAM,OAAO;EACtB,MAAM,IAAI,eAAe,GAAG;CAC9B;CAGF,OAAO;EACL,SAAS,MAAM,SAAS,KAAK;EAC7B,QAAQ,oBAAoB,KAAA,CAAS;EACrC,eAAe;EACf,aAAa,mBAAmB,QAAQ;EACxC,iBAAiB,uBAAuB,QAAQ;CAClD;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACpoBA,IAAI,YAAY;AAEhB,IAAI,OAAO,WAAW,aAAa;CACjC,OAAO,iBAAiB,sBAAsB;EAC5C,cAAc,IAAI;EAOlB,iBAAiB;GACf,IAAI,CAAC,WAAW,cAAc,KAAK;EACrC,GAAG,CAAC;CACN,CAAC;CAID,OAAO,iBAAiB,kBAAkB;EACxC,YAAY;EACZ,cAAc,IAAI;CACpB,CAAC;CAED,OAAO,iBAAiB,aAAa,UAAU;EAI7C,IAAI,MAAM,WAAW;GACnB,YAAY;GACZ,cAAc,KAAK;EACrB;CACF,CAAC;AACH;;;;;AAMA,SAAgB,kBAA2B;CACzC,OAAO;AACT;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACzBA,IAAM,sBAAsB;CAE1B;CAEA;CAEA;CAEA;AACF;;AAGA,IAAM,0BAA0B;;;;;;AAOhC,SAAS,UAAU,OAAwB;CACzC,IAAI,OAAO,UAAU,UAAU,OAAO,MAAM,YAAY;CACxD,IAAI,iBAAiB,OAAO,OAAO,MAAM,QAAQ,YAAY;CAC7D,IAAI,SAAS,OAAO,UAAU,YAAY,aAAa,OAAO;EAC5D,MAAM,UAAW,MAA+B;EAChD,IAAI,OAAO,YAAY,UAAU,OAAO,QAAQ,YAAY;CAC9D;CACA,OAAO;AACT;;;;;AAMA,SAAgB,uBAAuB,OAAyB;CAC9D,OAAO,UAAU,KAAK,CAAC,CAAC,SAAS,uBAAuB;AAC1D;;;;;AAMA,SAAgB,iBAAiB,OAAyB;CACxD,IAAI,iBAAiB,SAAS,MAAM,SAAS,kBAAkB,OAAO;CACtE,MAAM,UAAU,UAAU,KAAK;CAC/B,OAAO,oBAAoB,MAAM,YAAY,QAAQ,SAAS,OAAO,CAAC;AACxE;;;;;;;;;;AAaA,IAAI,QAAQ;;;;;;;AAQZ,IAAI,WAAW;;AAGf,SAAgB,gBAAyB;CACvC,OAAO;AACT;;;;;;AAOA,SAAgB,kBAAwB;CACtC,QAAQ;AACV;;;;;;;;;;;AAYA,SAAgB,mBAAmB,OAAyB;CAC1D,IAAI,CAAC,UAAU,OAAO;CAUtB,IAAI,gBAAgB,GAAG,OAAO;CAE9B,IAAI,CAAC,uBAAuB,KAAK,KAAK,CAAC,iBAAiB,KAAK,GAAG,OAAO;CACvE,QAAQ;CACR,OAAO;AACT;;;;;;;;;;;;;;;;;;;;;;AChIA,SAAgB,WAAW,OAAyB;CAClD,IAAI,iBAAiB,kBAAkB;EACrC,gBAAgB;EAChB,OAAO;CACT;CACA,OAAO,mBAAmB,KAAK;AACjC;;;;;;;;;;;;;ACSA,SAAgB,oBAAoB,MAAuC;;CAEzE,SAAS,WAAW,UAA4B;EAC9C,IAAI,KAAK,YACP,KAAK,WAAW,QAAQ;OAExB,SAAS;CAEb;CAEA,SAAS,wBAAwB,SAAuB;EACtD,iBAAiB;GACf,KAAK,SAAS,GAAG,OAAO;GACxB,OAAO,cAAc,IAAI,MAAM,wBAAwB,CAAC;EAC1D,CAAC;CACH;CAEA,SAAS,uBAAuB,MAAoB;EAClD,iBAAiB;GACf,IAAI,KAAK,eAAe,IAAI,MAAM,MAChC,KAAK,SAAS,GAAG,CAAC;GAEpB,OAAO,cAAc,IAAI,MAAM,wBAAwB,CAAC;EAC1D,CAAC;CACH;CAEA,OAAO;EAAE;EAAyB;CAAuB;AAC3D;;;;;;;;;;;;;;AAeA,SAAgB,SAAS,KAAa,SAAiC;CACrE,kBAAkB,IAAI;CACtB,aAAa,KAAK,OAAO;CACzB,OAAO,IAAI,cAAqB,CAAC,CAAC;AACpC;;;;;;;;;;AA8CA,SAAgB,eAAe,EAAE,cAAc,aAAoC;CACjF,eAAe,gBAAgB,OAAoB,KAAa,SAAgC;EAC9F,IAAI,aAAa,MAAM,OAAO;EAC9B,MAAM,SAAS,KAAK,OAAO;CAC7B;CAEA,SAAS,oBAAoB,KAAa,SAAiC;EACzE,UAAU;EACV,OAAO,SAAS,KAAK,OAAO;CAC9B;CAEA,OAAO;EAAE;EAAiB;CAAoB;AAChD;;;;;;;;;;;;;;;;;;;;;;;AAgCA,SAAgB,yBAAyB,EACvC,cACA,iBACA,YACyB;CACzB,OAAO,eAAe,2BACpB,OACA,OACA,KACA,SACkB;EAClB,IAAI,iBAAiB,eAAe;GAGlC,IAAI,aAAa,MAAM,OAAO,OAAO;GACrC,MAAM,SAAS,MAAM,WAAW;GAChC,OAAO;EACT;EAKA,IACE,iBAAiB,uBACjB,iBAAiB,kBACjB,WAAW,KAAK,GAChB;GACA,MAAM,gBAAgB,OAAO,KAAK,OAAO;GACzC,OAAO;EACT;EACA,OAAO;CACT;AACF;;;;;;;;;;;AAYA,SAAgB,aAAa,KAAa,SAAuB;CAC/D,MAAM,UAAU,IAAI,IAAI,SAAS,OAAO,SAAS,MAAM;CACvD,MAAM,SAAS,IAAI,IAAI,KAAK,OAAO,SAAS,MAAM;CAClD,IAAI,OAAO,aAAa,QAAQ,YAAY,OAAO,WAAW,QAAQ,QAAQ;EAC5E,OAAO,SAAS,OAAO;EACvB,OAAO,SAAS,OAAO;CACzB,OACE,OAAO,SAAS,OAAO;AAE3B;;;;;;;;;;;;;;;;;;;;;;;;;;;AC1LA,IAAM,sBAAsB,cAAiC,CAAC,CAAC;AAuF/D,IAAM,oCAAoB,IAAI,QAAgB;AAI9C,IAAM,gCAAgB,IAAI,QAAgB;AAO1C,IAAI,2BAA0C;AAI9C,IAAa,sBAAb,cAAyC,UAGvC;CACA,YAAY,OAAiC;EAC3C,MAAM,KAAK;EACX,KAAK,QAAQ;GAAE,UAAU;GAAO,OAAO;EAAK;CAC9C;CAEA,OAAO,yBAAyB,OAAwC;EAKtE,IAAI,gBAAgB,GAClB,OAAO;GAAE,UAAU;GAAO,OAAO;EAAK;EAGxC,OAAO;GAAE,UAAU;GAAM;EAAM;CACjC;CAEA,kBAAkB,OAAoB;EAUpC,mBAAmB,KAAK;CAC1B;CAEA,mBAAmB,WAA2C;EAI5D,IAAI,KAAK,MAAM,YAAY,UAAU,aAAa,KAAK,MAAM,UAC3D,KAAK,SAAS;GAAE,UAAU;GAAO,OAAO;EAAK,CAAC;CAElD;;CAGA,cAAsB;EACpB,KAAK,SAAS;GAAE,UAAU;GAAO,OAAO;EAAK,CAAC;CAChD;CAEA,OAAgB,cAAc;CAG9B,IAAY,gBAAmC;EAC7C,MAAM,SAAS,KAAK,WAAW,CAAC;EAChC,MAAM,MAAM,KAAK,MAAM;EAGvB,OAAO,OAAO,CAAC,OAAO,SAAS,GAAG,IAAI,CAAC,GAAG,QAAQ,GAAG,IAAI;CAC3D;CAEA,SAAoB;EAClB,IAAI,CAAC,KAAK,MAAM,YAAY,CAAC,KAAK,MAAM,OACtC,OAAO,cACL,oBAAoB,UACpB,EAAE,OAAO,KAAK,cAAc,GAC5B,KAAK,MAAM,QACb;EAGF,MAAM,QAAQ,KAAK,MAAM;EACzB,MAAM,SAAS,YAAY,KAAK;EAehC,IAAI,QAAQ,SAAS,YAAY;GAC/B,IAAI,OAAO,WAAW,aACpB,MAAM;GAQR,IAAI,EADF,kBAAkB,IAAI,KAAK,KAAK,6BAA6B,OAAO,WACjD;IACnB,kBAAkB,IAAI,KAAK;IAC3B,MAAM,SAAS,gBAAgB;IAC/B,IAAI,QAAQ;KACV,2BAA2B,OAAO;KAGlC,qBAAqB;MACnB,OACG,SAAS,OAAO,UAAU,EAAE,SAAS,KAAK,CAAC,CAAC,CAC5C,YAAY;OAEX,OAAO,SAAS,OAAO,OAAO;MAChC,CAAC,CAAC,CACD,cAAc;OACb,2BAA2B;MAC7B,CAAC;KACL,CAAC;IACH,OAIE,OAAO,SAAS,OAAO,OAAO;GAElC;GAMA,OAAO,KAAK,eAAe,IAAI;EACjC;EAYA,IAAI,QAAQ,SAAS,UAAU,MAAM,QAAQ,OAAO,MAAM,KAAK,OAAO,OAAO,SAAS,GAKhF;OAAA,CAAC,KAAK,MAAM,cAAc,CAAC,KAAK,MAAM,gBAAgB;IACxD,MAAM,YAAY,OAAO,OAAO,MAAM,MAAc,KAAK,cAAc,SAAS,CAAC,CAAC;IAClF,IAAI,CAAC,KAAK,MAAM,cAAc,cAAc,KAAK,MAAM,YACrD,MAAM;GAEV;;EAKF,IAAI,KAAK,MAAM,YAAY,CAAC,KAAK,MAAM,cAAc,QAAQ,SAAS,QACpE,MAAM;EAaR,IAAI,KAAK,MAAM,cAAc,QAAQ,SAAS,QAAQ;GACpD,IAAI,OAAO,WAAW,aACpB,MAAM;GAER,IAAI,CAAC,cAAc,IAAI,KAAK,GAAG;IAC7B,cAAc,IAAI,KAAK;IACvB,MAAM,SAAS,gBAAgB;IAC/B,MAAM,UAAU,OAAO,SAAS,WAAW,OAAO,SAAS;IAG3D,MAAM,SAAS,QAAQ,cAAc,KAAK;IAC1C,qBAAqB;KACnB,SAAc,QAAQ,OAAO;IAC/B,CAAC;GACH;GACA,OAAO;EACT;EAIA,IAAI,KAAK,MAAM,UAAU,MAAM;GAC7B,MAAM,cAAc,eAAe,QAAQ,KAAK;GAChD,IAAI,eAAe,QAAQ,CAAC,cAAc,KAAK,MAAM,QAAQ,WAAW,GAEtE,MAAM;EAEV;EAaA,IAAI,QAAQ,SAAS,QAAQ;GAC3B,MAAM,UAAU,WAAW;GAC3B,IAAI,SAAS,aAAa;IACxB,QAAQ,YAAY,yBAAyB;IAC7C,IAAI,CAAC,KAAK,MAAM,gBACd,QAAQ,YAAY,aAAa,OAAO;GAE5C;EACF;EAEA,OAAO,KAAK,eAAe,KAAK,eAAe,QAAQ,KAAK,CAAC;CAC/D;;;;;;;;CASA,eAAuB,QAA6B,OAAyB;EAC3E,IAAI,QAAQ,SAAS,QAAQ;GAC3B,IAAI,KAAK,MAAM,UAAU,MACvB,OAAO,cAAc,KAAK,MAAM,mBAA4B;IAC1D,QAAQ,OAAO;IACf,MAAM,OAAO;GACf,CAAC;GAEH,OAAO,cAAc,KAAK,MAAM,mBAA4B;IAC1D,OAAO,UAAU,OAAO,QAAQ,KAAK;IACrC,QAAQ;IACR,OAAO,KAAK;IACZ,QAAQ,OAAO;IACf,MAAM,OAAO;GACf,CAAC;EACH;EAGA,MAAM,SACJ,QAAQ,SAAS,iBAAiB;GAAE,MAAM,OAAO;GAAM,MAAM,OAAO;EAAK,IAAI;EAE/E,OAAO,cAAc,KAAK,MAAM,mBAA4B;GAC1D;GACA;GACA,OAAO,KAAK;EACd,CAAC;CACH;;;;;;;;;CAUA,eAAuB,MAA4B;EACjD,IAAI,KAAK,MAAM,gBAAgB,OAAO;EACtC,OAAO,cAAc,kBAAkB,MAAM,IAAI;CACnD;AACF;;;;;AAQA,SAAS,YAAY,OAAmC;CACtD,MAAM,MAAO,MAA8B;CAC3C,IAAI,OAAO,QAAQ,UAAU,OAAO;CACpC,IAAI;EACF,MAAM,SAAS,KAAK,MAAM,GAAG;EAC7B,IAAI,UAAU,OAAO,WAAW,YAAY,OAAO,OAAO,SAAS,UACjE,OAAO;CAEX,SAAS,GAAG;EACV,QAAQ,MAAM,4CAA4C,aAAa,QAAQ,EAAE,UAAU,CAAC;CAC9F;CACA,OAAO;AACT;;;;;;;;;;;;;;;;;AAkBA,SAAS,UAAU,QAAgB,UAAwB;CAEzD,IAAI,SAAS,SAAS,cAAc,OAAO;CAC3C,MAAM,wBAAQ,IAAI,MAAM,6BAA6B,QAAQ;CAC7D,MAAM,OAAO;CACb,OAAO;AACT;;;;;AAMA,SAAS,eAAe,QAA6B,OAA6B;CAChF,IAAI,QAAQ,SAAS,QAAQ,OAAO,OAAO;CAC3C,IAAI,QAAQ,SAAS,gBAAgB,OAAO,OAAO;CACnD,IAAI,QAAQ,SAAS,YAAY,OAAO,OAAO;CAG/C,MAAM,QAAQ,MAAM,QAAQ,MAAM,mCAAmC;CACrE,IAAI,OAAO,OAAO,SAAS,MAAM,IAAI,EAAE;CAGvC,OAAO;AACT;;;;;AAMA,SAAS,cAAc,gBAAwB,aAA8B;CAE3E,IAAI,mBAAmB,KAAK,OAAO,eAAe,OAAO,eAAe;CACxE,IAAI,mBAAmB,KAAK,OAAO,eAAe,OAAO,eAAe;CAExE,OAAO,mBAAmB;AAC5B"}
1
+ {"version":3,"file":"error-boundary-BndF-3Td.js","names":[],"sources":["../../src/shared/payload-root.ts","../../src/client/rsc-fetch.ts","../../src/client/unload-guard.ts","../../src/client/stale-client.ts","../../src/client/router-skew.ts","../../src/client/router-effects.ts","../../src/client/error-boundary.tsx"],"sourcesContent":["/**\n * The shape of an RSC payload's root row.\n *\n * Every tree the server sends the browser is serialized as\n * `{ tree, params, slotParams }` — the route's element tree, plus the coerced\n * params it rendered with, as *siblings* rather than as a wrapper around the\n * tree. See design/41-global-params.md §\"Transport\".\n *\n * Two reasons it is a sibling field rather than a provider inside the payload\n * (TIM-1297, which is TIM-1294's mechanism corrected):\n *\n * 1. **A provider inside the payload cannot serve a partial navigation.** The\n * client keeps the previous tree and splices the new payload in at the\n * first updated `SegmentOutlet`, so the payload's own provider lands\n * *below* the retained region — and the retained region's root is the\n * *departing* route's provider, which therefore wins for every reader in a\n * skipped layout. Wrapping something above it does not help: context\n * resolves to the nearest provider, and the departing one is nearer.\n * Params belong to the *navigation*, so the provider has to be owned by the\n * client, above the merge point, and there must be exactly one.\n *\n * 2. **Element props are not readable at the instant the root resolves.**\n * Flight outlines a React element's props and fills the record in after the\n * root row resolves, so a reader that unblocks on the root sees `{}` — and\n * since the object is filled *in place*, its identity never changes and any\n * memo keyed on it caches the empty snapshot permanently. A plain model\n * field has no such window: the root chunk stays blocked until everything\n * it references has resolved.\n *\n * This module is the single declaration of that shape. The server builds roots\n * through `server/publish-params.ts` and the client reads them through the\n * helpers here — producer and consumer do not each describe the payload.\n */\n\nimport type { CoercedParams } from './param-value.ts';\nimport type { SlotParamsRecord } from './slot-params.ts';\n\n/** The params published beside a tree. */\nexport interface PublishedParams {\n /** The main route's coerced params. */\n params: CoercedParams;\n /** Per-slot params, or null when the route rendered no slots with params. */\n slotParams: SlotParamsRecord | null;\n}\n\n/**\n * Params ready to publish: settled, or a promise the root provider suspends on.\n *\n * Both forms exist on purpose. The hydration path cannot await — `hydrateRoot`\n * has to be called before the inline Flight stream has necessarily produced\n * the root row — so it hands over the promise and suspends exactly where React\n * was already going to suspend on the tree. Every other path settles it inside\n * the navigation transition first, which is what keeps a decode failure inside\n * the app's own error boundaries instead of above them (TIM-1297).\n */\nexport type ParamsSource = PublishedParams | Promise<PublishedParams>;\n\n/** The root row of an RSC payload. */\nexport interface RscPayloadRoot extends PublishedParams {\n /** The route's element tree. */\n tree: unknown;\n}\n\n/**\n * What a reader gets when the value it was handed is not a payload root.\n *\n * In the browser this is unreachable for a real response: all four producers\n * of a route payload go through `withPublishedParams`, and a client talking to\n * a different build is answered with 204 at Stage 1c\n * (`server/pipeline-phases.ts`) before any payload exists. It is reachable in\n * the router's test/fallback path, where `decodeRsc` is absent and the\n * \"payload\" is the raw response text.\n */\nexport const NO_PUBLISHED_PARAMS: PublishedParams = { params: {}, slotParams: null };\n\n/** True when `value` carries published params — a root, or a read of one. */\nfunction hasPublishedParams(value: unknown): value is PublishedParams {\n return (\n typeof value === 'object' &&\n value !== null &&\n 'params' in value &&\n typeof (value as PublishedParams).params === 'object' &&\n (value as PublishedParams).params !== null\n );\n}\n\n/**\n * Read the params published beside a tree.\n *\n * Accepts a payload root *or* a `PublishedParams` already split off one, so\n * the client can pre-resolve on the navigation path and still hand the same\n * value to the same provider. Never throws.\n */\nexport function readPublishedParams(source: unknown): PublishedParams {\n if (!hasPublishedParams(source)) return NO_PUBLISHED_PARAMS;\n return { params: source.params, slotParams: source.slotParams ?? null };\n}\n\n/**\n * Read the renderable tree out of a payload root.\n *\n * Returns the value unchanged when it is not a root — the router's fallback\n * path stores raw response text under the same name, and a test asserting on\n * that text should see the text.\n */\nexport function readPayloadTree(root: unknown): unknown {\n if (typeof root === 'object' && root !== null && 'tree' in root && hasPublishedParams(root)) {\n return (root as RscPayloadRoot).tree;\n }\n return root;\n}\n\n/** A decoded payload, split into the two things the client does with it. */\nexport interface SplitPayload {\n /** The renderable tree — an element, or a thenable React suspends on. */\n tree: unknown;\n /**\n * The params published beside it. Settled when the root already is, so the\n * hydration path can render without an extra await.\n *\n * **Never rejects.** A decode failure has to surface where React renders\n * `tree`, so the error boundary *around the tree* catches it. Rejecting here\n * too would take down the provider above every boundary in the app, turning\n * a contained failure into a blank page (TIM-1297).\n */\n params: ParamsSource;\n}\n\n/**\n * Split a decoded payload root into its tree and its params.\n *\n * Accepts a settled root or a thenable of one, and preserves which it was: a\n * settled root splits synchronously, so no code path gains a suspend point it\n * did not already have.\n */\nexport function splitPayloadRoot(root: unknown): SplitPayload {\n if (\n typeof root !== 'object' ||\n root === null ||\n typeof (root as PromiseLike<unknown>).then !== 'function'\n ) {\n return { tree: readPayloadTree(root), params: readPublishedParams(root) };\n }\n const settled = root as PromiseLike<unknown>;\n const tree = Promise.resolve(settled).then(readPayloadTree);\n // The rejection still reaches React through its own read of `tree`; this\n // only marks it handled for the case where nothing renders it (a partial\n // navigation whose segment update finds no matching outlet).\n tree.catch(() => {});\n const params = Promise.resolve(settled).then(readPublishedParams, () => NO_PUBLISHED_PARAMS);\n return { tree, params };\n}\n","/**\n * RSC Fetch — handles fetching and parsing RSC Flight payloads.\n *\n * Extracted from router.ts to keep both files under the 500-line limit.\n * This module handles:\n * - Cache-busting URL generation for RSC requests\n * - Building RSC request headers (Accept, X-Timber-State-Tree)\n * - Extracting metadata from RSC response headers\n * - Fetching and decoding RSC payloads\n *\n * See design/19-client-navigation.md §\"RSC Payload Handling\"\n */\n\nimport {\n rscCacheKey,\n recordLookup,\n randomRscCacheKey,\n RSC_KEY_PARAM,\n} from '../shared/rsc-cache-key.ts';\nimport type { RscManifest, RscManifestEntry } from '../shared/rsc-manifest.ts';\nimport {\n splitPayloadRoot,\n readPublishedParams,\n type PublishedParams,\n} from '../shared/payload-root.ts';\nimport { normalizeBasePath, stripBasePath, withBasePath } from '../shared/base-path.ts';\nimport { rscPayloadPath } from '../shared/rsc-payload-path.ts';\nimport type { SegmentInfo, StateTree } from '../shared/segment-info.ts';\nimport { TIMBER_ERROR_HEADER } from '../shared/rsc-error-envelope.ts';\nimport { RSC_CONTENT_TYPE, isRscContentType } from '../shared/rsc-media-type.ts';\nimport type { RouterDeps } from './router-types.ts';\n\n// ─── Types ───────────────────────────────────────────────────────\n\n/** Result of fetching an RSC payload — includes segment metadata. */\nexport interface FetchResult {\n /** The renderable tree, split off the payload root. */\n payload: unknown;\n /**\n * The params the same payload published beside that tree.\n *\n * Not a separate channel: both fields come out of one `splitPayloadRoot`\n * call on one decoded root, so a response cannot deliver a tree and another\n * route's params. They used to arrive on their own header, which is how a\n * navigation could render one route and publish another's (TIM-1285,\n * TIM-1294).\n */\n params: PublishedParams | Promise<PublishedParams>;\n /**\n * Promise that settles when the RSC decode completes or fails.\n * The payload thenable is NOT awaited before returning (for streaming),\n * so callers must monitor this to catch async decode errors\n * (truncated streams, Flight parse failures) that would otherwise\n * become unhandled rejections.\n */\n decodePromise: Promise<void> | null;\n /** Segment metadata from X-Timber-Segments header for populating the segment cache. */\n segmentInfo: SegmentInfo[] | null;\n /** Segment paths that were skipped by the server (for client-side merging). */\n skippedSegments: string[] | null;\n}\n\n// ─── URL Helpers ─────────────────────────────────────────────────\n\n/**\n * Append a `_rsc=<key>` query parameter to the URL (TIM-1268).\n *\n * The key is a deterministic hash of the RSC request headers, so identical\n * requests share a URL and the payload becomes cacheable. It also keeps the\n * document and the payload on different URLs, so a shared cache cannot\n * return HTML for an RSC request (or vice versa) even where `Vary` is\n * opt-in configuration rather than default behaviour — Cloudflare and\n * CloudFront both. See GHSA-wfc6-r584-vfw7, design/13-security.md.\n *\n * The origin recomputes this key from the received headers and refuses to\n * let a mismatched response be shared-cached, so a caller cannot claim\n * another client's key while sending its own headers.\n *\n * Falls back to a random value when no key can be derived — `crypto.subtle`\n * is absent in non-secure browsing contexts (plain `http://` on a LAN\n * address). Dropping the parameter instead would put the payload back on\n * the document URL, where a query-keyed cache that ignores `Vary` could\n * store a Flight response under `/about` and serve it to an HTML\n * navigation. The random parameter this feature replaced did separate those\n * representations, and the fallback must not be worse than what it\n * replaced. The origin cannot verify a random key, so it refuses to\n * share-cache the response — which is exactly the old behaviour, where a\n * unique URL was never a cache hit anyway.\n *\n * Strips any #fragment before appending — fragments are client-only and\n * fetch() discards them, so _rsc would land inside the hash and be lost.\n */\nasync function appendRscParam(url: string, headers: Record<string, string>): Promise<string> {\n const hashIndex = url.indexOf('#');\n const urlWithoutHash = hashIndex === -1 ? url : url.slice(0, hashIndex);\n const key = (await rscCacheKey(recordLookup(headers))) ?? randomRscCacheKey();\n const separator = urlWithoutHash.includes('?') ? '&' : '?';\n return `${urlWithoutHash}${separator}${RSC_KEY_PARAM}=${key}`;\n}\n\n// ─── Deployment ID ───────────────────────────────────────────────\n\n/**\n * The client's deployment ID, set at bootstrap from the runtime config.\n * Sent with every RSC/action request for version skew detection.\n * Null in dev mode. See TIM-446.\n */\nlet clientDeploymentId: string | null = null;\n\n/** Set the client deployment ID. Called once at bootstrap. */\nexport function setClientDeploymentId(id: string | null): void {\n clientDeploymentId = id;\n}\n\n/** Get the client deployment ID. */\nexport function getClientDeploymentId(): string | null {\n return clientDeploymentId;\n}\n\n// ─── Base Path ───────────────────────────────────────────────────\n\n/**\n * The deployment base path (Vite's resolved `base`), normalized.\n * Set at bootstrap from `virtual:timber-config`; `'/'` unless the app is\n * deployed under a sub-path. See design/11-platform.md, TIM-1261.\n *\n * **The base is applied in exactly one place: at fetch time**\n * (`toStaticRscUrl`). The static build writes the RSC manifest with\n * root-relative keys *and* root-relative URLs, so the generated site stays\n * relocatable and there is no second copy of the base to drift from this one.\n */\nlet basePath = '/';\n\n/** Set the deployment base path. Called once at bootstrap. */\nexport function setBasePath(base: string | undefined): void {\n basePath = normalizeBasePath(base);\n}\n\n/** Get the normalized deployment base path. @internal Exported for testing. */\nexport function getBasePath(): string {\n return basePath;\n}\n\n// ─── Static Mode ────────────────────────────────────────────────\n\n/**\n * When true, RSC fetches use _rsc/*.rsc file URLs instead of\n * the route URL with Accept headers. Static hosts ignore Accept\n * headers, so the client must fetch the pre-generated .rsc files\n * directly. Set at bootstrap from virtual:timber-config output mode.\n */\nlet staticMode = false;\n\nexport function setStaticMode(enabled: boolean): void {\n staticMode = enabled;\n}\n\nexport function isStaticMode(): boolean {\n return staticMode;\n}\n\n/**\n * RSC manifest mapping unhashed → content-hashed `.rsc` URLs. Populated from\n * `window.__TIMBER_RSC_MANIFEST__` (injected into HTML during static\n * generation). Shape shared with the static build that writes it — see\n * `shared/rsc-manifest.ts`. See TIM-1254.\n */\nlet rscManifest: RscManifest | null = null;\n\nfunction getRscManifest(): RscManifest | null {\n if (rscManifest) return rscManifest;\n if (typeof window !== 'undefined' && (window as any).__TIMBER_RSC_MANIFEST__) {\n rscManifest = (window as any).__TIMBER_RSC_MANIFEST__;\n }\n return rscManifest;\n}\n\n/** @internal Exported for testing. */\nexport function setRscManifest(manifest: RscManifest | null): void {\n rscManifest = manifest;\n}\n\n/** A resolved static payload target: the URL to fetch, and how it was resolved. */\ninterface StaticRscTarget {\n /** Fetchable URL — base-prefixed. */\n url: string;\n /**\n * True when the RSC manifest supplied a content-hashed filename. The hash\n * guarantees freshness, so no `_rsc` cache key is appended (TIM-1254).\n */\n hashed: boolean;\n}\n\n/**\n * Resolve a route URL to the `_rsc/*.rsc` file to fetch. The naming rule is\n * `shared/rsc-payload-path.ts`, shared with the build that writes the files.\n *\n * When an RSC manifest is available (hashed filenames from TIM-1254),\n * the manifest is consulted to resolve to the hashed path.\n *\n * / → /_rsc/index.rsc (or /_rsc/index-B7YxEKdN.rsc with manifest)\n * /about → /_rsc/about/index.rsc (or /_rsc/about/index-C8ZzFLfO.rsc)\n * /blog/hello → /_rsc/blog/hello/index.rsc\n *\n * The manifest is written and read in route space (no base prefix), so the\n * base is applied here — once, on the way out. See client/base-path.ts.\n */\nfunction toStaticRscUrl(url: string): StaticRscTarget {\n const unhashed = toUnhashedRscUrl(url);\n const hashedUrl = manifestLookup(unhashed);\n return { url: withBasePath(basePath, hashedUrl ?? unhashed), hashed: hashedUrl !== null };\n}\n\n/**\n * Compute the unhashed `_rsc/*.rsc` path for a route URL.\n *\n * Returns a **route-space** path: the deployment base is stripped from the\n * incoming URL and is NOT re-applied, because this value doubles as the RSC\n * manifest key, and the manifest is base-less. `toStaticRscUrl` applies the\n * base to whatever is actually fetched.\n *\n * @internal Exported for testing.\n */\nexport function toUnhashedRscUrl(url: string): string {\n const hashIndex = url.indexOf('#');\n const queryIndex = url.indexOf('?');\n const hashEnd = hashIndex === -1 ? url.length : hashIndex;\n const queryEnd = queryIndex === -1 ? url.length : queryIndex;\n const end = Math.min(hashEnd, queryEnd);\n let pathname = stripBasePath(basePath, url.slice(0, end));\n // Strip trailing slash (unless root) to match static build output naming\n if (pathname.length > 1 && pathname.endsWith('/')) {\n pathname = pathname.slice(0, -1);\n }\n return rscPayloadPath(pathname);\n}\n\n/**\n * Look up a key in the RSC manifest, falling back to percent-decoded\n * lookup for encoded browser URLs. Returns the entry or null.\n */\nfunction manifestEntry(key: string): RscManifestEntry | null {\n const manifest = getRscManifest();\n if (!manifest) return null;\n if (manifest[key]) return manifest[key];\n try {\n const decoded = decodeURIComponent(key);\n if (decoded !== key && manifest[decoded]) return manifest[decoded];\n } catch {\n // Malformed percent sequence (e.g. /100%.rsc) — skip decoded lookup\n }\n return null;\n}\n\n/**\n * Look up the hashed URL for an unhashed RSC path.\n */\nfunction manifestLookup(key: string): string | null {\n return manifestEntry(key)?.url ?? null;\n}\n\n// The manifest used to inline each route's params, because a generated\n// `.rsc` file carries no response headers and the header channel therefore\n// did not exist on the static path (TIM-1255, TIM-1285). The `.rsc` file now\n// carries them itself — they are props inside the payload — so the static\n// path needs no special case at all.\n\n// ─── Reload Signal ───────────────────────────────────────────────\n\n/** Header name used by the server to signal a version skew reload. */\nexport const RELOAD_HEADER = 'X-Timber-Reload';\n\n/** Header name for the client's deployment ID. */\nexport const DEPLOYMENT_ID_HEADER = 'X-Timber-Deployment-Id';\n\n/**\n * Check if a response signals a version skew reload.\n * Triggers a full page reload if the server indicates the client is stale.\n */\nexport function checkReloadSignal(response: Response): boolean {\n return response.headers.get(RELOAD_HEADER) === '1';\n}\n\n// ─── Header Builder ──────────────────────────────────────────────\n\n/**\n * Build the headers for an RSC payload request.\n *\n * **Every header added here is hashed into the payload URL** as the `_rsc`\n * cache key (see `rscCacheKey`), and URLs are logged by proxies, CDNs, and\n * origin access logs. So: no credentials, no session identifiers, no\n * user-supplied content. `fnv1aHash` is a cache-key hash, not a KDF — a\n * low-entropy secret would be recoverable from the URL by brute force.\n * Authentication already travels on cookies, which are not part of this\n * object and never enter the key.\n */\nexport function buildRscHeaders(\n stateTree: StateTree | undefined,\n currentUrl?: string\n): Record<string, string> {\n const headers: Record<string, string> = {\n Accept: RSC_CONTENT_TYPE,\n };\n if (stateTree) {\n headers['X-Timber-State-Tree'] = JSON.stringify(stateTree);\n }\n // Send current URL for intercepting route resolution.\n // The server uses this to determine if an intercepting route should\n // render instead of the actual target route (modal pattern).\n // See design/07-routing.md §\"Intercepting Routes\"\n if (currentUrl) {\n headers['X-Timber-URL'] = currentUrl;\n }\n // Send deployment ID for version skew detection (TIM-446).\n // The server compares this against the current build's ID.\n // On mismatch, the server signals a reload instead of returning\n // an RSC payload with mismatched module references.\n if (clientDeploymentId) {\n headers[DEPLOYMENT_ID_HEADER] = clientDeploymentId;\n }\n return headers;\n}\n\n// ─── Response Header Extraction ──────────────────────────────────\n\n/** Dev-only warning for malformed framework headers. Tree-shaken in production. */\nfunction warnMalformedHeader(headerName: string, raw: string): void {\n if (process.env.NODE_ENV !== 'production') {\n const preview = raw.length > 200 ? raw.slice(0, 200) + '…' : raw;\n console.warn(\n `[timber] Malformed ${headerName} header \\u2014 JSON.parse failed. ` +\n `This indicates a framework bug or header corruption. Raw (first 200 chars): ${preview}`\n );\n }\n}\n\n/**\n * Extract segment metadata from the X-Timber-Segments response header.\n * Returns null if the header is missing or malformed.\n *\n * Format: JSON array of {path, isRequestDependent} objects describing the rendered\n * segment chain from root to leaf. Used to populate the client-side\n * segment cache for state tree diffing on subsequent navigations.\n */\nexport function extractSegmentInfo(response: Response): SegmentInfo[] | null {\n const header = response.headers.get('X-Timber-Segments');\n if (!header) return null;\n try {\n return JSON.parse(header);\n } catch {\n warnMalformedHeader('X-Timber-Segments', header);\n return null;\n }\n}\n\n/**\n * Extract skipped segment paths from the X-Timber-Skipped-Segments header.\n * Returns null if the header is missing or malformed.\n *\n * When the server skips sync layouts the client already has cached,\n * it sends this header listing the skipped segment paths (outermost first).\n * The client uses this to merge the partial payload with cached segments.\n */\nexport function extractSkippedSegments(response: Response): string[] | null {\n const header = response.headers.get('X-Timber-Skipped-Segments');\n if (!header) return null;\n try {\n const parsed = JSON.parse(header);\n return Array.isArray(parsed) ? parsed : null;\n } catch {\n warnMalformedHeader('X-Timber-Skipped-Segments', header);\n return null;\n }\n}\n\n// ─── Redirect Error ──────────────────────────────────────────────\n\n/**\n * Thrown when an RSC payload response contains X-Timber-Redirect header.\n * Caught in navigate() to trigger a soft router navigation to the redirect target.\n */\nexport class RedirectError extends Error {\n readonly redirectUrl: string;\n constructor(url: string) {\n super(`Server redirect to ${url}`);\n this.redirectUrl = url;\n }\n}\n\n/**\n * Thrown when the server signals a version skew (X-Timber-Reload header).\n * Caught in navigate() to trigger a full page reload.\n * See TIM-446.\n */\nexport class VersionSkewError extends Error {\n constructor() {\n super('Version skew detected — server has been redeployed');\n }\n}\n\n/**\n * Thrown when the server returns an error for an RSC payload request.\n * The server sends X-Timber-Error header and a JSON body instead of a\n * broken RSC stream for any RenderError (4xx or 5xx). Caught in\n * navigate() to trigger a hard navigation so the server can render\n * the error page as HTML.\n *\n * See design/10-error-handling.md §\"Error Page Rendering for Client Navigation\"\n */\nexport class ServerErrorResponse extends Error {\n readonly status: number;\n readonly url: string;\n constructor(status: number, url: string) {\n super(`Server error ${status} during navigation to ${url}`);\n this.status = status;\n this.url = url;\n }\n}\n\n/**\n * Thrown when the RSC fetch response has a Content-Type that is not\n * text/x-component — e.g., a static asset (image, CSS, JS) served\n * for a same-origin URL that isn't a route. The response body is\n * cancelled immediately (headers-only cost). Caught in navigate()\n * to trigger a hard navigation; caught in prefetch() to store a\n * negative cache entry so click hard-navigates without a second fetch.\n *\n * See TIM-1231.\n */\nexport class NonRscResponse extends Error {\n readonly url: string;\n constructor(url: string) {\n super(`Non-RSC response for ${url}`);\n this.url = url;\n }\n}\n\n// ─── Stream Completion Tracking ───────────────────────────────────\n\n/**\n * Wrap a response body stream to track when it's fully consumed.\n * Returns a new body that passes all chunks through unchanged, plus\n * a `done` promise that resolves when the last chunk is read (or\n * rejects if the stream errors).\n *\n * createFromFetch's thenable resolves on shell arrival, but callers need to\n * know when the stream is fully decoded: that is what `navigateTransition`'s\n * returned promise means, and what the router's pending store, the Navigation\n * API deferred and `<Link>`'s `isPending` are all timed against.\n */\nfunction trackStreamCompletion(body: ReadableStream<Uint8Array>): {\n body: ReadableStream<Uint8Array>;\n done: Promise<void>;\n} {\n let resolveDone!: () => void;\n let rejectDone!: (e: unknown) => void;\n const done = new Promise<void>((res, rej) => {\n resolveDone = res;\n rejectDone = rej;\n });\n\n const reader = body.getReader();\n const tracked = new ReadableStream<Uint8Array>({\n async pull(controller) {\n try {\n const result = await reader.read();\n if (result.done) {\n controller.close();\n resolveDone();\n } else {\n controller.enqueue(result.value);\n }\n } catch (error) {\n controller.error(error);\n rejectDone(error);\n }\n },\n cancel(reason) {\n reader.cancel(reason);\n resolveDone();\n },\n });\n\n return { body: tracked, done };\n}\n\n// ─── Fetch ───────────────────────────────────────────────────────\n\n/**\n * Fetch an RSC payload from the server. If a decodeRsc function is provided,\n * the response is decoded into a React element tree via createFromFetch.\n * Otherwise, the raw response text is returned (test mode).\n */\nexport async function fetchRscPayload(\n url: string,\n deps: RouterDeps,\n stateTree?: { segments: string[] },\n currentUrl?: string,\n signal?: AbortSignal\n): Promise<FetchResult> {\n // In static mode, fetch the pre-generated _rsc/*.rsc file directly\n // instead of the route URL with Accept headers. Static hosts ignore\n // Accept headers, so the route URL would return HTML.\n const staticTarget = staticMode ? toStaticRscUrl(url) : null;\n const fetchTarget = staticTarget ? staticTarget.url : url;\n // Static hosts resolve neither partial payloads nor intercepting routes,\n // so neither the state tree nor the source URL is sent — and neither may\n // fragment the cache key of a file that does not vary on them.\n const headers = staticMode\n ? buildRscHeaders(undefined, undefined)\n : buildRscHeaders(stateTree, currentUrl);\n // Skip the cache key when the manifest supplies a content-hashed\n // URL — the hash itself guarantees freshness, and the param would defeat\n // the immutable cache headers on /_rsc/* (TIM-1254).\n const rscUrl = staticTarget?.hashed ? fetchTarget : await appendRscParam(fetchTarget, headers);\n // Deriving the cache key is async (SubtleCrypto), so the fetch is no\n // longer issued in the same tick as the call. A navigation superseded\n // during that window would otherwise still hit the network with an\n // already-aborted signal.\n signal?.throwIfAborted();\n if (deps.decodeRsc) {\n // Production path: use createFromFetch for streaming RSC decoding.\n // createFromFetch takes a Promise<Response> and progressively parses\n // the RSC Flight stream as chunks arrive.\n //\n // Intercept the response to read segment metadata before createFromFetch\n // consumes the body. Reading headers does NOT consume the body stream.\n const fetchPromise = deps.fetch(rscUrl, { headers, redirect: 'manual', signal });\n let segmentInfo: SegmentInfo[] | null = null;\n let skippedSegments: string[] | null = null;\n // Track when the full RSC body stream is consumed (not just shell).\n // Initialized to resolved for bodyless responses; overwritten when\n // the response has a body.\n let streamDone: Promise<void> = Promise.resolve();\n\n const wrappedPromise = fetchPromise.then((response) => {\n // Version skew detection (TIM-446): if the server signals a reload,\n // throw VersionSkewError so the caller (router navigate) can trigger\n // a full page reload.\n if (checkReloadSignal(response)) {\n throw new VersionSkewError();\n }\n // Detect server-side redirects. The server returns 204 + X-Timber-Redirect\n // for RSC payload requests instead of a raw 302, because fetch with\n // redirect: \"manual\" turns 302s into opaque redirects (status 0, null body)\n // which crashes createFromFetch when it tries to read the body stream.\n const redirectLocation =\n response.headers.get('X-Timber-Redirect') ||\n (response.status >= 300 && response.status < 400 ? response.headers.get('Location') : null);\n if (redirectLocation) {\n throw new RedirectError(redirectLocation);\n }\n // Detect server error responses. The server returns X-Timber-Error header\n // with a JSON body instead of a broken RSC stream for any RenderError\n // (4xx or 5xx). Hard-navigate so the server renders the error page as HTML.\n // See design/10-error-handling.md §\"Error Page Rendering for Client Navigation\"\n if (response.headers.get(TIMBER_ERROR_HEADER) === '1') {\n throw new ServerErrorResponse(response.status, url);\n }\n // Content-Type guard: reject non-RSC responses before createFromFetch\n // tries to parse the body as Flight data.\n // In static mode, accept octet-stream/text/plain/absent content-type\n // (static hosts serve .rsc files with these), but still reject 404s\n // and text/html (missing .rsc file → host returns 404 page or SPA\n // HTML fallback). See TIM-1231, TIM-1243, TIM-1247.\n if (staticMode) {\n const contentType = response.headers.get('content-type');\n if (\n !response.ok ||\n (contentType && contentType.split(';')[0].trim().toLowerCase() === 'text/html')\n ) {\n response.body?.cancel();\n throw new NonRscResponse(url);\n }\n } else {\n if (!isRscContentType(response.headers.get('content-type'))) {\n response.body?.cancel();\n throw new NonRscResponse(url);\n }\n }\n // Metadata (<title>/<meta>/<link>) now rides the RSC Flight payload\n // as React elements — React 19 Float handles them. See TIM-1151.\n segmentInfo = extractSegmentInfo(response);\n skippedSegments = extractSkippedSegments(response);\n\n // Wrap the body to track full stream consumption. createFromFetch's\n // thenable resolves when the root model (shell) arrives, but we need\n // to know when ALL chunks are read — that is what the navigation\n // promise resolving means, and what keeps the pending store set.\n if (response.body) {\n const tracked = trackStreamCompletion(response.body);\n streamDone = tracked.done;\n streamDone.catch(() => {}); // prevent unhandled rejection\n return new Response(tracked.body, {\n headers: response.headers,\n status: response.status,\n });\n }\n return response;\n });\n // Await headers so segmentInfo is populated.\n await wrappedPromise;\n // Start decoding but do NOT await — return the in-progress thenable.\n // React can render a Flight thenable directly: it suspends on unresolved\n // parts and progressively renders as chunks arrive, spreading work across\n // frames instead of blocking the main thread in one burst.\n const root = deps.decodeRsc(wrappedPromise);\n // One split, one root: the tree and the params it was rendered with come\n // apart here and nowhere else, so nothing downstream has to keep them\n // paired by hand.\n const { tree: payload, params } = splitPayloadRoot(root);\n // Combine stream completion with payload error propagation.\n // streamDone marks the end of the RSC stream, which is what the\n // navigation promise resolves on. payloadError propagates\n // decode failures (stale client references, Flight parse errors)\n // so the router's catch block can trigger recovery (stale reload).\n const payloadError = new Promise<void>((_, reject) => {\n Promise.resolve(root).then(() => {}, reject);\n });\n payloadError.catch(() => {});\n const decodePromise = Promise.race([streamDone, payloadError]);\n // Mark handled, like payloadError above. The same rejection reaches the\n // router through `payload`, and a navigation that recovers by hard\n // navigating (version skew, non-RSC response) hangs deliberately and never\n // awaits this one — leaving it to surface as an unhandled rejection.\n decodePromise.catch(() => {});\n return {\n payload,\n params,\n decodePromise,\n segmentInfo,\n skippedSegments,\n };\n }\n // Test/fallback path: return raw text\n const response = await deps.fetch(rscUrl, { headers, redirect: 'manual', signal });\n // Check for redirect in test path too\n if (response.status >= 300 && response.status < 400) {\n const location = response.headers.get('Location');\n if (location) {\n throw new RedirectError(location);\n }\n }\n // Server error guard (same as production path above).\n if (response.headers.get(TIMBER_ERROR_HEADER) === '1') {\n throw new ServerErrorResponse(response.status, url);\n }\n // Content-Type guard (same as production path above). See TIM-1231, TIM-1243, TIM-1247.\n if (staticMode) {\n const fallbackContentType = response.headers.get('content-type');\n if (\n !response.ok ||\n (fallbackContentType &&\n fallbackContentType.split(';')[0].trim().toLowerCase() === 'text/html')\n ) {\n response.body?.cancel();\n throw new NonRscResponse(url);\n }\n } else {\n if (!isRscContentType(response.headers.get('content-type'))) {\n response.body?.cancel();\n throw new NonRscResponse(url);\n }\n }\n // Test/fallback path: raw text carries no payload root, so no params.\n return {\n payload: await response.text(),\n params: readPublishedParams(undefined),\n decodePromise: null,\n segmentInfo: extractSegmentInfo(response),\n skippedSegments: extractSkippedSegments(response),\n };\n}\n","/**\n * Page unload detection — suppresses spurious errors during page refresh/navigation.\n *\n * When the user refreshes the page or navigates away while React is still\n * streaming Suspense content, the aborted connection causes streaming errors.\n * These are not application errors — they're a side effect of the browser\n * tearing down the connection. This module tracks whether the page is being\n * unloaded so error boundaries and error handlers can suppress abort-related\n * errors during the unload window.\n *\n * This is the single owner of \"is this document going away\". Anything that\n * needs the answer imports `isPageUnloading()` — an unload flag maintained a\n * second time somewhere else is a flag that will drift out of agreement with\n * this one (TIM-1260).\n *\n * Mutable state is delegated to client/state.ts for singleton guarantees.\n * See design/18-build-system.md §\"Singleton State Registry\"\n *\n * See design/10-error-handling.md §\"Known limitation: deny() inside Suspense and hydration\"\n */\n\nimport { unloading, _setUnloading } from './state.ts';\n\n/**\n * Departure is *committed*: `pagehide` has fired, or this document has called\n * reload() on itself. `beforeunload` alone doesn't qualify — it is\n * speculative, and the user can still cancel out of it.\n */\nlet committed = false;\n\nif (typeof window !== 'undefined') {\n window.addEventListener('beforeunload', () => {\n _setUnloading(true);\n\n // An app with an unsaved-changes prompt can have this cancelled. Nothing\n // fires to say so, so the only signal is that the page is still running\n // afterwards — and timers are blocked while the modal is up, so this task\n // cannot run until the user has chosen. If they left, `pagehide` has\n // already committed and this is a no-op.\n setTimeout(() => {\n if (!committed) _setUnloading(false);\n }, 0);\n });\n\n // pagehide is the committed-departure event, and fires for both navigations\n // and bfcache entry. Safari in particular may skip beforeunload entirely.\n window.addEventListener('pagehide', () => {\n committed = true;\n _setUnloading(true);\n });\n\n window.addEventListener('pageshow', (event) => {\n // A bfcache restore un-does the teardown `pagehide` announced: the same\n // document is alive again, with this module's state intact. Without the\n // reset, every consumer would go on suppressing errors on a live page.\n if (event.persisted) {\n committed = false;\n _setUnloading(false);\n }\n });\n}\n\n/**\n * Returns true if the page is currently being unloaded (user refreshed\n * or navigated away). Error boundaries should suppress errors in this state.\n */\nexport function isPageUnloading(): boolean {\n return unloading;\n}\n","/**\n * Stale Client — knowing when this bundle has been superseded.\n *\n * Layer 1 of version skew handling (see design/33-version-skew.md) is\n * proactive: the server compares `X-Timber-Deployment-Id` against the current\n * build and answers a stale client with `X-Timber-Reload: 1`. That covers\n * every RSC navigation and every server action — as long as there is a server\n * in the request path.\n *\n * This module is the reactive half, plus the state both halves feed. It\n * recognizes the two error shapes a stale client produces on its own:\n *\n * 1. **Stale client references** — React's Flight client throws\n * \"Could not find the module …\" when an RSC payload names a client module\n * ID that isn't in this bundle's manifest.\n * 2. **Chunk load failures** — a content-hashed chunk that no longer exists on\n * the CDN fails to `import()`. Every engine words this differently.\n *\n * It matters most for `output: 'static'`, where there is no server to send the\n * reload header and this is the *only* skew detection that can run.\n *\n * **Nothing here reloads the page.** A stale client is recorded, not acted on;\n * the router turns the next user-initiated navigation into a full document\n * load. See design/33-version-skew.md §\"Recovery on the next navigation\" for\n * why — a self-issued `location.reload()` cannot tell a recovery from a bounce\n * between two live deployments, and mid-rollout it produces exactly that loop.\n *\n * See TIM-1260, TIM-1275.\n */\n\n// The only import: the shared unload state. Kept minimal because this module\n// is reached from the error boundary, a `'use client'` component evaluated in\n// the SSR graph too — a dependency on the client fetch layer would drag that\n// graph along with it.\nimport { isPageUnloading } from './unload-guard.ts';\n\n// ─── Error Shapes ────────────────────────────────────────────────\n\n/**\n * Substrings that identify a failed dynamic import, lowercased for\n * case-insensitive matching. Engines and bundlers each word this\n * differently and none of them expose a stable error code.\n */\nconst CHUNK_LOAD_PATTERNS = [\n // Chromium\n 'failed to fetch dynamically imported module',\n // Firefox\n 'error loading dynamically imported module',\n // Safari\n 'importing a module script failed',\n // Vite's preload helper, for the stylesheet of a lazily-imported chunk\n 'unable to preload css for',\n];\n\n/** Substring React's Flight client uses when a client module ID is unknown. */\nconst STALE_REFERENCE_PATTERN = 'could not find the module';\n\n/**\n * Pull a comparable message out of an unknown thrown value. Rejections cross\n * enough boundaries (event handlers, `unhandledrejection`, Vite's preload\n * event) that the value is not reliably an `Error`.\n */\nfunction messageOf(error: unknown): string {\n if (typeof error === 'string') return error.toLowerCase();\n if (error instanceof Error) return error.message.toLowerCase();\n if (error && typeof error === 'object' && 'message' in error) {\n const message = (error as { message: unknown }).message;\n if (typeof message === 'string') return message.toLowerCase();\n }\n return '';\n}\n\n/**\n * True when the error is React's \"client reference not in this manifest\"\n * failure — the payload was produced by a build this bundle doesn't match.\n */\nexport function isStaleClientReference(error: unknown): boolean {\n return messageOf(error).includes(STALE_REFERENCE_PATTERN);\n}\n\n/**\n * True when the error is a failed dynamic import or CSS preload — the\n * content-hashed chunk this bundle asks for is gone from the CDN.\n */\nexport function isChunkLoadError(error: unknown): boolean {\n if (error instanceof Error && error.name === 'ChunkLoadError') return true;\n const message = messageOf(error);\n return CHUNK_LOAD_PATTERNS.some((pattern) => message.includes(pattern));\n}\n\n// ─── Staleness ───────────────────────────────────────────────────\n\n/**\n * True once this client is known to be running against a superseded\n * deployment. One-way: a document that has gone stale cannot come back, since\n * the only way to get fresh code is to load a new document.\n *\n * In-memory on purpose. Persisting it would be wrong — the next document gets\n * whatever build the server hands it, and if that one is stale too it will\n * find out the same way.\n */\nlet stale = false;\n\n/**\n * True once {@link initStaleClient} has seen a deployment ID — i.e. this is a\n * production build. False in dev, where nothing is ever stale: HMR owns code\n * updates, and a failed dynamic import is a real error the developer needs to\n * see rather than something a page load fixes.\n */\nlet deployed = false;\n\n/** Whether this client has been superseded by a newer deployment. */\nexport function isClientStale(): boolean {\n return stale;\n}\n\n/**\n * Record that the origin has told us this client is stale — the `X-Timber-Reload`\n * header on an RSC or action response. Authoritative, so it is not gated on\n * anything: a dev server never sends the header in the first place.\n */\nexport function markClientStale(): void {\n stale = true;\n}\n\n/**\n * Record staleness if `error` is one of the shapes a superseded bundle\n * produces. Returns whether it did, so a caller mid-navigation can decide to\n * hard-navigate instead of surfacing the error.\n *\n * Inference, unlike {@link markClientStale}, so it is gated on being a\n * production build. This is the single place that gate lives — call sites\n * cannot see each other, and \"remember to gate the next one too\" is not a\n * guarantee.\n */\nexport function markStaleFromError(error: unknown): boolean {\n if (!deployed) return false;\n\n // A document being torn down aborts its in-flight dynamic imports, and an\n // aborted import reads exactly like a chunk the CDN has dropped. That is not\n // evidence of staleness, and the bit outlives the teardown that produced it:\n // a document entering the back-forward cache is frozen, not destroyed, so a\n // false positive recorded on the way out would cost the *restored* page its\n // client-side navigation for good. `unload-guard.ts` clears its own flag on\n // a persisted `pageshow`; this one is one-way by design, so it has to not be\n // set in the first place.\n if (isPageUnloading()) return false;\n\n if (!isStaleClientReference(error) && !isChunkLoadError(error)) return false;\n stale = true;\n return true;\n}\n\n// ─── Initialization ──────────────────────────────────────────────\n\n/**\n * The underlying error Vite attaches to a `vite:preloadError` event.\n *\n * Narrowed at runtime rather than cast: the property is Vite's, not ours, and\n * a shape change should fall back to matching the event itself rather than\n * silently reading `undefined` forever.\n */\nfunction preloadErrorPayload(event: Event): unknown {\n return 'payload' in event ? (event as Event & { payload: unknown }).payload : event;\n}\n\n/**\n * Arm the module, and register the listeners that catch stale-client errors\n * never passing through the router: a lazily imported client component, a\n * preloaded stylesheet, anything that rejects outside a navigation.\n *\n * No-op when `deploymentId` is null — that is dev. Called from the browser\n * entry before hydration, so it precedes any render or navigation that could\n * throw.\n */\nexport function initStaleClient(deploymentId: string | null): void {\n if (!deploymentId) return;\n deployed = true;\n\n // Vite dispatches this for failed chunk/CSS preloads. Note what is *not*\n // here: `preventDefault()`. Recording staleness is not recovering from this\n // error — the page is still broken until the user navigates — so Vite must\n // go on rethrowing it rather than have it silently swallowed.\n window.addEventListener('vite:preloadError', (event) => {\n markStaleFromError(preloadErrorPayload(event));\n });\n\n window.addEventListener('unhandledrejection', (event: PromiseRejectionEvent) => {\n markStaleFromError(event.reason);\n });\n}\n","/**\n * Router Skew Recording — turning a rejection into \"this bundle is superseded\".\n *\n * Detection lives in `stale-client.ts`; this is the one place the router (and\n * anything else holding an RSC rejection) turns an error into that verdict.\n * Recovery is not here: every path that gives up on client-side navigation —\n * skew or not — leaves through `createSpaExits()` in `router-effects.ts`.\n *\n * See design/33-version-skew.md §\"Recovery on the next navigation\"\n */\n\nimport { VersionSkewError } from './rsc-fetch.ts';\nimport { markClientStale, markStaleFromError } from './stale-client.ts';\n\n/**\n * Record a version skew if `error` is one, and report whether it was.\n *\n * `VersionSkewError` is the origin saying so outright; the reactive shapes are\n * inferred from the error text and are a no-op in dev.\n *\n * Closes over nothing — anywhere an RSC payload can fail needs it, including\n * places that have no router instance to hand.\n */\nexport function recordSkew(error: unknown): boolean {\n if (error instanceof VersionSkewError) {\n markClientStale();\n return true;\n }\n return markStaleFromError(error);\n}\n","/**\n * Router Effects — full-page navigation and post-paint scroll.\n *\n * The two things the router does *to the document* rather than to its own\n * state: leaving the SPA entirely, and moving the scroll position once React\n * has committed. Extracted from `router.ts` to keep that file focused on\n * navigation state (see design/18-build-system.md §\"No file >500 lines\").\n *\n * See design/19-client-navigation.md §\"Scroll Restoration\"\n */\n\nimport { setHardNavigating } from './navigation-root.tsx';\nimport type { RenderOwner } from './navigation-transition.ts';\nimport { RedirectError, ServerErrorResponse, NonRscResponse } from './rsc-fetch.ts';\nimport { recordSkew } from './router-skew.ts';\n\n/** The subset of `RouterDeps` the post-paint scroll effects need. */\nexport interface ScrollEffectDeps {\n scrollTo: (x: number, y: number) => void;\n afterPaint?: (callback: () => void) => void;\n scrollToHash?: (hash: string) => boolean;\n}\n\nexport interface ScrollEffects {\n /**\n * Schedule scroll restoration after the next paint and fire the\n * scroll-restored event. Used by navigate, popstate, and refresh.\n */\n restoreScrollAfterPaint: (scrollY: number) => void;\n /**\n * Scroll to the element matching the URL #fragment after paint, falling\n * back to scroll-to-top when no element matches (matching full-page-load\n * behavior for an unknown fragment). Used by forward navigation to a\n * hash-bearing URL (TIM-1035).\n */\n scrollToHashAfterPaint: (hash: string) => void;\n}\n\nexport function createScrollEffects(deps: ScrollEffectDeps): ScrollEffects {\n /** Run a callback after the next paint (after React commit). */\n function afterPaint(callback: () => void): void {\n if (deps.afterPaint) {\n deps.afterPaint(callback);\n } else {\n callback();\n }\n }\n\n function restoreScrollAfterPaint(scrollY: number): void {\n afterPaint(() => {\n deps.scrollTo(0, scrollY);\n window.dispatchEvent(new Event('timber:scroll-restored'));\n });\n }\n\n function scrollToHashAfterPaint(hash: string): void {\n afterPaint(() => {\n if (deps.scrollToHash?.(hash) !== true) {\n deps.scrollTo(0, 0);\n }\n window.dispatchEvent(new Event('timber:scroll-restored'));\n });\n }\n\n return { restoreScrollAfterPaint, scrollToHashAfterPaint };\n}\n\n/**\n * Leave the SPA and never come back.\n *\n * Every router path that gives up on client-side navigation does the same\n * three things: flag the hard navigation so the Navigation API and React stop\n * acting on a dying document, perform the document load, then block forever so\n * the caller cannot carry on rendering into it. The returned promise is\n * deliberately unresolvable — `location` assignment does not stop this turn of\n * the event loop.\n *\n * Callers: server error, non-RSC response, and every version skew path\n * ([33-version-skew.md](../../../../design/33-version-skew.md)).\n */\nexport function leaveSpa(url: string, fromUrl: string): Promise<never> {\n setHardNavigating(true);\n hardNavigate(url, fromUrl);\n return new Promise<never>(() => {});\n}\n\nexport interface SpaExitDeps {\n /** The owner that holds the router right now, or null when idle. */\n currentOwner: () => RenderOwner | null;\n /** Supersede whatever is in flight and take ownership. */\n supersede: () => void;\n}\n\nexport interface SpaExits {\n /**\n * Leave the SPA — but only if `owner` still owns the router. Every\n * failure a navigation can end on (a server error, a non-RSC response, a\n * version skew) can surface long after the user has clicked something else:\n * a fetch that was never cancellable, a Flight thenable that rejects late.\n * Hard navigating to the abandoned URL would undo that click, so a\n * navigation that no longer owns the router does nothing and lets the one\n * that does drive. Never resolves when it does navigate.\n *\n * Must be called from inside the `runNavigation()` callback. That function's\n * `finally` clears the current controller, so an ownership check downstream\n * of it compares against `null` and never leaves at all.\n */\n leaveSpaIfOwned: (owner: RenderOwner, url: string, fromUrl: string) => Promise<void>;\n\n /**\n * Leave the SPA from a path that never entered `runNavigation()` — the\n * stale-client short circuits, which decide before any fetch is attempted.\n *\n * Supersedes first. Nothing else on those paths takes ownership, so without\n * it an in-flight navigation keeps the router: when its fetch or decode\n * fails a moment later it still passes its own ownership check and hard\n * navigates to *its* URL, overwriting the document load started here.\n */\n leaveSpaSuperseding: (url: string, fromUrl: string) => Promise<never>;\n}\n\n/**\n * The two ownership-aware ways out of the SPA.\n *\n * Both wrap {@link leaveSpa} with the question \"is this navigation still the\n * one the user is waiting for?\" — asked before leaving on a failure, answered\n * by force on a path that decided before any navigation began. They live\n * together because every caller of `leaveSpa()` needs one or the other, and\n * an unguarded call is the bug (TIM-1275, TIM-1276).\n */\nexport function createSpaExits({ currentOwner, supersede }: SpaExitDeps): SpaExits {\n async function leaveSpaIfOwned(owner: RenderOwner, url: string, fromUrl: string): Promise<void> {\n if (currentOwner() !== owner) return;\n await leaveSpa(url, fromUrl);\n }\n\n function leaveSpaSuperseding(url: string, fromUrl: string): Promise<never> {\n supersede();\n return leaveSpa(url, fromUrl);\n }\n\n return { leaveSpaIfOwned, leaveSpaSuperseding };\n}\n\nexport interface NavigationRecoveryDeps {\n /** The owner that holds the router right now, or null when idle. */\n currentOwner: () => RenderOwner | null;\n leaveSpaIfOwned: SpaExits['leaveSpaIfOwned'];\n /** Replace the current entry with `url` — the router's own `navigate()`. */\n navigate: (url: string) => Promise<void>;\n}\n\n/**\n * Answer the failures an RSC fetch can end on, or report that this one is not\n * ours to answer.\n *\n * Returns `true` when the error was handled; the caller rethrows on `false`.\n * Handling a failure by leaving the SPA never resolves at all — the document\n * is going away and the caller must not carry on rendering into it.\n *\n * There is exactly one family of these and one response to it, so it lives in\n * one function rather than once per fetch path. `navigate()`, `refresh()` and\n * an uncached traversal all reach it: the latter two used to rethrow instead,\n * and since nobody awaits a traversal that meant pressing Back on a 500 left\n * the user on the old document with an unhandled rejection (TIM-1277).\n *\n * Order matters in exactly one way: `recordSkew()` ends in error-*text*\n * matching, so it goes last and can never pre-empt a framework control-flow\n * signal whose message happens to read like a stale bundle. The class checks\n * above it are mutually exclusive, so their order is free.\n *\n * Must be called from inside the `runNavigation()` callback — see\n * {@link SpaExits.leaveSpaIfOwned}.\n */\nexport function createNavigationRecovery({\n currentOwner,\n leaveSpaIfOwned,\n navigate,\n}: NavigationRecoveryDeps) {\n return async function recoverFromNavigationError(\n error: unknown,\n owner: RenderOwner,\n url: string,\n fromUrl: string\n ): Promise<boolean> {\n if (error instanceof RedirectError) {\n // Same ownership rule as leaving the SPA: a superseded navigation must\n // not steer the document to the destination it was abandoned for.\n if (currentOwner() !== owner) return true;\n await navigate(error.redirectUrl);\n return true;\n }\n // A server error, a non-RSC response and a version skew all end the same\n // way: hand `url` back to the server as a document load, so it can render\n // the outcome as HTML (design/10-error-handling.md §\"Error Page Rendering\n // for Client Navigation\").\n if (\n error instanceof ServerErrorResponse ||\n error instanceof NonRscResponse ||\n recordSkew(error)\n ) {\n await leaveSpaIfOwned(owner, url, fromUrl);\n return true;\n }\n return false;\n };\n}\n\n/**\n * Perform the full document load.\n *\n * `fromUrl` is the URL the navigation departed from, passed explicitly\n * because the address bar may have already been updated.\n *\n * When the target differs from the departure point only by #fragment,\n * assigning `href` is a hash change, not a load — so assign first (to update\n * the address bar) and then reload (TIM-1235).\n */\nexport function hardNavigate(url: string, fromUrl: string): void {\n const current = new URL(fromUrl, window.location.origin);\n const target = new URL(url, window.location.origin);\n if (target.pathname === current.pathname && target.search === current.search) {\n window.location.href = url;\n window.location.reload();\n } else {\n window.location.href = url;\n }\n}\n","'use client';\n\n/**\n * Framework-injected React error boundary.\n *\n * Catches errors thrown by children and renders a fallback component\n * with the appropriate props based on error type:\n * - DenySignal (4xx) → { status, data }\n * - RenderError (5xx) → { error, digest, reset }\n * - Unhandled error → { error, digest: null, reset }\n *\n * The `status` prop controls which errors this boundary catches:\n * - Specific code (e.g. 403) → only that status\n * - Category (400) → any 4xx\n * - Category (500) → any 5xx\n * - Omitted → catches everything (error.tsx behavior)\n *\n * See design/10-error-handling.md §\"Status-Code Files\"\n */\n\nimport { Component, createContext, createElement, useContext, type ReactNode } from 'react';\nimport { getSsrData } from './ssr-data.ts';\nimport { getRouterOrNull } from './router-ref.ts';\nimport { markStaleFromError } from './stale-client.ts';\nimport { StatusPageMarker } from './status-page-marker.tsx';\nimport { leaveSpa } from './router-effects.ts';\n// Page unload detection: when the page is being torn down, error boundaries\n// suppress activation — the error is from the aborted connection, not the\n// application. unload-guard.ts is the single owner of that state; this module\n// used to keep its own copy, which drifted (TIM-1260).\nimport { isPageUnloading } from './unload-guard.ts';\n\n// ─── Ancestry Context ────────────────────────────────────────────────────────\n\n/**\n * Tracks the chain of segment keys from root to the current boundary.\n * Each keyed `TimberErrorBoundary` appends its `segmentKey`; unkeyed\n * slot boundaries pass the parent value through. The claim rule uses\n * this to find the first *reachable* owner from the `owners` list,\n * eliminating the owner-below-throw-site escape. See TIM-1450.\n */\nconst DenyAncestryContext = createContext<readonly string[]>([]);\n\nexport { DenyAncestryContext };\n\n// ─── Digest Types ────────────────────────────────────────────────────────────\n\n/** Structured digest returned by RSC onError for DenySignal. */\ninterface DenyDigest {\n type: 'deny';\n status: number;\n data: unknown;\n /**\n * Ordered candidate owners — segment keys of segments with matching deny\n * pages, best match first. Present = addressed (a boundary should render\n * it). In-tree hoists produce a single-element list; late addressing\n * (TIM-1450) produces the full candidate list so the boundary can find the\n * first *reachable* owner via its ancestry context.\n */\n owners?: string[];\n}\n\n/** Structured digest returned by RSC onError for RenderError. */\ninterface RenderErrorDigest {\n type: 'render-error';\n code: string;\n data: unknown;\n status: number;\n}\n\n/** Structured digest returned by RSC onError for RedirectSignal. */\ninterface RedirectDigest {\n type: 'redirect';\n location: string;\n status: number;\n}\n\ntype ParsedDigest = DenyDigest | RenderErrorDigest | RedirectDigest;\n\n// ─── Props & State ───────────────────────────────────────────────────────────\n\nexport interface TimberErrorBoundaryProps {\n /** The component to render when an error is caught. */\n fallbackComponent?: (...args: unknown[]) => ReactNode;\n /**\n * Status code filter. If set, only catches errors matching this status.\n * 400 = any 4xx, 500 = any 5xx, specific number = exact match.\n */\n status?: number;\n /**\n * When true, this boundary wraps a parallel slot. Slot denials are\n * graceful degradation — they must NOT change the HTTP status code.\n */\n isSlotBoundary?: boolean;\n /**\n * Segment key of the segment that declared this boundary. Only meaningful\n * against a deny digest carrying `owners`. See the decline rule in\n * `render()` and TIM-1356.\n */\n segmentKey?: string;\n /**\n * Catch denies only; re-throw everything else. Set on the copy of a\n * segment's boundaries placed outside its layout, which gives a deny thrown\n * from that layout's own chrome a catcher keyed to the segment — without\n * changing where a 5xx from the same chrome goes. See TIM-1450.\n */\n denyOnly?: boolean;\n /**\n * The deny chain's terminal: the outermost framework boundary, which claims\n * any deny that streamed past every segment boundary and renders the\n * framework default page. Implies `denyOnly`. Only a deny thrown from a\n * layout's chrome with no status file at or above that layout gets here —\n * the segment copies (`denyOnly`) claim every other shape. See TIM-1450.\n */\n lastResort?: boolean;\n children: ReactNode;\n}\n\ninterface TimberErrorBoundaryState {\n hasError: boolean;\n error: Error | null;\n}\n\n// Module-level guard: the first boundary to catch a given redirect digest\n// schedules the navigation and marks it handled here. Subsequent renders\n// (React retries the error-boundary render path multiple times in dev, and\n// outer boundaries that also see the error) become no-ops, preventing\n// duplicate router.navigate() calls.\nconst _handledRedirects = new WeakSet<object>();\n\n// The same guard for the terminal's exit: React may render the fallback\n// path more than once for one error, and the document load must start once.\nconst _handledExits = new WeakSet<object>();\n\n// Companion guard keyed by location: client-render redirect() (the\n// next/navigation shim) throws a FRESH error instance on every render\n// attempt, and React replays the errored render — so the WeakSet alone\n// would schedule the navigation twice. While a navigation to a location\n// is pending, further redirect errors for the same location are no-ops.\nlet _pendingRedirectLocation: string | null = null;\n\n// ─── Component ───────────────────────────────────────────────────────────────\n\nexport class TimberErrorBoundary extends Component<\n TimberErrorBoundaryProps,\n TimberErrorBoundaryState\n> {\n constructor(props: TimberErrorBoundaryProps) {\n super(props);\n this.state = { hasError: false, error: null };\n }\n\n static getDerivedStateFromError(error: Error): TimberErrorBoundaryState {\n // Suppress error boundaries during page unload (refresh/navigate away).\n // The aborted connection causes React's streaming hydration to error,\n // but the page is about to be replaced — showing an error boundary\n // would be a jarring flash for the user.\n if (isPageUnloading()) {\n return { hasError: false, error: null };\n }\n\n return { hasError: true, error };\n }\n\n componentDidCatch(error: Error): void {\n // A stale bundle most often surfaces here rather than as a rejected\n // navigation: a client reference missing from this build's manifest, or\n // a lazily-imported chunk the CDN no longer has, thrown during render.\n //\n // Recording it is all that happens — this boundary still renders its\n // fallback, and the recovery is the user's next navigation, which the\n // router turns into a full document load. Reloading from here would be a\n // page load the user did not ask for, on a page that may be mid-rollout\n // and would come back just as stale (TIM-1275).\n markStaleFromError(error);\n }\n\n componentDidUpdate(prevProps: TimberErrorBoundaryProps): void {\n // Reset error state when children change (e.g. client-side navigation).\n // Without this, navigating from one error page to another keeps the\n // stale error — getDerivedStateFromError doesn't re-fire for new children.\n if (this.state.hasError && prevProps.children !== this.props.children) {\n this.setState({ hasError: false, error: null });\n }\n }\n\n /** Reset the error state so children re-render. */\n private reset = () => {\n this.setState({ hasError: false, error: null });\n };\n\n static override contextType = DenyAncestryContext;\n declare context: readonly string[];\n\n private get childAncestry(): readonly string[] {\n const parent = this.context ?? [];\n const key = this.props.segmentKey;\n // A segment has two keyed boundary sets (outside and inside its layout);\n // the inner one sees its own key already present and adds nothing.\n return key && !parent.includes(key) ? [...parent, key] : parent;\n }\n\n render(): ReactNode {\n if (!this.state.hasError || !this.state.error) {\n return createElement(\n DenyAncestryContext.Provider,\n { value: this.childAncestry },\n this.props.children\n );\n }\n\n const error = this.state.error;\n const parsed = parseDigest(error);\n\n // RedirectSignal handling splits by environment:\n //\n // - SSR (no window): re-throw so the Fizz shell fails and the server\n // pipeline catch block can produce a proper HTTP 3xx response. See\n // design/04-authorization.md.\n //\n // - Client (window exists): a RedirectSignal that leaked into the RSC\n // Flight payload during client navigation would otherwise bubble past\n // every error boundary to `window.onerror`, which in dev triggers the\n // Vite error overlay (false positive — navigation itself works).\n // Instead, ask the router to perform an SPA navigation to the target\n // and render nothing while it takes over. The router's own catch path\n // handles cross-origin / hard navigations. See TIM-838.\n if (parsed?.type === 'redirect') {\n if (typeof window === 'undefined') {\n throw error;\n }\n // Dedupe: React may render the error-boundary fallback path more\n // than once for a single caught error (dev-mode double render, and\n // additional boundaries above us in the tree). Only schedule the\n // navigation on the first render that sees this error instance.\n const alreadyHandled =\n _handledRedirects.has(error) || _pendingRedirectLocation === parsed.location;\n if (!alreadyHandled) {\n _handledRedirects.add(error);\n const router = getRouterOrNull();\n if (router) {\n _pendingRedirectLocation = parsed.location;\n // Schedule the navigation outside the render phase to avoid\n // setState-during-render warnings from router state updates.\n queueMicrotask(() => {\n router\n .navigate(parsed.location, { replace: true })\n .catch(() => {\n // Fall back to a hard navigation if the soft navigation fails.\n window.location.href = parsed.location;\n })\n .finally(() => {\n _pendingRedirectLocation = null;\n });\n });\n } else {\n // No router available (shouldn't happen post-bootstrap) — fall\n // back to a hard navigation so the user still ends up at the\n // target.\n window.location.href = parsed.location;\n }\n }\n // Nothing to show, but still a subtree replaced: a layout that\n // redirected after I/O never mounted its children, and the commit that\n // renders this empty spot must not teach the cache otherwise — or the\n // redirect's own navigation would advertise those layouts as mounted\n // and the server would skip into an outlet the client does not have.\n return this.markStatusPage(null);\n }\n\n // Owner-addressed denies: the digest carries an ordered `owners` list\n // of segment keys with matching deny pages. This boundary claims the\n // deny only when it is the first *reachable* owner — i.e. its\n // segmentKey appears in `owners` AND in its ancestry (the chain from\n // root to here). Unreachable owners (below the throw site) are skipped,\n // and the first ancestor that IS reachable claims. This eliminates the\n // owner-below-throw-site escape (TIM-1450).\n //\n // Unkeyed boundaries (slots, or boundaries without deny chains) never\n // claim an addressed deny. See TIM-1356.\n if (parsed?.type === 'deny' && Array.isArray(parsed.owners) && parsed.owners.length > 0) {\n // Slot boundaries always claim addressed denies — graceful degradation\n // within the slot is correct regardless of which segment the owner\n // chain resolved to. Without this, the deny escapes the slot and\n // becomes a page-level 4xx. See TIM-1450.\n if (!this.props.lastResort && !this.props.isSlotBoundary) {\n const reachable = parsed.owners.find((k: string) => this.childAncestry.includes(k));\n if (!this.props.segmentKey || reachable !== this.props.segmentKey) {\n throw error;\n }\n }\n }\n // Deny-only boundaries (the outer segment copies) re-throw everything\n // else so a crash in a layout's chrome still reaches the documented 5xx\n // flow instead of a status page for an unrelated error.\n if (this.props.denyOnly && !this.props.lastResort && parsed?.type !== 'deny') {\n throw error;\n }\n // The terminal is where that flow ends on the client. A non-deny row\n // nothing claimed — a RenderError or a plain crash on a route with no\n // matching `error.tsx` — has no page to render in-SPA; React would\n // unmount the root. So the terminal does what the router does for an\n // error envelope (design/10 §\"Error Page Rendering for Client\n // Navigation\"): leave the SPA, and let the document request render the\n // real error page with the real status. On the server it re-throws so\n // the Fizz shell fails and the pipeline serves that page directly. The\n // envelope still covers errors that land inside Flight's opening window;\n // this covers the ones that land after it, which used to be held for\n // (TIM-1495, TIM-1497).\n if (this.props.lastResort && parsed?.type !== 'deny') {\n if (typeof window === 'undefined') {\n throw error;\n }\n if (!_handledExits.has(error)) {\n _handledExits.add(error);\n const router = getRouterOrNull();\n const current = window.location.pathname + window.location.search;\n // The row arrived for the navigation in flight, whose URL is not on\n // the address bar yet; fall back to the current document.\n const target = router?.getPendingUrl() ?? current;\n queueMicrotask(() => {\n void leaveSpa(target, current);\n });\n }\n return null;\n }\n\n // If this boundary has a status filter, check whether the error matches.\n // Non-matching errors re-throw so an outer boundary can catch them.\n if (this.props.status != null) {\n const errorStatus = getErrorStatus(parsed, error);\n if (errorStatus == null || !statusMatches(this.props.status, errorStatus)) {\n // Re-throw: this boundary doesn't handle this error.\n throw error;\n }\n }\n\n // Report DenySignal handling so the pipeline skips the redundant\n // renderDenyPage() re-render.\n //\n // For segment boundaries: also set the HTTP status code from the\n // DenySignal so the Response has the correct 4xx status. This runs\n // synchronously during Fizz rendering, BEFORE onShellReady.\n //\n // For slot boundaries: set _denyHandledByBoundary but do NOT change\n // statusCode — slot denials are graceful degradation.\n //\n // See TIM-664, LOCAL-298.\n if (parsed?.type === 'deny') {\n const ssrData = getSsrData();\n if (ssrData?._navContext) {\n ssrData._navContext._denyHandledByBoundary = true;\n if (!this.props.isSlotBoundary) {\n ssrData._navContext.statusCode = parsed.status;\n }\n }\n }\n\n return this.markStatusPage(this.renderFallback(parsed, error));\n }\n\n /**\n * The status page this boundary shows for a row it claimed. Its shape is\n * the file's documented contract: TSX status files get `{ status,\n * data }`, `error.tsx` gets `{ error, digest, reset }` (plus\n * the deny extras for dual-shape implementations — without the error prop,\n * an error.tsx reading error.message crashes on deny fall-through, TIM-1081).\n */\n private renderFallback(parsed: ParsedDigest | null, error: Error): ReactNode {\n if (parsed?.type === 'deny') {\n if (this.props.status != null) {\n return createElement(this.props.fallbackComponent as never, {\n status: parsed.status,\n data: parsed.data,\n });\n }\n return createElement(this.props.fallbackComponent as never, {\n error: denyError(parsed.status, error),\n digest: null,\n reset: this.reset,\n status: parsed.status,\n data: parsed.data,\n });\n }\n\n // 5xx / RenderError / unhandled error\n const digest =\n parsed?.type === 'render-error' ? { code: parsed.code, data: parsed.data } : null;\n\n return createElement(this.props.fallbackComponent as never, {\n error,\n digest,\n reset: this.reset,\n });\n }\n\n /**\n * A status page (or, for a redirect row, nothing at all) replaced part of\n * the main route, and the segment cache must not learn a chain that never\n * mounted; the marker tells the navigation that commits this tree. A boundary that declined in `render()` re-threw\n * before getting here, so only the boundary that put a page on screen\n * speaks. A slot degrading leaves the page and its chain intact, so slot\n * boundaries stay silent. See `status-page-marker.tsx` (TIM-1495).\n */\n private markStatusPage(page: ReactNode): ReactNode {\n if (this.props.isSlotBoundary) return page;\n return createElement(StatusPageMarker, null, page);\n }\n}\n\n// ─── Helpers ─────────────────────────────────────────────────────────────────\n\n/**\n * Parse the structured digest from the error.\n * React sets `error.digest` from the string returned by RSC's onError.\n */\nfunction parseDigest(error: Error): ParsedDigest | null {\n const raw = (error as { digest?: string }).digest;\n if (typeof raw !== 'string') return null;\n try {\n const parsed = JSON.parse(raw);\n if (parsed && typeof parsed === 'object' && typeof parsed.type === 'string') {\n return parsed as ParsedDigest;\n }\n } catch (e) {\n console.debug('[timber] error digest is not valid JSON:', e instanceof Error ? e.message : e);\n }\n return null;\n}\n\n/**\n * The Error an `error.tsx` serving a deny should receive.\n *\n * The error that actually arrives here is React's, not the framework's: a\n * server-thrown error crossing Flight has its message and name replaced with\n * a generic redacted string in production, by design. An `error.tsx` written\n * per the docs branches on `error.message` — so handing it the redacted error\n * gives it nothing to branch on, while the in-tree path hands the same file a\n * `DenySignal` named error reading `Access denied with status <N>` (see\n * `renderDenyEntry`). This reconstructs that shape from the digest, which is\n * where the status already is.\n *\n * Derived from the status alone — no stack, no server data — so it is safe on\n * the client for the same reason the digest is. See design/13-security.md\n * §\"Errors don't leak\", TIM-1081.\n */\nfunction denyError(status: number, original: Error): Error {\n // A dev build may have preserved something more useful than the redaction.\n if (original.name === 'DenySignal') return original;\n const error = new Error(`Access denied with status ${status}`);\n error.name = 'DenySignal';\n return error;\n}\n\n/**\n * Extract the HTTP status code from a parsed digest or error message.\n * Falls back to message pattern matching for errors without a digest.\n */\nfunction getErrorStatus(parsed: ParsedDigest | null, error: Error): number | null {\n if (parsed?.type === 'deny') return parsed.status;\n if (parsed?.type === 'render-error') return parsed.status;\n if (parsed?.type === 'redirect') return parsed.status;\n\n // Fallback: parse DenySignal message pattern for errors that lost their digest\n const match = error.message.match(/^Access denied with status (\\d+)$/);\n if (match) return parseInt(match[1], 10);\n\n // Unhandled errors are implicitly 500\n return 500;\n}\n\n/**\n * Check whether an error's status matches the boundary's status filter.\n * Category markers (400, 500) match any status in that range.\n */\nfunction statusMatches(boundaryStatus: number, errorStatus: number): boolean {\n // Category catch-all: 400 matches any 4xx, 500 matches any 5xx\n if (boundaryStatus === 400) return errorStatus >= 400 && errorStatus <= 499;\n if (boundaryStatus === 500) return errorStatus >= 500 && errorStatus <= 599;\n // Exact match\n return boundaryStatus === errorStatus;\n}\n"],"mappings":"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"}
@@ -2,8 +2,8 @@ import { i as scanProjectSourceFiles, t as buildSourceExtensions } from "./scan-
2
2
  import { dirname, isAbsolute, join } from "node:path";
3
3
  import { createRequire } from "node:module";
4
4
  import { mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
5
- import { createHash, randomUUID } from "node:crypto";
6
5
  import { Server } from "node:tls";
6
+ import { createHash, randomUUID } from "node:crypto";
7
7
  import { mkdir, readFile, rename, stat, writeFile } from "node:fs/promises";
8
8
  //#region src/plugins/dev-discovery.ts
9
9
  /** Relative path of the discovery directory within the project root. */
@@ -301,4 +301,4 @@ async function writeGraphCache(root, cache) {
301
301
  //#endregion
302
302
  export { readGraphCache as a, isPidAlive as c, hashFingerprintEntries as i, registerDevDiscovery as l, computeProjectFingerprint as n, writeGraphCache as o, fingerprintEntriesForFiles as r, DEV_DISCOVERY_DIR as s, collectBaseFingerprintEntries as t };
303
303
 
304
- //# sourceMappingURL=graph-cache-Cv3njEH8.js.map
304
+ //# sourceMappingURL=graph-cache-CP4GEmf9.js.map