@timber-js/app 0.2.0-alpha.198 → 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-dVDi1GQz.js → cli-check-D6VolrDV.js} +3 -3
- package/dist/_chunks/{cli-check-dVDi1GQz.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-Ph6luW4c.js → convention-lint-fRkwVwEH.js} +25 -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/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/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-Ph6luW4c.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
|
@@ -2,23 +2,39 @@
|
|
|
2
2
|
* Navigation Lifecycle — who owns the router, and when a fetch may be cut.
|
|
3
3
|
*
|
|
4
4
|
* One navigation at a time owns the router. This module holds that ownership
|
|
5
|
-
*
|
|
6
|
-
* the wrapper every navigation runs inside (`runNavigation`),
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* `usePendingNavigation()` subscribe to.
|
|
5
|
+
* as a single `RenderOwner` slot, the rule for taking it (`createNavOwner`
|
|
6
|
+
* supersedes), the wrapper every navigation runs inside (`runNavigation`),
|
|
7
|
+
* and the pending store that `TopLoader` and `usePendingNavigation()`
|
|
8
|
+
* subscribe to.
|
|
10
9
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
10
|
+
* TIM-1481: replaced the transition counter, the `handedOffNavAborts` set,
|
|
11
|
+
* and the `AbortController`-as-owner pattern with `RenderOwner`. Supersession
|
|
12
|
+
* is "take the slot": settle the previous owner and abort its fetch unless
|
|
13
|
+
* handed off.
|
|
15
14
|
*
|
|
16
15
|
* See design/19-client-navigation.md §"How Pending State Works".
|
|
17
16
|
*/
|
|
18
17
|
|
|
19
|
-
import {
|
|
18
|
+
import { createRenderOwner, type RenderOwner } from './navigation-transition.ts';
|
|
20
19
|
import type { RouterPhase } from './router-types.ts';
|
|
21
20
|
|
|
21
|
+
/**
|
|
22
|
+
* A snapshot of "which page, and was a navigation in flight" at one instant.
|
|
23
|
+
* Captured before an action's POST and compared at response time so the
|
|
24
|
+
* router can tell whether the response still describes the page on screen.
|
|
25
|
+
*
|
|
26
|
+
* `seq` is a monotonic counter bumped on every `createNavOwner` — covering
|
|
27
|
+
* every path through `runNavigation`. `idle` is false when the router is
|
|
28
|
+
* mid-navigation OR a handed-off tree has not yet committed (the
|
|
29
|
+
* handover-to-commit window on the History API fallback).
|
|
30
|
+
*
|
|
31
|
+
* See design/19-client-navigation.md §State Update Invariants (TIM-1474).
|
|
32
|
+
*/
|
|
33
|
+
export interface NavigationEpoch {
|
|
34
|
+
readonly seq: number;
|
|
35
|
+
readonly idle: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
22
38
|
/** The subset of `RouterDeps` the navigation lifecycle needs. */
|
|
23
39
|
export interface NavigationLifecycleDeps {
|
|
24
40
|
/**
|
|
@@ -30,29 +46,33 @@ export interface NavigationLifecycleDeps {
|
|
|
30
46
|
}
|
|
31
47
|
|
|
32
48
|
export interface NavigationLifecycle {
|
|
33
|
-
/** The
|
|
34
|
-
|
|
49
|
+
/** The owner that holds the router right now, or null when idle. */
|
|
50
|
+
currentOwner: () => RenderOwner | null;
|
|
35
51
|
/**
|
|
36
|
-
* Take ownership of the router, superseding whatever held it.
|
|
37
|
-
*
|
|
52
|
+
* Take ownership of the router, superseding whatever held it. Returns
|
|
53
|
+
* a fresh `RenderOwner` for the new navigation. See the implementation
|
|
54
|
+
* for the three parts of superseding.
|
|
38
55
|
*/
|
|
39
|
-
|
|
56
|
+
createNavOwner: (
|
|
57
|
+
kind: 'navigation' | 'revalidation',
|
|
58
|
+
externalSignal?: AbortSignal
|
|
59
|
+
) => RenderOwner;
|
|
40
60
|
/**
|
|
41
61
|
* Run `fn` inside the abort/pending/cleanup lifecycle. AbortErrors are
|
|
42
62
|
* swallowed (not application errors); all other errors propagate.
|
|
43
63
|
*/
|
|
44
64
|
runNavigation: (
|
|
45
65
|
url: string,
|
|
46
|
-
fn: (
|
|
66
|
+
fn: (owner: RenderOwner) => Promise<void>,
|
|
47
67
|
externalSignal?: AbortSignal
|
|
48
68
|
) => Promise<void>;
|
|
49
69
|
/**
|
|
50
70
|
* Record that `owner`'s payload has reached React, so its stream is no
|
|
51
71
|
* longer cuttable. No-op when `owner` no longer owns the router.
|
|
52
72
|
*/
|
|
53
|
-
markHandedOff: (owner:
|
|
73
|
+
markHandedOff: (owner: RenderOwner) => void;
|
|
54
74
|
/** React has committed `owner`'s tree — every earlier handoff is off screen. */
|
|
55
|
-
forgetOlderHandoffs: (owner:
|
|
75
|
+
forgetOlderHandoffs: (owner: RenderOwner) => void;
|
|
56
76
|
|
|
57
77
|
/** Whether a navigation is currently in flight. */
|
|
58
78
|
isPending: () => boolean;
|
|
@@ -60,6 +80,33 @@ export interface NavigationLifecycle {
|
|
|
60
80
|
getPendingUrl: () => string | null;
|
|
61
81
|
/** Subscribe to pending state changes. */
|
|
62
82
|
onPendingChange: (listener: (pending: boolean) => void) => () => void;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Place a revalidation owner in the slot without bumping the navigation
|
|
86
|
+
* sequence. Unlike `createNavOwner`, this does not supersede what's in the
|
|
87
|
+
* slot — the caller (applyActionResult) has already verified the slot is
|
|
88
|
+
* idle via the epoch check. A concurrent navigation will supersede this
|
|
89
|
+
* owner through the normal `createNavOwner` path.
|
|
90
|
+
*/
|
|
91
|
+
placeRevalidationOwner: (owner: RenderOwner) => void;
|
|
92
|
+
/** Snapshot the router's navigation state (TIM-1474). */
|
|
93
|
+
epoch: () => NavigationEpoch;
|
|
94
|
+
/** True iff no navigation has started or is in flight since `e` was taken. */
|
|
95
|
+
isEpochCurrent: (e: NavigationEpoch) => boolean;
|
|
96
|
+
/**
|
|
97
|
+
* Run `task` now if the router is idle, otherwise once when it next becomes
|
|
98
|
+
* idle (pending false AND commit done). Single slot — a later request
|
|
99
|
+
* replaces an earlier one (TIM-1474).
|
|
100
|
+
*/
|
|
101
|
+
runWhenIdle: (task: () => void) => void;
|
|
102
|
+
/**
|
|
103
|
+
* Settle any handed-off-but-uncommitted owners as superseded, then flush
|
|
104
|
+
* the idle task if the router is now idle. Called by `syncShallowSearch`
|
|
105
|
+
* when a shallow URL update displaces a suspended transition — without
|
|
106
|
+
* it, `hasUncommittedNav` stays true indefinitely and `runWhenIdle`
|
|
107
|
+
* tasks never fire (TIM-1480).
|
|
108
|
+
*/
|
|
109
|
+
settleHandoffs: () => void;
|
|
63
110
|
}
|
|
64
111
|
|
|
65
112
|
/**
|
|
@@ -73,102 +120,83 @@ function isAbortError(error: unknown): boolean {
|
|
|
73
120
|
}
|
|
74
121
|
|
|
75
122
|
export function createNavigationLifecycle(deps: NavigationLifecycleDeps): NavigationLifecycle {
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
//
|
|
80
|
-
|
|
123
|
+
// The single ownership slot. One navigation at a time.
|
|
124
|
+
let current: RenderOwner | null = null;
|
|
125
|
+
|
|
126
|
+
// A handed-off-but-uncommitted owner that `runNavigation`'s finally has
|
|
127
|
+
// already cleared from `current`. Tracked separately so `createNavOwner`
|
|
128
|
+
// can settle it as superseded if a new navigation starts before the
|
|
129
|
+
// commit (P1 from codex on #1127). Cleared by `forgetOlderHandoffs`
|
|
130
|
+
// (commit) or by `createNavOwner` (supersession).
|
|
131
|
+
let pendingCommit: RenderOwner | null = null;
|
|
81
132
|
|
|
82
133
|
let routerPhase: RouterPhase = { phase: 'idle' };
|
|
83
134
|
const pendingListeners = new Set<(pending: boolean) => void>();
|
|
84
135
|
|
|
136
|
+
let navigationSeq = 0;
|
|
137
|
+
let idleTask: (() => void) | null = null;
|
|
138
|
+
|
|
85
139
|
/**
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
* Its response may still be streaming: a destination reveals as soon as
|
|
89
|
-
* React can render it, so the tree on screen routinely has Suspense
|
|
90
|
-
* boundaries still waiting on later Flight rows. Aborting that response
|
|
91
|
-
* rejects those rows, and the rejection surfaces through the tree into
|
|
92
|
-
* whatever error boundary the app has — replacing the page the user is
|
|
93
|
-
* looking at with an error state, while the successor navigation is still
|
|
94
|
-
* in flight (codex on #1004).
|
|
95
|
-
*
|
|
96
|
-
* So such a navigation's stream is allowed to finish. It is finite and
|
|
97
|
-
* already in flight; the alternative is a visible error on the page being
|
|
98
|
-
* departed from. Cancelling is still correct for a navigation whose payload
|
|
99
|
-
* never reached React at all, which is the case the abort was written for.
|
|
100
|
-
*
|
|
101
|
-
* **Handed over, not committed.** This is set when the tree is given to
|
|
102
|
-
* React, not when React commits it. Marking on commit is a whole React
|
|
103
|
-
* commit phase too late: the notification would be `NavigationRoot`'s
|
|
104
|
-
* layout effect, and React runs *descendant* layout effects first — so a
|
|
105
|
-
* destination that navigates from its own mount layout effect (a redirect
|
|
106
|
-
* guard) runs before the mark and its just-committed stream gets torn out
|
|
107
|
-
* from under it. `tests/navigation-supersede.test.ts` already pins that
|
|
108
|
-
* ordering, and there is no earlier hook short of an extra sibling fiber,
|
|
109
|
-
* which would shift every `useId` in the payload (see `server/ssr-wrappers`).
|
|
110
|
-
*
|
|
111
|
-
* Handing over is the right moment on its own terms, not merely a safe
|
|
112
|
-
* over-approximation: from the instant React holds the tree it may commit
|
|
113
|
-
* it without asking, so there is no later point at which "not on screen"
|
|
114
|
-
* is still knowable from out here. The residue is that a navigation
|
|
115
|
-
* superseded in the window between handover and commit keeps streaming a
|
|
116
|
-
* payload nobody sees — bandwidth on a finite response, against a visible
|
|
117
|
-
* error page the other way (codex on #1004, second round).
|
|
118
|
-
*
|
|
119
|
-
* A SET, not a single slot. Between a successor's handover and its commit
|
|
120
|
-
* React is still showing the previous tree — that is what the transition
|
|
121
|
-
* buys — so the previous navigation's stream is still feeding the screen and
|
|
122
|
-
* must stay unabortable. Entries are dropped when a later tree actually
|
|
123
|
-
* commits (`forgetOlderHandoffs`), which is the moment the trees they fed
|
|
124
|
-
* are gone. A single slot made a legitimate handover steal protection from
|
|
125
|
-
* a stream still on screen, and let a superseded navigation overwrite the
|
|
126
|
-
* winner's entry outright.
|
|
140
|
+
* Whether a handed-off tree has not yet committed. Derived from the slot:
|
|
141
|
+
* true when `current` is handed off but not yet settled.
|
|
127
142
|
*/
|
|
128
|
-
|
|
143
|
+
function hasUncommittedNav(): boolean {
|
|
144
|
+
return (
|
|
145
|
+
pendingCommit !== null || (current !== null && current.handedOff && current.outcome === null)
|
|
146
|
+
);
|
|
147
|
+
}
|
|
129
148
|
|
|
130
149
|
/**
|
|
131
150
|
* Cancel a navigation's RSC fetch — unless its tree is the one on screen.
|
|
132
151
|
*
|
|
133
152
|
* THE only place a navigation controller is aborted. Every path that gives
|
|
134
153
|
* up on a navigation calls this, so the "is this tree displayed?" question
|
|
135
|
-
* is asked once rather than at each site
|
|
136
|
-
*
|
|
137
|
-
* of `controller.abort()`.
|
|
154
|
+
* is asked once rather than at each site. A new abort path is a call to
|
|
155
|
+
* this, not a copy of `controller.abort()`.
|
|
138
156
|
*
|
|
139
|
-
* See
|
|
157
|
+
* See design/19-client-navigation.md §"A navigation that reached React
|
|
158
|
+
* keeps its stream".
|
|
140
159
|
*/
|
|
141
|
-
function abortUnlessHandedOff(
|
|
142
|
-
if (
|
|
143
|
-
|
|
160
|
+
function abortUnlessHandedOff(owner: RenderOwner): void {
|
|
161
|
+
if (owner.handedOff) return;
|
|
162
|
+
owner.fetchAbort.abort();
|
|
144
163
|
}
|
|
145
164
|
|
|
146
165
|
/**
|
|
147
|
-
* Create a new
|
|
148
|
-
*
|
|
149
|
-
*
|
|
166
|
+
* Create a new RenderOwner for a navigation, superseding any previous
|
|
167
|
+
* in-flight navigation. Optionally links to an external signal (e.g.,
|
|
168
|
+
* from the Navigation API's NavigateEvent.signal).
|
|
150
169
|
*
|
|
151
170
|
* Superseding is one operation with three parts:
|
|
152
171
|
* 1. Abort the previous navigation's fetch — UNLESS its tree is the one on
|
|
153
172
|
* screen, in which case tearing the stream down would error the page the
|
|
154
|
-
* user is currently looking at. See `
|
|
155
|
-
* 2.
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
* displace the successor.
|
|
173
|
+
* user is currently looking at. See `RenderOwner.handedOff`.
|
|
174
|
+
* 2. Settle the previous owner as 'superseded' so its transition detects
|
|
175
|
+
* it lost and never hands a stale tree to React. This replaces both
|
|
176
|
+
* `supersedeNavigationTransitions()` and the transition counter bump.
|
|
159
177
|
* 3. Resolve its Navigation API deferred — the superseded navigation's
|
|
160
178
|
* finally block is staleness-guarded (see TIM-1034) and no longer
|
|
161
179
|
* cleans up after itself, so the browser's native loading state for
|
|
162
180
|
* the dead navigation is cleared here.
|
|
163
181
|
*/
|
|
164
|
-
function
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
182
|
+
function createNavOwner(
|
|
183
|
+
kind: 'navigation' | 'revalidation',
|
|
184
|
+
externalSignal?: AbortSignal
|
|
185
|
+
): RenderOwner {
|
|
186
|
+
if (current) {
|
|
187
|
+
abortUnlessHandedOff(current);
|
|
188
|
+
current.settle('superseded');
|
|
168
189
|
deps.completeRouterNavigation?.();
|
|
169
190
|
}
|
|
170
|
-
|
|
171
|
-
|
|
191
|
+
// A handed-off owner whose runNavigation has already finished but whose
|
|
192
|
+
// tree hasn't committed yet. Settle it so its onCommit fires.
|
|
193
|
+
if (pendingCommit) {
|
|
194
|
+
pendingCommit.settle('superseded');
|
|
195
|
+
pendingCommit = null;
|
|
196
|
+
}
|
|
197
|
+
navigationSeq += 1;
|
|
198
|
+
const owner = createRenderOwner(kind);
|
|
199
|
+
current = owner;
|
|
172
200
|
|
|
173
201
|
// If an external signal is provided (e.g., Navigation API),
|
|
174
202
|
// forward its abort to our controller.
|
|
@@ -182,15 +210,13 @@ export function createNavigationLifecycle(deps: NavigationLifecycleDeps): Naviga
|
|
|
182
210
|
// external signal (codex on #1004).
|
|
183
211
|
if (externalSignal) {
|
|
184
212
|
if (externalSignal.aborted) {
|
|
185
|
-
abortUnlessHandedOff(
|
|
213
|
+
abortUnlessHandedOff(owner);
|
|
186
214
|
} else {
|
|
187
|
-
externalSignal.addEventListener('abort', () => abortUnlessHandedOff(
|
|
188
|
-
once: true,
|
|
189
|
-
});
|
|
215
|
+
externalSignal.addEventListener('abort', () => abortUnlessHandedOff(owner), { once: true });
|
|
190
216
|
}
|
|
191
217
|
}
|
|
192
218
|
|
|
193
|
-
return
|
|
219
|
+
return owner;
|
|
194
220
|
}
|
|
195
221
|
|
|
196
222
|
function setPending(value: boolean, url?: string): void {
|
|
@@ -217,65 +243,75 @@ export function createNavigationLifecycle(deps: NavigationLifecycleDeps): Naviga
|
|
|
217
243
|
|
|
218
244
|
/**
|
|
219
245
|
* Wrap a navigation in the standard abort/pending/cleanup lifecycle.
|
|
220
|
-
* Consolidates the
|
|
246
|
+
* Consolidates the createNavOwner + setPending + staleness-guarded
|
|
221
247
|
* finally that was duplicated across navigate, refresh, and both
|
|
222
248
|
* handlePopState paths. AbortErrors are swallowed (not application
|
|
223
249
|
* errors); all other errors propagate to the caller.
|
|
224
250
|
*/
|
|
225
251
|
async function runNavigation(
|
|
226
252
|
url: string,
|
|
227
|
-
fn: (
|
|
253
|
+
fn: (owner: RenderOwner) => Promise<void>,
|
|
228
254
|
externalSignal?: AbortSignal
|
|
229
255
|
): Promise<void> {
|
|
230
|
-
const
|
|
256
|
+
const owner = createNavOwner('navigation', externalSignal);
|
|
231
257
|
setPending(true, url);
|
|
232
258
|
try {
|
|
233
|
-
await fn(
|
|
259
|
+
await fn(owner);
|
|
234
260
|
} catch (error) {
|
|
235
261
|
if (isAbortError(error)) return;
|
|
236
262
|
throw error;
|
|
237
263
|
} finally {
|
|
238
|
-
if (
|
|
239
|
-
|
|
264
|
+
if (current === owner) {
|
|
265
|
+
current = null;
|
|
240
266
|
setPending(false);
|
|
241
267
|
deps.completeRouterNavigation?.();
|
|
268
|
+
flushIdleTask();
|
|
242
269
|
}
|
|
243
270
|
}
|
|
244
271
|
}
|
|
245
272
|
|
|
273
|
+
function flushIdleTask(): void {
|
|
274
|
+
if (routerPhase.phase !== 'idle' || hasUncommittedNav() || !idleTask) return;
|
|
275
|
+
const task = idleTask;
|
|
276
|
+
idleTask = null;
|
|
277
|
+
task();
|
|
278
|
+
}
|
|
279
|
+
|
|
246
280
|
return {
|
|
247
|
-
|
|
248
|
-
|
|
281
|
+
currentOwner: () => current,
|
|
282
|
+
createNavOwner,
|
|
249
283
|
runNavigation,
|
|
250
284
|
|
|
251
285
|
// Guarded on still owning the router. A superseded navigation can still
|
|
252
286
|
// reach here: a prefetch hit answers from `prefetchCache`, whose payload
|
|
253
287
|
// was fetched under the PREFETCH's controller, so aborting this
|
|
254
288
|
// navigation's controller does not stop it and `perform()` runs to
|
|
255
|
-
// completion. `navigateTransition`'s stale
|
|
289
|
+
// completion. `navigateTransition`'s stale check happens after
|
|
256
290
|
// `perform()` returns, so without this the loser would claim ownership
|
|
257
291
|
// moments before being discarded — and the next navigation would then
|
|
258
292
|
// abort the winner's still-streaming response, erroring the visible tree
|
|
259
293
|
// (codex on #1004).
|
|
260
|
-
markHandedOff(owner:
|
|
261
|
-
if (
|
|
262
|
-
|
|
294
|
+
markHandedOff(owner: RenderOwner): void {
|
|
295
|
+
if (current !== owner) return;
|
|
296
|
+
owner.handedOff = true;
|
|
297
|
+
pendingCommit = owner;
|
|
263
298
|
},
|
|
264
299
|
|
|
265
300
|
// React has committed THIS tree, so every earlier one is unmounted and
|
|
266
|
-
// their streams no longer feed anything on screen.
|
|
267
|
-
//
|
|
268
|
-
//
|
|
269
|
-
//
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
// navigation's stream is still live on screen and must stay unabortable.
|
|
275
|
-
forgetOlderHandoffs(owner: AbortController): void {
|
|
276
|
-
for (const controller of handedOffNavAborts) {
|
|
277
|
-
if (controller !== owner) handedOffNavAborts.delete(controller);
|
|
301
|
+
// their streams no longer feed anything on screen. The owner's `handedOff`
|
|
302
|
+
// boolean already captured the individual state; settling clears the
|
|
303
|
+
// "uncommitted" window. Unlike the old `handedOffNavAborts` set, there is
|
|
304
|
+
// no set to prune — the single slot means at most one owner is current.
|
|
305
|
+
forgetOlderHandoffs(owner: RenderOwner): void {
|
|
306
|
+
owner.settle('committed');
|
|
307
|
+
if (pendingCommit === owner) {
|
|
308
|
+
pendingCommit = null;
|
|
278
309
|
}
|
|
310
|
+
flushIdleTask();
|
|
311
|
+
},
|
|
312
|
+
|
|
313
|
+
placeRevalidationOwner(owner: RenderOwner): void {
|
|
314
|
+
current = owner;
|
|
279
315
|
},
|
|
280
316
|
|
|
281
317
|
isPending: () => routerPhase.phase === 'navigating',
|
|
@@ -284,5 +320,35 @@ export function createNavigationLifecycle(deps: NavigationLifecycleDeps): Naviga
|
|
|
284
320
|
pendingListeners.add(listener);
|
|
285
321
|
return () => pendingListeners.delete(listener);
|
|
286
322
|
},
|
|
323
|
+
|
|
324
|
+
epoch(): NavigationEpoch {
|
|
325
|
+
return {
|
|
326
|
+
seq: navigationSeq,
|
|
327
|
+
idle: routerPhase.phase === 'idle' && !hasUncommittedNav(),
|
|
328
|
+
};
|
|
329
|
+
},
|
|
330
|
+
isEpochCurrent(e: NavigationEpoch): boolean {
|
|
331
|
+
return (
|
|
332
|
+
e.idle && routerPhase.phase === 'idle' && !hasUncommittedNav() && navigationSeq === e.seq
|
|
333
|
+
);
|
|
334
|
+
},
|
|
335
|
+
runWhenIdle(task: () => void): void {
|
|
336
|
+
if (routerPhase.phase === 'idle' && !hasUncommittedNav()) {
|
|
337
|
+
task();
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
idleTask = task;
|
|
341
|
+
},
|
|
342
|
+
|
|
343
|
+
settleHandoffs(): void {
|
|
344
|
+
if (pendingCommit) {
|
|
345
|
+
pendingCommit.settle('superseded');
|
|
346
|
+
pendingCommit = null;
|
|
347
|
+
}
|
|
348
|
+
if (current !== null && current.handedOff && current.outcome === null) {
|
|
349
|
+
current.settle('superseded');
|
|
350
|
+
}
|
|
351
|
+
flushIdleTask();
|
|
352
|
+
},
|
|
287
353
|
};
|
|
288
354
|
}
|