@rangojs/router 0.0.0-experimental.fb4fdc18 → 0.0.0-experimental.fce7fbd1
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 +9 -9
- package/dist/bin/rango.js +147 -57
- package/dist/testing/vitest.js +48 -0
- package/dist/vite/index.js +914 -485
- package/package.json +55 -11
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +220 -30
- package/skills/caching/SKILL.md +116 -8
- package/skills/composability/SKILL.md +27 -2
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +3 -1
- package/skills/hooks/SKILL.md +214 -18
- package/skills/host-router/SKILL.md +45 -20
- package/skills/intercept/SKILL.md +26 -4
- package/skills/layout/SKILL.md +6 -7
- package/skills/links/SKILL.md +173 -17
- package/skills/loader/SKILL.md +149 -6
- package/skills/middleware/SKILL.md +13 -9
- package/skills/migrate-nextjs/SKILL.md +1 -1
- package/skills/mime-routes/SKILL.md +27 -0
- package/skills/observability/SKILL.md +137 -0
- package/skills/parallel/SKILL.md +5 -6
- package/skills/prerender/SKILL.md +14 -33
- package/skills/rango/SKILL.md +242 -26
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +58 -9
- package/skills/route/SKILL.md +13 -4
- package/skills/router-setup/SKILL.md +3 -3
- package/skills/server-actions/SKILL.md +53 -41
- package/skills/testing/SKILL.md +599 -0
- package/skills/typesafety/SKILL.md +310 -26
- package/skills/use-cache/SKILL.md +34 -5
- package/skills/view-transitions/SKILL.md +294 -0
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +117 -0
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/event-controller.ts +42 -66
- package/src/browser/history-state.ts +21 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/navigation-bridge.ts +6 -6
- package/src/browser/navigation-client.ts +12 -15
- package/src/browser/navigation-store.ts +7 -8
- package/src/browser/navigation-transaction.ts +10 -28
- package/src/browser/partial-update.ts +9 -19
- package/src/browser/react/NavigationProvider.tsx +29 -40
- package/src/browser/react/index.ts +3 -0
- package/src/browser/react/location-state-shared.ts +175 -4
- package/src/browser/react/location-state.ts +39 -13
- package/src/browser/react/use-handle.ts +17 -9
- package/src/browser/react/use-params.ts +3 -4
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +14 -1
- package/src/browser/response-adapter.ts +25 -0
- package/src/browser/rsc-router.tsx +30 -16
- package/src/browser/scroll-restoration.ts +22 -14
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +23 -30
- package/src/browser/types.ts +2 -0
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +60 -35
- package/src/build/generate-route-types.ts +2 -0
- package/src/build/index.ts +2 -0
- package/src/build/route-types/codegen.ts +4 -4
- package/src/build/route-types/include-resolution.ts +1 -1
- package/src/build/route-types/per-module-writer.ts +7 -4
- package/src/build/route-types/router-processing.ts +55 -14
- package/src/build/route-types/scan-filter.ts +1 -1
- package/src/build/route-types/source-scan.ts +118 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-scope.ts +28 -42
- package/src/cache/cf/cf-cache-store.ts +49 -6
- package/src/client.rsc.tsx +3 -0
- package/src/client.tsx +10 -8
- package/src/context-var.ts +5 -5
- package/src/decode-loader-results.ts +36 -0
- package/src/errors.ts +30 -1
- package/src/handle.ts +26 -13
- package/src/host/index.ts +2 -2
- package/src/host/router.ts +129 -57
- package/src/host/types.ts +31 -2
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +140 -20
- package/src/index.rsc.ts +6 -4
- package/src/index.ts +13 -6
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +2 -5
- package/src/loader.ts +3 -10
- package/src/missing-id-error.ts +68 -0
- package/src/prerender.ts +4 -4
- package/src/response-utils.ts +9 -0
- package/src/reverse.ts +65 -41
- package/src/route-content-wrapper.tsx +6 -28
- package/src/route-definition/dsl-helpers.ts +238 -263
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +37 -14
- package/src/route-definition/use-item-types.ts +32 -0
- package/src/route-types.ts +19 -41
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +15 -2
- package/src/router/error-handling.ts +1 -1
- package/src/router/handler-context.ts +4 -42
- package/src/router/intercept-resolution.ts +4 -18
- package/src/router/lazy-includes.ts +2 -2
- package/src/router/loader-resolution.ts +16 -2
- package/src/router/match-handlers.ts +62 -20
- package/src/router/match-middleware/cache-lookup.ts +44 -91
- package/src/router/match-middleware/cache-store.ts +3 -2
- package/src/router/match-result.ts +32 -30
- package/src/router/metrics.ts +1 -1
- package/src/router/middleware-types.ts +1 -1
- package/src/router/middleware.ts +46 -78
- package/src/router/prerender-match.ts +1 -1
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +4 -28
- package/src/router/revalidation.ts +43 -1
- package/src/router/router-interfaces.ts +45 -28
- package/src/router/router-options.ts +40 -1
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +19 -6
- package/src/router/segment-resolution/revalidation.ts +19 -6
- package/src/router/segment-resolution/view-transition-default.ts +36 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/telemetry.ts +99 -0
- package/src/router/types.ts +8 -0
- package/src/router.ts +37 -21
- package/src/rsc/handler-context.ts +2 -2
- package/src/rsc/handler.ts +20 -65
- package/src/rsc/helpers.ts +22 -2
- package/src/rsc/index.ts +1 -1
- package/src/rsc/origin-guard.ts +28 -10
- package/src/rsc/response-route-handler.ts +32 -52
- package/src/rsc/rsc-rendering.ts +27 -53
- package/src/rsc/runtime-warnings.ts +9 -10
- package/src/rsc/server-action.ts +13 -37
- package/src/rsc/ssr-setup.ts +16 -0
- package/src/rsc/types.ts +2 -2
- package/src/search-params.ts +4 -4
- package/src/segment-system.tsx +121 -65
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +118 -51
- package/src/server/cookie-store.ts +28 -4
- package/src/server/request-context.ts +10 -0
- package/src/static-handler.ts +1 -1
- package/src/testing/cache-status.ts +166 -0
- package/src/testing/collect-handle.ts +63 -0
- package/src/testing/dispatch.ts +440 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +154 -0
- package/src/testing/e2e/index.ts +149 -0
- package/src/testing/e2e/matchers.ts +51 -0
- package/src/testing/e2e/page-helpers.ts +272 -0
- package/src/testing/e2e/parity.ts +306 -0
- package/src/testing/e2e/server.ts +183 -0
- package/src/testing/flight-matchers.ts +104 -0
- package/src/testing/flight-runtime.d.ts +21 -0
- package/src/testing/flight.entry.ts +22 -0
- package/src/testing/flight.ts +182 -0
- package/src/testing/generated-routes.ts +223 -0
- package/src/testing/index.ts +105 -0
- package/src/testing/internal/context.ts +193 -0
- package/src/testing/render-route.tsx +536 -0
- package/src/testing/run-loader.ts +296 -0
- package/src/testing/run-middleware.ts +170 -0
- package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
- package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
- package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
- package/src/testing/vitest-stubs/version.ts +5 -0
- package/src/testing/vitest.ts +183 -0
- package/src/types/global-namespace.ts +39 -26
- package/src/types/handler-context.ts +56 -11
- package/src/types/index.ts +1 -0
- package/src/types/segments.ts +18 -1
- package/src/urls/include-helper.ts +10 -53
- package/src/urls/index.ts +0 -3
- package/src/urls/path-helper-types.ts +11 -3
- package/src/urls/path-helper.ts +17 -52
- package/src/urls/pattern-types.ts +36 -19
- package/src/urls/response-types.ts +20 -19
- package/src/urls/type-extraction.ts +26 -116
- package/src/urls/urls-function.ts +1 -5
- package/src/use-loader.tsx +413 -42
- package/src/vite/debug.ts +1 -0
- package/src/vite/discovery/bundle-postprocess.ts +6 -6
- package/src/vite/discovery/discover-routers.ts +70 -48
- package/src/vite/discovery/discovery-errors.ts +194 -0
- package/src/vite/discovery/prerender-collection.ts +19 -25
- package/src/vite/discovery/route-types-writer.ts +40 -84
- package/src/vite/discovery/state.ts +33 -0
- package/src/vite/discovery/virtual-module-codegen.ts +13 -23
- package/src/vite/index.ts +2 -0
- package/src/vite/plugin-types.ts +67 -0
- package/src/vite/plugins/cjs-to-esm.ts +3 -7
- package/src/vite/plugins/client-ref-hashing.ts +12 -1
- package/src/vite/plugins/cloudflare-protocol-stub.ts +1 -1
- package/src/vite/plugins/expose-action-id.ts +2 -2
- package/src/vite/plugins/expose-id-utils.ts +12 -8
- package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
- package/src/vite/plugins/expose-ids/handler-transform.ts +8 -61
- package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
- package/src/vite/plugins/expose-internal-ids.ts +47 -67
- package/src/vite/plugins/performance-tracks.ts +12 -16
- package/src/vite/plugins/use-cache-transform.ts +13 -11
- package/src/vite/plugins/version-injector.ts +2 -12
- package/src/vite/plugins/version-plugin.ts +59 -2
- package/src/vite/plugins/virtual-entries.ts +2 -2
- package/src/vite/rango.ts +67 -15
- package/src/vite/router-discovery.ts +208 -63
- package/src/vite/utils/ast-handler-extract.ts +15 -15
- package/src/vite/utils/bundle-analysis.ts +4 -2
- package/src/vite/utils/client-chunks.ts +190 -0
- package/src/vite/utils/forward-user-plugins.ts +193 -0
- package/src/vite/utils/manifest-utils.ts +21 -5
- package/src/vite/utils/shared-utils.ts +107 -26
- package/src/browser/action-response-classifier.ts +0 -99
|
@@ -268,6 +268,20 @@ function matchesActionId(
|
|
|
268
268
|
return entryActionId.endsWith(`#${subscriptionId}`);
|
|
269
269
|
}
|
|
270
270
|
|
|
271
|
+
// Coalesce rapid notifications into one microtask-deferred fan-out; the
|
|
272
|
+
// setTimeout(0) batching prevents render storms. Each notifier owns its timer
|
|
273
|
+
// so listener kinds coalesce independently.
|
|
274
|
+
function makeDebouncedNotifier(listeners: Set<() => void>): () => void {
|
|
275
|
+
let timeout: ReturnType<typeof setTimeout> | null = null;
|
|
276
|
+
return () => {
|
|
277
|
+
if (timeout !== null) clearTimeout(timeout);
|
|
278
|
+
timeout = setTimeout(() => {
|
|
279
|
+
timeout = null;
|
|
280
|
+
listeners.forEach((listener) => listener());
|
|
281
|
+
}, 0);
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
271
285
|
// ============================================================================
|
|
272
286
|
// Implementation
|
|
273
287
|
// ============================================================================
|
|
@@ -334,18 +348,7 @@ export function createEventController(
|
|
|
334
348
|
const actionListeners = new Map<string, Set<ActionStateListener>>();
|
|
335
349
|
const handleListeners = new Set<HandleListener>();
|
|
336
350
|
|
|
337
|
-
|
|
338
|
-
let notifyTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
339
|
-
|
|
340
|
-
function notify() {
|
|
341
|
-
if (notifyTimeout !== null) {
|
|
342
|
-
clearTimeout(notifyTimeout);
|
|
343
|
-
}
|
|
344
|
-
notifyTimeout = setTimeout(() => {
|
|
345
|
-
notifyTimeout = null;
|
|
346
|
-
stateListeners.forEach((listener) => listener());
|
|
347
|
-
}, 0);
|
|
348
|
-
}
|
|
351
|
+
const notify = makeDebouncedNotifier(stateListeners);
|
|
349
352
|
|
|
350
353
|
// Debounce per-action notifications
|
|
351
354
|
const actionNotifyTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
|
|
@@ -371,18 +374,7 @@ export function createEventController(
|
|
|
371
374
|
);
|
|
372
375
|
}
|
|
373
376
|
|
|
374
|
-
|
|
375
|
-
let handleNotifyTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
376
|
-
|
|
377
|
-
function notifyHandles() {
|
|
378
|
-
if (handleNotifyTimeout !== null) {
|
|
379
|
-
clearTimeout(handleNotifyTimeout);
|
|
380
|
-
}
|
|
381
|
-
handleNotifyTimeout = setTimeout(() => {
|
|
382
|
-
handleNotifyTimeout = null;
|
|
383
|
-
handleListeners.forEach((listener) => listener());
|
|
384
|
-
}, 0);
|
|
385
|
-
}
|
|
377
|
+
const notifyHandles = makeDebouncedNotifier(handleListeners);
|
|
386
378
|
|
|
387
379
|
// ========================================================================
|
|
388
380
|
// Derived State
|
|
@@ -429,22 +421,17 @@ export function createEventController(
|
|
|
429
421
|
}
|
|
430
422
|
|
|
431
423
|
function getActionState(actionId: string): TrackedActionState {
|
|
432
|
-
//
|
|
433
|
-
//
|
|
434
|
-
const
|
|
435
|
-
.filter(
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
(a) => matchesActionId(actionId, a.actionId) && a.phase === "settling",
|
|
444
|
-
)
|
|
445
|
-
.sort((a, b) => b.startedAt - a.startedAt)[0];
|
|
446
|
-
|
|
447
|
-
const entry = activeEntry || settlingEntry;
|
|
424
|
+
// Prefer the most-recent non-settling entry; fall back to most-recent
|
|
425
|
+
// settling so a just-settled action's result/error stays readable.
|
|
426
|
+
const entry = [...inflightActions.values()]
|
|
427
|
+
.filter((a) => matchesActionId(actionId, a.actionId))
|
|
428
|
+
.reduce<ActionEntry | undefined>((best, a) => {
|
|
429
|
+
if (!best) return a;
|
|
430
|
+
const aActive = a.phase !== "settling";
|
|
431
|
+
const bActive = best.phase !== "settling";
|
|
432
|
+
if (aActive !== bActive) return aActive ? a : best;
|
|
433
|
+
return a.startedAt > best.startedAt ? a : best;
|
|
434
|
+
}, undefined);
|
|
448
435
|
|
|
449
436
|
if (!entry) {
|
|
450
437
|
return { ...DEFAULT_ACTION_STATE };
|
|
@@ -632,6 +619,19 @@ export function createEventController(
|
|
|
632
619
|
doSettle();
|
|
633
620
|
}
|
|
634
621
|
|
|
622
|
+
// streamingEnded is forced here for the "streaming never started" case so
|
|
623
|
+
// tryFinalize can run; otherwise the streaming token's end() finalizes.
|
|
624
|
+
function settleWith(result: NonNullable<typeof pendingResult>) {
|
|
625
|
+
if (!inflightActions.has(id) || settled) return;
|
|
626
|
+
actionCompleted = true;
|
|
627
|
+
entry.completed = true;
|
|
628
|
+
pendingResult = result;
|
|
629
|
+
if (entry.phase === "fetching" || streamingEnded) {
|
|
630
|
+
streamingEnded = true;
|
|
631
|
+
tryFinalize();
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
635
|
return {
|
|
636
636
|
id,
|
|
637
637
|
abort,
|
|
@@ -668,35 +668,11 @@ export function createEventController(
|
|
|
668
668
|
},
|
|
669
669
|
|
|
670
670
|
complete(result?: unknown) {
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
actionCompleted = true;
|
|
674
|
-
entry.completed = true;
|
|
675
|
-
pendingResult = { type: "success", value: result };
|
|
676
|
-
|
|
677
|
-
// If streaming never started or already ended, finalize immediately
|
|
678
|
-
// Otherwise wait for streaming to end
|
|
679
|
-
if (entry.phase === "fetching" || streamingEnded) {
|
|
680
|
-
streamingEnded = true; // Mark as ended if never started
|
|
681
|
-
tryFinalize();
|
|
682
|
-
}
|
|
683
|
-
// If streaming is in progress, tryFinalize() will be called when streaming ends
|
|
671
|
+
settleWith({ type: "success", value: result });
|
|
684
672
|
},
|
|
685
673
|
|
|
686
674
|
fail(error: unknown) {
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
actionCompleted = true;
|
|
690
|
-
entry.completed = true;
|
|
691
|
-
pendingResult = { type: "error", value: error };
|
|
692
|
-
|
|
693
|
-
// If streaming never started or already ended, finalize immediately
|
|
694
|
-
// Otherwise wait for streaming to end
|
|
695
|
-
if (entry.phase === "fetching" || streamingEnded) {
|
|
696
|
-
streamingEnded = true; // Mark as ended if never started
|
|
697
|
-
tryFinalize();
|
|
698
|
-
}
|
|
699
|
-
// If streaming is in progress, tryFinalize() will be called when streaming ends
|
|
675
|
+
settleWith({ type: "error", value: error });
|
|
700
676
|
},
|
|
701
677
|
|
|
702
678
|
getRevalidatedSegments(): Set<string> {
|
|
@@ -61,6 +61,27 @@ export function buildHistoryState(
|
|
|
61
61
|
return Object.keys(result).length > 0 ? result : null;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Stamp an `idx` on the next history entry's state and call push/replaceState.
|
|
66
|
+
* Push increments the current idx; replace keeps it. Initial entry idx is 0.
|
|
67
|
+
* Used by useRouter().back() to detect "first entry in this session" without
|
|
68
|
+
* relying on the Navigation API.
|
|
69
|
+
*/
|
|
70
|
+
export function pushHistoryWithIdx(
|
|
71
|
+
state: Record<string, unknown> | null,
|
|
72
|
+
url: string,
|
|
73
|
+
replace: boolean,
|
|
74
|
+
): void {
|
|
75
|
+
const oldIdx = (window.history.state as { idx?: number } | null)?.idx ?? 0;
|
|
76
|
+
const newIdx = replace ? oldIdx : oldIdx + 1;
|
|
77
|
+
const finalState = { ...(state ?? {}), idx: newIdx };
|
|
78
|
+
if (replace) {
|
|
79
|
+
window.history.replaceState(finalState, "", url);
|
|
80
|
+
} else {
|
|
81
|
+
window.history.pushState(finalState, "", url);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
64
85
|
/**
|
|
65
86
|
* Merge server-set location state into the current history entry.
|
|
66
87
|
* Replaces the current history state and dispatches notification event
|
package/src/browser/index.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// ============================================================================
|
|
2
|
-
// Browser Module - Browser entry point for
|
|
2
|
+
// Browser Module - Browser entry point for Rango
|
|
3
3
|
// ============================================================================
|
|
4
4
|
//
|
|
5
5
|
// Usage:
|
|
6
|
-
// import { initBrowserApp,
|
|
6
|
+
// import { initBrowserApp, Rango } from "rsc-router/browser";
|
|
7
7
|
//
|
|
8
8
|
// For React components (Link, useNavigation, etc.):
|
|
9
9
|
// import { Link, useNavigation, useAction, href } from "rsc-router/client";
|
|
@@ -13,6 +13,6 @@
|
|
|
13
13
|
// Browser app initialization
|
|
14
14
|
export {
|
|
15
15
|
initBrowserApp,
|
|
16
|
-
|
|
16
|
+
Rango,
|
|
17
17
|
type InitBrowserAppOptions,
|
|
18
18
|
} from "./rsc-router.js";
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
createNavigationTransaction,
|
|
14
14
|
resolveNavigationState,
|
|
15
15
|
} from "./navigation-transaction.js";
|
|
16
|
-
import { buildHistoryState } from "./history-state.js";
|
|
16
|
+
import { buildHistoryState, pushHistoryWithIdx } from "./history-state.js";
|
|
17
17
|
import {
|
|
18
18
|
handleNavigationStart,
|
|
19
19
|
handleNavigationEnd,
|
|
@@ -204,11 +204,7 @@ export function createNavigationBridge(
|
|
|
204
204
|
},
|
|
205
205
|
{},
|
|
206
206
|
);
|
|
207
|
-
|
|
208
|
-
window.history.replaceState(historyState, "", url);
|
|
209
|
-
} else {
|
|
210
|
-
window.history.pushState(historyState, "", url);
|
|
211
|
-
}
|
|
207
|
+
pushHistoryWithIdx(historyState, url, options?.replace ?? false);
|
|
212
208
|
|
|
213
209
|
// Ensure new history entry has a scroll restoration key
|
|
214
210
|
ensureHistoryKey();
|
|
@@ -711,6 +707,10 @@ export function createNavigationBridge(
|
|
|
711
707
|
};
|
|
712
708
|
},
|
|
713
709
|
|
|
710
|
+
getVersion(): string | undefined {
|
|
711
|
+
return version;
|
|
712
|
+
},
|
|
713
|
+
|
|
714
714
|
updateVersion(newVersion: string): void {
|
|
715
715
|
version = newVersion;
|
|
716
716
|
setAppVersion(newVersion);
|
|
@@ -15,6 +15,7 @@ import { getRangoState } from "./rango-state.js";
|
|
|
15
15
|
import {
|
|
16
16
|
extractRscHeaderUrl,
|
|
17
17
|
emptyResponse,
|
|
18
|
+
handleReloadHeader,
|
|
18
19
|
teeWithCompletion,
|
|
19
20
|
} from "./response-adapter.js";
|
|
20
21
|
import {
|
|
@@ -148,21 +149,17 @@ export function createNavigationClient(
|
|
|
148
149
|
source: string,
|
|
149
150
|
): Response | Promise<Response> => {
|
|
150
151
|
// Version mismatch — server wants a full page reload
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
window.location.href = reload.url;
|
|
163
|
-
// Block further processing — page is reloading
|
|
164
|
-
return new Promise<Response>(() => {});
|
|
165
|
-
}
|
|
152
|
+
const reloadResult = handleReloadHeader(response, {
|
|
153
|
+
onBlocked: resolveStreamComplete,
|
|
154
|
+
onReload: (url) => {
|
|
155
|
+
if (tx) {
|
|
156
|
+
browserDebugLog(tx, `version mismatch, reloading (${source})`, {
|
|
157
|
+
reloadUrl: url,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
if (reloadResult) return reloadResult;
|
|
166
163
|
|
|
167
164
|
// Server-side redirect without state: the server returned 204 with
|
|
168
165
|
// X-RSC-Redirect instead of a 3xx (which fetch would auto-follow
|
|
@@ -283,18 +283,17 @@ export function createNavigationStore(
|
|
|
283
283
|
/**
|
|
284
284
|
* Create a debounced function that batches rapid calls
|
|
285
285
|
*/
|
|
286
|
+
// A non-keyed notifier is the keyed one restricted to a single constant key;
|
|
287
|
+
// its own keyed instance means the "" key never collides with action keys.
|
|
286
288
|
function createDebouncedNotifier<T extends (...args: any[]) => void>(
|
|
287
289
|
fn: T,
|
|
288
290
|
ms: number = 20,
|
|
289
291
|
): T {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
fn(...args);
|
|
296
|
-
}, ms);
|
|
297
|
-
}) as T;
|
|
292
|
+
const keyed = createKeyedDebouncedNotifier(
|
|
293
|
+
(_key: string, ...args: any[]) => fn(...args),
|
|
294
|
+
ms,
|
|
295
|
+
);
|
|
296
|
+
return ((...args: Parameters<T>) => keyed("", ...args)) as T;
|
|
298
297
|
}
|
|
299
298
|
|
|
300
299
|
/**
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
} from "./scroll-restoration.js";
|
|
12
12
|
import type { EventController, NavigationHandle } from "./event-controller.js";
|
|
13
13
|
import { debugLog } from "./logging.js";
|
|
14
|
-
import { buildHistoryState } from "./history-state.js";
|
|
14
|
+
import { buildHistoryState, pushHistoryWithIdx } from "./history-state.js";
|
|
15
15
|
|
|
16
16
|
// Re-export for consumers that import from navigation-transaction
|
|
17
17
|
export { resolveNavigationState } from "./history-state.js";
|
|
@@ -186,12 +186,8 @@ export function createNavigationTransaction(
|
|
|
186
186
|
// Used to detect when location state is being cleared.
|
|
187
187
|
const oldState = window.history.state;
|
|
188
188
|
|
|
189
|
-
// Update browser URL
|
|
190
|
-
|
|
191
|
-
window.history.replaceState(historyState, "", url);
|
|
192
|
-
} else {
|
|
193
|
-
window.history.pushState(historyState, "", url);
|
|
194
|
-
}
|
|
189
|
+
// Update browser URL (stamps history.state.idx for back() first-entry detection)
|
|
190
|
+
pushHistoryWithIdx(historyState, url, replace ?? false);
|
|
195
191
|
// Ensure new history entry has a scroll restoration key
|
|
196
192
|
ensureHistoryKey();
|
|
197
193
|
|
|
@@ -240,30 +236,16 @@ export function createNavigationTransaction(
|
|
|
240
236
|
segments: ResolvedSegment[],
|
|
241
237
|
overrides?: BoundCommitOverrides,
|
|
242
238
|
) => {
|
|
243
|
-
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
// Allow overrides to force replace (e.g., for intercepts)
|
|
247
|
-
const finalReplace =
|
|
248
|
-
overrides?.replace !== undefined ? overrides.replace : opts.replace;
|
|
249
|
-
// Intercept info: overrides take precedence, fallback to opts
|
|
250
|
-
const intercept =
|
|
251
|
-
overrides?.intercept !== undefined
|
|
252
|
-
? overrides.intercept
|
|
253
|
-
: opts.intercept;
|
|
239
|
+
const finalScroll = overrides?.scroll ?? opts.scroll;
|
|
240
|
+
const finalReplace = overrides?.replace ?? opts.replace;
|
|
241
|
+
const intercept = overrides?.intercept ?? opts.intercept;
|
|
254
242
|
const interceptSourceUrl =
|
|
255
|
-
overrides?.interceptSourceUrl
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
//
|
|
259
|
-
const cacheOnly =
|
|
260
|
-
overrides?.cacheOnly !== undefined
|
|
261
|
-
? overrides.cacheOnly
|
|
262
|
-
: opts.cacheOnly;
|
|
263
|
-
// User state: overrides take precedence, fallback to opts
|
|
243
|
+
overrides?.interceptSourceUrl ?? opts.interceptSourceUrl;
|
|
244
|
+
const cacheOnly = overrides?.cacheOnly ?? opts.cacheOnly;
|
|
245
|
+
// state is `unknown` (null is meaningful) so `??` would wrongly drop a
|
|
246
|
+
// null override; serverState always comes from overrides, never opts.
|
|
264
247
|
const state =
|
|
265
248
|
overrides?.state !== undefined ? overrides.state : opts.state;
|
|
266
|
-
// Server-set location state: only from overrides (set by partial-update)
|
|
267
249
|
const serverState = overrides?.serverState;
|
|
268
250
|
return commit({
|
|
269
251
|
...opts,
|
|
@@ -103,7 +103,7 @@ export type UpdateMode =
|
|
|
103
103
|
/** Source URL for intercept restore (popstate cache miss) */
|
|
104
104
|
interceptSourceUrl?: string;
|
|
105
105
|
}
|
|
106
|
-
| { type: "leave-intercept" }
|
|
106
|
+
| { type: "leave-intercept"; interceptSourceUrl?: string }
|
|
107
107
|
| { type: "stale-revalidation"; interceptSourceUrl?: string }
|
|
108
108
|
| { type: "action"; interceptSourceUrl?: string };
|
|
109
109
|
|
|
@@ -169,13 +169,7 @@ export function createPartialUpdater(
|
|
|
169
169
|
// Capture history key at start for stale revalidation consistency check
|
|
170
170
|
const historyKeyAtStart = store.getHistoryKey();
|
|
171
171
|
|
|
172
|
-
|
|
173
|
-
const interceptSourceUrl =
|
|
174
|
-
mode.type === "stale-revalidation" ||
|
|
175
|
-
mode.type === "action" ||
|
|
176
|
-
mode.type === "navigate"
|
|
177
|
-
? mode.interceptSourceUrl
|
|
178
|
-
: undefined;
|
|
172
|
+
const interceptSourceUrl = mode.interceptSourceUrl;
|
|
179
173
|
|
|
180
174
|
// When leaving intercept, filter out intercept-specific segments
|
|
181
175
|
let segments: string[];
|
|
@@ -218,13 +212,11 @@ export function createPartialUpdater(
|
|
|
218
212
|
// When navigating with targetCacheSegments, use those for consistency.
|
|
219
213
|
// Otherwise fall back to current page's segments (for same-route revalidation).
|
|
220
214
|
const targetCache =
|
|
221
|
-
mode.type === "navigate"
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
const cachedSegsSource =
|
|
227
|
-
targetCache && targetCache.length > 0 ? "history-cache" : "current-page";
|
|
215
|
+
mode.type === "navigate" && mode.targetCacheSegments?.length
|
|
216
|
+
? mode.targetCacheSegments
|
|
217
|
+
: undefined;
|
|
218
|
+
const cachedSegs = targetCache ?? getCurrentCachedSegments();
|
|
219
|
+
const cachedSegsSource = targetCache ? "history-cache" : "current-page";
|
|
228
220
|
debugLog(
|
|
229
221
|
`[Browser] cachedSegs source: ${cachedSegsSource} (${cachedSegs.length} segments: ${cachedSegs.map((s) => s.id).join(", ")})`,
|
|
230
222
|
);
|
|
@@ -318,7 +310,7 @@ export function createPartialUpdater(
|
|
|
318
310
|
.filter(Boolean) as ResolvedSegment[];
|
|
319
311
|
|
|
320
312
|
// When navigating with cached segments to a different route, render them.
|
|
321
|
-
if (mode.type === "navigate" && targetCache
|
|
313
|
+
if (mode.type === "navigate" && targetCache) {
|
|
322
314
|
debugLog(
|
|
323
315
|
"[Browser] No diff but navigating with cached segments - rendering target route",
|
|
324
316
|
);
|
|
@@ -606,9 +598,7 @@ export function createPartialUpdater(
|
|
|
606
598
|
})
|
|
607
599
|
: tx.commit(segmentIds, segments);
|
|
608
600
|
|
|
609
|
-
const fullHasTransition = segments
|
|
610
|
-
(s: ResolvedSegment) => s.transition,
|
|
611
|
-
);
|
|
601
|
+
const fullHasTransition = shouldStartViewTransition(segments);
|
|
612
602
|
const fullScrollPayload = toScrollPayload(fullScroll);
|
|
613
603
|
|
|
614
604
|
if (mode.type === "stale-revalidation") {
|
|
@@ -28,7 +28,7 @@ import { NonceContext } from "./nonce-context.js";
|
|
|
28
28
|
import type { ResolvedThemeConfig, Theme } from "../../theme/types.js";
|
|
29
29
|
import { cancelAllPrefetches } from "../prefetch/queue.js";
|
|
30
30
|
import { handleNavigationEnd } from "../scroll-restoration.js";
|
|
31
|
-
import type
|
|
31
|
+
import { createAppShellRef, type AppShellRef } from "../app-shell.js";
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
34
|
* Process handles from an async generator, updating the event controller
|
|
@@ -217,38 +217,33 @@ export function NavigationProvider({
|
|
|
217
217
|
await bridge.refresh();
|
|
218
218
|
}, []);
|
|
219
219
|
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
//
|
|
223
|
-
|
|
220
|
+
// basename/version are always read through a shell ref so the context value
|
|
221
|
+
// has a single shape: a supplied appShellRef stays live (app-switch updates
|
|
222
|
+
// it), the standalone fallback is a frozen ref over the mount-time props.
|
|
223
|
+
const fallbackShellRef = useRef<AppShellRef | null>(null);
|
|
224
|
+
if (!fallbackShellRef.current) {
|
|
225
|
+
fallbackShellRef.current = createAppShellRef({ basename, version });
|
|
226
|
+
}
|
|
227
|
+
const shellRef = appShellRef ?? fallbackShellRef.current;
|
|
228
|
+
|
|
224
229
|
const contextValue = useMemo<NavigationStoreContextValue>(() => {
|
|
225
|
-
|
|
226
|
-
const value = {
|
|
227
|
-
store,
|
|
228
|
-
eventController,
|
|
229
|
-
navigate,
|
|
230
|
-
refresh,
|
|
231
|
-
} as NavigationStoreContextValue;
|
|
232
|
-
Object.defineProperty(value, "basename", {
|
|
233
|
-
configurable: true,
|
|
234
|
-
enumerable: true,
|
|
235
|
-
get: () => appShellRef.get().basename,
|
|
236
|
-
});
|
|
237
|
-
Object.defineProperty(value, "version", {
|
|
238
|
-
configurable: true,
|
|
239
|
-
enumerable: true,
|
|
240
|
-
get: () => appShellRef.get().version,
|
|
241
|
-
});
|
|
242
|
-
return value;
|
|
243
|
-
}
|
|
244
|
-
return {
|
|
230
|
+
const value = {
|
|
245
231
|
store,
|
|
246
232
|
eventController,
|
|
247
233
|
navigate,
|
|
248
234
|
refresh,
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
235
|
+
} as NavigationStoreContextValue;
|
|
236
|
+
Object.defineProperty(value, "basename", {
|
|
237
|
+
configurable: true,
|
|
238
|
+
enumerable: true,
|
|
239
|
+
get: () => shellRef.get().basename,
|
|
240
|
+
});
|
|
241
|
+
Object.defineProperty(value, "version", {
|
|
242
|
+
configurable: true,
|
|
243
|
+
enumerable: true,
|
|
244
|
+
get: () => shellRef.get().version,
|
|
245
|
+
});
|
|
246
|
+
return value;
|
|
252
247
|
}, []);
|
|
253
248
|
|
|
254
249
|
// Connection warmup: keep TLS alive after idle periods.
|
|
@@ -410,21 +405,15 @@ export function NavigationProvider({
|
|
|
410
405
|
}).catch((err) =>
|
|
411
406
|
console.error("[NavigationProvider] Error consuming handles:", err),
|
|
412
407
|
);
|
|
413
|
-
} else if (update.metadata.cachedHandleData) {
|
|
414
|
-
// For back/forward navigation from cache, restore the cached handleData
|
|
415
|
-
// This restores breadcrumbs to the exact state they were when the page was cached
|
|
416
|
-
eventController.setHandleData(
|
|
417
|
-
update.metadata.cachedHandleData,
|
|
418
|
-
update.metadata.matched,
|
|
419
|
-
false, // full replace - restore entire cached state
|
|
420
|
-
);
|
|
421
408
|
} else if (update.metadata.matched) {
|
|
422
|
-
//
|
|
409
|
+
// cachedHandleData present -> full restore (back/forward); absent ->
|
|
410
|
+
// partial cleanup of segments no longer matched.
|
|
411
|
+
const cached = update.metadata.cachedHandleData;
|
|
423
412
|
eventController.setHandleData(
|
|
424
|
-
{},
|
|
413
|
+
cached ?? {},
|
|
425
414
|
update.metadata.matched,
|
|
426
|
-
|
|
427
|
-
update.metadata.resolvedIds,
|
|
415
|
+
cached === undefined,
|
|
416
|
+
cached === undefined ? update.metadata.resolvedIds : undefined,
|
|
428
417
|
);
|
|
429
418
|
}
|
|
430
419
|
});
|
|
@@ -20,6 +20,9 @@ export { useSegments, type SegmentsState } from "./use-segments.js";
|
|
|
20
20
|
// Handle data hook
|
|
21
21
|
export { useHandle } from "./use-handle.js";
|
|
22
22
|
|
|
23
|
+
// Mount-aware reverse hook
|
|
24
|
+
export { useReverse } from "./use-reverse.js";
|
|
25
|
+
|
|
23
26
|
// Client cache controls hook
|
|
24
27
|
export {
|
|
25
28
|
useClientCache,
|