@timber-js/app 0.2.0-alpha.5 → 0.2.0-alpha.50
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/LICENSE +8 -0
- package/dist/_chunks/{als-registry-B7DbZ2hS.js → als-registry-Ba7URUIn.js} +1 -1
- package/dist/_chunks/als-registry-Ba7URUIn.js.map +1 -0
- package/dist/_chunks/chunk-DYhsFzuS.js +33 -0
- package/dist/_chunks/{debug-gwlJkDuf.js → debug-ECi_61pb.js} +2 -2
- package/dist/_chunks/debug-ECi_61pb.js.map +1 -0
- package/dist/_chunks/define-TK8C1M3x.js +279 -0
- package/dist/_chunks/define-TK8C1M3x.js.map +1 -0
- package/dist/_chunks/define-cookie-k9btcEfI.js +93 -0
- package/dist/_chunks/define-cookie-k9btcEfI.js.map +1 -0
- package/dist/_chunks/error-boundary-B9vT_YK_.js +211 -0
- package/dist/_chunks/error-boundary-B9vT_YK_.js.map +1 -0
- package/dist/_chunks/{format-DviM89f0.js → format-cX7wzEp2.js} +2 -2
- package/dist/_chunks/{format-DviM89f0.js.map → format-cX7wzEp2.js.map} +1 -1
- package/dist/_chunks/{interception-BOoWmLUA.js → interception-D2djYaIm.js} +112 -77
- package/dist/_chunks/interception-D2djYaIm.js.map +1 -0
- package/dist/_chunks/{metadata-routes-Cjmvi3rQ.js → metadata-routes-BU684ls2.js} +1 -1
- package/dist/_chunks/{metadata-routes-Cjmvi3rQ.js.map → metadata-routes-BU684ls2.js.map} +1 -1
- package/dist/_chunks/{request-context-DIkVh_jG.js → request-context-0h-6Voad.js} +95 -69
- package/dist/_chunks/request-context-0h-6Voad.js.map +1 -0
- package/dist/_chunks/segment-context-DBn-nrMN.js +69 -0
- package/dist/_chunks/segment-context-DBn-nrMN.js.map +1 -0
- package/dist/_chunks/stale-reload-4L-_skC7.js +47 -0
- package/dist/_chunks/stale-reload-4L-_skC7.js.map +1 -0
- package/dist/_chunks/{tracing-Cwn7697K.js → tracing-JI4cYUdz.js} +17 -3
- package/dist/_chunks/{tracing-Cwn7697K.js.map → tracing-JI4cYUdz.js.map} +1 -1
- package/dist/_chunks/{use-query-states-D5KaffOK.js → use-query-states-wEXY2JQB.js} +1 -1
- package/dist/_chunks/{use-query-states-D5KaffOK.js.map → use-query-states-wEXY2JQB.js.map} +1 -1
- package/dist/_chunks/wrappers-C9XPg7-U.js +63 -0
- package/dist/_chunks/wrappers-C9XPg7-U.js.map +1 -0
- package/dist/adapters/compress-module.d.ts.map +1 -1
- package/dist/adapters/nitro.d.ts +17 -1
- package/dist/adapters/nitro.d.ts.map +1 -1
- package/dist/adapters/nitro.js +56 -13
- package/dist/adapters/nitro.js.map +1 -1
- package/dist/cache/fast-hash.d.ts +22 -0
- package/dist/cache/fast-hash.d.ts.map +1 -0
- package/dist/cache/index.d.ts +5 -2
- package/dist/cache/index.d.ts.map +1 -1
- package/dist/cache/index.js +90 -20
- package/dist/cache/index.js.map +1 -1
- package/dist/cache/register-cached-function.d.ts.map +1 -1
- package/dist/cache/singleflight.d.ts +18 -1
- package/dist/cache/singleflight.d.ts.map +1 -1
- package/dist/cache/timber-cache.d.ts +1 -1
- package/dist/cache/timber-cache.d.ts.map +1 -1
- package/dist/client/error-boundary.d.ts +10 -1
- package/dist/client/error-boundary.d.ts.map +1 -1
- package/dist/client/error-boundary.js +1 -125
- package/dist/client/index.d.ts +3 -2
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +213 -93
- package/dist/client/index.js.map +1 -1
- package/dist/client/link.d.ts +22 -8
- package/dist/client/link.d.ts.map +1 -1
- package/dist/client/navigation-context.d.ts +2 -2
- package/dist/client/router.d.ts +25 -3
- package/dist/client/router.d.ts.map +1 -1
- package/dist/client/rsc-fetch.d.ts +23 -2
- package/dist/client/rsc-fetch.d.ts.map +1 -1
- package/dist/client/segment-cache.d.ts +1 -1
- package/dist/client/segment-cache.d.ts.map +1 -1
- package/dist/client/segment-context.d.ts +1 -1
- package/dist/client/segment-context.d.ts.map +1 -1
- package/dist/client/segment-merger.d.ts.map +1 -1
- package/dist/client/stale-reload.d.ts +15 -0
- package/dist/client/stale-reload.d.ts.map +1 -1
- package/dist/client/top-loader.d.ts +1 -1
- package/dist/client/top-loader.d.ts.map +1 -1
- package/dist/client/transition-root.d.ts +1 -1
- package/dist/client/transition-root.d.ts.map +1 -1
- package/dist/client/use-params.d.ts +2 -2
- package/dist/client/use-params.d.ts.map +1 -1
- package/dist/client/use-query-states.d.ts +1 -1
- package/dist/codec.d.ts +21 -0
- package/dist/codec.d.ts.map +1 -0
- package/dist/cookies/define-cookie.d.ts +33 -12
- package/dist/cookies/define-cookie.d.ts.map +1 -1
- package/dist/cookies/index.js +1 -83
- package/dist/fonts/css.d.ts +1 -0
- package/dist/fonts/css.d.ts.map +1 -1
- package/dist/index.d.ts +112 -35
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +467 -246
- package/dist/index.js.map +1 -1
- package/dist/params/define.d.ts +76 -0
- package/dist/params/define.d.ts.map +1 -0
- package/dist/params/index.d.ts +8 -0
- package/dist/params/index.d.ts.map +1 -0
- package/dist/params/index.js +105 -0
- package/dist/params/index.js.map +1 -0
- package/dist/plugins/adapter-build.d.ts.map +1 -1
- package/dist/plugins/build-manifest.d.ts.map +1 -1
- package/dist/plugins/client-chunks.d.ts +32 -0
- package/dist/plugins/client-chunks.d.ts.map +1 -0
- package/dist/plugins/dev-error-overlay.d.ts +26 -1
- package/dist/plugins/dev-error-overlay.d.ts.map +1 -1
- package/dist/plugins/entries.d.ts.map +1 -1
- package/dist/plugins/fonts.d.ts +7 -0
- package/dist/plugins/fonts.d.ts.map +1 -1
- package/dist/plugins/routing.d.ts.map +1 -1
- package/dist/plugins/server-bundle.d.ts.map +1 -1
- package/dist/plugins/static-build.d.ts.map +1 -1
- package/dist/routing/codegen.d.ts +2 -2
- package/dist/routing/codegen.d.ts.map +1 -1
- package/dist/routing/index.js +1 -1
- package/dist/routing/scanner.d.ts.map +1 -1
- package/dist/routing/status-file-lint.d.ts +2 -1
- package/dist/routing/status-file-lint.d.ts.map +1 -1
- package/dist/routing/types.d.ts +6 -4
- package/dist/routing/types.d.ts.map +1 -1
- package/dist/rsc-runtime/rsc.d.ts +1 -1
- package/dist/rsc-runtime/rsc.d.ts.map +1 -1
- package/dist/rsc-runtime/ssr.d.ts +12 -0
- package/dist/rsc-runtime/ssr.d.ts.map +1 -1
- package/dist/search-params/codecs.d.ts +1 -1
- package/dist/search-params/define.d.ts +159 -0
- package/dist/search-params/define.d.ts.map +1 -0
- package/dist/search-params/index.d.ts +4 -5
- package/dist/search-params/index.d.ts.map +1 -1
- package/dist/search-params/index.js +4 -474
- package/dist/search-params/registry.d.ts +1 -1
- package/dist/search-params/wrappers.d.ts +53 -0
- package/dist/search-params/wrappers.d.ts.map +1 -0
- package/dist/server/access-gate.d.ts +4 -0
- package/dist/server/access-gate.d.ts.map +1 -1
- package/dist/server/action-client.d.ts.map +1 -1
- package/dist/server/action-encryption.d.ts +76 -0
- package/dist/server/action-encryption.d.ts.map +1 -0
- package/dist/server/action-handler.d.ts.map +1 -1
- package/dist/server/als-registry.d.ts +18 -4
- package/dist/server/als-registry.d.ts.map +1 -1
- package/dist/server/build-manifest.d.ts +2 -2
- package/dist/server/debug.d.ts +1 -1
- package/dist/server/default-logger.d.ts +22 -0
- package/dist/server/default-logger.d.ts.map +1 -0
- package/dist/server/deny-renderer.d.ts.map +1 -1
- package/dist/server/early-hints.d.ts +13 -5
- package/dist/server/early-hints.d.ts.map +1 -1
- package/dist/server/error-boundary-wrapper.d.ts +4 -0
- package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
- package/dist/server/flight-injection-state.d.ts +66 -0
- package/dist/server/flight-injection-state.d.ts.map +1 -0
- package/dist/server/flight-scripts.d.ts +39 -0
- package/dist/server/flight-scripts.d.ts.map +1 -0
- package/dist/server/flush.d.ts.map +1 -1
- package/dist/server/form-data.d.ts +29 -0
- package/dist/server/form-data.d.ts.map +1 -1
- package/dist/server/html-injectors.d.ts +51 -11
- package/dist/server/html-injectors.d.ts.map +1 -1
- package/dist/server/index.d.ts +4 -2
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1974 -1648
- package/dist/server/index.js.map +1 -1
- package/dist/server/logger.d.ts +24 -7
- package/dist/server/logger.d.ts.map +1 -1
- package/dist/server/node-stream-transforms.d.ts +113 -0
- package/dist/server/node-stream-transforms.d.ts.map +1 -0
- package/dist/server/pipeline.d.ts +7 -4
- package/dist/server/pipeline.d.ts.map +1 -1
- package/dist/server/primitives.d.ts +30 -3
- package/dist/server/primitives.d.ts.map +1 -1
- package/dist/server/render-timeout.d.ts +51 -0
- package/dist/server/render-timeout.d.ts.map +1 -0
- package/dist/server/request-context.d.ts +65 -38
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/route-element-builder.d.ts +7 -0
- package/dist/server/route-element-builder.d.ts.map +1 -1
- package/dist/server/route-handler.d.ts.map +1 -1
- package/dist/server/route-matcher.d.ts +2 -2
- package/dist/server/route-matcher.d.ts.map +1 -1
- package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
- package/dist/server/rsc-entry/helpers.d.ts +46 -3
- package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
- package/dist/server/rsc-entry/index.d.ts +6 -1
- package/dist/server/rsc-entry/index.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-stream.d.ts +9 -0
- package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
- package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
- package/dist/server/slot-resolver.d.ts +1 -1
- package/dist/server/slot-resolver.d.ts.map +1 -1
- package/dist/server/ssr-entry.d.ts +22 -0
- package/dist/server/ssr-entry.d.ts.map +1 -1
- package/dist/server/ssr-render.d.ts +39 -21
- package/dist/server/ssr-render.d.ts.map +1 -1
- package/dist/server/ssr-wrappers.d.ts +50 -0
- package/dist/server/ssr-wrappers.d.ts.map +1 -0
- package/dist/server/tracing.d.ts +10 -0
- package/dist/server/tracing.d.ts.map +1 -1
- package/dist/server/tree-builder.d.ts +19 -12
- package/dist/server/tree-builder.d.ts.map +1 -1
- package/dist/server/types.d.ts +1 -3
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/version-skew.d.ts +61 -0
- package/dist/server/version-skew.d.ts.map +1 -0
- package/dist/server/waituntil-bridge.d.ts.map +1 -1
- package/dist/shared/merge-search-params.d.ts +22 -0
- package/dist/shared/merge-search-params.d.ts.map +1 -0
- package/dist/shims/navigation-client.d.ts +1 -1
- package/dist/shims/navigation-client.d.ts.map +1 -1
- package/dist/shims/navigation.d.ts +1 -1
- package/dist/shims/navigation.d.ts.map +1 -1
- package/dist/utils/state-machine.d.ts +80 -0
- package/dist/utils/state-machine.d.ts.map +1 -0
- package/package.json +17 -14
- package/src/adapters/compress-module.ts +24 -4
- package/src/adapters/nitro.ts +58 -9
- package/src/cache/fast-hash.ts +34 -0
- package/src/cache/index.ts +5 -2
- package/src/cache/register-cached-function.ts +7 -3
- package/src/cache/singleflight.ts +62 -4
- package/src/cache/timber-cache.ts +40 -29
- package/src/cli.ts +0 -0
- package/src/client/browser-entry.ts +133 -93
- package/src/client/error-boundary.tsx +18 -1
- package/src/client/index.ts +10 -1
- package/src/client/link.tsx +78 -19
- package/src/client/navigation-context.ts +2 -2
- package/src/client/router.ts +105 -60
- package/src/client/rsc-fetch.ts +63 -2
- package/src/client/segment-cache.ts +1 -1
- package/src/client/segment-context.ts +6 -1
- package/src/client/segment-merger.ts +2 -8
- package/src/client/stale-reload.ts +32 -6
- package/src/client/top-loader.tsx +10 -9
- package/src/client/transition-root.tsx +7 -1
- package/src/client/use-params.ts +3 -3
- package/src/client/use-query-states.ts +1 -1
- package/src/codec.ts +21 -0
- package/src/cookies/define-cookie.ts +69 -18
- package/src/fonts/css.ts +2 -1
- package/src/index.ts +280 -85
- package/src/params/define.ts +260 -0
- package/src/params/index.ts +28 -0
- package/src/plugins/adapter-build.ts +6 -0
- package/src/plugins/build-manifest.ts +11 -0
- package/src/plugins/client-chunks.ts +65 -0
- package/src/plugins/dev-error-overlay.ts +70 -1
- package/src/plugins/dev-server.ts +38 -4
- package/src/plugins/entries.ts +5 -7
- package/src/plugins/fonts.ts +93 -42
- package/src/plugins/routing.ts +40 -14
- package/src/plugins/server-bundle.ts +32 -1
- package/src/plugins/shims.ts +1 -1
- package/src/plugins/static-build.ts +8 -4
- package/src/routing/codegen.ts +109 -88
- package/src/routing/scanner.ts +55 -6
- package/src/routing/status-file-lint.ts +2 -1
- package/src/routing/types.ts +7 -4
- package/src/rsc-runtime/rsc.ts +2 -0
- package/src/rsc-runtime/ssr.ts +50 -0
- package/src/rsc-runtime/vendor-types.d.ts +7 -0
- package/src/search-params/codecs.ts +1 -1
- package/src/search-params/define.ts +518 -0
- package/src/search-params/index.ts +12 -18
- package/src/search-params/registry.ts +1 -1
- package/src/search-params/wrappers.ts +85 -0
- package/src/server/access-gate.tsx +40 -9
- package/src/server/action-client.ts +7 -1
- package/src/server/action-encryption.ts +144 -0
- package/src/server/action-handler.ts +19 -2
- package/src/server/als-registry.ts +18 -4
- package/src/server/build-manifest.ts +4 -4
- package/src/server/compress.ts +25 -7
- package/src/server/debug.ts +1 -1
- package/src/server/default-logger.ts +98 -0
- package/src/server/deny-renderer.ts +2 -1
- package/src/server/early-hints.ts +36 -15
- package/src/server/error-boundary-wrapper.ts +57 -14
- package/src/server/flight-injection-state.ts +113 -0
- package/src/server/flight-scripts.ts +59 -0
- package/src/server/flush.ts +2 -1
- package/src/server/form-data.ts +76 -0
- package/src/server/html-injectors.ts +261 -117
- package/src/server/index.ts +9 -4
- package/src/server/logger.ts +38 -35
- package/src/server/node-stream-transforms.ts +504 -0
- package/src/server/pipeline.ts +131 -39
- package/src/server/primitives.ts +47 -5
- package/src/server/render-timeout.ts +108 -0
- package/src/server/request-context.ts +119 -119
- package/src/server/route-element-builder.ts +106 -114
- package/src/server/route-handler.ts +2 -1
- package/src/server/route-matcher.ts +2 -2
- package/src/server/rsc-entry/error-renderer.ts +5 -3
- package/src/server/rsc-entry/helpers.ts +122 -3
- package/src/server/rsc-entry/index.ts +108 -43
- package/src/server/rsc-entry/rsc-payload.ts +52 -12
- package/src/server/rsc-entry/rsc-stream.ts +49 -12
- package/src/server/rsc-entry/ssr-renderer.ts +40 -13
- package/src/server/slot-resolver.ts +222 -217
- package/src/server/ssr-entry.ts +209 -30
- package/src/server/ssr-render.ts +289 -67
- package/src/server/ssr-wrappers.tsx +139 -0
- package/src/server/tracing.ts +23 -0
- package/src/server/tree-builder.ts +91 -57
- package/src/server/types.ts +1 -3
- package/src/server/version-skew.ts +104 -0
- package/src/server/waituntil-bridge.ts +4 -1
- package/src/shared/merge-search-params.ts +48 -0
- package/src/shims/navigation-client.ts +1 -1
- package/src/shims/navigation.ts +1 -1
- package/src/utils/state-machine.ts +111 -0
- package/dist/_chunks/als-registry-B7DbZ2hS.js.map +0 -1
- package/dist/_chunks/debug-gwlJkDuf.js.map +0 -1
- package/dist/_chunks/interception-BOoWmLUA.js.map +0 -1
- package/dist/_chunks/request-context-DIkVh_jG.js.map +0 -1
- package/dist/_chunks/ssr-data-MjmprTmO.js +0 -88
- package/dist/_chunks/ssr-data-MjmprTmO.js.map +0 -1
- package/dist/_chunks/use-cookie-DX-l1_5E.js +0 -91
- package/dist/_chunks/use-cookie-DX-l1_5E.js.map +0 -1
- package/dist/client/error-boundary.js.map +0 -1
- package/dist/cookies/index.js.map +0 -1
- package/dist/plugins/dynamic-transform.d.ts +0 -72
- package/dist/plugins/dynamic-transform.d.ts.map +0 -1
- package/dist/search-params/analyze.d.ts +0 -54
- package/dist/search-params/analyze.d.ts.map +0 -1
- package/dist/search-params/builtin-codecs.d.ts +0 -105
- package/dist/search-params/builtin-codecs.d.ts.map +0 -1
- package/dist/search-params/create.d.ts +0 -106
- package/dist/search-params/create.d.ts.map +0 -1
- package/dist/search-params/index.js.map +0 -1
- package/dist/server/prerender.d.ts +0 -77
- package/dist/server/prerender.d.ts.map +0 -1
- package/dist/server/response-cache.d.ts +0 -53
- package/dist/server/response-cache.d.ts.map +0 -1
- package/src/plugins/dynamic-transform.ts +0 -161
- package/src/search-params/analyze.ts +0 -192
- package/src/search-params/builtin-codecs.ts +0 -228
- package/src/search-params/create.ts +0 -321
- package/src/server/prerender.ts +0 -139
- package/src/server/response-cache.ts +0 -277
package/src/client/router.ts
CHANGED
|
@@ -6,13 +6,13 @@ import type { SegmentInfo } from './segment-cache';
|
|
|
6
6
|
import { HistoryStack } from './history';
|
|
7
7
|
import type { HeadElement } from './head';
|
|
8
8
|
import { setCurrentParams } from './use-params.js';
|
|
9
|
-
import { setNavigationState } from './navigation-context.js';
|
|
10
9
|
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
} from './
|
|
15
|
-
import {
|
|
10
|
+
setNavigationState,
|
|
11
|
+
getNavigationState,
|
|
12
|
+
type NavigationState,
|
|
13
|
+
} from './navigation-context.js';
|
|
14
|
+
import { SegmentElementCache, cacheSegmentElements, mergeSegmentTree } from './segment-merger.js';
|
|
15
|
+
import { fetchRscPayload, RedirectError, VersionSkewError } from './rsc-fetch.js';
|
|
16
16
|
import type { FetchResult } from './rsc-fetch.js';
|
|
17
17
|
|
|
18
18
|
// ─── Types ───────────────────────────────────────────────────────
|
|
@@ -35,8 +35,12 @@ export type RscDecoder = (fetchPromise: Promise<Response>) => unknown;
|
|
|
35
35
|
* Function that renders a decoded RSC element tree into the DOM.
|
|
36
36
|
* In production: reactRoot.render(element).
|
|
37
37
|
* In tests: a no-op or mock.
|
|
38
|
+
*
|
|
39
|
+
* Receives the current NavigationState explicitly — no temporal
|
|
40
|
+
* coupling with setNavigationState/getNavigationState. The renderer
|
|
41
|
+
* wraps the element in NavigationProvider with this state.
|
|
38
42
|
*/
|
|
39
|
-
export type RootRenderer = (element: unknown) => void;
|
|
43
|
+
export type RootRenderer = (element: unknown, navState: NavigationState) => void;
|
|
40
44
|
|
|
41
45
|
/**
|
|
42
46
|
* Platform dependencies injected for testability. In production these
|
|
@@ -68,13 +72,17 @@ export interface RouterDeps {
|
|
|
68
72
|
*
|
|
69
73
|
* The `perform` callback receives a `wrapPayload` function to wrap the
|
|
70
74
|
* decoded RSC payload with NavigationProvider + NuqsAdapter before
|
|
71
|
-
* TransitionRoot sets it as the new element.
|
|
75
|
+
* TransitionRoot sets it as the new element. The `wrapPayload` function
|
|
76
|
+
* receives the NavigationState explicitly — no temporal coupling with
|
|
77
|
+
* getNavigationState().
|
|
72
78
|
*
|
|
73
79
|
* If not provided (tests), the router falls back to renderRoot.
|
|
74
80
|
*/
|
|
75
81
|
navigateTransition?: (
|
|
76
82
|
pendingUrl: string,
|
|
77
|
-
perform: (
|
|
83
|
+
perform: (
|
|
84
|
+
wrapPayload: (payload: unknown, navState: NavigationState) => unknown
|
|
85
|
+
) => Promise<unknown>
|
|
78
86
|
) => Promise<void>;
|
|
79
87
|
}
|
|
80
88
|
|
|
@@ -134,21 +142,40 @@ function isAbortError(error: unknown): boolean {
|
|
|
134
142
|
* Create a router instance. In production, called once at app hydration
|
|
135
143
|
* with real browser APIs. In tests, called with mock dependencies.
|
|
136
144
|
*/
|
|
145
|
+
/**
|
|
146
|
+
* Router navigation phase — discriminated union replacing scattered
|
|
147
|
+
* `pending` + `pendingUrl` boolean flags.
|
|
148
|
+
*
|
|
149
|
+
* - `idle`: No navigation in flight. The committed params/pathname
|
|
150
|
+
* are current.
|
|
151
|
+
* - `navigating`: A fetch or render is in progress. `targetUrl` is
|
|
152
|
+
* the destination being navigated to.
|
|
153
|
+
*/
|
|
154
|
+
export type RouterPhase = { phase: 'idle' } | { phase: 'navigating'; targetUrl: string };
|
|
155
|
+
|
|
137
156
|
export function createRouter(deps: RouterDeps): RouterInstance {
|
|
138
157
|
const segmentCache = new SegmentCache();
|
|
139
158
|
const prefetchCache = new PrefetchCache();
|
|
140
159
|
const historyStack = new HistoryStack();
|
|
141
160
|
const segmentElementCache = new SegmentElementCache();
|
|
142
161
|
|
|
143
|
-
let
|
|
144
|
-
let pendingUrl: string | null = null;
|
|
162
|
+
let routerPhase: RouterPhase = { phase: 'idle' };
|
|
145
163
|
const pendingListeners = new Set<(pending: boolean) => void>();
|
|
146
164
|
|
|
147
165
|
function setPending(value: boolean, url?: string): void {
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
166
|
+
const next: RouterPhase =
|
|
167
|
+
value && url ? { phase: 'navigating', targetUrl: url } : { phase: 'idle' };
|
|
168
|
+
// Skip no-op updates
|
|
169
|
+
if (
|
|
170
|
+
routerPhase.phase === next.phase &&
|
|
171
|
+
(routerPhase.phase === 'idle' ||
|
|
172
|
+
(routerPhase.phase === 'navigating' &&
|
|
173
|
+
next.phase === 'navigating' &&
|
|
174
|
+
routerPhase.targetUrl === next.targetUrl))
|
|
175
|
+
) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
routerPhase = next;
|
|
152
179
|
// Notify external store listeners (non-React consumers).
|
|
153
180
|
// React-facing pending state is handled by useOptimistic in
|
|
154
181
|
// TransitionRoot via navigateTransition — not this function.
|
|
@@ -167,9 +194,9 @@ export function createRouter(deps: RouterDeps): RouterInstance {
|
|
|
167
194
|
}
|
|
168
195
|
|
|
169
196
|
/** Render a decoded RSC payload into the DOM if a renderer is available. */
|
|
170
|
-
function renderPayload(payload: unknown): void {
|
|
197
|
+
function renderPayload(payload: unknown, navState: NavigationState): void {
|
|
171
198
|
if (deps.renderRoot) {
|
|
172
|
-
deps.renderRoot(payload);
|
|
199
|
+
deps.renderRoot(payload, navState);
|
|
173
200
|
}
|
|
174
201
|
}
|
|
175
202
|
|
|
@@ -198,32 +225,34 @@ export function createRouter(deps: RouterDeps): RouterInstance {
|
|
|
198
225
|
/**
|
|
199
226
|
* Update navigation state (params + pathname) for the next render.
|
|
200
227
|
*
|
|
201
|
-
* Sets
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
*
|
|
228
|
+
* Sets the module-level fallback (for tests and SSR) and the
|
|
229
|
+
* globalThis bridge, then returns the NavigationState so callers
|
|
230
|
+
* can pass it explicitly to renderRoot/wrapPayload — eliminating
|
|
231
|
+
* temporal coupling with getNavigationState().
|
|
205
232
|
*/
|
|
206
233
|
function updateNavigationState(
|
|
207
234
|
params: Record<string, string | string[]> | null | undefined,
|
|
208
235
|
url: string
|
|
209
|
-
):
|
|
236
|
+
): NavigationState {
|
|
210
237
|
const resolvedParams = params ?? {};
|
|
211
238
|
// Module-level fallback for tests (no NavigationProvider) and SSR
|
|
212
239
|
setCurrentParams(resolvedParams);
|
|
213
|
-
//
|
|
240
|
+
// globalThis bridge — kept for backward compat
|
|
214
241
|
const pathname = url.startsWith('http') ? new URL(url).pathname : url.split('?')[0] || '/';
|
|
215
|
-
|
|
242
|
+
const navState: NavigationState = { params: resolvedParams, pathname };
|
|
243
|
+
setNavigationState(navState);
|
|
244
|
+
return navState;
|
|
216
245
|
}
|
|
217
246
|
|
|
218
247
|
/**
|
|
219
248
|
* Render a payload via navigateTransition (production) or renderRoot (tests).
|
|
220
|
-
* The perform callback should fetch data, update state, and return the
|
|
221
|
-
*
|
|
222
|
-
*
|
|
249
|
+
* The perform callback should fetch data, update state, and return the
|
|
250
|
+
* FetchResult plus the NavigationState (so it can be passed explicitly
|
|
251
|
+
* to wrapPayload/renderRoot without temporal coupling).
|
|
223
252
|
*/
|
|
224
253
|
async function renderViaTransition(
|
|
225
254
|
url: string,
|
|
226
|
-
perform: () => Promise<FetchResult>
|
|
255
|
+
perform: () => Promise<FetchResult & { navState: NavigationState }>
|
|
227
256
|
): Promise<HeadElement[] | null> {
|
|
228
257
|
if (deps.navigateTransition) {
|
|
229
258
|
let headElements: HeadElement[] | null = null;
|
|
@@ -239,7 +268,9 @@ export function createRouter(deps: RouterDeps): RouterInstance {
|
|
|
239
268
|
headElements: result.headElements,
|
|
240
269
|
params: result.params,
|
|
241
270
|
});
|
|
242
|
-
|
|
271
|
+
// Pass navState explicitly — wrapPayload wraps element in
|
|
272
|
+
// NavigationProvider with this state, no getNavigationState() needed.
|
|
273
|
+
return wrapPayload(merged, result.navState);
|
|
243
274
|
});
|
|
244
275
|
return headElements;
|
|
245
276
|
}
|
|
@@ -253,7 +284,7 @@ export function createRouter(deps: RouterDeps): RouterInstance {
|
|
|
253
284
|
headElements: result.headElements,
|
|
254
285
|
params: result.params,
|
|
255
286
|
});
|
|
256
|
-
renderPayload(merged);
|
|
287
|
+
renderPayload(merged, result.navState);
|
|
257
288
|
return result.headElements;
|
|
258
289
|
}
|
|
259
290
|
|
|
@@ -273,6 +304,17 @@ export function createRouter(deps: RouterDeps): RouterInstance {
|
|
|
273
304
|
}
|
|
274
305
|
}
|
|
275
306
|
|
|
307
|
+
/**
|
|
308
|
+
* Schedule scroll restoration after the next paint and fire the
|
|
309
|
+
* scroll-restored event. Used by navigate, popstate, and refresh.
|
|
310
|
+
*/
|
|
311
|
+
function restoreScrollAfterPaint(scrollY: number): void {
|
|
312
|
+
afterPaint(() => {
|
|
313
|
+
deps.scrollTo(0, scrollY);
|
|
314
|
+
window.dispatchEvent(new Event('timber:scroll-restored'));
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
276
318
|
/**
|
|
277
319
|
* Core navigation logic shared between the transition and fallback paths.
|
|
278
320
|
* Fetches the RSC payload, updates all state, and returns the result.
|
|
@@ -280,7 +322,7 @@ export function createRouter(deps: RouterDeps): RouterInstance {
|
|
|
280
322
|
async function performNavigationFetch(
|
|
281
323
|
url: string,
|
|
282
324
|
options: { replace: boolean }
|
|
283
|
-
): Promise<FetchResult> {
|
|
325
|
+
): Promise<FetchResult & { navState: NavigationState }> {
|
|
284
326
|
// Check prefetch cache first. PrefetchResult has optional segmentInfo/params
|
|
285
327
|
// fields — normalize to null for FetchResult compatibility.
|
|
286
328
|
const prefetched = prefetchCache.consume(url);
|
|
@@ -320,10 +362,10 @@ export function createRouter(deps: RouterDeps): RouterInstance {
|
|
|
320
362
|
// Update the segment cache with the new route's segment tree.
|
|
321
363
|
updateSegmentCache(result.segmentInfo);
|
|
322
364
|
|
|
323
|
-
// Update navigation state
|
|
324
|
-
updateNavigationState(result.params, url);
|
|
365
|
+
// Update navigation state and capture it for explicit passing.
|
|
366
|
+
const navState = updateNavigationState(result.params, url);
|
|
325
367
|
|
|
326
|
-
return result;
|
|
368
|
+
return { ...result, navState };
|
|
327
369
|
}
|
|
328
370
|
|
|
329
371
|
async function navigate(url: string, options: NavigationOptions = {}): Promise<void> {
|
|
@@ -354,15 +396,18 @@ export function createRouter(deps: RouterDeps): RouterInstance {
|
|
|
354
396
|
// Scroll-to-top on forward navigation, or restore captured position
|
|
355
397
|
// for scroll={false}. React's render() on the document root can reset
|
|
356
398
|
// scroll during DOM reconciliation, so all scroll must be actively managed.
|
|
357
|
-
|
|
358
|
-
if (scroll) {
|
|
359
|
-
deps.scrollTo(0, 0);
|
|
360
|
-
} else {
|
|
361
|
-
deps.scrollTo(0, currentScrollY);
|
|
362
|
-
}
|
|
363
|
-
window.dispatchEvent(new Event('timber:scroll-restored'));
|
|
364
|
-
});
|
|
399
|
+
restoreScrollAfterPaint(scroll ? 0 : currentScrollY);
|
|
365
400
|
} catch (error) {
|
|
401
|
+
// Version skew — server has been redeployed. Trigger full page reload
|
|
402
|
+
// so the browser fetches the new bundle. See TIM-446.
|
|
403
|
+
if (error instanceof VersionSkewError) {
|
|
404
|
+
// Import triggerStaleReload dynamically to avoid circular deps
|
|
405
|
+
// and keep the reload logic centralized with its loop guard.
|
|
406
|
+
const { triggerStaleReload } = await import('./stale-reload.js');
|
|
407
|
+
triggerStaleReload();
|
|
408
|
+
// Return a never-resolving promise — page is reloading.
|
|
409
|
+
return new Promise(() => {}) as never;
|
|
410
|
+
}
|
|
366
411
|
// Server-side redirect during RSC fetch → soft router navigation.
|
|
367
412
|
if (error instanceof RedirectError) {
|
|
368
413
|
setPending(false);
|
|
@@ -388,8 +433,8 @@ export function createRouter(deps: RouterDeps): RouterInstance {
|
|
|
388
433
|
const result = await fetchRscPayload(currentUrl, deps);
|
|
389
434
|
// History push handled by renderViaTransition (stores merged payload)
|
|
390
435
|
updateSegmentCache(result.segmentInfo);
|
|
391
|
-
updateNavigationState(result.params, currentUrl);
|
|
392
|
-
return result;
|
|
436
|
+
const navState = updateNavigationState(result.params, currentUrl);
|
|
437
|
+
return { ...result, navState };
|
|
393
438
|
});
|
|
394
439
|
|
|
395
440
|
applyHead(headElements);
|
|
@@ -406,13 +451,10 @@ export function createRouter(deps: RouterDeps): RouterInstance {
|
|
|
406
451
|
|
|
407
452
|
if (entry && entry.payload !== null) {
|
|
408
453
|
// Replay cached payload — no server roundtrip
|
|
409
|
-
updateNavigationState(entry.params, url);
|
|
410
|
-
renderPayload(entry.payload);
|
|
454
|
+
const navState = updateNavigationState(entry.params, url);
|
|
455
|
+
renderPayload(entry.payload, navState);
|
|
411
456
|
applyHead(entry.headElements);
|
|
412
|
-
|
|
413
|
-
deps.scrollTo(0, scrollY);
|
|
414
|
-
window.dispatchEvent(new Event('timber:scroll-restored'));
|
|
415
|
-
});
|
|
457
|
+
restoreScrollAfterPaint(scrollY);
|
|
416
458
|
} else {
|
|
417
459
|
// No cached payload — fetch from server.
|
|
418
460
|
// This happens when navigating back to the initial SSR'd page
|
|
@@ -421,19 +463,18 @@ export function createRouter(deps: RouterDeps): RouterInstance {
|
|
|
421
463
|
setPending(true, url);
|
|
422
464
|
try {
|
|
423
465
|
const headElements = await renderViaTransition(url, async () => {
|
|
424
|
-
const stateTree = segmentCache.serializeStateTree(
|
|
466
|
+
const stateTree = segmentCache.serializeStateTree(
|
|
467
|
+
segmentElementCache.getMergeablePaths()
|
|
468
|
+
);
|
|
425
469
|
const result = await fetchRscPayload(url, deps, stateTree);
|
|
426
470
|
updateSegmentCache(result.segmentInfo);
|
|
427
|
-
updateNavigationState(result.params, url);
|
|
471
|
+
const navState = updateNavigationState(result.params, url);
|
|
428
472
|
// History push handled by renderViaTransition (stores merged payload)
|
|
429
|
-
return result;
|
|
473
|
+
return { ...result, navState };
|
|
430
474
|
});
|
|
431
475
|
|
|
432
476
|
applyHead(headElements);
|
|
433
|
-
|
|
434
|
-
deps.scrollTo(0, scrollY);
|
|
435
|
-
window.dispatchEvent(new Event('timber:scroll-restored'));
|
|
436
|
-
});
|
|
477
|
+
restoreScrollAfterPaint(scrollY);
|
|
437
478
|
} finally {
|
|
438
479
|
setPending(false);
|
|
439
480
|
}
|
|
@@ -465,8 +506,8 @@ export function createRouter(deps: RouterDeps): RouterInstance {
|
|
|
465
506
|
navigate,
|
|
466
507
|
refresh,
|
|
467
508
|
handlePopState,
|
|
468
|
-
isPending: () =>
|
|
469
|
-
getPendingUrl: () =>
|
|
509
|
+
isPending: () => routerPhase.phase === 'navigating',
|
|
510
|
+
getPendingUrl: () => (routerPhase.phase === 'navigating' ? routerPhase.targetUrl : null),
|
|
470
511
|
onPendingChange(listener) {
|
|
471
512
|
pendingListeners.add(listener);
|
|
472
513
|
return () => pendingListeners.delete(listener);
|
|
@@ -483,7 +524,11 @@ export function createRouter(deps: RouterDeps): RouterInstance {
|
|
|
483
524
|
payload: merged,
|
|
484
525
|
headElements,
|
|
485
526
|
});
|
|
486
|
-
|
|
527
|
+
// Revalidation doesn't change params/pathname — preserve current state.
|
|
528
|
+
// DO NOT call updateNavigationState(null, ...) here: that normalizes
|
|
529
|
+
// params to {}, clearing dynamic route params on every action response.
|
|
530
|
+
const navState = getNavigationState();
|
|
531
|
+
renderPayload(merged, navState);
|
|
487
532
|
applyHead(headElements);
|
|
488
533
|
},
|
|
489
534
|
initSegmentCache: (segments: SegmentInfo[]) => updateSegmentCache(segments),
|
package/src/client/rsc-fetch.ts
CHANGED
|
@@ -23,7 +23,7 @@ export interface FetchResult {
|
|
|
23
23
|
headElements: HeadElement[] | null;
|
|
24
24
|
/** Segment metadata from X-Timber-Segments header for populating the segment cache. */
|
|
25
25
|
segmentInfo: SegmentInfo[] | null;
|
|
26
|
-
/** Route params from X-Timber-Params header for populating
|
|
26
|
+
/** Route params from X-Timber-Params header for populating useSegmentParams(). */
|
|
27
27
|
params: Record<string, string | string[]> | null;
|
|
28
28
|
/** Segment paths that were skipped by the server (for client-side merging). */
|
|
29
29
|
skippedSegments: string[] | null;
|
|
@@ -58,6 +58,43 @@ function appendRscParam(url: string): string {
|
|
|
58
58
|
return `${url}${separator}_rsc=${generateCacheBustId()}`;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
// ─── Deployment ID ───────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The client's deployment ID, set at bootstrap from the runtime config.
|
|
65
|
+
* Sent with every RSC/action request for version skew detection.
|
|
66
|
+
* Null in dev mode. See TIM-446.
|
|
67
|
+
*/
|
|
68
|
+
let clientDeploymentId: string | null = null;
|
|
69
|
+
|
|
70
|
+
/** Set the client deployment ID. Called once at bootstrap. */
|
|
71
|
+
export function setClientDeploymentId(id: string | null): void {
|
|
72
|
+
clientDeploymentId = id;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Get the client deployment ID. */
|
|
76
|
+
export function getClientDeploymentId(): string | null {
|
|
77
|
+
return clientDeploymentId;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ─── Reload Signal ───────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
/** Header name used by the server to signal a version skew reload. */
|
|
83
|
+
export const RELOAD_HEADER = 'X-Timber-Reload';
|
|
84
|
+
|
|
85
|
+
/** Header name for the client's deployment ID. */
|
|
86
|
+
export const DEPLOYMENT_ID_HEADER = 'X-Timber-Deployment-Id';
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Check if a response signals a version skew reload.
|
|
90
|
+
* Triggers a full page reload if the server indicates the client is stale.
|
|
91
|
+
*/
|
|
92
|
+
export function checkReloadSignal(response: Response): boolean {
|
|
93
|
+
return response.headers.get(RELOAD_HEADER) === '1';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ─── Header Builder ──────────────────────────────────────────────
|
|
97
|
+
|
|
61
98
|
export function buildRscHeaders(
|
|
62
99
|
stateTree: { segments: string[] } | undefined,
|
|
63
100
|
currentUrl?: string
|
|
@@ -75,6 +112,13 @@ export function buildRscHeaders(
|
|
|
75
112
|
if (currentUrl) {
|
|
76
113
|
headers['X-Timber-URL'] = currentUrl;
|
|
77
114
|
}
|
|
115
|
+
// Send deployment ID for version skew detection (TIM-446).
|
|
116
|
+
// The server compares this against the current build's ID.
|
|
117
|
+
// On mismatch, the server signals a reload instead of returning
|
|
118
|
+
// an RSC payload with mismatched module references.
|
|
119
|
+
if (clientDeploymentId) {
|
|
120
|
+
headers[DEPLOYMENT_ID_HEADER] = clientDeploymentId;
|
|
121
|
+
}
|
|
78
122
|
return headers;
|
|
79
123
|
}
|
|
80
124
|
|
|
@@ -135,7 +179,7 @@ export function extractSkippedSegments(response: Response): string[] | null {
|
|
|
135
179
|
* Extract route params from the X-Timber-Params response header.
|
|
136
180
|
* Returns null if the header is missing or malformed.
|
|
137
181
|
*
|
|
138
|
-
* Used to populate
|
|
182
|
+
* Used to populate useSegmentParams() after client-side navigation.
|
|
139
183
|
*/
|
|
140
184
|
export function extractParams(response: Response): Record<string, string | string[]> | null {
|
|
141
185
|
const header = response.headers.get('X-Timber-Params');
|
|
@@ -161,6 +205,17 @@ export class RedirectError extends Error {
|
|
|
161
205
|
}
|
|
162
206
|
}
|
|
163
207
|
|
|
208
|
+
/**
|
|
209
|
+
* Thrown when the server signals a version skew (X-Timber-Reload header).
|
|
210
|
+
* Caught in navigate() to trigger a full page reload via triggerStaleReload().
|
|
211
|
+
* See TIM-446.
|
|
212
|
+
*/
|
|
213
|
+
export class VersionSkewError extends Error {
|
|
214
|
+
constructor() {
|
|
215
|
+
super('Version skew detected — server has been redeployed');
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
164
219
|
// ─── Fetch ───────────────────────────────────────────────────────
|
|
165
220
|
|
|
166
221
|
/**
|
|
@@ -192,6 +247,12 @@ export async function fetchRscPayload(
|
|
|
192
247
|
let params: Record<string, string | string[]> | null = null;
|
|
193
248
|
let skippedSegments: string[] | null = null;
|
|
194
249
|
const wrappedPromise = fetchPromise.then((response) => {
|
|
250
|
+
// Version skew detection (TIM-446): if the server signals a reload,
|
|
251
|
+
// throw VersionSkewError so the caller (router navigate) can trigger
|
|
252
|
+
// a full page reload via triggerStaleReload().
|
|
253
|
+
if (checkReloadSignal(response)) {
|
|
254
|
+
throw new VersionSkewError();
|
|
255
|
+
}
|
|
195
256
|
// Detect server-side redirects. The server returns 204 + X-Timber-Redirect
|
|
196
257
|
// for RSC payload requests instead of a raw 302, because fetch with
|
|
197
258
|
// redirect: "manual" turns 302s into opaque redirects (status 0, null body)
|
|
@@ -11,7 +11,7 @@ export interface PrefetchResult {
|
|
|
11
11
|
headElements: HeadElement[] | null;
|
|
12
12
|
/** Segment metadata from X-Timber-Segments header for populating the segment cache. */
|
|
13
13
|
segmentInfo?: SegmentInfo[] | null;
|
|
14
|
-
/** Route params from X-Timber-Params header for populating
|
|
14
|
+
/** Route params from X-Timber-Params header for populating useSegmentParams(). */
|
|
15
15
|
params?: Record<string, string | string[]> | null;
|
|
16
16
|
/** Segment paths skipped by the server (for client-side merging). */
|
|
17
17
|
skippedSegments?: string[] | null;
|
|
@@ -52,7 +52,12 @@ interface SegmentProviderProps {
|
|
|
52
52
|
* Wraps each layout to provide segment position context.
|
|
53
53
|
* Injected by rsc-entry.ts during element tree construction.
|
|
54
54
|
*/
|
|
55
|
-
export function SegmentProvider({
|
|
55
|
+
export function SegmentProvider({
|
|
56
|
+
segments,
|
|
57
|
+
segmentId: _segmentId,
|
|
58
|
+
parallelRouteKeys,
|
|
59
|
+
children,
|
|
60
|
+
}: SegmentProviderProps) {
|
|
56
61
|
const value = useMemo(
|
|
57
62
|
() => ({ segments, parallelRouteKeys }),
|
|
58
63
|
// segments and parallelRouteKeys are static per layout — they don't change
|
|
@@ -186,10 +186,7 @@ function walkChildren(children: ReactNode, out: CachedSegmentEntry[]): void {
|
|
|
186
186
|
* Cache all segment subtrees from a fully-rendered RSC element tree.
|
|
187
187
|
* Call this after every full RSC payload render (navigate, refresh, hydration).
|
|
188
188
|
*/
|
|
189
|
-
export function cacheSegmentElements(
|
|
190
|
-
element: unknown,
|
|
191
|
-
cache: SegmentElementCache
|
|
192
|
-
): void {
|
|
189
|
+
export function cacheSegmentElements(element: unknown, cache: SegmentElementCache): void {
|
|
193
190
|
const segments = extractSegments(element);
|
|
194
191
|
for (const entry of segments) {
|
|
195
192
|
cache.set(entry.segmentPath, entry);
|
|
@@ -208,10 +205,7 @@ export function cacheSegmentElements(
|
|
|
208
205
|
*/
|
|
209
206
|
type TreePath = Array<{ element: ReactElement; childIndex: number }>;
|
|
210
207
|
|
|
211
|
-
function findSegmentProviderPath(
|
|
212
|
-
node: ReactElement,
|
|
213
|
-
targetPath?: string
|
|
214
|
-
): TreePath | null {
|
|
208
|
+
function findSegmentProviderPath(node: ReactElement, targetPath?: string): TreePath | null {
|
|
215
209
|
const children = (node.props as { children?: ReactNode }).children;
|
|
216
210
|
if (children == null) return null;
|
|
217
211
|
|
|
@@ -32,6 +32,34 @@ export function isStaleClientReference(error: unknown): boolean {
|
|
|
32
32
|
return msg.includes('Could not find the module') || msg.includes('client reference not found');
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Check if an error is a chunk load failure from a dynamic import.
|
|
37
|
+
*
|
|
38
|
+
* After a deployment, old chunk filenames no longer exist. When the client
|
|
39
|
+
* tries to dynamically import a chunk that's been replaced, the browser
|
|
40
|
+
* throws one of these errors:
|
|
41
|
+
*
|
|
42
|
+
* - Chromium: "Failed to fetch dynamically imported module: <url>"
|
|
43
|
+
* - Firefox: "error loading dynamically imported module: <url>"
|
|
44
|
+
* - Safari: "Importing a module script failed."
|
|
45
|
+
* - Vite/Rollup: "Unable to preload CSS for <url>"
|
|
46
|
+
*
|
|
47
|
+
* See TIM-446
|
|
48
|
+
*/
|
|
49
|
+
export function isChunkLoadError(error: unknown): boolean {
|
|
50
|
+
if (!(error instanceof Error)) return false;
|
|
51
|
+
const msg = error.message.toLowerCase();
|
|
52
|
+
return (
|
|
53
|
+
msg.includes('failed to fetch dynamically imported module') ||
|
|
54
|
+
msg.includes('error loading dynamically imported module') ||
|
|
55
|
+
msg.includes('importing a module script failed') ||
|
|
56
|
+
msg.includes('unable to preload css') ||
|
|
57
|
+
// Webpack-style chunk load errors (unlikely in Vite but defensive)
|
|
58
|
+
msg.includes('loading chunk') ||
|
|
59
|
+
msg.includes('loading css chunk')
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
35
63
|
/**
|
|
36
64
|
* Trigger a full page reload to pick up new bundles.
|
|
37
65
|
*
|
|
@@ -48,8 +76,8 @@ export function triggerStaleReload(): boolean {
|
|
|
48
76
|
if (sessionStorage.getItem(RELOAD_FLAG_KEY)) {
|
|
49
77
|
console.warn(
|
|
50
78
|
'[timber] Stale client reference detected again after reload. ' +
|
|
51
|
-
|
|
52
|
-
|
|
79
|
+
'Not reloading to prevent infinite loop. ' +
|
|
80
|
+
'This may indicate a deployment issue — try a hard refresh.'
|
|
53
81
|
);
|
|
54
82
|
return false;
|
|
55
83
|
}
|
|
@@ -59,7 +87,7 @@ export function triggerStaleReload(): boolean {
|
|
|
59
87
|
|
|
60
88
|
console.warn(
|
|
61
89
|
'[timber] Stale client reference detected — the server has been ' +
|
|
62
|
-
|
|
90
|
+
'redeployed with new bundles. Reloading to pick up the new version.'
|
|
63
91
|
);
|
|
64
92
|
|
|
65
93
|
window.location.reload();
|
|
@@ -67,9 +95,7 @@ export function triggerStaleReload(): boolean {
|
|
|
67
95
|
} catch {
|
|
68
96
|
// sessionStorage may be unavailable (private browsing, storage full, etc.)
|
|
69
97
|
// Fall back to reloading without loop protection
|
|
70
|
-
console.warn(
|
|
71
|
-
'[timber] Stale client reference detected. Reloading page.'
|
|
72
|
-
);
|
|
98
|
+
console.warn('[timber] Stale client reference detected. Reloading page.');
|
|
73
99
|
window.location.reload();
|
|
74
100
|
return true;
|
|
75
101
|
}
|
|
@@ -39,7 +39,7 @@ export interface TopLoaderConfig {
|
|
|
39
39
|
color?: string;
|
|
40
40
|
/** Bar height in pixels. Default: 3. */
|
|
41
41
|
height?: number;
|
|
42
|
-
/** Show subtle glow/shadow effect. Default:
|
|
42
|
+
/** Show subtle glow/shadow effect. Default: false. */
|
|
43
43
|
shadow?: boolean;
|
|
44
44
|
/** Delay in ms before showing the bar. Default: 0. */
|
|
45
45
|
delay?: number;
|
|
@@ -51,7 +51,7 @@ export interface TopLoaderConfig {
|
|
|
51
51
|
|
|
52
52
|
const DEFAULT_COLOR = '#2299DD';
|
|
53
53
|
const DEFAULT_HEIGHT = 3;
|
|
54
|
-
const DEFAULT_SHADOW =
|
|
54
|
+
const DEFAULT_SHADOW = false;
|
|
55
55
|
const DEFAULT_DELAY = 0;
|
|
56
56
|
const DEFAULT_Z_INDEX = 1600;
|
|
57
57
|
|
|
@@ -183,18 +183,19 @@ export function TopLoader({ config }: { config?: TopLoaderConfig }): React.React
|
|
|
183
183
|
};
|
|
184
184
|
|
|
185
185
|
// Clean up the finishing phase when the finish animation completes.
|
|
186
|
-
const handleAnimationEnd =
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
186
|
+
const handleAnimationEnd =
|
|
187
|
+
phase === 'finishing'
|
|
188
|
+
? (e: React.AnimationEvent) => {
|
|
189
|
+
if (e.animationName === FINISH_KEYFRAMES) {
|
|
190
|
+
setPhase('hidden');
|
|
191
|
+
}
|
|
190
192
|
}
|
|
191
|
-
|
|
192
|
-
: undefined;
|
|
193
|
+
: undefined;
|
|
193
194
|
|
|
194
195
|
return createElement(
|
|
195
196
|
'div',
|
|
196
197
|
{
|
|
197
|
-
style: containerStyle,
|
|
198
|
+
'style': containerStyle,
|
|
198
199
|
'aria-hidden': 'true',
|
|
199
200
|
'data-timber-top-loader': '',
|
|
200
201
|
},
|
|
@@ -62,7 +62,13 @@ let _navigateTransition:
|
|
|
62
62
|
* Non-navigation renders:
|
|
63
63
|
* transitionRender(newWrappedElement);
|
|
64
64
|
*/
|
|
65
|
-
export function TransitionRoot({
|
|
65
|
+
export function TransitionRoot({
|
|
66
|
+
initial,
|
|
67
|
+
topLoaderConfig,
|
|
68
|
+
}: {
|
|
69
|
+
initial: ReactNode;
|
|
70
|
+
topLoaderConfig?: TopLoaderConfig;
|
|
71
|
+
}): ReactNode {
|
|
66
72
|
const [element, setElement] = useState<ReactNode>(initial);
|
|
67
73
|
const [pendingUrl, setPendingUrl] = useState<string | null>(null);
|
|
68
74
|
const [, startTransition] = useTransition();
|
package/src/client/use-params.ts
CHANGED
|
@@ -119,9 +119,9 @@ export function notifyParamsListeners(): void {
|
|
|
119
119
|
* exact params shape from the generated Routes interface.
|
|
120
120
|
* @overload Fallback — returns the generic params record.
|
|
121
121
|
*/
|
|
122
|
-
export function
|
|
123
|
-
export function
|
|
124
|
-
export function
|
|
122
|
+
export function useSegmentParams<R extends keyof Routes>(route: R): Routes[R]['params'];
|
|
123
|
+
export function useSegmentParams(route?: string): Record<string, string | string[]>;
|
|
124
|
+
export function useSegmentParams(_route?: string): Record<string, string | string[]> {
|
|
125
125
|
// Try reading from NavigationContext (client-side, inside React tree).
|
|
126
126
|
// During SSR, no NavigationProvider is mounted, so this returns null.
|
|
127
127
|
// When called outside a React component, useContext throws — caught below.
|
|
@@ -17,7 +17,7 @@ import type {
|
|
|
17
17
|
SearchParamsDefinition,
|
|
18
18
|
SetParams,
|
|
19
19
|
QueryStatesOptions,
|
|
20
|
-
} from '#/search-params/
|
|
20
|
+
} from '#/search-params/define.js';
|
|
21
21
|
import { getSearchParams } from '#/search-params/registry.js';
|
|
22
22
|
|
|
23
23
|
// ─── Codec Bridge ─────────────────────────────────────────────────
|
package/src/codec.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared codec protocol for parsing and serializing string values.
|
|
3
|
+
*
|
|
4
|
+
* Used by both search params and cookies. Any object with parse + serialize
|
|
5
|
+
* methods satisfies this interface. nuqs parsers are valid codecs natively.
|
|
6
|
+
*
|
|
7
|
+
* Design doc: design/23a-search-params-triage.md §"Unify Codec<T> type"
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A codec that converts between string values and typed values.
|
|
12
|
+
*
|
|
13
|
+
* The canonical protocol shared across search params, cookies, and
|
|
14
|
+
* any future timber feature that needs string ↔ typed conversion.
|
|
15
|
+
*/
|
|
16
|
+
export interface Codec<T> {
|
|
17
|
+
/** String → typed value. Receives undefined when the value is absent. */
|
|
18
|
+
parse(value: string | string[] | undefined): T;
|
|
19
|
+
/** Typed value → string. Return null to omit/clear. */
|
|
20
|
+
serialize(value: T): string | null;
|
|
21
|
+
}
|