@timber-js/app 0.2.0-alpha.197 → 0.2.0-alpha.199
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_chunks/{actions-BS-m5SLv.js → actions-d1hCqnU3.js} +35 -8
- package/dist/_chunks/actions-d1hCqnU3.js.map +1 -0
- package/dist/_chunks/als-registry-C6kcfprT.js.map +1 -1
- package/dist/_chunks/{cache-api-DqzgTEqk.js → cache-api-ByagcC-J.js} +2 -2
- package/dist/_chunks/{cache-api-DqzgTEqk.js.map → cache-api-ByagcC-J.js.map} +1 -1
- package/dist/_chunks/canonicalize-CgHoscYO.js +66 -0
- package/dist/_chunks/canonicalize-CgHoscYO.js.map +1 -0
- package/dist/_chunks/{chains-CZG7E5zg.js → chains-Bpb0W4ax.js} +3 -3
- package/dist/_chunks/{chains-CZG7E5zg.js.map → chains-Bpb0W4ax.js.map} +1 -1
- package/dist/_chunks/{cli-check-bE3H5Bjr.js → cli-check-D6VolrDV.js} +3 -3
- package/dist/_chunks/{cli-check-bE3H5Bjr.js.map → cli-check-D6VolrDV.js.map} +1 -1
- package/dist/_chunks/{cli-schema-sync-DTy_-Msq.js → cli-schema-sync-D6rO-VcS.js} +2 -2
- package/dist/_chunks/{cli-schema-sync-DTy_-Msq.js.map → cli-schema-sync-D6rO-VcS.js.map} +1 -1
- package/dist/_chunks/{convention-lint-n3RJLgww.js → convention-lint-fRkwVwEH.js} +27 -4
- package/dist/_chunks/convention-lint-fRkwVwEH.js.map +1 -0
- package/dist/_chunks/error-boundary-BfPHZjm0.js +1050 -0
- package/dist/_chunks/error-boundary-BfPHZjm0.js.map +1 -0
- package/dist/_chunks/{live-graph-BXDsdzBv.js → live-graph-D_2D32Ad.js} +3 -3
- package/dist/_chunks/{live-graph-BXDsdzBv.js.map → live-graph-D_2D32Ad.js.map} +1 -1
- package/dist/_chunks/{logger-DDirEsn7.js → logger-uLBuGKDI.js} +471 -440
- package/dist/_chunks/logger-uLBuGKDI.js.map +1 -0
- package/dist/_chunks/{navigation-root-B00jjGd5.js → navigation-context-D0TU0Jog.js} +3 -101
- package/dist/_chunks/navigation-context-D0TU0Jog.js.map +1 -0
- package/dist/_chunks/navigation-root-mHSK9psY.js +126 -0
- package/dist/_chunks/{navigation-root-B00jjGd5.js.map → navigation-root-mHSK9psY.js.map} +1 -1
- package/dist/_chunks/{poison-scan-BoDLgbix.js → poison-scan-Bm9Yyqk9.js} +2 -2
- package/dist/_chunks/{poison-scan-BoDLgbix.js.map → poison-scan-Bm9Yyqk9.js.map} +1 -1
- package/dist/_chunks/{scanner-tdFPvDYi.js → scanner-AiazgH_f.js} +6 -5
- package/dist/_chunks/scanner-AiazgH_f.js.map +1 -0
- package/dist/_chunks/{segment-keys-BhqoHiLc.js → segment-keys-lqtdookO.js} +2 -65
- package/dist/_chunks/segment-keys-lqtdookO.js.map +1 -0
- package/dist/_chunks/{ssr-data-BQGhTPAK.js → ssr-data-D6T6Y3ef.js} +4 -26
- package/dist/_chunks/ssr-data-D6T6Y3ef.js.map +1 -0
- package/dist/_chunks/state-FippDgxN.js +52 -0
- package/dist/_chunks/state-FippDgxN.js.map +1 -0
- package/dist/_chunks/status-page-marker-DwQBrLBz.js +496 -0
- package/dist/_chunks/status-page-marker-DwQBrLBz.js.map +1 -0
- package/dist/_chunks/{walkers-DNX05dC0.js → walkers-B6XUtmqK.js} +2 -2
- package/dist/_chunks/{walkers-DNX05dC0.js.map → walkers-B6XUtmqK.js.map} +1 -1
- package/dist/analyze/crawl-entry.js +2 -2
- package/dist/analyze/graph-command.js +2 -2
- package/dist/cache/index.js +1 -1
- package/dist/cli.js +2 -2
- package/dist/client/browser-entry/action-dispatch.d.ts +6 -4
- package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -1
- package/dist/client/browser-entry/action-queue.d.ts +44 -0
- package/dist/client/browser-entry/action-queue.d.ts.map +1 -0
- package/dist/client/browser-entry/router-init.d.ts.map +1 -1
- package/dist/client/deny-last-resort.d.ts +29 -0
- package/dist/client/deny-last-resort.d.ts.map +1 -0
- package/dist/client/error-boundary.d.ts +48 -2
- package/dist/client/error-boundary.d.ts.map +1 -1
- package/dist/client/error-boundary.js +2 -2
- package/dist/client/history.d.ts +21 -2
- package/dist/client/history.d.ts.map +1 -1
- package/dist/client/index.js +34 -14
- package/dist/client/index.js.map +1 -1
- package/dist/client/internal.d.ts +1 -0
- package/dist/client/internal.d.ts.map +1 -1
- package/dist/client/internal.js +272 -1225
- package/dist/client/internal.js.map +1 -1
- package/dist/client/link.d.ts.map +1 -1
- package/dist/client/navigation-commit.d.ts +12 -19
- package/dist/client/navigation-commit.d.ts.map +1 -1
- package/dist/client/navigation-transition.d.ts +62 -11
- package/dist/client/navigation-transition.d.ts.map +1 -1
- package/dist/client/router-effects.d.ts +9 -8
- package/dist/client/router-effects.d.ts.map +1 -1
- package/dist/client/router-lifecycle.d.ts +60 -17
- package/dist/client/router-lifecycle.d.ts.map +1 -1
- package/dist/client/router-pipeline.d.ts +7 -4
- package/dist/client/router-pipeline.d.ts.map +1 -1
- package/dist/client/router-types.d.ts +63 -8
- package/dist/client/router-types.d.ts.map +1 -1
- package/dist/client/router.d.ts.map +1 -1
- package/dist/client/rsc-fetch.d.ts +0 -9
- package/dist/client/rsc-fetch.d.ts.map +1 -1
- package/dist/client/segment-cache.d.ts +23 -8
- package/dist/client/segment-cache.d.ts.map +1 -1
- package/dist/client/state.d.ts +16 -0
- package/dist/client/state.d.ts.map +1 -1
- package/dist/client/status-page-marker.d.ts +25 -0
- package/dist/client/status-page-marker.d.ts.map +1 -0
- package/dist/cookies/index.js +1 -1
- package/dist/dev-tools/holding-server.d.ts +4 -17
- package/dist/dev-tools/holding-server.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +74 -175
- package/dist/index.js.map +1 -1
- package/dist/plugins/dev-server.d.ts.map +1 -1
- package/dist/routing/codegen-write.d.ts.map +1 -1
- package/dist/routing/index.js +2 -2
- package/dist/routing/interception.d.ts +2 -2
- package/dist/routing/slot-placement.d.ts +2 -2
- package/dist/server/access-gate.d.ts +73 -1
- package/dist/server/access-gate.d.ts.map +1 -1
- package/dist/server/action-handler.d.ts.map +1 -1
- package/dist/server/actions.d.ts +16 -1
- package/dist/server/actions.d.ts.map +1 -1
- package/dist/server/als-registry.d.ts +3 -9
- package/dist/server/als-registry.d.ts.map +1 -1
- package/dist/server/children-interception.d.ts +1 -1
- package/dist/server/default-status-page.d.ts +2 -2
- package/dist/server/default-status-page.d.ts.map +1 -1
- package/dist/server/deny-boundary.d.ts +15 -9
- package/dist/server/deny-boundary.d.ts.map +1 -1
- package/dist/server/deny-renderer.d.ts.map +1 -1
- package/dist/server/error-boundary-wrapper.d.ts +21 -4
- package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
- package/dist/server/error-response-headers.d.ts +3 -0
- package/dist/server/error-response-headers.d.ts.map +1 -0
- package/dist/server/index.js +3 -3
- package/dist/server/index.js.map +1 -1
- package/dist/server/internal.d.ts +1 -2
- package/dist/server/internal.d.ts.map +1 -1
- package/dist/server/internal.js +2339 -2506
- package/dist/server/internal.js.map +1 -1
- package/dist/server/metadata-collector.d.ts +2 -5
- package/dist/server/metadata-collector.d.ts.map +1 -1
- package/dist/server/param-coercion.d.ts +10 -3
- package/dist/server/param-coercion.d.ts.map +1 -1
- package/dist/server/pipeline-outcome.d.ts.map +1 -1
- package/dist/server/pipeline-phases.d.ts +11 -0
- package/dist/server/pipeline-phases.d.ts.map +1 -1
- package/dist/server/port-resolution.d.ts +3 -89
- package/dist/server/port-resolution.d.ts.map +1 -1
- package/dist/server/primitives.d.ts +38 -10
- package/dist/server/primitives.d.ts.map +1 -1
- package/dist/server/response-cache-policy.d.ts +3 -0
- package/dist/server/response-cache-policy.d.ts.map +1 -0
- package/dist/server/route-element-builder.d.ts +11 -41
- package/dist/server/route-element-builder.d.ts.map +1 -1
- package/dist/server/route-element-helpers.d.ts +12 -0
- package/dist/server/route-element-helpers.d.ts.map +1 -0
- package/dist/server/route-module-loader.d.ts +37 -0
- package/dist/server/route-module-loader.d.ts.map +1 -0
- package/dist/server/rsc-cache-key-guard.d.ts.map +1 -1
- package/dist/server/rsc-entry/action-middleware-runner.d.ts.map +1 -1
- package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
- package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-payload.d.ts +22 -1
- package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-stream.d.ts +4 -11
- package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
- package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
- package/dist/server/rsc-error-envelope.d.ts +11 -0
- package/dist/server/rsc-error-envelope.d.ts.map +1 -0
- package/dist/server/skippable-prefix.d.ts +18 -15
- package/dist/server/skippable-prefix.d.ts.map +1 -1
- package/dist/server/slot-resolver.d.ts.map +1 -1
- package/dist/server/slot-subtree-contain.d.ts +54 -0
- package/dist/server/slot-subtree-contain.d.ts.map +1 -0
- package/dist/server/stream-utils.d.ts.map +1 -1
- package/dist/server/utils/element-type.d.ts +10 -0
- package/dist/server/utils/element-type.d.ts.map +1 -1
- package/dist/shared/rsc-error-envelope.d.ts +0 -9
- package/dist/shared/rsc-error-envelope.d.ts.map +1 -1
- package/dist/shared/status-reason-phrase.d.ts +26 -0
- package/dist/shared/status-reason-phrase.d.ts.map +1 -0
- package/docs/api/30-api-server.mdx +4 -2
- package/docs/api/31-api-client.mdx +5 -1
- package/docs/api/36-cli.mdx +5 -3
- package/docs/learn/12-error-handling.mdx +5 -1
- package/package.json +10 -10
- package/src/client/browser-entry/action-dispatch.ts +166 -99
- package/src/client/browser-entry/action-queue.ts +90 -0
- package/src/client/browser-entry/router-init.ts +60 -35
- package/src/client/deny-last-resort.tsx +54 -0
- package/src/client/error-boundary.tsx +144 -42
- package/src/client/history.ts +52 -3
- package/src/client/internal.ts +1 -0
- package/src/client/link.tsx +70 -35
- package/src/client/navigation-commit.ts +79 -27
- package/src/client/navigation-transition.ts +176 -127
- package/src/client/router-effects.ts +14 -17
- package/src/client/router-lifecycle.ts +181 -115
- package/src/client/router-pipeline.ts +94 -71
- package/src/client/router-types.ts +61 -7
- package/src/client/router.ts +147 -74
- package/src/client/rsc-fetch.ts +0 -13
- package/src/client/segment-cache.ts +43 -10
- package/src/client/state.ts +26 -0
- package/src/client/status-page-marker.tsx +32 -0
- package/src/dev-tools/holding-server.ts +4 -17
- package/src/index.ts +18 -34
- package/src/plugins/dev-server.ts +2 -1
- package/src/react-canary.d.ts +2 -0
- package/src/routing/codegen-write.ts +2 -0
- package/src/routing/interception.ts +2 -2
- package/src/routing/slot-placement.ts +2 -2
- package/src/server/access-gate.tsx +89 -21
- package/src/server/action-client.ts +2 -2
- package/src/server/action-handler.ts +23 -10
- package/src/server/actions.ts +81 -34
- package/src/server/als-registry.ts +3 -9
- package/src/server/children-interception.ts +1 -1
- package/src/server/default-status-page.ts +7 -47
- package/src/server/deny-boundary.ts +45 -28
- package/src/server/deny-renderer.ts +6 -2
- package/src/server/error-boundary-wrapper.ts +23 -4
- package/src/server/error-response-headers.ts +18 -0
- package/src/server/internal.ts +2 -10
- package/src/server/metadata-collector.ts +3 -18
- package/src/server/param-coercion.ts +13 -4
- package/src/server/pipeline-outcome.ts +35 -13
- package/src/server/pipeline-phases.ts +22 -16
- package/src/server/port-resolution.ts +3 -165
- package/src/server/prebuilt-builder.ts +4 -4
- package/src/server/primitives.ts +75 -11
- package/src/server/response-cache-policy.ts +45 -0
- package/src/server/route-element-builder.ts +149 -412
- package/src/server/route-element-helpers.ts +37 -0
- package/src/server/route-handler.ts +2 -2
- package/src/server/route-module-loader.ts +161 -0
- package/src/server/rsc-cache-key-guard.ts +2 -42
- package/src/server/rsc-entry/action-middleware-runner.ts +4 -4
- package/src/server/rsc-entry/api-handler.ts +5 -5
- package/src/server/rsc-entry/error-renderer.ts +3 -4
- package/src/server/rsc-entry/helpers.ts +1 -1
- package/src/server/rsc-entry/index.ts +3 -3
- package/src/server/rsc-entry/render-route.ts +4 -8
- package/src/server/rsc-entry/rsc-payload.ts +59 -42
- package/src/server/rsc-entry/rsc-stream.ts +48 -27
- package/src/server/rsc-entry/ssr-renderer.ts +6 -10
- package/src/server/rsc-error-envelope.ts +18 -0
- package/src/server/skippable-prefix.ts +105 -7
- package/src/server/slot-resolver.ts +43 -12
- package/src/server/slot-subtree-contain.ts +255 -0
- package/src/server/stream-utils.ts +12 -8
- package/src/server/utils/element-type.ts +18 -2
- package/src/shared/rsc-error-envelope.ts +0 -15
- package/src/shared/status-reason-phrase.ts +61 -0
- package/dist/_chunks/actions-BS-m5SLv.js.map +0 -1
- package/dist/_chunks/convention-lint-n3RJLgww.js.map +0 -1
- package/dist/_chunks/error-boundary-BvRCCmbN.js +0 -353
- package/dist/_chunks/error-boundary-BvRCCmbN.js.map +0 -1
- package/dist/_chunks/logger-DDirEsn7.js.map +0 -1
- package/dist/_chunks/mdx-file-CXyHGUpS.js +0 -25
- package/dist/_chunks/mdx-file-CXyHGUpS.js.map +0 -1
- package/dist/_chunks/router-ref-8gr8qsxN.js +0 -28
- package/dist/_chunks/router-ref-8gr8qsxN.js.map +0 -1
- package/dist/_chunks/rsc-error-envelope-tT5PJs4q.js +0 -40
- package/dist/_chunks/rsc-error-envelope-tT5PJs4q.js.map +0 -1
- package/dist/_chunks/scanner-tdFPvDYi.js.map +0 -1
- package/dist/_chunks/segment-keys-BhqoHiLc.js.map +0 -1
- package/dist/_chunks/ssr-data-BQGhTPAK.js.map +0 -1
- package/dist/server/tree-builder.d.ts +0 -150
- package/dist/server/tree-builder.d.ts.map +0 -1
- package/src/server/tree-builder.ts +0 -313
|
@@ -15,6 +15,58 @@ import type { HistoryStack } from './history.ts';
|
|
|
15
15
|
import type { SegmentInfo, StateTree } from '../shared/segment-info.ts';
|
|
16
16
|
import type { ParamsSource } from '../shared/payload-root.ts';
|
|
17
17
|
import { setNavigationState, type NavigationState } from './navigation-context.ts';
|
|
18
|
+
import {
|
|
19
|
+
statusPageRendered,
|
|
20
|
+
_setStatusPageRendered,
|
|
21
|
+
onStatusPageRendered,
|
|
22
|
+
_setOnStatusPageRendered,
|
|
23
|
+
} from './state.ts';
|
|
24
|
+
|
|
25
|
+
// ─── What a status page teaches the cache ─────────────────────────────────
|
|
26
|
+
//
|
|
27
|
+
// A status page replaces a subtree: a deny page lands at the segment owning
|
|
28
|
+
// the matched file and nothing below it mounts; an `error.tsx` fallback
|
|
29
|
+
// discards everything under the boundary that rendered it. `X-Timber-Segments`
|
|
30
|
+
// nonetheless describes the whole chain the payload was built against — it
|
|
31
|
+
// must, because a partial navigation picks its merge point out of it — so the
|
|
32
|
+
// cache has to be told not to learn from such a tree.
|
|
33
|
+
//
|
|
34
|
+
// The HTTP status used to say so, and could not be trusted to: an RSC payload
|
|
35
|
+
// response commits one macrotask after its first chunk, and a nested
|
|
36
|
+
// component that denies after I/O puts its row in the stream after the
|
|
37
|
+
// status line has gone out (TIM-1489). The only thing that reliably knows a
|
|
38
|
+
// status page rendered is the status page itself, and the only moment that
|
|
39
|
+
// matters is the commit that puts it on screen — so every status page the
|
|
40
|
+
// framework produces is wrapped in `StatusPageMarker`, whose layout effect
|
|
41
|
+
// reports on each commit that renders it. Layout effects run child-before-
|
|
42
|
+
// parent, so the report lands before `NavigationRoot`'s publish of that
|
|
43
|
+
// same commit, and never for a render React discards. Without Suspense
|
|
44
|
+
// around the row, the transition cannot commit until the row has arrived, so
|
|
45
|
+
// this covers the delayed nested case too, which no status ever could.
|
|
46
|
+
//
|
|
47
|
+
// The report also clears the cache on the spot, because not every commit
|
|
48
|
+
// has a navigation to consume it: a deny row arriving inside a Suspense
|
|
49
|
+
// boundary after its navigation published, or a status document being
|
|
50
|
+
// hydrated, would otherwise leave a tree in the cache that is no longer on
|
|
51
|
+
// screen. Consuming the flag at commit is what keeps the same navigation from
|
|
52
|
+
// re-seeding it a moment later. Both are over-approximations — a page-level
|
|
53
|
+
// deny leaves every layout standing — at the cost of one full render on the
|
|
54
|
+
// next navigation.
|
|
55
|
+
//
|
|
56
|
+
// Slot boundaries do not report: a slot degrading is the page staying, and
|
|
57
|
+
// the chain it mounted is the chain the header describes. See TIM-1356,
|
|
58
|
+
// TIM-1495, design/04-authorization.md §"Where a Deny Page Renders".
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Record that a status page for the main route is on screen. Called from
|
|
62
|
+
* `StatusPageMarker`'s layout effect; consumed by the commit of the
|
|
63
|
+
* navigation whose tree it belongs to, and acted on immediately for the
|
|
64
|
+
* commits that have none.
|
|
65
|
+
*/
|
|
66
|
+
export function reportStatusPageRendered(): void {
|
|
67
|
+
_setStatusPageRendered(true);
|
|
68
|
+
onStatusPageRendered?.();
|
|
69
|
+
}
|
|
18
70
|
|
|
19
71
|
/** Everything a commit needs to describe the page it makes current. */
|
|
20
72
|
export interface NavigationCommitInput {
|
|
@@ -38,22 +90,6 @@ export interface NavigationCommitInput {
|
|
|
38
90
|
/** When true, clear the segment cache if segmentInfo is absent
|
|
39
91
|
* (popstate replay for entries without layout metadata). */
|
|
40
92
|
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;
|
|
57
93
|
/**
|
|
58
94
|
* Layouts the server skipped on this response (`X-Timber-Skipped-Segments`),
|
|
59
95
|
* when it came from a fetch. Omitted by replays and revalidation.
|
|
@@ -65,9 +101,11 @@ export interface NavigationCommitInput {
|
|
|
65
101
|
* replayed later, shows whatever the client holds *then*, not what was on
|
|
66
102
|
* screen when the entry committed. Such entries store a null payload and
|
|
67
103
|
* traversals to them fetch instead. Decided here, not at the fetch call
|
|
68
|
-
* sites,
|
|
69
|
-
*
|
|
70
|
-
*
|
|
104
|
+
* sites, because there are three of those (`performNavigationFetch`,
|
|
105
|
+
* `refresh()`, and uncached traversal) and a rule applied at one of them
|
|
106
|
+
* holds by accident: `performNavigationFetch` had the check and
|
|
107
|
+
* `fetchCommitAndRender` did not, so a back/forward fetch that was
|
|
108
|
+
* answered with skips stored the holes (TIM-1432).
|
|
71
109
|
*/
|
|
72
110
|
skippedSegments?: string[] | null;
|
|
73
111
|
}
|
|
@@ -93,7 +131,7 @@ function hasSkippedSlot(segmentInfo: SegmentInfo[] | null | undefined): boolean
|
|
|
93
131
|
* The metadata a history entry keeps. `skipped` describes the *response*
|
|
94
132
|
* — "this slot's content was omitted" — and the entry stores no such
|
|
95
133
|
* payload (it stores null). Left on the entry, the flag outlives the
|
|
96
|
-
* response: `
|
|
134
|
+
* response: `applyActionResult()` reuses the entry's metadata beside a full
|
|
97
135
|
* re-render and would have that judged non-replayable too.
|
|
98
136
|
*/
|
|
99
137
|
function storedSegmentInfo(segmentInfo: SegmentInfo[] | null | undefined) {
|
|
@@ -145,6 +183,10 @@ export function createNavigationCommitter(deps: {
|
|
|
145
183
|
}): NavigationCommitter {
|
|
146
184
|
const { segmentCache, historyStack } = deps;
|
|
147
185
|
|
|
186
|
+
// See `reportStatusPageRendered`: a status page that commits with no
|
|
187
|
+
// navigation to consume the report still empties the cache.
|
|
188
|
+
_setOnStatusPageRendered(() => segmentCache.clear());
|
|
189
|
+
|
|
148
190
|
/**
|
|
149
191
|
* Update the segment cache from server-provided segment metadata.
|
|
150
192
|
*
|
|
@@ -211,18 +253,28 @@ export function createNavigationCommitter(deps: {
|
|
|
211
253
|
*/
|
|
212
254
|
prepareNavigation(url: string, opts: NavigationCommitInput): PreparedNavigation {
|
|
213
255
|
const navState = deriveNavigationState(url);
|
|
214
|
-
|
|
215
|
-
// A tree rendered by an error response is not one to cache — see
|
|
216
|
-
// `status` on NavigationCommitInput. Normalized here, once, so no fetch
|
|
217
|
-
// caller can forget it; the history entry records the same thing, or a
|
|
218
|
-
// traversal back to this URL would replay the metadata this drops.
|
|
219
|
-
const cacheable = opts.status === undefined || opts.status < 400;
|
|
220
|
-
const segmentInfo = cacheable ? opts.segmentInfo : [];
|
|
221
256
|
const payload = isReplayable(opts) ? opts.payload : null;
|
|
222
257
|
|
|
258
|
+
// The tree this navigation renders has not rendered yet. Whatever a
|
|
259
|
+
// boundary reported before now belongs to a commit that had no
|
|
260
|
+
// navigation to consume it — a hydration, a row that arrived inside a
|
|
261
|
+
// Suspense boundary after its navigation had published — and must not
|
|
262
|
+
// be charged to this one.
|
|
263
|
+
_setStatusPageRendered(false);
|
|
264
|
+
|
|
223
265
|
return {
|
|
224
266
|
navState,
|
|
225
267
|
commit() {
|
|
268
|
+
// A tree in which a boundary rendered a status page is not one to
|
|
269
|
+
// cache — see the module note. Read at commit rather than at
|
|
270
|
+
// prepare because that is when the boundary has spoken: the row it
|
|
271
|
+
// renders may not have existed when the response headers were read.
|
|
272
|
+
// The history entry records the same thing, or a traversal back to
|
|
273
|
+
// this URL would replay the metadata this drops.
|
|
274
|
+
const cacheable = !statusPageRendered;
|
|
275
|
+
_setStatusPageRendered(false);
|
|
276
|
+
const segmentInfo = cacheable ? opts.segmentInfo : [];
|
|
277
|
+
|
|
226
278
|
if (segmentInfo && segmentInfo.length > 0) {
|
|
227
279
|
updateSegmentCache(segmentInfo);
|
|
228
280
|
} else if (segmentInfo?.length === 0 || opts.clearSegmentCacheOnEmpty) {
|
|
@@ -94,114 +94,95 @@ export interface TransitionResult<E = ReactNode> {
|
|
|
94
94
|
commit: () => void;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
// navigateTransition() is called. Used to detect stale transitions:
|
|
100
|
-
// if a newer transition started while the current one's perform()
|
|
101
|
-
// was in flight, the current transition is stale and should reject.
|
|
102
|
-
//
|
|
103
|
-
// Separate from the link-pending navId (which only increments on
|
|
104
|
-
// link clicks). This counter covers all navigation types: link clicks,
|
|
105
|
-
// programmatic navigate(), refresh(), and handlePopState().
|
|
106
|
-
//
|
|
107
|
-
// Uses globalThis for singleton guarantee across chunks — same pattern
|
|
108
|
-
// as NavigationContext and the link pending store.
|
|
97
|
+
/** What happened to this navigation's tree. */
|
|
98
|
+
export type CommitOutcome = 'committed' | 'superseded' | 'failed';
|
|
109
99
|
|
|
110
|
-
|
|
100
|
+
// ─── RenderOwner ────────────────────────────────────────────────
|
|
111
101
|
|
|
112
102
|
/**
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if (!existing) {
|
|
126
|
-
const created: TransitionCounter = { id: 0, waiters: new Set() };
|
|
127
|
-
g[NAV_TRANSITION_KEY] = created;
|
|
128
|
-
return created;
|
|
129
|
-
}
|
|
130
|
-
// A duplicated copy of this module may have created the singleton before
|
|
131
|
-
// `waiters` existed. The object is shared across chunks, so fill it in
|
|
132
|
-
// rather than replacing it — replacing would strand the other copy's id.
|
|
133
|
-
existing.waiters ??= new Set();
|
|
134
|
-
return existing as TransitionCounter;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/** Bump the counter and wake everything waiting on an older transition. */
|
|
138
|
-
function bumpTransitionCounter(): number {
|
|
139
|
-
const counter = getTransitionCounter();
|
|
140
|
-
counter.id += 1;
|
|
141
|
-
for (const wake of [...counter.waiters]) wake();
|
|
142
|
-
return counter.id;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Invalidate all in-flight navigation transitions. Any navigateTransition()
|
|
147
|
-
* call whose perform() has not yet committed will reject with AbortError
|
|
148
|
-
* instead of committing its element.
|
|
103
|
+
* A per-render identity that replaces the transition counter, the
|
|
104
|
+
* `handedOffNavAborts` set, and the `AbortController`-as-owner pattern.
|
|
105
|
+
*
|
|
106
|
+
* The navigation lifecycle holds one slot (`current: RenderOwner | null`).
|
|
107
|
+
* All supersession is "take the slot":
|
|
108
|
+
*
|
|
109
|
+
* - Navigation takes unconditionally (previous owner gets settled + aborted
|
|
110
|
+
* if !handedOff).
|
|
111
|
+
* - Revalidation takes only if empty (epoch check).
|
|
112
|
+
* - Handover gate before root.render checks `current === owner`.
|
|
113
|
+
* - `settle()` is called from NavigationRoot publish (committed), slot
|
|
114
|
+
* takeover (superseded), or rejection (failed).
|
|
149
115
|
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
* navigation
|
|
154
|
-
*
|
|
116
|
+
* `settle()` records the outcome and resolves `displaced` for non-committed
|
|
117
|
+
* outcomes. The `outcome` field is first-wins with one exception:
|
|
118
|
+
* `settle('superseded')` overrides a prior `'committed'`, because a
|
|
119
|
+
* committed-but-still-decoding navigation can be displaced by a successor
|
|
120
|
+
* and the post-decode check must throw rather than letting the old
|
|
121
|
+
* navigation continue.
|
|
122
|
+
*
|
|
123
|
+
* TIM-1481: replaces counter + waiters + bumpTransitionCounter +
|
|
124
|
+
* supersedeNavigationTransitions + handedOffNavAborts + globalThis singleton.
|
|
155
125
|
*/
|
|
156
|
-
export
|
|
157
|
-
|
|
126
|
+
export interface RenderOwner {
|
|
127
|
+
readonly kind: 'navigation' | 'revalidation';
|
|
128
|
+
readonly fetchAbort: AbortController;
|
|
129
|
+
handedOff: boolean;
|
|
130
|
+
/** Sync read for the post-perform() check. null until settled. */
|
|
131
|
+
outcome: CommitOutcome | null;
|
|
132
|
+
/**
|
|
133
|
+
* Resolves when this owner is displaced — a successor supersedes it or it
|
|
134
|
+
* fails. Does NOT resolve on commit: a committed navigation still waits
|
|
135
|
+
* for its decode to finish. Used to stop waiting on a decode that is no
|
|
136
|
+
* longer this navigation's business.
|
|
137
|
+
*/
|
|
138
|
+
readonly displaced: Promise<void>;
|
|
139
|
+
/**
|
|
140
|
+
* Record this owner's outcome and, for non-committed outcomes, resolve
|
|
141
|
+
* `displaced`. First-wins with one exception: `settle('superseded')`
|
|
142
|
+
* overrides a prior `'committed'`.
|
|
143
|
+
*/
|
|
144
|
+
settle: (outcome: CommitOutcome) => void;
|
|
158
145
|
}
|
|
159
146
|
|
|
160
147
|
/**
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
* A superseded navigation must stop waiting on its own stream. The stream is
|
|
165
|
-
* deliberately NOT aborted once its tree has been handed to React (the tree
|
|
166
|
-
* may be on screen with boundaries still feeding from it — see
|
|
167
|
-
* `handedOffNavAbort` in `client/router-lifecycle.ts`), so there is nothing
|
|
168
|
-
* left to make `decodePromise` settle promptly. Awaiting it bare would keep
|
|
169
|
-
* the loser's `router.navigate()` promise pending for the rest of the stream
|
|
170
|
-
* — and forever if it stalls — which is what `<Link>`'s `isPending` is timed
|
|
171
|
-
* against, so the losing link would sit spinning while the winner loaded
|
|
172
|
-
* (codex on #1004).
|
|
173
|
-
*
|
|
174
|
-
* A decode *failure* still propagates: it is a real error for this
|
|
175
|
-
* navigation, and the caller's recovery is timed against it.
|
|
148
|
+
* Create a fresh RenderOwner. Exported so tests that call
|
|
149
|
+
* `navigateTransition` directly can construct one without a router.
|
|
176
150
|
*/
|
|
177
|
-
function
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
)
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
151
|
+
export function createRenderOwner(kind: 'navigation' | 'revalidation'): RenderOwner {
|
|
152
|
+
let resolveDisplaced!: () => void;
|
|
153
|
+
const displaced = new Promise<void>((r) => {
|
|
154
|
+
resolveDisplaced = r;
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const owner: RenderOwner = {
|
|
158
|
+
kind,
|
|
159
|
+
fetchAbort: new AbortController(),
|
|
160
|
+
handedOff: false,
|
|
161
|
+
outcome: null,
|
|
162
|
+
displaced,
|
|
163
|
+
settle(outcome: CommitOutcome): void {
|
|
164
|
+
if (owner.outcome === null) {
|
|
165
|
+
owner.outcome = outcome;
|
|
166
|
+
} else if (outcome === 'superseded' && owner.outcome === 'committed') {
|
|
167
|
+
// A committed-but-still-decoding navigation can be displaced by a
|
|
168
|
+
// successor. Override so the post-decode check throws AbortError
|
|
169
|
+
// rather than letting the old navigation continue with scroll
|
|
170
|
+
// restoration while a newer navigation is active.
|
|
171
|
+
owner.outcome = 'superseded';
|
|
191
172
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
(
|
|
200
|
-
|
|
201
|
-
reject(error instanceof Error ? error : new Error(String(error)));
|
|
173
|
+
// Resolve `displaced` for any non-committed outcome, even if the
|
|
174
|
+
// outcome field was already set. A committed-but-still-decoding
|
|
175
|
+
// navigation must be woken when a successor supersedes it — the old
|
|
176
|
+
// counter-based waiter did this because the counter bumped regardless
|
|
177
|
+
// of commit state. A commit does NOT resolve displaced: that would
|
|
178
|
+
// end the decode wait early, clearing pending state before the Flight
|
|
179
|
+
// stream finishes.
|
|
180
|
+
if (outcome !== 'committed') {
|
|
181
|
+
resolveDisplaced();
|
|
202
182
|
}
|
|
203
|
-
|
|
204
|
-
}
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
return owner;
|
|
205
186
|
}
|
|
206
187
|
|
|
207
188
|
// ─── navigateTransition ─────────────────────────────────────────
|
|
@@ -235,44 +216,112 @@ function settleOnDecodeOrSupersession(
|
|
|
235
216
|
* one's `perform()` was in flight — the stale tree is never handed to React
|
|
236
217
|
* and its `commit` never runs (TIM-629, TIM-1301).
|
|
237
218
|
*
|
|
219
|
+
* `onCommit` is the caller's hook on the *commit*, which the returned promise
|
|
220
|
+
* deliberately does not wait for. It runs exactly once: when React commits
|
|
221
|
+
* this navigation's tree, or — so nothing waits on a commit that will never
|
|
222
|
+
* come — when the navigation is abandoned instead: superseded by a newer
|
|
223
|
+
* transition (before or after its tree was handed to React) or failed. It is
|
|
224
|
+
* how `<Link>` keeps `isPending` up until the destination is on screen
|
|
225
|
+
* without scheduling anything against React's lanes (TIM-1418).
|
|
226
|
+
*
|
|
227
|
+
* `owner` is the `RenderOwner` for this render (TIM-1481). Supersession is
|
|
228
|
+
* detected via `owner.outcome` (sync) and `owner.displaced` (async).
|
|
229
|
+
* No module-level state; no globalThis singleton.
|
|
230
|
+
*
|
|
238
231
|
* Used for: navigate(), refresh(), popstate with fetch.
|
|
239
232
|
*/
|
|
240
233
|
export function navigateTransition(
|
|
234
|
+
owner: RenderOwner,
|
|
241
235
|
perform: () => Promise<TransitionResult>,
|
|
242
|
-
render: NavigationRender
|
|
236
|
+
render: NavigationRender,
|
|
237
|
+
onCommit?: (outcome: CommitOutcome) => void
|
|
243
238
|
): Promise<void> {
|
|
244
|
-
// Increment the transition counter SYNCHRONOUSLY (before any await). Each
|
|
245
|
-
// call gets a unique transId; the counter is the same globalThis
|
|
246
|
-
// singleton, so a newer call always has a higher id.
|
|
247
|
-
const counter = getTransitionCounter();
|
|
248
|
-
const transId = bumpTransitionCounter();
|
|
249
|
-
|
|
250
239
|
const superseded = () => new DOMException('Navigation superseded', 'AbortError');
|
|
251
240
|
|
|
241
|
+
// `onCommit`, exactly once, on whichever comes first: the commit, a
|
|
242
|
+
// supersession, or a failure.
|
|
243
|
+
let commitSettled = onCommit === undefined;
|
|
244
|
+
const settleCommit = (outcome: CommitOutcome): void => {
|
|
245
|
+
if (commitSettled) return;
|
|
246
|
+
commitSettled = true;
|
|
247
|
+
onCommit?.(outcome);
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
// Watch for displacement from outside (a successor taking the slot).
|
|
251
|
+
if (!commitSettled) {
|
|
252
|
+
void owner.displaced.then(() => {
|
|
253
|
+
if (owner.outcome === 'superseded') settleCommit('superseded');
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
252
257
|
return (async () => {
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
258
|
+
try {
|
|
259
|
+
const { element, decodePromise, commit } = await perform();
|
|
260
|
+
if (owner.outcome !== null) {
|
|
261
|
+
decodePromise?.catch(() => {});
|
|
262
|
+
throw superseded();
|
|
263
|
+
}
|
|
264
|
+
// Hand the tree over with its commit attached. NavigationRoot runs it if
|
|
265
|
+
// and when React commits this tree, so a navigation that is superseded
|
|
266
|
+
// while its payload streams publishes nothing (TIM-1301).
|
|
267
|
+
//
|
|
268
|
+
// This is THE update that must not hide the departing page (TIM-1306);
|
|
269
|
+
// `render` is the synchronous transition around `root.render`.
|
|
270
|
+
render(element, () => {
|
|
271
|
+
try {
|
|
272
|
+
commit();
|
|
273
|
+
} finally {
|
|
274
|
+
settleCommit('committed');
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
// React may commit the tree before this settles — that is the point:
|
|
278
|
+
// the destination reveals as React is able to render it
|
|
279
|
+
// rather than waiting for the whole Flight stream. The await is here so
|
|
280
|
+
// the promise this function hands back still means "the payload is
|
|
281
|
+
// decoded", which is what the router's scroll restoration, the
|
|
282
|
+
// Navigation API deferred and `<Link>`'s `isPending` are timed against.
|
|
283
|
+
//
|
|
284
|
+
// ...unless this navigation loses first, in which case it stops waiting
|
|
285
|
+
// on a stream that is no longer its business.
|
|
286
|
+
if (decodePromise) {
|
|
287
|
+
await settleOnDecodeOrDisplacement(decodePromise, owner);
|
|
288
|
+
}
|
|
289
|
+
if (owner.outcome !== null && owner.outcome !== 'committed') {
|
|
290
|
+
throw superseded();
|
|
291
|
+
}
|
|
292
|
+
} catch (error) {
|
|
293
|
+
settleCommit('failed');
|
|
294
|
+
throw error;
|
|
257
295
|
}
|
|
258
|
-
// Hand the tree over with its commit attached. NavigationRoot runs it if
|
|
259
|
-
// and when React commits this tree, so a navigation that is superseded
|
|
260
|
-
// while its payload streams publishes nothing (TIM-1301).
|
|
261
|
-
//
|
|
262
|
-
// This is THE update that must not hide the departing page (TIM-1306);
|
|
263
|
-
// `render` is the synchronous transition around `root.render`.
|
|
264
|
-
render(element, commit);
|
|
265
|
-
// React may commit the tree before this settles — that is the point:
|
|
266
|
-
// the destination reveals as React is able to render it
|
|
267
|
-
// rather than waiting for the whole Flight stream. The await is here so
|
|
268
|
-
// the promise this function hands back still means "the payload is
|
|
269
|
-
// decoded", which is what the router's scroll restoration, the
|
|
270
|
-
// Navigation API deferred and `<Link>`'s `isPending` are timed against.
|
|
271
|
-
//
|
|
272
|
-
// ...unless this navigation loses first, in which case it stops waiting
|
|
273
|
-
// on a stream that is no longer its business. See
|
|
274
|
-
// `settleOnDecodeOrSupersession`.
|
|
275
|
-
if (decodePromise) await settleOnDecodeOrSupersession(decodePromise, counter, transId);
|
|
276
|
-
if (counter.id !== transId) throw superseded();
|
|
277
296
|
})();
|
|
278
297
|
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Wait for the payload to finish decoding, OR for this owner to be
|
|
301
|
+
* displaced — whichever happens first.
|
|
302
|
+
*
|
|
303
|
+
* A superseded navigation must stop waiting on its own stream. The stream is
|
|
304
|
+
* deliberately NOT aborted once its tree has been handed to React (the tree
|
|
305
|
+
* may be on screen with boundaries still feeding from it — see
|
|
306
|
+
* `RenderOwner.handedOff`), so there is nothing left to make `decodePromise`
|
|
307
|
+
* settle promptly. Awaiting it bare would keep the loser's
|
|
308
|
+
* `router.navigate()` promise pending for the rest of the stream — and
|
|
309
|
+
* forever if it stalls — which is what `<Link>`'s `isPending` is timed
|
|
310
|
+
* against, so the losing link would sit spinning while the winner loaded
|
|
311
|
+
* (codex on #1004).
|
|
312
|
+
*
|
|
313
|
+
* A decode *failure* still propagates: it is a real error for this
|
|
314
|
+
* navigation, and the caller's recovery is timed against it.
|
|
315
|
+
*/
|
|
316
|
+
function settleOnDecodeOrDisplacement(
|
|
317
|
+
decodePromise: Promise<void>,
|
|
318
|
+
owner: RenderOwner
|
|
319
|
+
): Promise<void> {
|
|
320
|
+
// Only bail early for displacement (superseded/failed), not for commit.
|
|
321
|
+
// A committed tree still needs to wait for its decode to finish — pending
|
|
322
|
+
// state and the Link indicator are timed against it.
|
|
323
|
+
if (owner.outcome !== null && owner.outcome !== 'committed') {
|
|
324
|
+
return Promise.resolve();
|
|
325
|
+
}
|
|
326
|
+
return Promise.race([decodePromise, owner.displaced]);
|
|
327
|
+
}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { setHardNavigating } from './navigation-root.tsx';
|
|
13
|
+
import type { RenderOwner } from './navigation-transition.ts';
|
|
13
14
|
import { RedirectError, ServerErrorResponse, NonRscResponse } from './rsc-fetch.ts';
|
|
14
15
|
import { recordSkew } from './router-skew.ts';
|
|
15
16
|
|
|
@@ -84,15 +85,15 @@ export function leaveSpa(url: string, fromUrl: string): Promise<never> {
|
|
|
84
85
|
}
|
|
85
86
|
|
|
86
87
|
export interface SpaExitDeps {
|
|
87
|
-
/** The
|
|
88
|
-
|
|
88
|
+
/** The owner that holds the router right now, or null when idle. */
|
|
89
|
+
currentOwner: () => RenderOwner | null;
|
|
89
90
|
/** Supersede whatever is in flight and take ownership. */
|
|
90
91
|
supersede: () => void;
|
|
91
92
|
}
|
|
92
93
|
|
|
93
94
|
export interface SpaExits {
|
|
94
95
|
/**
|
|
95
|
-
* Leave the SPA — but only if `
|
|
96
|
+
* Leave the SPA — but only if `owner` still owns the router. Every
|
|
96
97
|
* failure a navigation can end on (a server error, a non-RSC response, a
|
|
97
98
|
* version skew) can surface long after the user has clicked something else:
|
|
98
99
|
* a fetch that was never cancellable, a Flight thenable that rejects late.
|
|
@@ -104,7 +105,7 @@ export interface SpaExits {
|
|
|
104
105
|
* `finally` clears the current controller, so an ownership check downstream
|
|
105
106
|
* of it compares against `null` and never leaves at all.
|
|
106
107
|
*/
|
|
107
|
-
leaveSpaIfOwned: (
|
|
108
|
+
leaveSpaIfOwned: (owner: RenderOwner, url: string, fromUrl: string) => Promise<void>;
|
|
108
109
|
|
|
109
110
|
/**
|
|
110
111
|
* Leave the SPA from a path that never entered `runNavigation()` — the
|
|
@@ -127,13 +128,9 @@ export interface SpaExits {
|
|
|
127
128
|
* together because every caller of `leaveSpa()` needs one or the other, and
|
|
128
129
|
* an unguarded call is the bug (TIM-1275, TIM-1276).
|
|
129
130
|
*/
|
|
130
|
-
export function createSpaExits({
|
|
131
|
-
async function leaveSpaIfOwned(
|
|
132
|
-
|
|
133
|
-
url: string,
|
|
134
|
-
fromUrl: string
|
|
135
|
-
): Promise<void> {
|
|
136
|
-
if (currentNavAbort() !== navAbort) return;
|
|
131
|
+
export function createSpaExits({ currentOwner, supersede }: SpaExitDeps): SpaExits {
|
|
132
|
+
async function leaveSpaIfOwned(owner: RenderOwner, url: string, fromUrl: string): Promise<void> {
|
|
133
|
+
if (currentOwner() !== owner) return;
|
|
137
134
|
await leaveSpa(url, fromUrl);
|
|
138
135
|
}
|
|
139
136
|
|
|
@@ -146,8 +143,8 @@ export function createSpaExits({ currentNavAbort, supersede }: SpaExitDeps): Spa
|
|
|
146
143
|
}
|
|
147
144
|
|
|
148
145
|
export interface NavigationRecoveryDeps {
|
|
149
|
-
/** The
|
|
150
|
-
|
|
146
|
+
/** The owner that holds the router right now, or null when idle. */
|
|
147
|
+
currentOwner: () => RenderOwner | null;
|
|
151
148
|
leaveSpaIfOwned: SpaExits['leaveSpaIfOwned'];
|
|
152
149
|
/** Replace the current entry with `url` — the router's own `navigate()`. */
|
|
153
150
|
navigate: (url: string) => Promise<void>;
|
|
@@ -176,20 +173,20 @@ export interface NavigationRecoveryDeps {
|
|
|
176
173
|
* {@link SpaExits.leaveSpaIfOwned}.
|
|
177
174
|
*/
|
|
178
175
|
export function createNavigationRecovery({
|
|
179
|
-
|
|
176
|
+
currentOwner,
|
|
180
177
|
leaveSpaIfOwned,
|
|
181
178
|
navigate,
|
|
182
179
|
}: NavigationRecoveryDeps) {
|
|
183
180
|
return async function recoverFromNavigationError(
|
|
184
181
|
error: unknown,
|
|
185
|
-
|
|
182
|
+
owner: RenderOwner,
|
|
186
183
|
url: string,
|
|
187
184
|
fromUrl: string
|
|
188
185
|
): Promise<boolean> {
|
|
189
186
|
if (error instanceof RedirectError) {
|
|
190
187
|
// Same ownership rule as leaving the SPA: a superseded navigation must
|
|
191
188
|
// not steer the document to the destination it was abandoned for.
|
|
192
|
-
if (
|
|
189
|
+
if (currentOwner() !== owner) return true;
|
|
193
190
|
await navigate(error.redirectUrl);
|
|
194
191
|
return true;
|
|
195
192
|
}
|
|
@@ -202,7 +199,7 @@ export function createNavigationRecovery({
|
|
|
202
199
|
error instanceof NonRscResponse ||
|
|
203
200
|
recordSkew(error)
|
|
204
201
|
) {
|
|
205
|
-
await leaveSpaIfOwned(
|
|
202
|
+
await leaveSpaIfOwned(owner, url, fromUrl);
|
|
206
203
|
return true;
|
|
207
204
|
}
|
|
208
205
|
return false;
|