@timber-js/app 0.2.0-alpha.186 → 0.2.0-alpha.188
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_chunks/{actions-C-Rw9vPc.js → actions-35jnMdeJ.js} +4 -3
- package/dist/_chunks/{actions-C-Rw9vPc.js.map → actions-35jnMdeJ.js.map} +1 -1
- package/dist/_chunks/als-registry-C6kcfprT.js +41 -0
- package/dist/_chunks/als-registry-C6kcfprT.js.map +1 -0
- package/dist/_chunks/{als-slots-mFweg276.js → als-slots-BEEIPKYm.js} +3 -4
- package/dist/_chunks/{als-slots-mFweg276.js.map → als-slots-BEEIPKYm.js.map} +1 -1
- package/dist/_chunks/{cache-api-Cd0VZ_Pd.js → cache-api-DjNrIWRR.js} +7 -13
- package/dist/_chunks/cache-api-DjNrIWRR.js.map +1 -0
- package/dist/_chunks/cli-check-CpmN7Nh-.js +256 -0
- package/dist/_chunks/cli-check-CpmN7Nh-.js.map +1 -0
- package/dist/_chunks/cli-schema-sync-CGMp_Psg.js +298 -0
- package/dist/_chunks/cli-schema-sync-CGMp_Psg.js.map +1 -0
- package/dist/_chunks/{cloudflare-Cs0uZXea.js → cloudflare-CGP6BZKO.js} +4 -3
- package/dist/_chunks/{cloudflare-Cs0uZXea.js.map → cloudflare-CGP6BZKO.js.map} +1 -1
- package/dist/_chunks/convention-lint-kXsgc_-7.js +784 -0
- package/dist/_chunks/convention-lint-kXsgc_-7.js.map +1 -0
- package/dist/_chunks/{error-boundary-DpYRI_I1.js → error-boundary-D-ODYX41.js} +25 -2
- package/dist/_chunks/error-boundary-D-ODYX41.js.map +1 -0
- package/dist/_chunks/file-cache-DmX7OqZP.js +454 -0
- package/dist/_chunks/file-cache-DmX7OqZP.js.map +1 -0
- package/dist/_chunks/{logger-N7e5auP0.js → logger-D8xJZXIN.js} +27 -40
- package/dist/_chunks/logger-D8xJZXIN.js.map +1 -0
- package/dist/_chunks/mdx-file-C005ay-P.js +25 -0
- package/dist/_chunks/mdx-file-C005ay-P.js.map +1 -0
- package/dist/_chunks/{plugin-context-DEGLSJs3.js → plugin-context-rCinWLiE.js} +7 -2
- package/dist/_chunks/{plugin-context-DEGLSJs3.js.map → plugin-context-rCinWLiE.js.map} +1 -1
- package/dist/_chunks/purge-store-Byr8XOjU.js +14 -0
- package/dist/_chunks/purge-store-Byr8XOjU.js.map +1 -0
- package/dist/_chunks/{resolve-schema-Dz3fcFUo.js → resolve-schema-5ma5pp1b.js} +2 -2
- package/dist/_chunks/{resolve-schema-Dz3fcFUo.js.map → resolve-schema-5ma5pp1b.js.map} +1 -1
- package/dist/_chunks/rsc-media-type-DRqE_lD_.js +46 -0
- package/dist/_chunks/rsc-media-type-DRqE_lD_.js.map +1 -0
- package/dist/_chunks/{cli-schema-sync-CKgHC2MB.js → scanner-C8b0Gcw3.js} +5 -298
- package/dist/_chunks/scanner-C8b0Gcw3.js.map +1 -0
- package/dist/_chunks/schema-bridge-Cc2Gngu1.js +199 -0
- package/dist/_chunks/schema-bridge-Cc2Gngu1.js.map +1 -0
- package/dist/_chunks/segment-classify-C539Pa2O.js.map +1 -1
- package/dist/_chunks/{param-value-C8TNYchQ.js → segment-context-CjOlyB8Y.js} +33 -2
- package/dist/_chunks/segment-context-CjOlyB8Y.js.map +1 -0
- package/dist/_chunks/segment-keys-BawYuNFO.js.map +1 -1
- package/dist/_chunks/{use-query-states-DFvWd-EA.js → use-query-states-BbU5Ge1V.js} +74 -26
- package/dist/_chunks/use-query-states-BbU5Ge1V.js.map +1 -0
- package/dist/_chunks/{navigation-root-B29qg0_T.js → use-segment-params-C4r4BD9T.js} +129 -5
- package/dist/_chunks/use-segment-params-C4r4BD9T.js.map +1 -0
- package/dist/_chunks/walkers-RzN6AFjr.js +141 -0
- package/dist/_chunks/walkers-RzN6AFjr.js.map +1 -0
- package/dist/adapters/cloudflare-dev.js +1 -1
- package/dist/adapters/cloudflare-kv-cache.js +1 -1
- package/dist/adapters/cloudflare.d.ts.map +1 -1
- package/dist/adapters/cloudflare.js +1 -1
- package/dist/adapters/compress-module.d.ts +12 -0
- package/dist/adapters/compress-module.d.ts.map +1 -1
- package/dist/adapters/nitro.js +54 -2
- package/dist/adapters/nitro.js.map +1 -1
- package/dist/cache/index.js +1 -1
- package/dist/cdn/cloudflare-purge.js +30 -0
- package/dist/cdn/cloudflare-purge.js.map +1 -0
- package/dist/cdn/fastly-purge.js +33 -0
- package/dist/cdn/fastly-purge.js.map +1 -0
- package/dist/cdn/index.js +94 -0
- package/dist/cdn/index.js.map +1 -0
- package/dist/cdn/workers-cache-purge.js +35 -0
- package/dist/cdn/workers-cache-purge.js.map +1 -0
- package/dist/cli-check.d.ts +153 -0
- package/dist/cli-check.d.ts.map +1 -0
- package/dist/cli.d.ts +34 -8
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +46 -21
- package/dist/cli.js.map +1 -1
- package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -1
- package/dist/client/browser-entry/index.d.ts +1 -1
- package/dist/client/browser-entry/index.d.ts.map +1 -1
- package/dist/client/error-boundary.d.ts +6 -0
- package/dist/client/error-boundary.d.ts.map +1 -1
- package/dist/client/error-boundary.js +1 -1
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +27 -33
- package/dist/client/index.js.map +1 -1
- package/dist/client/internal.js +33 -17
- package/dist/client/internal.js.map +1 -1
- package/dist/client/link.d.ts +1 -7
- package/dist/client/link.d.ts.map +1 -1
- package/dist/client/navigation-commit.d.ts +16 -0
- package/dist/client/navigation-commit.d.ts.map +1 -1
- package/dist/client/router-pipeline.d.ts.map +1 -1
- package/dist/client/router.d.ts.map +1 -1
- package/dist/client/rsc-fetch.d.ts +9 -1
- package/dist/client/rsc-fetch.d.ts.map +1 -1
- package/dist/client/segment-cache.d.ts +14 -0
- package/dist/client/segment-cache.d.ts.map +1 -1
- package/dist/client/use-query-states.d.ts +9 -3
- package/dist/client/use-query-states.d.ts.map +1 -1
- package/dist/codec.js +1 -1
- package/dist/cookies/define-cookie.d.ts.map +1 -1
- package/dist/cookies/index.js +2 -2
- package/dist/cookies/index.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +142 -477
- package/dist/index.js.map +1 -1
- package/dist/params/index.js +1 -1
- package/dist/plugin-context.d.ts +15 -0
- package/dist/plugin-context.d.ts.map +1 -1
- package/dist/plugins/routing.d.ts +0 -9
- package/dist/plugins/routing.d.ts.map +1 -1
- package/dist/plugins/shims.d.ts.map +1 -1
- package/dist/plugins/static-build.d.ts +24 -0
- package/dist/plugins/static-build.d.ts.map +1 -1
- package/dist/routing/codegen-shared.d.ts +3 -44
- package/dist/routing/codegen-shared.d.ts.map +1 -1
- package/dist/routing/codegen-types.d.ts +10 -31
- package/dist/routing/codegen-types.d.ts.map +1 -1
- package/dist/routing/codegen-write.d.ts +51 -0
- package/dist/routing/codegen-write.d.ts.map +1 -0
- package/dist/routing/codegen.d.ts.map +1 -1
- package/dist/routing/convention-lint.d.ts +18 -4
- package/dist/routing/convention-lint.d.ts.map +1 -1
- package/dist/routing/export-detect.d.ts +16 -0
- package/dist/routing/export-detect.d.ts.map +1 -1
- package/dist/routing/index.js +3 -2
- package/dist/routing/link-codegen.d.ts +19 -4
- package/dist/routing/link-codegen.d.ts.map +1 -1
- package/dist/routing/manifest-codegen.d.ts +1 -7
- package/dist/routing/manifest-codegen.d.ts.map +1 -1
- package/dist/routing/segment-keys.d.ts +22 -0
- package/dist/routing/segment-keys.d.ts.map +1 -1
- package/dist/routing/types.d.ts +0 -6
- package/dist/routing/types.d.ts.map +1 -1
- package/dist/schema-bridge.d.ts +60 -9
- package/dist/schema-bridge.d.ts.map +1 -1
- package/dist/search-params/define.d.ts +62 -8
- package/dist/search-params/define.d.ts.map +1 -1
- package/dist/search-params/index.d.ts +0 -1
- package/dist/search-params/index.d.ts.map +1 -1
- package/dist/search-params/index.js +66 -29
- package/dist/search-params/index.js.map +1 -1
- package/dist/search-params/parse-total.d.ts +70 -0
- package/dist/search-params/parse-total.d.ts.map +1 -0
- package/dist/search-params/wrappers.d.ts +26 -3
- package/dist/search-params/wrappers.d.ts.map +1 -1
- package/dist/server/access-gate.d.ts +19 -8
- package/dist/server/access-gate.d.ts.map +1 -1
- package/dist/server/action-handler.d.ts.map +1 -1
- package/dist/server/als-registry.d.ts +16 -0
- package/dist/server/als-registry.d.ts.map +1 -1
- package/dist/server/chain-url-parts.d.ts +2 -3
- package/dist/server/chain-url-parts.d.ts.map +1 -1
- package/dist/server/compress.d.ts.map +1 -1
- package/dist/server/deny-boundary.d.ts +148 -15
- package/dist/server/deny-boundary.d.ts.map +1 -1
- package/dist/server/deny-renderer.d.ts +2 -2
- package/dist/server/deny-renderer.d.ts.map +1 -1
- package/dist/server/error-boundary-wrapper.d.ts +85 -15
- package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
- package/dist/server/index.d.ts +0 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +3 -2
- package/dist/server/index.js.map +1 -1
- package/dist/server/internal.d.ts +3 -1
- package/dist/server/internal.d.ts.map +1 -1
- package/dist/server/internal.js +344 -237
- package/dist/server/internal.js.map +1 -1
- package/dist/server/metadata-collector.d.ts +52 -0
- package/dist/server/metadata-collector.d.ts.map +1 -0
- package/dist/server/param-coercion.d.ts +12 -5
- package/dist/server/param-coercion.d.ts.map +1 -1
- package/dist/server/pipeline-helpers.d.ts.map +1 -1
- package/dist/server/pipeline-outcome.d.ts.map +1 -1
- package/dist/server/pipeline-phases.d.ts.map +1 -1
- package/dist/server/primitives.d.ts +23 -0
- package/dist/server/primitives.d.ts.map +1 -1
- package/dist/server/route-element-builder.d.ts +1 -12
- package/dist/server/route-element-builder.d.ts.map +1 -1
- package/dist/server/rsc-cache-key-guard.d.ts.map +1 -1
- package/dist/server/rsc-entry/deny-fallback.d.ts.map +1 -1
- package/dist/server/rsc-entry/error-renderer.d.ts +1 -1
- package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
- package/dist/server/rsc-entry/helpers.d.ts +0 -7
- package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
- package/dist/server/rsc-entry/index.d.ts +0 -1
- package/dist/server/rsc-entry/index.d.ts.map +1 -1
- package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
- package/dist/server/rsc-entry/revalidate-renderer.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-payload.d.ts +1 -3
- package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-stream.d.ts +12 -0
- package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
- package/dist/server/rsc-entry/ssr-renderer.d.ts +0 -2
- package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
- package/dist/server/skippable-prefix.d.ts +3 -0
- package/dist/server/skippable-prefix.d.ts.map +1 -1
- package/dist/server/slot-resolver.d.ts +18 -19
- package/dist/server/slot-resolver.d.ts.map +1 -1
- package/dist/server/ssr-bridge-types.d.ts +11 -0
- package/dist/server/ssr-bridge-types.d.ts.map +1 -1
- package/dist/server/ssr-entry.d.ts +0 -1
- package/dist/server/ssr-entry.d.ts.map +1 -1
- package/dist/server/state-tree-diff.d.ts +9 -16
- package/dist/server/state-tree-diff.d.ts.map +1 -1
- package/dist/server/static-generator.d.ts.map +1 -1
- package/dist/server/status-code-resolver.d.ts +8 -1
- package/dist/server/status-code-resolver.d.ts.map +1 -1
- package/dist/server/tree-builder.d.ts +28 -43
- package/dist/server/tree-builder.d.ts.map +1 -1
- package/dist/server/types.d.ts +12 -7
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/utils/element-type.d.ts +40 -0
- package/dist/server/utils/element-type.d.ts.map +1 -0
- package/dist/shared/rsc-media-type.d.ts +40 -0
- package/dist/shared/rsc-media-type.d.ts.map +1 -0
- package/dist/shared/segment-info.d.ts +7 -0
- package/dist/shared/segment-info.d.ts.map +1 -1
- package/docs/api/30-api-server.mdx +1 -1
- package/docs/api/33-api-search-params.mdx +38 -16
- package/docs/api/35-api-typescript.mdx +3 -3
- package/docs/api/36-cli.mdx +34 -7
- package/docs/learn/00-introduction.mdx +1 -1
- package/docs/learn/02-pages-and-layouts.mdx +1 -1
- package/docs/learn/05-typed-params.mdx +8 -8
- package/docs/learn/07-typed-routes.mdx +12 -7
- package/docs/learn/11-error-handling.mdx +16 -0
- package/docs/more/01-advanced-routing.mdx +1 -1
- package/docs/more/03-coming-from-nextjs.mdx +2 -2
- package/docs/more/50-ai-agent-instructions.mdx +6 -4
- package/package.json +8 -5
- package/src/adapters/cloudflare.ts +4 -1
- package/src/adapters/compress-module.ts +79 -1
- package/src/cli-check.ts +458 -0
- package/src/cli.ts +59 -24
- package/src/client/browser-entry/action-dispatch.ts +2 -1
- package/src/client/browser-entry/index.ts +0 -5
- package/src/client/error-boundary.tsx +65 -1
- package/src/client/index.ts +14 -3
- package/src/client/link.tsx +65 -64
- package/src/client/navigation-commit.ts +27 -4
- package/src/client/params-context.ts +4 -4
- package/src/client/router-pipeline.ts +4 -0
- package/src/client/router.ts +1 -0
- package/src/client/rsc-fetch.ts +14 -4
- package/src/client/segment-cache.ts +26 -5
- package/src/client/use-query-states.ts +102 -39
- package/src/cookies/define-cookie.ts +6 -1
- package/src/index.ts +20 -3
- package/src/plugin-context.ts +26 -0
- package/src/plugins/routing.ts +84 -146
- package/src/plugins/shims.ts +0 -1
- package/src/plugins/static-build.ts +78 -24
- package/src/routing/codegen-shared.ts +3 -79
- package/src/routing/codegen-types.ts +10 -31
- package/src/routing/codegen-write.ts +139 -0
- package/src/routing/codegen.ts +56 -182
- package/src/routing/convention-lint.ts +139 -40
- package/src/routing/export-detect.ts +151 -7
- package/src/routing/link-codegen.ts +32 -65
- package/src/routing/manifest-codegen.ts +1 -59
- package/src/routing/scanner.ts +3 -3
- package/src/routing/segment-keys.ts +37 -0
- package/src/routing/types.ts +0 -6
- package/src/schema-bridge.ts +180 -58
- package/src/search-params/define.ts +102 -37
- package/src/search-params/index.ts +0 -1
- package/src/search-params/parse-total.ts +78 -0
- package/src/search-params/wrappers.ts +60 -11
- package/src/server/access-gate.tsx +61 -68
- package/src/server/action-handler.ts +1 -4
- package/src/server/als-registry.ts +16 -0
- package/src/server/chain-url-parts.ts +2 -3
- package/src/server/compress.ts +9 -1
- package/src/server/deny-boundary.ts +269 -41
- package/src/server/deny-renderer.ts +32 -21
- package/src/server/error-boundary-wrapper.ts +166 -79
- package/src/server/index.ts +1 -3
- package/src/server/internal.ts +2 -2
- package/src/server/metadata-collector.ts +115 -0
- package/src/server/param-coercion.ts +13 -61
- package/src/server/pipeline-helpers.ts +2 -2
- package/src/server/pipeline-outcome.ts +2 -1
- package/src/server/pipeline-phases.ts +9 -9
- package/src/server/primitives.ts +25 -0
- package/src/server/route-element-builder.ts +169 -194
- package/src/server/rsc-cache-key-guard.ts +2 -8
- package/src/server/rsc-entry/deny-fallback.ts +3 -2
- package/src/server/rsc-entry/error-renderer.ts +31 -11
- package/src/server/rsc-entry/helpers.ts +3 -12
- package/src/server/rsc-entry/index.ts +0 -5
- package/src/server/rsc-entry/render-route.ts +14 -11
- package/src/server/rsc-entry/revalidate-renderer.ts +2 -1
- package/src/server/rsc-entry/rsc-payload.ts +104 -20
- package/src/server/rsc-entry/rsc-stream.ts +25 -2
- package/src/server/rsc-entry/ssr-renderer.ts +28 -16
- package/src/server/skippable-prefix.ts +21 -39
- package/src/server/slot-resolver.ts +69 -189
- package/src/server/ssr-bridge-types.ts +12 -0
- package/src/server/ssr-entry.ts +8 -6
- package/src/server/state-tree-diff.ts +11 -62
- package/src/server/static-generator.ts +3 -2
- package/src/server/status-code-resolver.ts +28 -11
- package/src/server/tree-builder.ts +35 -228
- package/src/server/types.ts +12 -7
- package/src/server/utils/element-type.ts +72 -0
- package/src/shared/rsc-media-type.ts +43 -0
- package/src/shared/segment-info.ts +7 -0
- package/dist/_chunks/cache-api-Cd0VZ_Pd.js.map +0 -1
- package/dist/_chunks/cli-schema-sync-CKgHC2MB.js.map +0 -1
- package/dist/_chunks/error-boundary-DpYRI_I1.js.map +0 -1
- package/dist/_chunks/logger-N7e5auP0.js.map +0 -1
- package/dist/_chunks/navigation-root-B29qg0_T.js.map +0 -1
- package/dist/_chunks/param-value-C8TNYchQ.js.map +0 -1
- package/dist/_chunks/registry-DbJPKoBp.js +0 -20
- package/dist/_chunks/registry-DbJPKoBp.js.map +0 -1
- package/dist/_chunks/schema-bridge-DT_Tn0Xf.js +0 -119
- package/dist/_chunks/schema-bridge-DT_Tn0Xf.js.map +0 -1
- package/dist/_chunks/segment-context-ZDnXDkbz.js +0 -34
- package/dist/_chunks/segment-context-ZDnXDkbz.js.map +0 -1
- package/dist/_chunks/use-query-states-DFvWd-EA.js.map +0 -1
- package/dist/_chunks/use-segment-params-ClyUNq4d.js +0 -128
- package/dist/_chunks/use-segment-params-ClyUNq4d.js.map +0 -1
- package/dist/_chunks/walkers-BhhwI9TD.js +0 -936
- package/dist/_chunks/walkers-BhhwI9TD.js.map +0 -1
- package/dist/search-params/registry.d.ts +0 -20
- package/dist/search-params/registry.d.ts.map +0 -1
- package/dist/segment-params/define.d.ts +0 -83
- package/dist/segment-params/define.d.ts.map +0 -1
- package/dist/segment-params/index.d.ts +0 -3
- package/dist/segment-params/index.d.ts.map +0 -1
- package/dist/segment-params/index.js +0 -70
- package/dist/segment-params/index.js.map +0 -1
- package/src/search-params/registry.ts +0 -31
- package/src/segment-params/define.ts +0 -226
- package/src/segment-params/index.ts +0 -9
|
@@ -35,6 +35,12 @@ interface DenyDigest {
|
|
|
35
35
|
type: 'deny';
|
|
36
36
|
status: number;
|
|
37
37
|
data: unknown;
|
|
38
|
+
/**
|
|
39
|
+
* Segment key of the segment that owns the matched deny page, present when
|
|
40
|
+
* an in-tree catch site matched a file it may not render at its own
|
|
41
|
+
* position and re-threw to reach the owner. See TIM-1356.
|
|
42
|
+
*/
|
|
43
|
+
ownerKey?: string;
|
|
38
44
|
}
|
|
39
45
|
|
|
40
46
|
/** Structured digest returned by RSC onError for RenderError. */
|
|
@@ -78,6 +84,12 @@ export interface TimberErrorBoundaryProps {
|
|
|
78
84
|
* graceful degradation — they must NOT change the HTTP status code.
|
|
79
85
|
*/
|
|
80
86
|
isSlotBoundary?: boolean;
|
|
87
|
+
/**
|
|
88
|
+
* Segment key of the segment that declared this boundary. Only meaningful
|
|
89
|
+
* against a deny digest carrying an `ownerKey`. See the decline rule in
|
|
90
|
+
* `render()` and TIM-1356.
|
|
91
|
+
*/
|
|
92
|
+
segmentKey?: string;
|
|
81
93
|
children: ReactNode;
|
|
82
94
|
}
|
|
83
95
|
|
|
@@ -209,6 +221,34 @@ export class TimberErrorBoundary extends Component<
|
|
|
209
221
|
return null;
|
|
210
222
|
}
|
|
211
223
|
|
|
224
|
+
// Owner-addressed denies: an in-tree catch site already resolved the
|
|
225
|
+
// fallback chain and matched a file it may not render at its own
|
|
226
|
+
// position, so it re-threw to reach the segment that owns that file.
|
|
227
|
+
// Every boundary but that one declines — otherwise the nearest boundary
|
|
228
|
+
// whose status filter happens to match catches first and renders the
|
|
229
|
+
// deny page at its own depth, wrapped in the layouts the hoist existed
|
|
230
|
+
// to skip, and from a different file than the chain chose. (Boundary
|
|
231
|
+
// nesting is per-segment `{status} → category → error`; the 4xx chain
|
|
232
|
+
// runs status files across ALL segments before any error.tsx, so a
|
|
233
|
+
// leaf error.tsx would otherwise beat a root 404.tsx.)
|
|
234
|
+
//
|
|
235
|
+
// Declining is the default: an unkeyed boundary — a slot boundary, or
|
|
236
|
+
// one from a construction site that builds no deny chains — never
|
|
237
|
+
// claims a deny addressed to a segment. See TIM-1356.
|
|
238
|
+
// `typeof === 'string'`, not a null check: the digest is untrusted JSON —
|
|
239
|
+
// `parseDigest` validates only `type`, and any component can set
|
|
240
|
+
// `error.digest` itself. A non-string `ownerKey` is unequal to every
|
|
241
|
+
// boundary's key, so a truthiness test would let one error decline its
|
|
242
|
+
// way past the entire tree and blank the page, where it would otherwise
|
|
243
|
+
// have hit the nearest matching boundary.
|
|
244
|
+
if (
|
|
245
|
+
parsed?.type === 'deny' &&
|
|
246
|
+
typeof parsed.ownerKey === 'string' &&
|
|
247
|
+
parsed.ownerKey !== this.props.segmentKey
|
|
248
|
+
) {
|
|
249
|
+
throw error;
|
|
250
|
+
}
|
|
251
|
+
|
|
212
252
|
// If this boundary has a status filter, check whether the error matches.
|
|
213
253
|
// Non-matching errors re-throw so an outer boundary can catch them.
|
|
214
254
|
if (this.props.status != null) {
|
|
@@ -272,7 +312,7 @@ export class TimberErrorBoundary extends Component<
|
|
|
272
312
|
});
|
|
273
313
|
}
|
|
274
314
|
return createElement(this.props.fallbackComponent as never, {
|
|
275
|
-
error,
|
|
315
|
+
error: denyError(parsed.status, error),
|
|
276
316
|
digest: null,
|
|
277
317
|
reset: this.reset,
|
|
278
318
|
status: parsed.status,
|
|
@@ -312,6 +352,30 @@ function parseDigest(error: Error): ParsedDigest | null {
|
|
|
312
352
|
return null;
|
|
313
353
|
}
|
|
314
354
|
|
|
355
|
+
/**
|
|
356
|
+
* The Error an `error.tsx` serving a deny should receive.
|
|
357
|
+
*
|
|
358
|
+
* The error that actually arrives here is React's, not the framework's: a
|
|
359
|
+
* server-thrown error crossing Flight has its message and name replaced with
|
|
360
|
+
* a generic redacted string in production, by design. An `error.tsx` written
|
|
361
|
+
* per the docs branches on `error.message` — so handing it the redacted error
|
|
362
|
+
* gives it nothing to branch on, while the in-tree path hands the same file a
|
|
363
|
+
* `DenySignal` named error reading `Access denied with status <N>` (see
|
|
364
|
+
* `renderDenyEntry`). This reconstructs that shape from the digest, which is
|
|
365
|
+
* where the status already is.
|
|
366
|
+
*
|
|
367
|
+
* Derived from the status alone — no stack, no server data — so it is safe on
|
|
368
|
+
* the client for the same reason the digest is. See design/13-security.md
|
|
369
|
+
* §"Errors don't leak", TIM-1081.
|
|
370
|
+
*/
|
|
371
|
+
function denyError(status: number, original: Error): Error {
|
|
372
|
+
// A dev build may have preserved something more useful than the redaction.
|
|
373
|
+
if (original.name === 'DenySignal') return original;
|
|
374
|
+
const error = new Error(`Access denied with status ${status}`);
|
|
375
|
+
error.name = 'DenySignal';
|
|
376
|
+
return error;
|
|
377
|
+
}
|
|
378
|
+
|
|
315
379
|
/**
|
|
316
380
|
* Extract the HTTP status code from a parsed digest or error message.
|
|
317
381
|
* Falls back to message pattern matching for errors without a digest.
|
package/src/client/index.ts
CHANGED
|
@@ -75,11 +75,22 @@ export { useSelectedLayoutSegment, useSelectedLayoutSegments } from './use-selec
|
|
|
75
75
|
export { useActionState, useFormAction } from './form';
|
|
76
76
|
export type { UseActionStateFn, UseActionStateReturn, FormErrorsResult } from './form';
|
|
77
77
|
|
|
78
|
-
// Params
|
|
78
|
+
// Params. Called with no argument this returns the untyped accumulated params;
|
|
79
|
+
// pass the route's SEGMENT_PATH (from its generated `./$segment` module) to get
|
|
80
|
+
// the exact types `app/schema.ts` declares — `useSegmentParams(SEGMENT_PATH)`.
|
|
79
81
|
export { useSegmentParams } from './use-segment-params';
|
|
80
82
|
|
|
81
|
-
// Query states
|
|
82
|
-
//
|
|
83
|
+
// Query states. `definition.useQueryStates()` is the usual form; this standalone
|
|
84
|
+
// export is the inline codec-map form, for shared param bundles reused across
|
|
85
|
+
// routes (design/23-search-params.md §"Client Access").
|
|
86
|
+
//
|
|
87
|
+
// Until TIM-1341 this export did not exist, yet four doc sites told users to
|
|
88
|
+
// import it from here. It type-checked only because the generated `.d.ts`
|
|
89
|
+
// augmented this module with a `useQueryStates` overload — so TypeScript
|
|
90
|
+
// believed in a binding that was never emitted, and the documented import
|
|
91
|
+
// failed at build time. Deleting that overload removed the lie; exporting the
|
|
92
|
+
// hook makes the docs true.
|
|
93
|
+
export { useQueryStates } from './use-query-states';
|
|
83
94
|
|
|
84
95
|
// Cookies — useCookie lives on defineCookie().useCookie().
|
|
85
96
|
// Types still exported for advanced use.
|
package/src/client/link.tsx
CHANGED
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
// Typed Link: design/09-typescript.md §"Typed Link"
|
|
15
15
|
// - href validated against known routes (via codegen overloads, not runtime)
|
|
16
16
|
// - params prop typed per-route, URL interpolated at runtime
|
|
17
|
-
// - searchParams prop
|
|
17
|
+
// - searchParams prop is a query string (from `definition.buildSearchParams()`)
|
|
18
|
+
// or a plain object whose values are String()-coerced
|
|
18
19
|
// - params and fully-resolved string href are mutually exclusive
|
|
19
20
|
// - searchParams and inline query string are mutually exclusive
|
|
20
21
|
|
|
@@ -25,8 +26,6 @@ import {
|
|
|
25
26
|
type ReactNode,
|
|
26
27
|
type MouseEvent as ReactMouseEvent,
|
|
27
28
|
} from 'react';
|
|
28
|
-
import type { SearchParamsDefinition } from '../search-params/define.js';
|
|
29
|
-
import { getSearchParamsDefinition } from '../search-params/registry.js';
|
|
30
29
|
import type { LinkFunction } from './index.js';
|
|
31
30
|
import { classifyUrlSegment, type UrlSegment } from '../routing/segment-classify.js';
|
|
32
31
|
import {
|
|
@@ -131,26 +130,37 @@ export type LinkSegmentParams<T> = {
|
|
|
131
130
|
|
|
132
131
|
// ─── searchParams prop shapes ────────────────────────────────────
|
|
133
132
|
//
|
|
134
|
-
//
|
|
135
|
-
// searchParams={{ page: 2, q: 'boots' }}
|
|
136
|
-
// The framework looks up the route's SearchParamsDefinition from the
|
|
137
|
-
// search-params registry at runtime (see TIM-830).
|
|
133
|
+
// Two shapes, discriminated at runtime by `typeof === 'string'`:
|
|
138
134
|
//
|
|
139
|
-
//
|
|
140
|
-
//
|
|
141
|
-
//
|
|
142
|
-
//
|
|
135
|
+
// 1. Codec-aware (preferred) — a query string built from a definition:
|
|
136
|
+
// searchParams={productParams.buildSearchParams({ page: 2, search: 'boots' })}
|
|
137
|
+
// `buildSearchParams` applies each codec, honours `withUrlKey` aliases,
|
|
138
|
+
// and omits values equal to their default. It is typed `Partial<T>` at
|
|
139
|
+
// the call site, so the definition supplies the type checking.
|
|
143
140
|
//
|
|
144
|
-
//
|
|
141
|
+
// 2. Plain object (escape hatch) — every value is String()-coerced:
|
|
142
|
+
// searchParams={{ ref: 'email' }}
|
|
143
|
+
// No codecs, no aliases. This is for one-off params that have no
|
|
144
|
+
// definition. Passing a *definition's* keys this way is a mistake the
|
|
145
|
+
// types cannot catch: `{ search: 'x' }` emits `?search=x`, where the
|
|
146
|
+
// definition would emit `?q=x`. Prefer (1) whenever a definition exists.
|
|
147
|
+
//
|
|
148
|
+
// Why a STRING and not a URLSearchParams: a Link is routinely rendered by a
|
|
149
|
+
// Server Component, so this prop crosses the RSC Flight boundary before the
|
|
150
|
+
// client Link runs. `URLSearchParams` is iterable, and React serializes any
|
|
151
|
+
// iterable as an array — so the prop arrived as `[['pg','2'], …]`, fell into
|
|
152
|
+
// the plain-object branch, and rendered `?0=pg&0=2`. Strings survive Flight
|
|
153
|
+
// unchanged. (Codex P1 on PR #1021; reproduced against a live dev server.)
|
|
154
|
+
//
|
|
155
|
+
// TIM-1343 removed the third shape — a flat values object resolved against a
|
|
156
|
+
// runtime registry keyed by href. It required scanning a `params.ts`
|
|
157
|
+
// convention file, a generated registry module, and eager imports of every
|
|
158
|
+
// route's definition into all three entries, to save one `.buildSearchParams`
|
|
159
|
+
// call. See design/23-search-params.md §"Link Integration".
|
|
145
160
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
146
161
|
export type ParamValue = string | number | string[] | { toString(): string; [key: string]: any };
|
|
147
162
|
|
|
148
|
-
type
|
|
149
|
-
definition: SearchParamsDefinition<Record<string, unknown>>;
|
|
150
|
-
values: Record<string, unknown>;
|
|
151
|
-
};
|
|
152
|
-
type FlatSearchParamsProp = Record<string, unknown>;
|
|
153
|
-
type LinkSearchParamsProp = WrappedSearchParamsProp | FlatSearchParamsProp;
|
|
163
|
+
type LinkSearchParamsProp = string | Record<string, unknown>;
|
|
154
164
|
|
|
155
165
|
/**
|
|
156
166
|
* Runtime-only loose props used internally by the Link implementation.
|
|
@@ -303,16 +313,35 @@ export function interpolateParams(pattern: string, params: Record<string, ParamV
|
|
|
303
313
|
*
|
|
304
314
|
* Handles:
|
|
305
315
|
* - params interpolation into route patterns
|
|
306
|
-
* - searchParams serialization
|
|
316
|
+
* - searchParams serialization (see the two shapes documented above)
|
|
307
317
|
* - Validation that searchParams and inline query strings are exclusive
|
|
308
318
|
*/
|
|
309
319
|
/**
|
|
310
|
-
*
|
|
311
|
-
*
|
|
312
|
-
*
|
|
320
|
+
* Tolerate a leading '?'. `buildSearchParams()` never emits one, but callers
|
|
321
|
+
* hand-rolling a query string reasonably might, and silently producing
|
|
322
|
+
* `?%3Fa=b` for it would be a worse failure than accepting both.
|
|
323
|
+
*/
|
|
324
|
+
function stripLeadingQuestionMark(qs: string): string {
|
|
325
|
+
return qs.startsWith('?') ? qs.slice(1) : qs;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Escape-hatch serialization for a plain object: String()-coerce every
|
|
330
|
+
* value, append arrays as repeated keys, skip null/undefined.
|
|
331
|
+
*
|
|
332
|
+
* Deliberately codec-free and alias-free — see the shape docs above.
|
|
313
333
|
*/
|
|
314
|
-
function
|
|
315
|
-
|
|
334
|
+
function coerceToQueryString(values: Record<string, unknown>): string {
|
|
335
|
+
const usp = new URLSearchParams();
|
|
336
|
+
for (const [key, val] of Object.entries(values)) {
|
|
337
|
+
if (val === undefined || val === null) continue;
|
|
338
|
+
if (Array.isArray(val)) {
|
|
339
|
+
for (const item of val) usp.append(key, String(item));
|
|
340
|
+
} else {
|
|
341
|
+
usp.set(key, String(val));
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return usp.toString();
|
|
316
345
|
}
|
|
317
346
|
|
|
318
347
|
export function resolveHref(
|
|
@@ -337,46 +366,18 @@ export function resolveHref(
|
|
|
337
366
|
);
|
|
338
367
|
}
|
|
339
368
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
// virtual:timber-search-params-registry module generated by the
|
|
353
|
-
// timber-routing Vite plugin.
|
|
354
|
-
definition = getSearchParamsDefinition(href) as
|
|
355
|
-
| SearchParamsDefinition<Record<string, unknown>>
|
|
356
|
-
| undefined;
|
|
357
|
-
values = searchParams;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
if (definition) {
|
|
361
|
-
const qs = definition.serialize(values);
|
|
362
|
-
if (qs) {
|
|
363
|
-
resolvedPath = `${resolvedPath}?${qs}`;
|
|
364
|
-
}
|
|
365
|
-
} else {
|
|
366
|
-
// No registered definition — serialize flat object as plain query params.
|
|
367
|
-
const usp = new URLSearchParams();
|
|
368
|
-
for (const [key, val] of Object.entries(values)) {
|
|
369
|
-
if (val === undefined || val === null) continue;
|
|
370
|
-
if (Array.isArray(val)) {
|
|
371
|
-
for (const item of val) usp.append(key, String(item));
|
|
372
|
-
} else {
|
|
373
|
-
usp.set(key, String(val));
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
const qs = usp.toString();
|
|
377
|
-
if (qs) {
|
|
378
|
-
resolvedPath = `${resolvedPath}?${qs}`;
|
|
379
|
-
}
|
|
369
|
+
// A string is already a serialized query (from
|
|
370
|
+
// `definition.buildSearchParams()`), so it passes through untouched.
|
|
371
|
+
// `typeof` is the only discriminator that survives the RSC Flight
|
|
372
|
+
// boundary — see the shape docs above for what happened when this was
|
|
373
|
+
// an object test.
|
|
374
|
+
const qs =
|
|
375
|
+
typeof searchParams === 'string'
|
|
376
|
+
? stripLeadingQuestionMark(searchParams)
|
|
377
|
+
: coerceToQueryString(searchParams);
|
|
378
|
+
|
|
379
|
+
if (qs) {
|
|
380
|
+
resolvedPath = `${resolvedPath}?${qs}`;
|
|
380
381
|
}
|
|
381
382
|
}
|
|
382
383
|
|
|
@@ -38,6 +38,22 @@ export interface NavigationCommitInput {
|
|
|
38
38
|
/** When true, clear the segment cache if segmentInfo is absent
|
|
39
39
|
* (popstate replay for entries without layout metadata). */
|
|
40
40
|
clearSegmentCacheOnEmpty?: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* HTTP status of the response this navigation rendered, when it came from a
|
|
43
|
+
* fetch. Omitted by replays, which re-publish a tree already judged.
|
|
44
|
+
*
|
|
45
|
+
* An error response rendered a status page, and a status page replaces a
|
|
46
|
+
* subtree: the deny page lands at the segment owning the matched file and
|
|
47
|
+
* nothing below it mounts. `segmentInfo` nonetheless describes the whole
|
|
48
|
+
* chain the payload was built against — it must, because the merge point is
|
|
49
|
+
* chosen out of it — so the cache has to be told not to learn from this one.
|
|
50
|
+
*
|
|
51
|
+
* Enforced here rather than at the call sites because there are three of
|
|
52
|
+
* them (`performNavigationFetch`, `refresh()`, and uncached traversal, the
|
|
53
|
+
* last two through `fetchCommitAndRender`) and only one had the check.
|
|
54
|
+
* See TIM-1356.
|
|
55
|
+
*/
|
|
56
|
+
status?: number;
|
|
41
57
|
}
|
|
42
58
|
|
|
43
59
|
/**
|
|
@@ -151,12 +167,19 @@ export function createNavigationCommitter(deps: {
|
|
|
151
167
|
prepareNavigation(url: string, opts: NavigationCommitInput): PreparedNavigation {
|
|
152
168
|
const navState = deriveNavigationState(url);
|
|
153
169
|
|
|
170
|
+
// A tree rendered by an error response is not one to cache — see
|
|
171
|
+
// `status` on NavigationCommitInput. Normalized here, once, so no fetch
|
|
172
|
+
// caller can forget it; the history entry records the same thing, or a
|
|
173
|
+
// traversal back to this URL would replay the metadata this drops.
|
|
174
|
+
const cacheable = opts.status === undefined || opts.status < 400;
|
|
175
|
+
const segmentInfo = cacheable ? opts.segmentInfo : [];
|
|
176
|
+
|
|
154
177
|
return {
|
|
155
178
|
navState,
|
|
156
179
|
commit() {
|
|
157
|
-
if (
|
|
158
|
-
updateSegmentCache(
|
|
159
|
-
} else if (
|
|
180
|
+
if (segmentInfo && segmentInfo.length > 0) {
|
|
181
|
+
updateSegmentCache(segmentInfo);
|
|
182
|
+
} else if (segmentInfo?.length === 0 || opts.clearSegmentCacheOnEmpty) {
|
|
160
183
|
segmentCache.clear();
|
|
161
184
|
}
|
|
162
185
|
|
|
@@ -165,7 +188,7 @@ export function createNavigationCommitter(deps: {
|
|
|
165
188
|
historyStack.push(url, {
|
|
166
189
|
payload: opts.payload,
|
|
167
190
|
params: opts.params,
|
|
168
|
-
segmentInfo
|
|
191
|
+
segmentInfo,
|
|
169
192
|
});
|
|
170
193
|
},
|
|
171
194
|
};
|
|
@@ -64,10 +64,10 @@ export type ParamsContextValue = PublishedParams;
|
|
|
64
64
|
* do it: React's `react-server` export provides neither `createContext` nor
|
|
65
65
|
* `useContext`, and a *named* ESM import of a missing export fails at module
|
|
66
66
|
* instantiation — before any feature check could run. This module is reachable
|
|
67
|
-
* from
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
67
|
+
* from every entry a Server Component imports, so the named form crashed
|
|
68
|
+
* those entries outright (codex, PR #992; originally reproduced against
|
|
69
|
+
* `@timber-js/app/segment-params`, an entry point since deleted by TIM-1342 —
|
|
70
|
+
* the hazard is unchanged for the entries that remain).
|
|
71
71
|
*
|
|
72
72
|
* See design/19-client-navigation.md §"Singleton Guarantee via globalThis"
|
|
73
73
|
*/
|
|
@@ -314,6 +314,9 @@ export function createNavigationPipeline({
|
|
|
314
314
|
decodePromise: prefetched.decodePromise ?? null,
|
|
315
315
|
segmentInfo: prefetched.segmentInfo ?? null,
|
|
316
316
|
skippedSegments: prefetched.skippedSegments ?? null,
|
|
317
|
+
// Entries written before this field carried it are treated as
|
|
318
|
+
// cacheable, which is what they were before a deny could be one.
|
|
319
|
+
status: prefetched.status ?? 200,
|
|
317
320
|
}
|
|
318
321
|
: undefined;
|
|
319
322
|
|
|
@@ -364,6 +367,7 @@ export function createNavigationPipeline({
|
|
|
364
367
|
payload: isPartial || hasSkippedSlots ? null : payload,
|
|
365
368
|
params,
|
|
366
369
|
segmentInfo: result.segmentInfo,
|
|
370
|
+
status: result.status,
|
|
367
371
|
});
|
|
368
372
|
|
|
369
373
|
return {
|
package/src/client/router.ts
CHANGED
package/src/client/rsc-fetch.ts
CHANGED
|
@@ -27,6 +27,7 @@ import { normalizeBasePath, stripBasePath, withBasePath } from '../shared/base-p
|
|
|
27
27
|
import { rscPayloadPath } from '../shared/rsc-payload-path.js';
|
|
28
28
|
import type { SegmentInfo, StateTree } from '../shared/segment-info.js';
|
|
29
29
|
import { TIMBER_ERROR_HEADER } from '../shared/rsc-error-envelope.js';
|
|
30
|
+
import { RSC_CONTENT_TYPE } from '../shared/rsc-media-type.js';
|
|
30
31
|
import type { RouterDeps } from './router-types.js';
|
|
31
32
|
|
|
32
33
|
// ─── Types ───────────────────────────────────────────────────────
|
|
@@ -57,12 +58,17 @@ export interface FetchResult {
|
|
|
57
58
|
segmentInfo: SegmentInfo[] | null;
|
|
58
59
|
/** Segment paths that were skipped by the server (for client-side merging). */
|
|
59
60
|
skippedSegments: string[] | null;
|
|
61
|
+
/**
|
|
62
|
+
* HTTP status of the navigation response.
|
|
63
|
+
*
|
|
64
|
+
* Read at the commit site to decide whether the tree this payload produced
|
|
65
|
+
* is one worth caching. A deny page replaced a subtree, so `segmentInfo`
|
|
66
|
+
* describes more than actually mounted — but it cannot simply be emptied,
|
|
67
|
+
* because the merge point is chosen out of it. See TIM-1356.
|
|
68
|
+
*/
|
|
69
|
+
status: number;
|
|
60
70
|
}
|
|
61
71
|
|
|
62
|
-
// ─── Constants ───────────────────────────────────────────────────
|
|
63
|
-
|
|
64
|
-
export const RSC_CONTENT_TYPE = 'text/x-component';
|
|
65
|
-
|
|
66
72
|
// ─── URL Helpers ─────────────────────────────────────────────────
|
|
67
73
|
|
|
68
74
|
/**
|
|
@@ -531,6 +537,7 @@ export async function fetchRscPayload(
|
|
|
531
537
|
const fetchPromise = deps.fetch(rscUrl, { headers, redirect: 'manual', signal });
|
|
532
538
|
let segmentInfo: SegmentInfo[] | null = null;
|
|
533
539
|
let skippedSegments: string[] | null = null;
|
|
540
|
+
let status = 200;
|
|
534
541
|
// Track when the full RSC body stream is consumed (not just shell).
|
|
535
542
|
// Initialized to resolved for bodyless responses; overwritten when
|
|
536
543
|
// the response has a body.
|
|
@@ -586,6 +593,7 @@ export async function fetchRscPayload(
|
|
|
586
593
|
// as React elements — React 19 Float handles them. See TIM-1151.
|
|
587
594
|
segmentInfo = extractSegmentInfo(response);
|
|
588
595
|
skippedSegments = extractSkippedSegments(response);
|
|
596
|
+
status = response.status;
|
|
589
597
|
|
|
590
598
|
// Wrap the body to track full stream consumption. createFromFetch's
|
|
591
599
|
// thenable resolves when the root model (shell) arrives, but we need
|
|
@@ -634,6 +642,7 @@ export async function fetchRscPayload(
|
|
|
634
642
|
decodePromise,
|
|
635
643
|
segmentInfo,
|
|
636
644
|
skippedSegments,
|
|
645
|
+
status,
|
|
637
646
|
};
|
|
638
647
|
}
|
|
639
648
|
// Test/fallback path: return raw text
|
|
@@ -677,5 +686,6 @@ export async function fetchRscPayload(
|
|
|
677
686
|
decodePromise: null,
|
|
678
687
|
segmentInfo: extractSegmentInfo(response),
|
|
679
688
|
skippedSegments: extractSkippedSegments(response),
|
|
689
|
+
status: response.status,
|
|
680
690
|
};
|
|
681
691
|
}
|
|
@@ -26,6 +26,14 @@ export interface PrefetchResult {
|
|
|
26
26
|
params?: PublishedParams | Promise<PublishedParams>;
|
|
27
27
|
/** Segment paths skipped by the server (for client-side merging). */
|
|
28
28
|
skippedSegments?: string[] | null;
|
|
29
|
+
/**
|
|
30
|
+
* HTTP status of the prefetched response. A deny payload is an ordinary
|
|
31
|
+
* Flight response carrying a 4xx, so it prefetches and caches like any
|
|
32
|
+
* other — and the tree it renders is one the segment cache must not learn
|
|
33
|
+
* from. Absent on entries that predate the field, and on the negative
|
|
34
|
+
* sentinel. See TIM-1356.
|
|
35
|
+
*/
|
|
36
|
+
status?: number;
|
|
29
37
|
}
|
|
30
38
|
|
|
31
39
|
/**
|
|
@@ -56,6 +64,12 @@ export interface SegmentNode {
|
|
|
56
64
|
slots?: Map<string, SegmentNode>;
|
|
57
65
|
/** Whether this slot's access.ts denied on its last render. */
|
|
58
66
|
denied?: boolean;
|
|
67
|
+
/**
|
|
68
|
+
* Content key for slot nodes — opaque string the server computed from
|
|
69
|
+
* owner parts, slot name, entry file, and slot params. Advertised back
|
|
70
|
+
* to the server so it can decide skips by key membership (TIM-1370).
|
|
71
|
+
*/
|
|
72
|
+
contentKey?: string;
|
|
59
73
|
}
|
|
60
74
|
|
|
61
75
|
// ─── Segment Cache ───────────────────────────────────────────────
|
|
@@ -153,14 +167,20 @@ function collectTreePaths(node: SegmentNode, out: string[]): void {
|
|
|
153
167
|
}
|
|
154
168
|
}
|
|
155
169
|
|
|
156
|
-
/**
|
|
170
|
+
/**
|
|
171
|
+
* Recursively collect content keys from cacheable slots.
|
|
172
|
+
*
|
|
173
|
+
* The server advertises a content key per slot on every render (TIM-1370),
|
|
174
|
+
* encoding the owner's URL parts, the slot name, the matched entry file,
|
|
175
|
+
* and the slot's params. The client stores this key and sends it back on
|
|
176
|
+
* the next navigation so the server can decide skips by key membership —
|
|
177
|
+
* no departing URL reconstruction needed.
|
|
178
|
+
*/
|
|
157
179
|
function collectSyncSlots(node: SegmentNode, out: string[]): void {
|
|
158
180
|
if (node.slots) {
|
|
159
181
|
for (const slot of node.slots.values()) {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
if (!slot.isRequestDependent && !slot.denied) {
|
|
163
|
-
out.push(slot.segment);
|
|
182
|
+
if (!slot.isRequestDependent && !slot.denied && slot.contentKey) {
|
|
183
|
+
out.push(slot.contentKey);
|
|
164
184
|
}
|
|
165
185
|
}
|
|
166
186
|
}
|
|
@@ -239,6 +259,7 @@ export function buildSegmentTree(segments: SegmentInfo[]): SegmentNode | undefin
|
|
|
239
259
|
isRequestDependent: slotInfo.isRequestDependent,
|
|
240
260
|
children: new Map(),
|
|
241
261
|
denied: slotInfo.denied,
|
|
262
|
+
contentKey: slotInfo.contentKey,
|
|
242
263
|
};
|
|
243
264
|
|
|
244
265
|
if (!parentNode.slots) {
|