@rangojs/router 0.31.0 → 0.33.0
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/README.md +3 -2
- package/dist/bin/browser-7KIJ5HSV.js +15 -0
- package/dist/bin/chunk-3B7CKBRO.js +463 -0
- package/dist/bin/chunk-524VACCG.js +71 -0
- package/dist/bin/chunk-LD7UBHT4.js +196 -0
- package/dist/bin/chunk-TO6CRWLN.js +1429 -0
- package/dist/bin/chunk-TTXC2U3G.js +2477 -0
- package/dist/bin/client-urls-projection-DQLWPDAT.js +28 -0
- package/dist/bin/forward-user-plugins-URBZKJP4.js +62 -0
- package/dist/bin/rango.js +42 -10143
- package/dist/bin/runtime-EPUDEHO4.js +596 -0
- package/dist/bin/runtime-discovery-CZ2QHT4Q.js +352 -0
- package/dist/bin/state-ACD2MGKJ.js +16 -0
- package/dist/bin/temp-rsc-server-QRR6HD6T.js +3190 -0
- package/dist/types/bin/rango.d.ts +1 -0
- package/dist/types/browser/action-fence.d.ts +20 -0
- package/dist/types/browser/cookie-name.d.ts +51 -10
- package/dist/types/browser/history-state.d.ts +13 -2
- package/dist/types/browser/invalidate-client-cache.d.ts +8 -6
- package/dist/types/browser/navigation-bridge.d.ts +36 -1
- package/dist/types/browser/navigation-client.d.ts +19 -0
- package/dist/types/browser/navigation-store-handle.d.ts +37 -4
- package/dist/types/browser/navigation-store.d.ts +2 -2
- package/dist/types/browser/navigation-transaction.d.ts +0 -1
- package/dist/types/browser/partial-update.d.ts +13 -0
- package/dist/types/browser/prefetch/cache.d.ts +96 -8
- package/dist/types/browser/prefetch/fetch.d.ts +26 -2
- package/dist/types/browser/prefetch/queue.d.ts +3 -2
- package/dist/types/browser/rango-state.d.ts +34 -5
- package/dist/types/browser/react/Link.d.ts +19 -13
- package/dist/types/browser/react/location-state-shared.d.ts +2 -1
- package/dist/types/browser/types.d.ts +25 -0
- package/dist/types/build/generate-route-types.d.ts +1 -1
- package/dist/types/build/route-types/include-resolution.d.ts +9 -1
- package/dist/types/build/route-types/router-processing.d.ts +3 -1
- package/dist/types/cache/cache-key-utils.d.ts +12 -8
- package/dist/types/cache/cache-scope.d.ts +4 -2
- package/dist/types/cache/cf/cf-cache-types.d.ts +9 -5
- package/dist/types/cache/document-cache-policy.d.ts +9 -1
- package/dist/types/cache/memory-segment-store.d.ts +3 -2
- package/dist/types/cache/segment-codec.d.ts +2 -2
- package/dist/types/cache/shell-snapshot.d.ts +1 -1
- package/dist/types/cache/types.d.ts +12 -7
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +2 -1
- package/dist/types/client-urls/types.d.ts +7 -5
- package/dist/types/prefetch-markers.d.ts +9 -0
- package/dist/types/prefetch-source-scope.d.ts +66 -0
- package/dist/types/route-definition/helpers-types.d.ts +8 -5
- package/dist/types/route-definition/redirect.d.ts +5 -0
- package/dist/types/router/intercept-resolution.d.ts +25 -11
- package/dist/types/router/match-context.d.ts +1 -0
- package/dist/types/router/match-handlers.d.ts +1 -1
- package/dist/types/router/middleware-types.d.ts +6 -0
- package/dist/types/router/prefetch-cache-ttl.d.ts +11 -3
- package/dist/types/router/router-context.d.ts +1 -1
- package/dist/types/router/router-options.d.ts +6 -3
- package/dist/types/router/segment-resolution/loader-cache.d.ts +8 -0
- package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +23 -7
- package/dist/types/router/segment-resolution/revalidation.d.ts +19 -0
- package/dist/types/router/source-reads.d.ts +53 -0
- package/dist/types/router/types.d.ts +1 -1
- package/dist/types/rsc/helpers.d.ts +8 -3
- package/dist/types/rsc/prefetch-render-error.d.ts +7 -2
- package/dist/types/rsc/request-stages.d.ts +4 -0
- package/dist/types/rsc/shell-serve.d.ts +6 -1
- package/dist/types/server/context.d.ts +23 -6
- package/dist/types/server/cookie-store.d.ts +15 -1
- package/dist/types/server/document-location-state.d.ts +59 -0
- package/dist/types/server/handle-store.d.ts +2 -0
- package/dist/types/server/request-context.d.ts +65 -12
- package/dist/types/testing/e2e/server.d.ts +16 -1
- package/dist/types/testing/render-route.d.ts +83 -2
- package/dist/types/testing/serve-shell-request.d.ts +15 -3
- package/dist/types/types/handler-context.d.ts +21 -4
- package/dist/types/types/loader-types.d.ts +7 -4
- package/dist/types/types/segments.d.ts +18 -1
- package/dist/types/urls/path-helper-types.d.ts +7 -5
- package/dist/types/vite/temp-rsc-server.d.ts +2 -2
- package/dist/types/vite/utils/forward-user-plugins.d.ts +32 -2
- package/dist/vite/index.js +451 -191
- package/package.json +2 -2
- package/skills/app-structure/SKILL.md +74 -0
- package/skills/app-structure/e-commerce.md +10 -9
- package/skills/app-structure/large-app.md +15 -2
- package/skills/caching/SKILL.md +10 -5
- package/skills/client-urls/SKILL.md +18 -11
- package/skills/comparison/references/framework-comparison.md +16 -10
- package/skills/document-cache/SKILL.md +13 -0
- package/skills/hooks/navigation.md +8 -8
- package/skills/hooks/state.md +132 -12
- package/skills/intercept/SKILL.md +33 -11
- package/skills/layout/SKILL.md +11 -0
- package/skills/links/SKILL.md +101 -4
- package/skills/loader/SKILL.md +49 -22
- package/skills/migrate-nextjs/SKILL.md +8 -2
- package/skills/migrate-react-router/data-and-actions.md +5 -0
- package/skills/ppr/SKILL.md +38 -5
- package/skills/prerender/SKILL.md +4 -1
- package/skills/route/SKILL.md +6 -6
- package/skills/router-setup/SKILL.md +2 -1
- package/skills/server-actions/SKILL.md +49 -8
- package/skills/testing/client-components.md +73 -32
- package/skills/testing/serve-shell-request.md +45 -20
- package/skills/testing/server-actions.md +8 -0
- package/skills/ux-architecture/SKILL.md +67 -15
- package/src/bin/rango.ts +39 -13
- package/src/browser/action-fence.ts +72 -0
- package/src/browser/cookie-name.ts +82 -14
- package/src/browser/history-state.ts +24 -2
- package/src/browser/invalidate-client-cache.ts +9 -7
- package/src/browser/navigation-bridge.ts +164 -32
- package/src/browser/navigation-client.ts +95 -20
- package/src/browser/navigation-store-handle.ts +67 -5
- package/src/browser/navigation-store.ts +86 -17
- package/src/browser/navigation-transaction.ts +0 -6
- package/src/browser/partial-update.ts +31 -12
- package/src/browser/prefetch/cache.ts +188 -11
- package/src/browser/prefetch/fetch.ts +211 -51
- package/src/browser/prefetch/queue.ts +3 -2
- package/src/browser/rango-state.ts +50 -8
- package/src/browser/react/Link.tsx +24 -16
- package/src/browser/react/location-state-shared.ts +8 -1
- package/src/browser/react/use-navigation.ts +1 -1
- package/src/browser/rsc-router.tsx +13 -3
- package/src/browser/server-action-bridge.ts +3 -3
- package/src/browser/types.ts +25 -0
- package/src/build/generate-route-types.ts +1 -0
- package/src/build/route-types/include-resolution.ts +40 -3
- package/src/build/route-types/router-processing.ts +170 -25
- package/src/build/route-types/scan-filter.ts +1 -0
- package/src/build/runtime-discovery.ts +46 -8
- package/src/cache/cache-key-utils.ts +12 -8
- package/src/cache/cache-runtime.ts +5 -1
- package/src/cache/cache-scope.ts +28 -7
- package/src/cache/cf/cf-cache-types.ts +9 -5
- package/src/cache/document-cache-policy.ts +37 -1
- package/src/cache/document-cache.ts +12 -0
- package/src/cache/memory-segment-store.ts +3 -2
- package/src/cache/segment-codec.ts +2 -2
- package/src/cache/shell-snapshot.ts +1 -1
- package/src/cache/types.ts +12 -7
- package/src/cache/vercel/vercel-cache-store.ts +2 -1
- package/src/client-urls/types.ts +7 -5
- package/src/prefetch-markers.ts +10 -0
- package/src/prefetch-source-scope.ts +137 -0
- package/src/route-definition/helpers-types.ts +8 -5
- package/src/route-definition/redirect.ts +5 -0
- package/src/router/intercept-resolution.ts +94 -50
- package/src/router/loader-resolution.ts +8 -6
- package/src/router/match-api.ts +33 -29
- package/src/router/match-context.ts +5 -0
- package/src/router/match-handlers.ts +2 -1
- package/src/router/match-middleware/cache-lookup.ts +39 -14
- package/src/router/match-result.ts +45 -85
- package/src/router/middleware-types.ts +6 -0
- package/src/router/navigation-snapshot.ts +18 -3
- package/src/router/prefetch-cache-ttl.ts +15 -3
- package/src/router/revalidation.ts +63 -13
- package/src/router/router-context.ts +2 -1
- package/src/router/router-options.ts +6 -3
- package/src/router/segment-resolution/loader-cache.ts +21 -3
- package/src/router/segment-resolution/prefetch-deferral.ts +38 -31
- package/src/router/segment-resolution/revalidation.ts +40 -12
- package/src/router/source-reads.ts +122 -0
- package/src/router/types.ts +2 -1
- package/src/router.ts +11 -12
- package/src/rsc/helpers.ts +20 -6
- package/src/rsc/prefetch-render-error.ts +7 -2
- package/src/rsc/progressive-enhancement.ts +8 -2
- package/src/rsc/request-stages.ts +87 -1
- package/src/rsc/rsc-rendering.ts +78 -32
- package/src/rsc/server-action.ts +2 -1
- package/src/rsc/shell-capture.ts +4 -5
- package/src/rsc/shell-serve.ts +12 -1
- package/src/segment-system.tsx +1 -1
- package/src/server/context.ts +37 -6
- package/src/server/cookie-store.ts +22 -1
- package/src/server/document-location-state.ts +334 -0
- package/src/server/handle-store.ts +2 -0
- package/src/server/request-context.ts +137 -33
- package/src/testing/dispatch.ts +6 -0
- package/src/testing/e2e/fixture.ts +4 -12
- package/src/testing/e2e/server.ts +91 -4
- package/src/testing/render-route.tsx +309 -10
- package/src/testing/run-revalidate.ts +11 -1
- package/src/testing/serve-shell-request.ts +22 -3
- package/src/types/handler-context.ts +21 -4
- package/src/types/loader-types.ts +7 -4
- package/src/types/segments.ts +18 -1
- package/src/urls/path-helper-types.ts +7 -5
- package/src/vite/temp-rsc-server.ts +2 -2
- package/src/vite/utils/forward-user-plugins.ts +13 -5
package/src/bin/rango.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { resolve, dirname, basename } from "node:path";
|
|
2
3
|
import { readFileSync, statSync, existsSync } from "node:fs";
|
|
3
4
|
import {
|
|
4
5
|
findTsFiles,
|
|
@@ -8,6 +9,7 @@ import {
|
|
|
8
9
|
detectUnresolvableIncludesForUrlsFile,
|
|
9
10
|
findNestedRouterConflict,
|
|
10
11
|
formatNestedRouterConflictError,
|
|
12
|
+
isRoutableSourceFile,
|
|
11
13
|
type UnresolvableInclude,
|
|
12
14
|
} from "../build/generate-route-types.ts";
|
|
13
15
|
|
|
@@ -93,6 +95,19 @@ Examples:
|
|
|
93
95
|
);
|
|
94
96
|
}
|
|
95
97
|
|
|
98
|
+
/**
|
|
99
|
+
* A createRouter() file that is not a test: a test builds its own router (a
|
|
100
|
+
* Flight test's `*.rsc-test.tsx` usually mounts an inline urls() tree), and a
|
|
101
|
+
* named-routes.gen.ts beside it would merge its routes into the app's global
|
|
102
|
+
* GeneratedRouteMap. The same rule findRouterFiles() applies in Vite.
|
|
103
|
+
*/
|
|
104
|
+
function isRouterFile(filePath: string, source: string): boolean {
|
|
105
|
+
return (
|
|
106
|
+
isRoutableSourceFile(basename(filePath)) &&
|
|
107
|
+
/\bcreateRouter\s*[<(]/.test(source)
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
96
111
|
/**
|
|
97
112
|
* Walk up from a file path to find the project root (directory containing
|
|
98
113
|
* package.json or vite.config.ts).
|
|
@@ -141,7 +156,7 @@ function runStaticGeneration(args: string[], mode: "default" | "static") {
|
|
|
141
156
|
for (const filePath of files) {
|
|
142
157
|
try {
|
|
143
158
|
const source = readFileSync(filePath, "utf-8");
|
|
144
|
-
if (
|
|
159
|
+
if (isRouterFile(filePath, source)) {
|
|
145
160
|
routerFiles.push(filePath);
|
|
146
161
|
}
|
|
147
162
|
// Both spellings, matching the writer's own gate: "clientUrls(" does
|
|
@@ -181,7 +196,8 @@ function runStaticGeneration(args: string[], mode: "default" | "static") {
|
|
|
181
196
|
// Deduplicate diagnostics (router and urls detection may find the same issue)
|
|
182
197
|
const seen = new Set<string>();
|
|
183
198
|
const uniqueDiagnostics = allDiagnostics.filter((d) => {
|
|
184
|
-
|
|
199
|
+
// Every unreadable routes mount has pathPrefix "/": its detail names it.
|
|
200
|
+
const key = `${d.sourceFile}:${d.pathPrefix}:${d.reason}${d.reason === "unresolvable-routes" ? `:${d.detail}` : ""}`;
|
|
185
201
|
if (seen.has(key)) return false;
|
|
186
202
|
seen.add(key);
|
|
187
203
|
return true;
|
|
@@ -192,10 +208,11 @@ function runStaticGeneration(args: string[], mode: "default" | "static") {
|
|
|
192
208
|
console.error("\n[rango] Unresolvable routes detected:\n");
|
|
193
209
|
formatDiagnostics(uniqueDiagnostics);
|
|
194
210
|
console.error(
|
|
195
|
-
"\nThe static parser cannot resolve these includes or basename
|
|
196
|
-
"they use factory functions or dynamic expressions. A
|
|
197
|
-
"
|
|
198
|
-
"
|
|
211
|
+
"\nThe static parser cannot resolve these includes, routes or basename " +
|
|
212
|
+
"because they use factory functions or dynamic expressions. A router's " +
|
|
213
|
+
"routes must be a urls() call, a builder function or a variable holding " +
|
|
214
|
+
"urls(); a basename must be a string literal or a top-level const (in " +
|
|
215
|
+
"the router file or imported from a relative module).\n\n" +
|
|
199
216
|
"Options:\n" +
|
|
200
217
|
" rango generate <path> --runtime Use Vite-based discovery (requires vite)\n" +
|
|
201
218
|
" rango generate <path> --static Accept partial output (missing routes or basename prefix above)\n",
|
|
@@ -255,7 +272,7 @@ async function runRuntimeDiscovery(args: string[], configFile?: string) {
|
|
|
255
272
|
for (const filePath of files) {
|
|
256
273
|
try {
|
|
257
274
|
const source = readFileSync(filePath, "utf-8");
|
|
258
|
-
if (
|
|
275
|
+
if (isRouterFile(filePath, source)) {
|
|
259
276
|
routerEntries.push(filePath);
|
|
260
277
|
}
|
|
261
278
|
// Also generate per-module types for urls files. Both spellings — a
|
|
@@ -281,6 +298,10 @@ async function runRuntimeDiscovery(args: string[], configFile?: string) {
|
|
|
281
298
|
process.exit(1);
|
|
282
299
|
}
|
|
283
300
|
|
|
301
|
+
// Loads from its own chunk only because the bin is built with esbuild
|
|
302
|
+
// --splitting (package.json "build"). A single-file bundle folds this module
|
|
303
|
+
// in and hoists its Vite imports to the top, so every command, --help
|
|
304
|
+
// included, failed without vite installed.
|
|
284
305
|
let discoverAndWriteRouteTypes: typeof import("../build/runtime-discovery.ts").discoverAndWriteRouteTypes;
|
|
285
306
|
try {
|
|
286
307
|
const mod = await import("../build/runtime-discovery.ts");
|
|
@@ -290,9 +311,12 @@ async function runRuntimeDiscovery(args: string[], configFile?: string) {
|
|
|
290
311
|
err.code === "ERR_MODULE_NOT_FOUND" ||
|
|
291
312
|
err.code === "MODULE_NOT_FOUND"
|
|
292
313
|
) {
|
|
314
|
+
// runtime-discovery checks vite and @vitejs/plugin-rsc itself and names
|
|
315
|
+
// what to install, so reaching here means a bundled chunk file is
|
|
316
|
+
// missing: the installed package is incomplete.
|
|
293
317
|
console.error(
|
|
294
|
-
|
|
295
|
-
"
|
|
318
|
+
`[rango] Failed to load runtime discovery: ${err.message}\n` +
|
|
319
|
+
"The installed @rangojs/router looks broken (a CLI chunk is missing). Reinstall it.",
|
|
296
320
|
);
|
|
297
321
|
} else {
|
|
298
322
|
console.error(`[rango] Failed to load runtime discovery: ${err.message}`);
|
|
@@ -320,9 +344,11 @@ function formatDiagnostics(
|
|
|
320
344
|
const prefix =
|
|
321
345
|
d.reason === "unresolvable-basename"
|
|
322
346
|
? "createRouter basename"
|
|
323
|
-
: d.
|
|
324
|
-
?
|
|
325
|
-
:
|
|
347
|
+
: d.reason === "unresolvable-routes"
|
|
348
|
+
? "createRouter routes"
|
|
349
|
+
: d.namePrefix
|
|
350
|
+
? `${d.namePrefix}.*`
|
|
351
|
+
: `${d.pathPrefix}*`;
|
|
326
352
|
console.error(` ${prefix}`);
|
|
327
353
|
console.error(` Reason: ${d.reason} -- ${d.detail}`);
|
|
328
354
|
console.error(` Source: ${d.sourceFile}`);
|
|
@@ -38,12 +38,82 @@
|
|
|
38
38
|
|
|
39
39
|
let fenceCount = 0;
|
|
40
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Whether a cookie read saw the state rotate externally while the fence was
|
|
43
|
+
* up and no action's landing took it yet (navigation-store-handle.ts reports
|
|
44
|
+
* it). Almost always an action's own server-seat `invalidateClientCache()`
|
|
45
|
+
* `Set-Cookie`; a sibling tab's rotation in that window reads the same and
|
|
46
|
+
* costs one full flush, today's behavior. navigation-store.ts takes it when
|
|
47
|
+
* an action lands, so one rotation makes one landing flush, not every later
|
|
48
|
+
* landing of the batch.
|
|
49
|
+
*/
|
|
50
|
+
let rotatedInFence = false;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Whether a cookie read saw a write-only change of the state (a server
|
|
54
|
+
* action's response, this tab's or another's) while the fence was up and no
|
|
55
|
+
* action's landing took it yet. A landing marks every cache stale, so it
|
|
56
|
+
* takes this; when the fence drops with it still set (the actions that
|
|
57
|
+
* landed called keepClientCache(), failed before they were sent, or reloaded
|
|
58
|
+
* on a version mismatch), the drop hands it to `unlandedWriteHandler`.
|
|
59
|
+
*/
|
|
60
|
+
let wroteInFence = false;
|
|
61
|
+
|
|
62
|
+
let unlandedWriteHandler: (() => void) | null = null;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Register what runs when the fence drops holding a write no landing took:
|
|
66
|
+
* the navigation store's markCachesStaleAfterWrite (navigation-store-handle.ts).
|
|
67
|
+
*/
|
|
68
|
+
export function setUnlandedWriteHandler(handler: (() => void) | null): void {
|
|
69
|
+
unlandedWriteHandler = handler;
|
|
70
|
+
}
|
|
71
|
+
|
|
41
72
|
export function enterActionFence(): void {
|
|
73
|
+
if (fenceCount === 0) {
|
|
74
|
+
rotatedInFence = false;
|
|
75
|
+
wroteInFence = false;
|
|
76
|
+
}
|
|
42
77
|
fenceCount++;
|
|
43
78
|
}
|
|
44
79
|
|
|
45
80
|
export function exitActionFence(): void {
|
|
46
81
|
if (fenceCount > 0) fenceCount--;
|
|
82
|
+
if (fenceCount === 0 && wroteInFence) {
|
|
83
|
+
wroteInFence = false;
|
|
84
|
+
unlandedWriteHandler?.();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** A cookie read saw an external state rotation (rango-state.ts observer). */
|
|
89
|
+
export function noteStateRotation(): void {
|
|
90
|
+
if (fenceCount > 0) rotatedInFence = true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* A cookie read saw a write-only change (rango-state.ts write observer).
|
|
95
|
+
* Returns whether the fence is up and took it; with the fence down the
|
|
96
|
+
* caller marks the caches stale itself.
|
|
97
|
+
*/
|
|
98
|
+
export function noteStateWrite(): boolean {
|
|
99
|
+
if (fenceCount === 0) return false;
|
|
100
|
+
wroteInFence = true;
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** A landing marked every cache stale: a write seen so far is covered. */
|
|
105
|
+
export function takeStateWriteInActionFence(): void {
|
|
106
|
+
wroteInFence = false;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Whether the state rotated externally since the fence went up or the last
|
|
111
|
+
* landing took it, and clear it. False with the fence down.
|
|
112
|
+
*/
|
|
113
|
+
export function takeStateRotationInActionFence(): boolean {
|
|
114
|
+
const rotated = fenceCount > 0 && rotatedInFence;
|
|
115
|
+
rotatedInFence = false;
|
|
116
|
+
return rotated;
|
|
47
117
|
}
|
|
48
118
|
|
|
49
119
|
export function isActionFenceActive(): boolean {
|
|
@@ -53,4 +123,6 @@ export function isActionFenceActive(): boolean {
|
|
|
53
123
|
/** Test-only: reset the refcount between cases. */
|
|
54
124
|
export function __resetActionFence(): void {
|
|
55
125
|
fenceCount = 0;
|
|
126
|
+
rotatedInFence = false;
|
|
127
|
+
wroteInFence = false;
|
|
56
128
|
}
|
|
@@ -91,31 +91,48 @@ export function getRawCookieValue(
|
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
/**
|
|
94
|
-
* Encode a state value for the wire: `encodeURIComponent(version):timestamp
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
* shape survives and `:` is a legal cookie-value octet.
|
|
94
|
+
* Encode a state value for the wire: `encodeURIComponent(version):timestamp`,
|
|
95
|
+
* plus `:write` when the write part is not 0. Only the build-derived version
|
|
96
|
+
* is encoded (it is arbitrary); the `:` separators and the numeric parts stay
|
|
97
|
+
* raw, so the shape survives and `:` is a legal cookie-value octet. A value no
|
|
98
|
+
* server action has bumped keeps the two-part shape it had before the write
|
|
99
|
+
* part existed.
|
|
98
100
|
*/
|
|
99
|
-
export function encodeStateValue(
|
|
100
|
-
|
|
101
|
+
export function encodeStateValue(
|
|
102
|
+
version: string,
|
|
103
|
+
timestamp: number,
|
|
104
|
+
write = 0,
|
|
105
|
+
): string {
|
|
106
|
+
const base = `${encodeURIComponent(version)}:${timestamp}`;
|
|
107
|
+
return write === 0 ? base : `${base}:${write}`;
|
|
101
108
|
}
|
|
102
109
|
|
|
103
|
-
/**
|
|
110
|
+
/**
|
|
111
|
+
* Parsed state value. `version` is decoded; `timestamp` (the invalidation
|
|
112
|
+
* part) and `write` are the raw integers. `write` is 0 for a two-part value.
|
|
113
|
+
*/
|
|
104
114
|
export interface StateValue {
|
|
105
115
|
version: string;
|
|
106
116
|
timestamp: number;
|
|
117
|
+
write: number;
|
|
107
118
|
}
|
|
108
119
|
|
|
109
120
|
/**
|
|
110
|
-
* Decode a wire value back into `{version, timestamp}`.
|
|
111
|
-
*
|
|
112
|
-
*
|
|
121
|
+
* Decode a wire value back into `{version, timestamp, write}`. Accepts the
|
|
122
|
+
* two-part `{version}:{timestamp}` (a value no action bumped, or one an open
|
|
123
|
+
* tab wrote before the write part existed) and the three-part
|
|
124
|
+
* `{version}:{timestamp}:{write}`. Returns null for a malformed value (no `:`,
|
|
125
|
+
* empty version, a non-numeric part, or more parts) so callers mint fresh
|
|
126
|
+
* instead of trusting garbage.
|
|
113
127
|
*/
|
|
114
128
|
export function decodeStateValue(raw: string): StateValue | null {
|
|
115
129
|
const colon = raw.indexOf(":");
|
|
116
130
|
if (colon <= 0) return null;
|
|
117
|
-
const
|
|
118
|
-
if (
|
|
131
|
+
const parts = raw.slice(colon + 1).split(":");
|
|
132
|
+
if (parts.length > 2) return null;
|
|
133
|
+
const timestamp = parseStatePart(parts[0]!);
|
|
134
|
+
const write = parts.length === 2 ? parseStatePart(parts[1]!) : 0;
|
|
135
|
+
if (timestamp === null || write === null) return null;
|
|
119
136
|
let version: string;
|
|
120
137
|
try {
|
|
121
138
|
version = decodeURIComponent(raw.slice(0, colon));
|
|
@@ -124,7 +141,31 @@ export function decodeStateValue(raw: string): StateValue | null {
|
|
|
124
141
|
// a thrown URIError here would 500 the server seat or fail client boot.
|
|
125
142
|
return null;
|
|
126
143
|
}
|
|
127
|
-
return { version, timestamp };
|
|
144
|
+
return { version, timestamp, write };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function parseStatePart(part: string): number | null {
|
|
148
|
+
if (part === "") return null;
|
|
149
|
+
const value = Number(part);
|
|
150
|
+
return Number.isFinite(value) ? value : null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* The `{version}:{timestamp}` part of a wire value: what every in-memory
|
|
155
|
+
* comparison on the client reads (the prefetch cache keys, the action
|
|
156
|
+
* fence's rotation check, the external-rotation observer). A change to it is
|
|
157
|
+
* a hard invalidation (a deploy, or invalidateClientCache() on either seat);
|
|
158
|
+
* a change to the write part alone is a server action's write, which marks
|
|
159
|
+
* caches stale and flushes nothing. The full value, write part included, is
|
|
160
|
+
* what requests send as `X-Rango-State`, so the browser HTTP cache (Vary)
|
|
161
|
+
* misses after every write. A raw cut, no decode: the version is
|
|
162
|
+
* percent-encoded, so the second `:` is always the write separator.
|
|
163
|
+
*/
|
|
164
|
+
export function stateEpoch(raw: string): string {
|
|
165
|
+
const first = raw.indexOf(":");
|
|
166
|
+
if (first < 0) return raw;
|
|
167
|
+
const second = raw.indexOf(":", first + 1);
|
|
168
|
+
return second < 0 ? raw : raw.slice(0, second);
|
|
128
169
|
}
|
|
129
170
|
|
|
130
171
|
/**
|
|
@@ -132,7 +173,9 @@ export function decodeStateValue(raw: string): StateValue | null {
|
|
|
132
173
|
* one, so a re-mint inside the same millisecond (or a backward clock step) still
|
|
133
174
|
* differs from the current value. `prevRaw` is the current wire value (the
|
|
134
175
|
* client's in-memory mirror, or the server's inbound header/cookie) or null; its
|
|
135
|
-
* timestamp is the floor.
|
|
176
|
+
* timestamp is the floor. The write part starts over (left out): the new
|
|
177
|
+
* timestamp alone makes the value one no response was cached under. Shared by
|
|
178
|
+
* both seats so the monotonic guard lives once.
|
|
136
179
|
*/
|
|
137
180
|
export function mintStateValue(
|
|
138
181
|
version: string,
|
|
@@ -143,6 +186,31 @@ export function mintStateValue(
|
|
|
143
186
|
return encodeStateValue(version, ts);
|
|
144
187
|
}
|
|
145
188
|
|
|
189
|
+
/**
|
|
190
|
+
* Bump the write part of `prevRaw`, keeping its version and timestamp: what
|
|
191
|
+
* the response of a server action without a directive sets. Keeping the
|
|
192
|
+
* version keeps the bump soft for a client still on another build (a form
|
|
193
|
+
* posted from a document served before a deploy); its own boot handles the
|
|
194
|
+
* deploy. Returns null when `prevRaw` is absent or malformed: a client with
|
|
195
|
+
* no state value sends no `X-Rango-State` and has nothing in the HTTP cache
|
|
196
|
+
* keyed by one.
|
|
197
|
+
*
|
|
198
|
+
* The new write part must be one no response was cached under. It is
|
|
199
|
+
* strictly greater than the previous one, and it is the current time in
|
|
200
|
+
* microseconds with random low digits: two actions that start from the same
|
|
201
|
+
* value (two tabs, or two in flight in one tab) can run in the same server
|
|
202
|
+
* millisecond (a Worker's clock does not move between I/O), and the second
|
|
203
|
+
* response must not set the value the first one set, under which a prefetch
|
|
204
|
+
* between them may have been cached with the second write missing.
|
|
205
|
+
*/
|
|
206
|
+
export function bumpStateWrite(prevRaw: string | null): string | null {
|
|
207
|
+
const prev = prevRaw ? decodeStateValue(prevRaw) : null;
|
|
208
|
+
if (!prev) return null;
|
|
209
|
+
const fresh = Date.now() * 1000 + Math.floor(Math.random() * 1000);
|
|
210
|
+
const write = Math.max(fresh, prev.write + 1);
|
|
211
|
+
return encodeStateValue(prev.version, prev.timestamp, write);
|
|
212
|
+
}
|
|
213
|
+
|
|
146
214
|
/**
|
|
147
215
|
* Attribute string for the state cookie. Session cookie (no Max-Age/Expires),
|
|
148
216
|
* Path=/ (whole app), SameSite=Lax (sent on top-level navigations), and Secure
|
|
@@ -158,8 +158,9 @@ export function stripShellMissMarker(): void {
|
|
|
158
158
|
* the server rendered this document without them. One pass before hydration
|
|
159
159
|
* (initBrowserApp; renderRoute's `hydrate` mode), so no reader needs to know
|
|
160
160
|
* the page began as a document load, and a slot nobody reads is removed too.
|
|
161
|
-
*
|
|
162
|
-
* (
|
|
161
|
+
* Server-set state is not lost to it: the state a document response carries
|
|
162
|
+
* is merged after this pass (applyDocumentLocationState), so a `~r` slot the
|
|
163
|
+
* server set for this load survives this load and no later one.
|
|
163
164
|
*
|
|
164
165
|
* A slot is recognised by its key alone (the `~r` suffix): at start-up the
|
|
165
166
|
* module that defines it may not be loaded.
|
|
@@ -185,6 +186,27 @@ export function clearLocationStateOnDocumentLoad(): void {
|
|
|
185
186
|
if (next) window.history.replaceState(next, "");
|
|
186
187
|
}
|
|
187
188
|
|
|
189
|
+
/**
|
|
190
|
+
* Merge the location state a document response carried (its bootstrap
|
|
191
|
+
* payload's `metadata.locationState`, server/document-location-state.ts)
|
|
192
|
+
* into the entry the document loaded on. Start-up runs it after
|
|
193
|
+
* clearLocationStateOnDocumentLoad and the version is set, before the entry
|
|
194
|
+
* is committed for readers (initBrowserApp; renderRoute's `hydrate` mode),
|
|
195
|
+
* so readers get it once they have hydrated, as stored state. Only typed
|
|
196
|
+
* slots are taken.
|
|
197
|
+
*/
|
|
198
|
+
export function applyDocumentLocationState(
|
|
199
|
+
locationState: Record<string, unknown> | undefined,
|
|
200
|
+
): void {
|
|
201
|
+
if (!locationState) return;
|
|
202
|
+
let slots: Record<string, unknown> | undefined;
|
|
203
|
+
for (const key of Object.keys(locationState)) {
|
|
204
|
+
if (!isLocationStateKey(key)) continue;
|
|
205
|
+
(slots ??= {})[key] = locationState[key];
|
|
206
|
+
}
|
|
207
|
+
if (slots) mergeLocationState(slots);
|
|
208
|
+
}
|
|
209
|
+
|
|
188
210
|
/**
|
|
189
211
|
* Merge server-set location state into the current history entry and return
|
|
190
212
|
* the entry state written. Readers see it when the caller commits it
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Client seat of `invalidateClientCache()` (the `default` export condition).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* the history cache is marked stale (SWR), the prefetch map
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
4
|
+
* The full client invalidation, more than a server action without a
|
|
5
|
+
* directive does: the history cache is marked stale (SWR), the prefetch map
|
|
6
|
+
* is flushed (in-flight prefetches aborted, visible links prefetch again), the
|
|
7
|
+
* state rotates, and sibling tabs are broadcast to, which adopt the new state
|
|
8
|
+
* and flush too (`invalidateCacheAndBroadcast()`). A directive-free action
|
|
9
|
+
* only marks history and prefetches stale and rotates nothing
|
|
10
|
+
* (`markCacheAsStaleAndBroadcast()`). History is marked stale, not cleared, so
|
|
11
|
+
* Back renders the cached entry instantly and revalidates.
|
|
10
12
|
*/
|
|
11
13
|
|
|
12
14
|
import { getRegisteredStore } from "./navigation-store-handle.js";
|
|
@@ -27,7 +29,7 @@ export function invalidateClientCache(): void {
|
|
|
27
29
|
|
|
28
30
|
const store = getRegisteredStore();
|
|
29
31
|
if (store) {
|
|
30
|
-
store.
|
|
32
|
+
store.invalidateCacheAndBroadcast();
|
|
31
33
|
return;
|
|
32
34
|
}
|
|
33
35
|
|
|
@@ -43,7 +43,7 @@ import {
|
|
|
43
43
|
generateHistoryKey,
|
|
44
44
|
liveHandleDataForCache,
|
|
45
45
|
} from "./navigation-store.js";
|
|
46
|
-
import type { EventController } from "./event-controller.js";
|
|
46
|
+
import type { EventController, NavigationEntry } from "./event-controller.js";
|
|
47
47
|
import { isInterceptOnlyCache } from "./intercept-utils.js";
|
|
48
48
|
import { actionBoundaryAt, idsAboveBoundary } from "./segment-reconciler.js";
|
|
49
49
|
import {
|
|
@@ -140,6 +140,82 @@ function isRestorable(
|
|
|
140
140
|
);
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
/**
|
|
144
|
+
* The request a stale-prefetch revalidation makes (scheduleStalePrefetch-
|
|
145
|
+
* Revalidation): the ids it holds, and the intercept and action error
|
|
146
|
+
* boundary on screen.
|
|
147
|
+
*/
|
|
148
|
+
export interface StalePrefetchRevalidation {
|
|
149
|
+
segmentIds: string[];
|
|
150
|
+
intercept: { isIntercept: boolean; sourceUrl?: string };
|
|
151
|
+
boundary: string | undefined;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* A click that adopted a prefetch a server action marked stale
|
|
156
|
+
* (prefetch/cache.ts isPrefetchStale) committed the pre-write payload, as any
|
|
157
|
+
* prefetched click commits. Once that navigation settled
|
|
158
|
+
* (NavigationEntry.settled: its commit, any `prefetch: false` fill and its
|
|
159
|
+
* stream, so this never supersedes the click nor cuts a response
|
|
160
|
+
* that arrived short), `revalidate` runs the page's request in
|
|
161
|
+
* Back's lane (the bridge's revalidateInBackground), with the ids on screen
|
|
162
|
+
* and the action error boundary it shows held, as Back onto a stale entry
|
|
163
|
+
* does.
|
|
164
|
+
*
|
|
165
|
+
* The entry is marked stale first, so a Back onto it revalidates when this
|
|
166
|
+
* one never ran or was left. Nothing runs when the click did not commit, or
|
|
167
|
+
* when the page moved since (a newer navigation aborts the click's entry, a
|
|
168
|
+
* shallow one moves the history key) or another navigation is open. An
|
|
169
|
+
* action in flight does not hold it back, as it does not hold Back's: its
|
|
170
|
+
* follow-up, when it owes one, starts a navigation that supersedes this one.
|
|
171
|
+
*
|
|
172
|
+
* Exported so renderRoute (testing/render-route.tsx) drives these guards and
|
|
173
|
+
* this plan rather than a copy of them.
|
|
174
|
+
*/
|
|
175
|
+
export function scheduleStalePrefetchRevalidation(
|
|
176
|
+
store: Pick<
|
|
177
|
+
NavigationStore,
|
|
178
|
+
"getCachedSegments" | "getHistoryKey" | "updateCacheHandleData"
|
|
179
|
+
>,
|
|
180
|
+
eventController: Pick<EventController, "getUnsettledNavigation">,
|
|
181
|
+
entry: Pick<NavigationEntry, "settled">,
|
|
182
|
+
historyKey: string,
|
|
183
|
+
revalidate: (request: StalePrefetchRevalidation) => void,
|
|
184
|
+
): void {
|
|
185
|
+
void entry.settled.then((committed) => {
|
|
186
|
+
if (!committed) return;
|
|
187
|
+
const cached = store.getCachedSegments(historyKey);
|
|
188
|
+
if (!cached) return;
|
|
189
|
+
store.updateCacheHandleData(historyKey, cached.handleData ?? {}, true);
|
|
190
|
+
if (
|
|
191
|
+
store.getHistoryKey() !== historyKey ||
|
|
192
|
+
eventController.getUnsettledNavigation() ||
|
|
193
|
+
!isRestorable(cached.segments)
|
|
194
|
+
) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const historyState = window.history.state;
|
|
198
|
+
const isIntercept = historyState?.intercept === true;
|
|
199
|
+
const boundaryAt = isIntercept ? -1 : actionBoundaryAt(cached.segments);
|
|
200
|
+
if (boundaryAt === 0) return;
|
|
201
|
+
const boundary = cached.segments[boundaryAt]?.id;
|
|
202
|
+
debugLog("[Browser] Stale prefetch committed, background revalidating");
|
|
203
|
+
// A deferred Meta still pending when its handles were cached: a whole
|
|
204
|
+
// render re-streams them (handlePopstate, handlesPending).
|
|
205
|
+
const segmentIds = cached.handlesPending
|
|
206
|
+
? []
|
|
207
|
+
: idsAboveBoundary(
|
|
208
|
+
cached.segments.map((s) => s.id),
|
|
209
|
+
boundary,
|
|
210
|
+
);
|
|
211
|
+
revalidate({
|
|
212
|
+
segmentIds,
|
|
213
|
+
intercept: { isIntercept, sourceUrl: historyState?.sourceUrl },
|
|
214
|
+
boundary,
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
143
219
|
/**
|
|
144
220
|
* Create a navigation bridge for handling client-side navigation
|
|
145
221
|
*
|
|
@@ -236,6 +312,53 @@ export function createNavigationBridge(
|
|
|
236
312
|
});
|
|
237
313
|
}
|
|
238
314
|
|
|
315
|
+
/**
|
|
316
|
+
* Back's stale-revalidation lane: a background request for the page on
|
|
317
|
+
* screen (skipLoadingState: useNavigation() stays idle, useLinkStatus shows
|
|
318
|
+
* nothing) that holds `segmentIds` and an action error boundary on screen,
|
|
319
|
+
* re-caches the entry (cacheOnly) and commits as kind "revalidate". Used by
|
|
320
|
+
* a Back/Forward restore of a stale entry (handlePopstate) and after a click
|
|
321
|
+
* on a stale prefetch (scheduleStalePrefetchRevalidation).
|
|
322
|
+
*/
|
|
323
|
+
function revalidateInBackground(
|
|
324
|
+
url: string,
|
|
325
|
+
segmentIds: string[],
|
|
326
|
+
intercept: { isIntercept: boolean; sourceUrl?: string },
|
|
327
|
+
boundary: string | undefined,
|
|
328
|
+
): void {
|
|
329
|
+
const tx = createNavigationTransaction(store, eventController, url, {
|
|
330
|
+
skipLoadingState: true,
|
|
331
|
+
replace: true,
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
fetchPartialUpdate(
|
|
335
|
+
url,
|
|
336
|
+
segmentIds,
|
|
337
|
+
false,
|
|
338
|
+
tx.handle.signal,
|
|
339
|
+
tx.with({
|
|
340
|
+
url,
|
|
341
|
+
replace: true,
|
|
342
|
+
scroll: false,
|
|
343
|
+
intercept: intercept.isIntercept,
|
|
344
|
+
interceptSourceUrl: intercept.sourceUrl,
|
|
345
|
+
cacheOnly: true,
|
|
346
|
+
}),
|
|
347
|
+
{
|
|
348
|
+
type: "stale-revalidation",
|
|
349
|
+
interceptSourceUrl: intercept.sourceUrl,
|
|
350
|
+
boundary,
|
|
351
|
+
},
|
|
352
|
+
)
|
|
353
|
+
.catch((error) => {
|
|
354
|
+
if (isBackgroundSuppressible(error)) return;
|
|
355
|
+
console.error("[Browser] Background revalidation failed:", error);
|
|
356
|
+
})
|
|
357
|
+
.finally(() => {
|
|
358
|
+
tx[Symbol.dispose]();
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
239
362
|
const bridge: NavigationBridge = {
|
|
240
363
|
/**
|
|
241
364
|
* Navigate to a URL
|
|
@@ -387,6 +510,17 @@ export function createNavigationBridge(
|
|
|
387
510
|
store.updateCacheHandleData(sourceHistoryKey, currentHandleData);
|
|
388
511
|
}
|
|
389
512
|
|
|
513
|
+
// Read the rango state BEFORE the target entry's stale bit, as
|
|
514
|
+
// handlePopstate does. Another tab's write reaches this tab only through
|
|
515
|
+
// a read (rango-state.ts write observer) when its broadcast does not:
|
|
516
|
+
// the pages share no segment, or crossTabSync is off. The first read
|
|
517
|
+
// otherwise comes from the navigation client, after the held ids below
|
|
518
|
+
// are chosen, and the server keeps what the client says it holds when
|
|
519
|
+
// the default allows (router/revalidation.ts: a route whose params and
|
|
520
|
+
// search are the entry's, a layout loader with no revalidate()), so the
|
|
521
|
+
// entry's pre-write copy would commit as fresh.
|
|
522
|
+
getRangoState();
|
|
523
|
+
|
|
390
524
|
// Check if we have cached segments for target URL
|
|
391
525
|
const historyKey = generateHistoryKey(url);
|
|
392
526
|
const cached = store.getCachedSegments(historyKey);
|
|
@@ -441,6 +575,9 @@ export function createNavigationBridge(
|
|
|
441
575
|
state: resolvedState,
|
|
442
576
|
skipLoadingState: false,
|
|
443
577
|
});
|
|
578
|
+
// This navigation's entry when its response was a prefetch an action
|
|
579
|
+
// marked stale (scheduleStalePrefetchRevalidation).
|
|
580
|
+
let stalePrefetchEntry: NavigationEntry | null = null;
|
|
444
581
|
// A cross-route clientUrls() navigation presents its destination
|
|
445
582
|
// optimistically at once: transition({ when }) decides THERE (the first
|
|
446
583
|
// presentation), over the destination route's predicate and those of
|
|
@@ -520,8 +657,30 @@ export function createNavigationBridge(
|
|
|
520
657
|
...(clientUrlPresentation && {
|
|
521
658
|
presentation: clientUrlPresentation,
|
|
522
659
|
}),
|
|
660
|
+
// The last response decides: a fresh refetch after a stale
|
|
661
|
+
// adoption (a fill's start-over) clears it.
|
|
662
|
+
onStalePrefetch: (stale: boolean) => {
|
|
663
|
+
const entry = eventController.getUnsettledNavigation();
|
|
664
|
+
stalePrefetchEntry =
|
|
665
|
+
stale && entry?.abort === tx.handle.abort ? entry : null;
|
|
666
|
+
},
|
|
523
667
|
},
|
|
524
668
|
);
|
|
669
|
+
if (stalePrefetchEntry && !tx.handle.signal.aborted) {
|
|
670
|
+
scheduleStalePrefetchRevalidation(
|
|
671
|
+
store,
|
|
672
|
+
eventController,
|
|
673
|
+
stalePrefetchEntry,
|
|
674
|
+
store.getHistoryKey(),
|
|
675
|
+
({ segmentIds, intercept, boundary }) =>
|
|
676
|
+
revalidateInBackground(
|
|
677
|
+
window.location.href,
|
|
678
|
+
segmentIds,
|
|
679
|
+
intercept,
|
|
680
|
+
boundary,
|
|
681
|
+
),
|
|
682
|
+
);
|
|
683
|
+
}
|
|
525
684
|
} catch (error) {
|
|
526
685
|
// Aborted, or superseded by a newer navigation. A superseded nav may
|
|
527
686
|
// reject with a non-AbortError (e.g. a Flight decode that fails after its
|
|
@@ -846,39 +1005,12 @@ export function createNavigationBridge(
|
|
|
846
1005
|
// the deferred-Meta-aborted case, so action/cross-tab SWR keeps the
|
|
847
1006
|
// cheap diff path.
|
|
848
1007
|
const segmentIds = cached?.handlesPending ? [] : heldIds;
|
|
849
|
-
|
|
850
|
-
const tx = createNavigationTransaction(
|
|
851
|
-
store,
|
|
852
|
-
eventController,
|
|
853
|
-
url,
|
|
854
|
-
{ skipLoadingState: true, replace: true },
|
|
855
|
-
);
|
|
856
|
-
|
|
857
|
-
fetchPartialUpdate(
|
|
1008
|
+
revalidateInBackground(
|
|
858
1009
|
url,
|
|
859
1010
|
segmentIds,
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
url,
|
|
864
|
-
replace: true,
|
|
865
|
-
scroll: false,
|
|
866
|
-
intercept: isIntercept,
|
|
867
|
-
interceptSourceUrl,
|
|
868
|
-
cacheOnly: true,
|
|
869
|
-
}),
|
|
870
|
-
{ type: "stale-revalidation", interceptSourceUrl, boundary },
|
|
871
|
-
)
|
|
872
|
-
.catch((error) => {
|
|
873
|
-
if (isBackgroundSuppressible(error)) return;
|
|
874
|
-
console.error(
|
|
875
|
-
"[Browser] Background revalidation failed:",
|
|
876
|
-
error,
|
|
877
|
-
);
|
|
878
|
-
})
|
|
879
|
-
.finally(() => {
|
|
880
|
-
tx[Symbol.dispose]();
|
|
881
|
-
});
|
|
1011
|
+
{ isIntercept, sourceUrl: interceptSourceUrl },
|
|
1012
|
+
boundary,
|
|
1013
|
+
);
|
|
882
1014
|
}
|
|
883
1015
|
};
|
|
884
1016
|
// React commits a transition started while window.event is the
|