@rangojs/router 0.0.0-experimental.124 → 0.0.0-experimental.126
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 +6 -4
- package/dist/bin/rango.js +3 -4
- package/dist/vite/index.js +315 -68
- package/package.json +19 -18
- package/skills/breadcrumbs/SKILL.md +60 -0
- package/skills/hooks/SKILL.md +2 -2
- package/skills/route/SKILL.md +6 -0
- package/skills/server-actions/SKILL.md +25 -1
- package/skills/testing/SKILL.md +17 -17
- package/skills/testing/cache-prerender.md +29 -3
- package/skills/testing/flight.md +13 -10
- package/skills/testing/render-handler.md +3 -0
- package/skills/testing/server-tree.md +1 -1
- package/skills/testing/setup.md +1 -1
- package/src/__internal.ts +0 -65
- package/src/browser/action-coordinator.ts +1 -1
- package/src/browser/action-fence.ts +10 -0
- package/src/browser/event-controller.ts +1 -83
- package/src/browser/navigation-store-handle.ts +3 -4
- package/src/browser/navigation-store.ts +0 -39
- package/src/browser/navigation-transaction.ts +0 -32
- package/src/browser/partial-update.ts +23 -84
- package/src/browser/prefetch/cache.ts +6 -45
- package/src/browser/prefetch/queue.ts +6 -3
- package/src/browser/rango-state.ts +2 -23
- package/src/browser/react/Link.tsx +0 -2
- package/src/browser/react/NavigationProvider.tsx +2 -1
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/filter-segment-order.ts +0 -2
- package/src/browser/react/index.ts +0 -45
- package/src/browser/react/location-state-shared.ts +0 -13
- package/src/browser/react/location-state.ts +0 -1
- package/src/browser/react/use-action.ts +6 -15
- package/src/browser/react/use-handle.ts +0 -5
- package/src/browser/react/use-link-status.ts +0 -4
- package/src/browser/react/use-navigation.ts +0 -3
- package/src/browser/react/use-params.ts +0 -2
- package/src/browser/react/use-router.ts +2 -1
- package/src/browser/react/use-search-params.ts +0 -5
- package/src/browser/react/use-segments.ts +0 -13
- package/src/browser/rsc-router.tsx +10 -3
- package/src/browser/server-action-bridge.ts +51 -3
- package/src/browser/types.ts +23 -5
- package/src/browser/validate-redirect-origin.ts +43 -16
- package/src/build/index.ts +8 -9
- package/src/build/route-trie.ts +46 -11
- package/src/build/route-types/param-extraction.ts +6 -3
- package/src/build/route-types/router-processing.ts +0 -8
- package/src/cache/cache-policy.ts +0 -54
- package/src/cache/cache-runtime.ts +48 -24
- package/src/cache/cache-scope.ts +0 -27
- package/src/cache/cache-tag.ts +0 -37
- package/src/cache/cf/cf-cache-store.ts +72 -45
- package/src/cache/cf/index.ts +0 -24
- package/src/cache/document-cache.ts +10 -36
- package/src/cache/handle-snapshot.ts +0 -40
- package/src/cache/index.ts +0 -27
- package/src/cache/memory-segment-store.ts +0 -52
- package/src/cache/profile-registry.ts +6 -30
- package/src/cache/read-through-swr.ts +41 -11
- package/src/cache/segment-codec.ts +0 -16
- package/src/cache/types.ts +0 -98
- package/src/client.rsc.tsx +4 -22
- package/src/client.tsx +19 -32
- package/src/context-var.ts +12 -0
- package/src/defer.ts +196 -0
- package/src/deps/ssr.ts +0 -1
- package/src/handle.ts +2 -12
- package/src/handles/MetaTags.tsx +0 -14
- package/src/handles/breadcrumbs.ts +16 -5
- package/src/handles/meta.ts +0 -39
- package/src/host/cookie-handler.ts +0 -36
- package/src/host/errors.ts +0 -24
- package/src/host/index.ts +6 -0
- package/src/host/pattern-matcher.ts +7 -50
- package/src/host/router.ts +1 -65
- package/src/host/testing.ts +0 -16
- package/src/host/types.ts +6 -2
- package/src/href-client.ts +0 -4
- package/src/index.rsc.ts +27 -2
- package/src/index.ts +7 -0
- package/src/internal-debug.ts +2 -4
- package/src/loader.rsc.ts +4 -15
- package/src/loader.ts +3 -9
- package/src/network-error-thrower.tsx +1 -6
- package/src/outlet-provider.tsx +1 -5
- package/src/prerender/param-hash.ts +10 -11
- package/src/prerender/store.ts +23 -30
- package/src/prerender.ts +34 -0
- package/src/redirect-origin.ts +100 -0
- package/src/root-error-boundary.tsx +1 -19
- package/src/route-content-wrapper.tsx +1 -44
- package/src/route-definition/dsl-helpers.ts +7 -19
- package/src/route-definition/helpers-types.ts +3 -3
- package/src/route-definition/redirect.ts +43 -9
- package/src/route-definition/resolve-handler-use.ts +6 -0
- package/src/route-map-builder.ts +0 -16
- package/src/router/content-negotiation.ts +0 -13
- package/src/router/error-handling.ts +12 -16
- package/src/router/find-match.ts +4 -31
- package/src/router/intercept-resolution.ts +10 -1
- package/src/router/lazy-includes.ts +1 -57
- package/src/router/loader-resolution.ts +25 -23
- package/src/router/logging.ts +0 -6
- package/src/router/manifest.ts +1 -25
- package/src/router/match-api.ts +0 -20
- package/src/router/match-context.ts +0 -22
- package/src/router/match-handlers.ts +0 -43
- package/src/router/match-middleware/background-revalidation.ts +0 -7
- package/src/router/match-middleware/cache-lookup.ts +96 -179
- package/src/router/match-middleware/cache-store.ts +0 -31
- package/src/router/match-middleware/intercept-resolution.ts +0 -22
- package/src/router/match-middleware/segment-resolution.ts +0 -22
- package/src/router/match-pipelines.ts +1 -42
- package/src/router/match-result.ts +1 -52
- package/src/router/metrics.ts +0 -34
- package/src/router/middleware-types.ts +0 -116
- package/src/router/middleware.ts +77 -60
- package/src/router/navigation-snapshot.ts +0 -51
- package/src/router/params-util.ts +23 -0
- package/src/router/pattern-matching.ts +5 -56
- package/src/router/prerender-match.ts +56 -51
- package/src/router/request-classification.ts +1 -38
- package/src/router/revalidation.ts +14 -62
- package/src/router/route-snapshot.ts +0 -1
- package/src/router/router-context.ts +0 -27
- package/src/router/router-interfaces.ts +10 -0
- package/src/router/segment-resolution/fresh.ts +25 -57
- package/src/router/segment-resolution/helpers.ts +34 -0
- package/src/router/segment-resolution/loader-cache.ts +35 -23
- package/src/router/segment-resolution/revalidation.ts +188 -283
- package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
- package/src/router/segment-resolution.ts +4 -1
- package/src/router/segment-wrappers.ts +0 -3
- package/src/router/telemetry-otel.ts +0 -20
- package/src/router/telemetry.ts +0 -22
- package/src/router/timeout.ts +0 -20
- package/src/router/trie-matching.ts +66 -45
- package/src/router/types.ts +1 -63
- package/src/router/url-params.ts +0 -5
- package/src/router.ts +8 -11
- package/src/rsc/handler-context.ts +1 -0
- package/src/rsc/handler.ts +20 -4
- package/src/rsc/helpers.ts +71 -3
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/origin-guard.ts +9 -15
- package/src/rsc/progressive-enhancement.ts +10 -1
- package/src/rsc/redirect-guard.ts +99 -0
- package/src/rsc/response-route-handler.ts +23 -18
- package/src/rsc/rsc-rendering.ts +2 -7
- package/src/rsc/runtime-warnings.ts +14 -0
- package/src/rsc/server-action.ts +34 -29
- package/src/rsc/types.ts +6 -3
- package/src/search-params.ts +0 -16
- package/src/segment-loader-promise.ts +14 -2
- package/src/segment-system.tsx +79 -88
- package/src/server/handle-store.ts +7 -24
- package/src/server/loader-registry.ts +5 -24
- package/src/server/request-context.ts +29 -92
- package/src/ssr/index.tsx +14 -14
- package/src/static-handler.ts +2 -27
- package/src/testing/cache-status.ts +44 -48
- package/src/testing/collect-handle.ts +1 -24
- package/src/testing/dispatch.ts +43 -6
- package/src/testing/e2e/index.ts +1 -22
- package/src/testing/e2e/matchers.ts +0 -16
- package/src/testing/flight-matchers.ts +0 -13
- package/src/testing/flight-normalize.ts +3 -30
- package/src/testing/flight.ts +46 -48
- package/src/testing/generated-routes.ts +1 -41
- package/src/testing/index.ts +1 -21
- package/src/testing/internal/context.ts +3 -45
- package/src/testing/internal/seed-vars.ts +0 -26
- package/src/testing/render-handler.ts +31 -61
- package/src/testing/render-route.tsx +75 -103
- package/src/testing/run-loader.ts +0 -96
- package/src/testing/run-middleware.ts +0 -26
- package/src/theme/ThemeProvider.tsx +0 -52
- package/src/theme/ThemeScript.tsx +0 -6
- package/src/theme/constants.ts +0 -12
- package/src/theme/index.ts +0 -7
- package/src/theme/theme-context.ts +1 -5
- package/src/theme/theme-script.ts +0 -14
- package/src/theme/use-theme.ts +0 -3
- package/src/types/boundaries.ts +0 -35
- package/src/types/error-types.ts +25 -89
- package/src/types/global-namespace.ts +4 -14
- package/src/types/handler-context.ts +28 -9
- package/src/types/index.ts +0 -10
- package/src/types/request-scope.ts +0 -19
- package/src/types/route-config.ts +6 -50
- package/src/types/route-entry.ts +0 -6
- package/src/types/segments.ts +0 -13
- package/src/urls/include-helper.ts +0 -4
- package/src/urls/index.ts +0 -6
- package/src/urls/path-helper-types.ts +2 -2
- package/src/urls/path-helper.ts +0 -54
- package/src/urls/urls-function.ts +0 -13
- package/src/use-loader.tsx +0 -186
- package/src/vite/discovery/bundle-postprocess.ts +2 -1
- package/src/vite/discovery/discover-routers.ts +28 -18
- package/src/vite/discovery/prerender-collection.ts +2 -4
- package/src/vite/discovery/state.ts +5 -0
- package/src/vite/discovery/virtual-module-codegen.ts +1 -11
- package/src/vite/plugin-types.ts +35 -9
- package/src/vite/plugins/cjs-to-esm.ts +0 -11
- package/src/vite/plugins/client-ref-dedup.ts +0 -11
- package/src/vite/plugins/client-ref-hashing.ts +0 -10
- package/src/vite/plugins/cloudflare-protocol-stub.ts +0 -20
- package/src/vite/plugins/expose-action-id.ts +2 -73
- package/src/vite/plugins/expose-id-utils.ts +0 -55
- package/src/vite/plugins/expose-ids/export-analysis.ts +0 -38
- package/src/vite/plugins/expose-ids/handler-transform.ts +0 -15
- package/src/vite/plugins/expose-ids/loader-transform.ts +0 -15
- package/src/vite/plugins/expose-ids/router-transform.ts +0 -13
- package/src/vite/plugins/expose-internal-ids.ts +10 -0
- package/src/vite/plugins/performance-tracks.ts +0 -3
- package/src/vite/plugins/refresh-cmd.ts +1 -1
- package/src/vite/plugins/use-cache-transform.ts +21 -46
- package/src/vite/plugins/version-injector.ts +0 -20
- package/src/vite/plugins/version-plugin.ts +1 -49
- package/src/vite/plugins/virtual-entries.ts +0 -15
- package/src/vite/rango.ts +2 -108
- package/src/vite/router-discovery.ts +9 -1
- package/src/vite/utils/ast-handler-extract.ts +0 -16
- package/src/vite/utils/bundle-analysis.ts +6 -13
- package/src/vite/utils/client-chunks.ts +0 -6
- package/src/vite/utils/forward-user-plugins.ts +0 -22
- package/src/vite/utils/manifest-utils.ts +0 -4
- package/src/vite/utils/package-resolution.ts +1 -73
- package/src/vite/utils/prerender-utils.ts +0 -35
- package/src/vite/utils/shared-utils.ts +3 -35
- package/src/browser/shallow.ts +0 -40
- package/src/handles/index.ts +0 -7
- package/src/router/middleware-cookies.ts +0 -55
|
@@ -31,6 +31,12 @@
|
|
|
31
31
|
*
|
|
32
32
|
* Replaces the previous browser HTTP cache approach which was unreliable
|
|
33
33
|
* due to response draining race conditions and browser inconsistencies.
|
|
34
|
+
*
|
|
35
|
+
* State here lives in module-level singletons (cache, inflight, generation,
|
|
36
|
+
* cacheTTL, etc.) rather than a per-instance factory. This is correct because
|
|
37
|
+
* exactly one router is live per document — an SPA navigation crossing a
|
|
38
|
+
* host-router boundary forces a full document reload — so the singletons are
|
|
39
|
+
* effectively per-document. Unit tests reset them via clearPrefetchCache().
|
|
34
40
|
*/
|
|
35
41
|
|
|
36
42
|
import { abortAllPrefetches } from "./queue.js";
|
|
@@ -61,9 +67,6 @@ export interface DecodedPrefetch {
|
|
|
61
67
|
scope: "source" | "wildcard";
|
|
62
68
|
}
|
|
63
69
|
|
|
64
|
-
// Default TTL: 5 minutes. Overridden by initPrefetchCache() with
|
|
65
|
-
// the server-configured prefetchCacheTTL from router options.
|
|
66
|
-
// 0 disables the in-memory cache entirely.
|
|
67
70
|
let cacheTTL = 300_000;
|
|
68
71
|
|
|
69
72
|
/**
|
|
@@ -92,41 +95,12 @@ interface PrefetchCacheEntry {
|
|
|
92
95
|
const cache = new Map<string, PrefetchCacheEntry>();
|
|
93
96
|
const inflight = new Set<string>();
|
|
94
97
|
|
|
95
|
-
/**
|
|
96
|
-
* In-flight promise map. When a prefetch fetch+decode is in progress, its
|
|
97
|
-
* Promise<DecodedPrefetch | null> is stored here so navigation can await it
|
|
98
|
-
* instead of starting a duplicate request. Resolves to null when the prefetch
|
|
99
|
-
* failed, was aborted, or carried a control header (reload/redirect) that the
|
|
100
|
-
* navigation must re-fetch to act on.
|
|
101
|
-
*/
|
|
102
98
|
const inflightPromises = new Map<string, Promise<DecodedPrefetch | null>>();
|
|
103
99
|
|
|
104
|
-
/**
|
|
105
|
-
* Alias map for in-flight promises registered under multiple keys (see
|
|
106
|
-
* dual inflight in prefetch/fetch.ts). Records each key's sibling set so
|
|
107
|
-
* that consuming or clearing any one key atomically removes every alias —
|
|
108
|
-
* guaranteeing a single consumer for the shared decode.
|
|
109
|
-
*/
|
|
110
100
|
const inflightAliases = new Map<string, string[]>();
|
|
111
101
|
|
|
112
|
-
/**
|
|
113
|
-
* Keys whose in-flight prefetch promise was adopted by a navigation (via
|
|
114
|
-
* `consumeInflightPrefetch`). A `DecodedPrefetch` carries a single-use
|
|
115
|
-
* `metadata.handles` async generator; the adopter drains it. The same entry is
|
|
116
|
-
* also published to the `cache` map by `storePrefetch` when the fetch resolves
|
|
117
|
-
* — which runs AFTER adoption (adoption only succeeds while the fetch is still
|
|
118
|
-
* in flight, so the entry is not yet cached). Without this guard the adopted,
|
|
119
|
-
* now-drained entry would be left in the cache and served to a later navigation
|
|
120
|
-
* whose handle generator yields nothing, silently dropping that route's
|
|
121
|
-
* breadcrumbs. Recording the adopted keys lets `storePrefetch` skip publishing
|
|
122
|
-
* them, keeping the existing one-time-consumption contract (a consumed prefetch
|
|
123
|
-
* is gone; the next navigation re-fetches).
|
|
124
|
-
*/
|
|
125
102
|
const adoptedKeys = new Set<string>();
|
|
126
103
|
|
|
127
|
-
// Generation counter incremented on each clearPrefetchCache(). Fetches that
|
|
128
|
-
// started before a clear carry a stale generation and must not store their
|
|
129
|
-
// response (the data may be stale due to a server action invalidation).
|
|
130
104
|
let generation = 0;
|
|
131
105
|
|
|
132
106
|
/**
|
|
@@ -306,9 +280,6 @@ export function markPrefetchInflight(key: string): void {
|
|
|
306
280
|
inflight.add(key);
|
|
307
281
|
}
|
|
308
282
|
|
|
309
|
-
/**
|
|
310
|
-
* Store the in-flight Promise for a prefetch so navigation can reuse it.
|
|
311
|
-
*/
|
|
312
283
|
export function setInflightPromise(
|
|
313
284
|
key: string,
|
|
314
285
|
promise: Promise<DecodedPrefetch | null>,
|
|
@@ -337,20 +308,10 @@ export function clearPrefetchInflight(key: string): void {
|
|
|
337
308
|
inflight.delete(k);
|
|
338
309
|
inflightPromises.delete(k);
|
|
339
310
|
inflightAliases.delete(k);
|
|
340
|
-
// Clear any adopted marker too, so a fetch that failed before storePrefetch
|
|
341
|
-
// (the marker's normal consumer) does not strand it across the next prefetch.
|
|
342
311
|
adoptedKeys.delete(k);
|
|
343
312
|
});
|
|
344
313
|
}
|
|
345
314
|
|
|
346
|
-
/**
|
|
347
|
-
* Invalidate all prefetch state. Called when server actions mutate data.
|
|
348
|
-
* Clears the in-memory cache, cancels in-flight prefetches, and rotates
|
|
349
|
-
* the Rango state key so CDN-cached responses are also invalidated.
|
|
350
|
-
*
|
|
351
|
-
* Uses abortAllPrefetches (hard cancel) because in-flight responses
|
|
352
|
-
* may contain stale data after a mutation.
|
|
353
|
-
*/
|
|
354
315
|
export function clearPrefetchCache(): void {
|
|
355
316
|
generation++;
|
|
356
317
|
inflight.clear();
|
|
@@ -71,10 +71,13 @@ function scheduleDrain(): void {
|
|
|
71
71
|
Promise.race([waitForViewportImages(), wait(IMAGE_WAIT_TIMEOUT)]),
|
|
72
72
|
)
|
|
73
73
|
.then(() => {
|
|
74
|
-
|
|
75
|
-
//
|
|
76
|
-
//
|
|
74
|
+
// Stale drain: a cancel/abort happened while we were waiting, and a fresh
|
|
75
|
+
// scheduleDrain may already own drainScheduled for the new generation.
|
|
76
|
+
// Bail WITHOUT clearing the flag so we don't clobber the live wait's
|
|
77
|
+
// single-in-flight-drain coalescing (clearing it here would let the next
|
|
78
|
+
// enqueue start a third overlapping wait).
|
|
77
79
|
if (gen !== drainGeneration) return;
|
|
80
|
+
drainScheduled = false;
|
|
78
81
|
if (queue.length > 0) drain();
|
|
79
82
|
});
|
|
80
83
|
}
|
|
@@ -32,23 +32,13 @@ import {
|
|
|
32
32
|
serializeStateCookie,
|
|
33
33
|
} from "./cookie-name.js";
|
|
34
34
|
|
|
35
|
-
// The resolved cookie name this document is bound to (server-resolved, read
|
|
36
|
-
// from payload metadata at boot). Bare default until initRangoState runs.
|
|
37
35
|
let cookieName: string = DEFAULT_STATE_COOKIE_PREFIX;
|
|
38
36
|
|
|
39
|
-
// Build version for this document, used as the prefix of minted values.
|
|
40
37
|
let currentVersion = "0";
|
|
41
38
|
|
|
42
|
-
// Write-through mirror of the value. Authoritative only when the cookie is
|
|
43
|
-
// unreadable. `cookieBacked` records whether the mirror was last confirmed
|
|
44
|
-
// present in the jar, so a present->absent transition (an external clear) is
|
|
45
|
-
// detected exactly once instead of re-firing on every subsequent read.
|
|
46
39
|
let mirror: string | null = null;
|
|
47
40
|
let cookieBacked = false;
|
|
48
41
|
|
|
49
|
-
// External-rotation observer, registered by the store-handle wiring (so a
|
|
50
|
-
// sibling tab's rotation or a server Set-Cookie marks the history cache stale).
|
|
51
|
-
// Null until registered; self-rotations never call it.
|
|
52
42
|
let externalRotationObserver: ((value: string) => void) | null = null;
|
|
53
43
|
|
|
54
44
|
/**
|
|
@@ -81,7 +71,6 @@ function readCookie(name: string): CookieRead {
|
|
|
81
71
|
} catch {
|
|
82
72
|
return { readable: false, value: null };
|
|
83
73
|
}
|
|
84
|
-
// Shared parser with the server seat so both read the same jar entry.
|
|
85
74
|
return { readable: true, value: getRawCookieValue(raw, name) };
|
|
86
75
|
}
|
|
87
76
|
|
|
@@ -91,14 +80,9 @@ function writeCookie(name: string, value: string): void {
|
|
|
91
80
|
typeof location !== "undefined" && location.protocol === "https:";
|
|
92
81
|
try {
|
|
93
82
|
document.cookie = serializeStateCookie(name, value, secure);
|
|
94
|
-
} catch {
|
|
95
|
-
// Write failures are silently absorbed; the mirror carries the value.
|
|
96
|
-
}
|
|
83
|
+
} catch {}
|
|
97
84
|
}
|
|
98
85
|
|
|
99
|
-
// Mint a fresh value: same version, a timestamp strictly greater than the
|
|
100
|
-
// current one (the in-memory mirror is the previous value). The monotonic guard
|
|
101
|
-
// lives in mintStateValue, shared with the server seat.
|
|
102
86
|
function mintValue(): string {
|
|
103
87
|
return mintStateValue(currentVersion, mirror);
|
|
104
88
|
}
|
|
@@ -195,9 +179,6 @@ export function invalidateRangoState(): void {
|
|
|
195
179
|
writeCookie(cookieName, mirror);
|
|
196
180
|
}
|
|
197
181
|
|
|
198
|
-
// One-time migration: remove the legacy localStorage keys this mechanism used
|
|
199
|
-
// before the cookie cutover. No value porting — a fresh cookie mint just misses
|
|
200
|
-
// cleanly. Idempotent: scans for `rango-state` and `rango-state:{routerId}`.
|
|
201
182
|
function cleanupLegacyStorage(): void {
|
|
202
183
|
if (typeof localStorage === "undefined") return;
|
|
203
184
|
try {
|
|
@@ -209,7 +190,5 @@ function cleanupLegacyStorage(): void {
|
|
|
209
190
|
}
|
|
210
191
|
}
|
|
211
192
|
for (const key of toRemove) localStorage.removeItem(key);
|
|
212
|
-
} catch {
|
|
213
|
-
// localStorage unavailable; nothing to clean.
|
|
214
|
-
}
|
|
193
|
+
} catch {}
|
|
215
194
|
}
|
|
@@ -39,8 +39,6 @@ import {
|
|
|
39
39
|
unobserveForPrefetch,
|
|
40
40
|
} from "../prefetch/observer.js";
|
|
41
41
|
|
|
42
|
-
// Touch device detection for adaptive strategy.
|
|
43
|
-
// Checked once at module load (Link.tsx is "use client", runs only in browser).
|
|
44
42
|
const isTouchDevice =
|
|
45
43
|
typeof window !== "undefined" && window.matchMedia("(hover: none)").matches;
|
|
46
44
|
|
|
@@ -29,6 +29,7 @@ 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
31
|
import { createAppShellRef, type AppShellRef } from "../app-shell.js";
|
|
32
|
+
import { debugLog } from "../logging.js";
|
|
32
33
|
|
|
33
34
|
/**
|
|
34
35
|
* Process handles from an async generator, updating the event controller
|
|
@@ -70,7 +71,7 @@ async function processHandles(
|
|
|
70
71
|
// This prevents handle data from cancelled navigations polluting
|
|
71
72
|
// the current route's breadcrumbs (e.g., quick popstate after clicking a link).
|
|
72
73
|
if (historyKey !== store.getHistoryKey()) {
|
|
73
|
-
|
|
74
|
+
debugLog(
|
|
74
75
|
"[NavigationProvider] Stopping handle processing - user navigated away",
|
|
75
76
|
);
|
|
76
77
|
return;
|
|
@@ -14,17 +14,21 @@ export interface ScrollRestorationProps {
|
|
|
14
14
|
* Return location.pathname to restore scroll based on path
|
|
15
15
|
* (useful for keeping scroll position on the same page).
|
|
16
16
|
*
|
|
17
|
+
* Provide a stable reference: a module-level function or one wrapped in
|
|
18
|
+
* useCallback. The init effect re-runs when getKey's identity changes, and
|
|
19
|
+
* teardown clears in-memory scroll positions — a fresh inline arrow on every
|
|
20
|
+
* parent render would discard unpersisted positions mid-session.
|
|
21
|
+
*
|
|
17
22
|
* @example
|
|
18
23
|
* ```tsx
|
|
24
|
+
* // Stable module-level getKey (recommended)
|
|
25
|
+
* const byPathname = (location) => location.pathname;
|
|
26
|
+
*
|
|
19
27
|
* // Restore based on pathname (same URL = same scroll)
|
|
20
|
-
* <ScrollRestoration
|
|
21
|
-
* getKey={(location) => location.pathname}
|
|
22
|
-
* />
|
|
28
|
+
* <ScrollRestoration getKey={byPathname} />
|
|
23
29
|
*
|
|
24
30
|
* // Restore based on unique history entry (default)
|
|
25
|
-
* <ScrollRestoration
|
|
26
|
-
* getKey={(location) => location.key}
|
|
27
|
-
* />
|
|
31
|
+
* // <ScrollRestoration /> — omit getKey to use location.key
|
|
28
32
|
* ```
|
|
29
33
|
*/
|
|
30
34
|
getKey?: (location: {
|
|
@@ -46,8 +46,6 @@ export function filterSegmentOrder(matched: string[]): string[] {
|
|
|
46
46
|
const slots = slotsByParent.get(id);
|
|
47
47
|
if (slots) result.push(...slots);
|
|
48
48
|
}
|
|
49
|
-
// Defensive: any slot whose parent is missing from the filtered list still
|
|
50
|
-
// gets included rather than silently dropped. Shouldn't happen in practice.
|
|
51
49
|
for (const [parent, slots] of slotsByParent) {
|
|
52
50
|
if (!nonSlotSet.has(parent)) result.push(...slots);
|
|
53
51
|
}
|
|
@@ -1,49 +1,4 @@
|
|
|
1
|
-
// React exports for browser navigation
|
|
2
|
-
|
|
3
|
-
// Hook with Zustand-style selectors
|
|
4
|
-
export { useNavigation } from "./use-navigation.js";
|
|
5
|
-
|
|
6
|
-
// Router actions hook (stable reference, no re-renders)
|
|
7
|
-
export { useRouter } from "./use-router.js";
|
|
8
|
-
|
|
9
|
-
// URL hooks
|
|
10
|
-
export { usePathname } from "./use-pathname.js";
|
|
11
|
-
export { useSearchParams } from "./use-search-params.js";
|
|
12
|
-
export { useParams } from "./use-params.js";
|
|
13
|
-
|
|
14
|
-
// Action state tracking hook
|
|
15
|
-
export { useAction, type TrackedActionState } from "./use-action.js";
|
|
16
|
-
|
|
17
|
-
// Segments state hook
|
|
18
|
-
export { useSegments, type SegmentsState } from "./use-segments.js";
|
|
19
|
-
|
|
20
|
-
// Handle data hook
|
|
21
|
-
export { useHandle } from "./use-handle.js";
|
|
22
|
-
|
|
23
|
-
// Mount-aware reverse hook
|
|
24
|
-
export { useReverse } from "./use-reverse.js";
|
|
25
|
-
|
|
26
|
-
// Provider
|
|
27
1
|
export {
|
|
28
2
|
NavigationProvider,
|
|
29
3
|
type NavigationProviderProps,
|
|
30
4
|
} from "./NavigationProvider.js";
|
|
31
|
-
|
|
32
|
-
// Context (for advanced usage)
|
|
33
|
-
export {
|
|
34
|
-
NavigationStoreContext,
|
|
35
|
-
type NavigationStoreContextValue,
|
|
36
|
-
} from "./context.js";
|
|
37
|
-
|
|
38
|
-
// Link component
|
|
39
|
-
export { Link, type LinkProps, type PrefetchStrategy } from "./Link.js";
|
|
40
|
-
|
|
41
|
-
// Link status hook
|
|
42
|
-
export { useLinkStatus, type LinkStatus } from "./use-link-status.js";
|
|
43
|
-
|
|
44
|
-
// Scroll restoration
|
|
45
|
-
export {
|
|
46
|
-
ScrollRestoration,
|
|
47
|
-
useScrollRestoration,
|
|
48
|
-
type ScrollRestorationProps,
|
|
49
|
-
} from "./ScrollRestoration.js";
|
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared location state utilities - works in both RSC and client contexts
|
|
3
|
-
* No "use client" directive so it can be imported from RSC
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
1
|
import type { ReactElement } from "react";
|
|
7
2
|
|
|
8
3
|
/**
|
|
@@ -26,16 +21,8 @@ export interface LocationStateOptions {
|
|
|
26
21
|
|
|
27
22
|
type LocationStateUnsafeFn = (...args: never[]) => unknown;
|
|
28
23
|
|
|
29
|
-
// Broadest constructor signature (`abstract` covers both abstract and concrete
|
|
30
|
-
// classes). A class passed as state has a `new` signature, not a call signature,
|
|
31
|
-
// so it slips past LocationStateUnsafeFn; at runtime the lazy-getter path
|
|
32
|
-
// (`typeof value === "function"`) then mistakes it for a getter and throws.
|
|
33
24
|
type LocationStateUnsafeCtor = abstract new (...args: never[]) => unknown;
|
|
34
25
|
|
|
35
|
-
// `unknown` cannot be verified serializable, so it is rejected (callers must
|
|
36
|
-
// supply a concrete type). `any` deliberately defeats type checking and is NOT
|
|
37
|
-
// guardable — it is assignable to the branded error too, so the check always
|
|
38
|
-
// passes; it remains an explicit escape hatch.
|
|
39
26
|
type IsAny<T> = 0 extends 1 & T ? true : false;
|
|
40
27
|
type IsUnknown<T> =
|
|
41
28
|
IsAny<T> extends true ? false : unknown extends T ? true : false;
|
|
@@ -24,32 +24,24 @@ const DEFAULT_ACTION_STATE: TrackedActionState = {
|
|
|
24
24
|
result: null,
|
|
25
25
|
};
|
|
26
26
|
|
|
27
|
-
/**
|
|
28
|
-
* Normalize action ID - returns the ID as-is
|
|
29
|
-
*
|
|
30
|
-
* Server actions have IDs like "hash#actionName" or "src/actions.ts#actionName".
|
|
31
|
-
* When using function references, we use the full ID for exact matching.
|
|
32
|
-
* When using strings, the event controller supports suffix matching
|
|
33
|
-
* (e.g., "addToCart" matches "hash#addToCart").
|
|
34
|
-
*/
|
|
35
|
-
function normalizeActionId(actionId: string): string {
|
|
36
|
-
return actionId;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
27
|
/**
|
|
40
28
|
* Extract action ID from a server action function or string.
|
|
41
29
|
*
|
|
42
30
|
* Actions passed as props from server components lose their metadata
|
|
43
31
|
* during RSC serialization - use a string action name instead.
|
|
32
|
+
*
|
|
33
|
+
* The extracted $$id (e.g. "hash#actionName" or "src/actions.ts#actionName")
|
|
34
|
+
* is returned as-is. Suffix-vs-exact matching against this ID happens
|
|
35
|
+
* downstream in the event controller, not here.
|
|
44
36
|
*/
|
|
45
|
-
|
|
37
|
+
function getActionId(action: ServerActionFunction | string): string {
|
|
46
38
|
invariant(
|
|
47
39
|
typeof action === "function" || typeof action === "string",
|
|
48
40
|
`useAction: action must be a function or string, got ${typeof action}`,
|
|
49
41
|
);
|
|
50
42
|
const actionId = (action as any)?.$$id;
|
|
51
43
|
if (actionId) {
|
|
52
|
-
return
|
|
44
|
+
return actionId;
|
|
53
45
|
}
|
|
54
46
|
|
|
55
47
|
// If action is a string, use it directly
|
|
@@ -162,7 +154,6 @@ export function useAction<T>(
|
|
|
162
154
|
});
|
|
163
155
|
const prevSelected = useRef(baseState);
|
|
164
156
|
prevSelected.current = baseState;
|
|
165
|
-
// useOptimistic allows immediate updates during transitions/actions
|
|
166
157
|
const [optimisticState, setOptimisticState] = useOptimistic<
|
|
167
158
|
T | TrackedActionState
|
|
168
159
|
>(null!);
|
|
@@ -43,7 +43,6 @@ export function useHandle<T, A, S>(
|
|
|
43
43
|
): Rango.FlightSerialize<A> | S {
|
|
44
44
|
const ctx = useContext(NavigationStoreContext);
|
|
45
45
|
|
|
46
|
-
// Initial state from context event controller, or empty fallback without provider.
|
|
47
46
|
const [value, setValue] = useState<Rango.FlightSerialize<A> | S>(() => {
|
|
48
47
|
if (!ctx) {
|
|
49
48
|
const collected = collectHandleData(
|
|
@@ -54,7 +53,6 @@ export function useHandle<T, A, S>(
|
|
|
54
53
|
return selector ? selector(collected) : collected;
|
|
55
54
|
}
|
|
56
55
|
|
|
57
|
-
// On client, use event controller state
|
|
58
56
|
const state = ctx.eventController.getHandleState();
|
|
59
57
|
const collected = collectHandleData(
|
|
60
58
|
handle,
|
|
@@ -65,15 +63,12 @@ export function useHandle<T, A, S>(
|
|
|
65
63
|
});
|
|
66
64
|
const [optimisticValue, setOptimisticValue] = useOptimistic(value);
|
|
67
65
|
|
|
68
|
-
// Track previous value for shallow comparison
|
|
69
66
|
const prevValueRef = useRef(value);
|
|
70
67
|
prevValueRef.current = value;
|
|
71
68
|
|
|
72
|
-
// Ref keeps the latest selector without re-subscribing on every render.
|
|
73
69
|
const selectorRef = useRef(selector);
|
|
74
70
|
selectorRef.current = selector;
|
|
75
71
|
|
|
76
|
-
// Subscribe to handle data changes (client only)
|
|
77
72
|
useEffect(() => {
|
|
78
73
|
if (!ctx) return;
|
|
79
74
|
|
|
@@ -82,11 +82,9 @@ export function useLinkStatus(): LinkStatus {
|
|
|
82
82
|
const linkTo = useContext(LinkContext);
|
|
83
83
|
const ctx = useContext(NavigationStoreContext);
|
|
84
84
|
|
|
85
|
-
// Get origin for URL normalization (stable across renders)
|
|
86
85
|
const origin =
|
|
87
86
|
typeof window !== "undefined" ? window.location.origin : "http://localhost";
|
|
88
87
|
|
|
89
|
-
// Base state for useOptimistic
|
|
90
88
|
const [basePending, setBasePending] = useState<boolean>(() => {
|
|
91
89
|
if (!ctx || linkTo === null) {
|
|
92
90
|
return false;
|
|
@@ -97,7 +95,6 @@ export function useLinkStatus(): LinkStatus {
|
|
|
97
95
|
|
|
98
96
|
const prevPending = useRef(basePending);
|
|
99
97
|
|
|
100
|
-
// useOptimistic allows immediate updates during transitions
|
|
101
98
|
const [pending, setOptimisticPending] = useOptimistic(basePending);
|
|
102
99
|
|
|
103
100
|
useEffect(() => {
|
|
@@ -105,7 +102,6 @@ export function useLinkStatus(): LinkStatus {
|
|
|
105
102
|
return;
|
|
106
103
|
}
|
|
107
104
|
|
|
108
|
-
// Subscribe to navigation state changes
|
|
109
105
|
return ctx.eventController.subscribe(() => {
|
|
110
106
|
const state = ctx.eventController.getState();
|
|
111
107
|
const isPending = isPendingFor(linkTo, state.pendingUrl, origin);
|
|
@@ -46,7 +46,6 @@ export function useNavigation<T>(
|
|
|
46
46
|
throw new Error("useNavigation must be used within NavigationProvider");
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
// Base state for useOptimistic
|
|
50
49
|
const [baseValue, setBaseValue] = useState<T | PublicNavigationState>(() => {
|
|
51
50
|
const publicState = toPublicState(ctx.eventController.getState());
|
|
52
51
|
return selector ? selector(publicState) : publicState;
|
|
@@ -59,7 +58,6 @@ export function useNavigation<T>(
|
|
|
59
58
|
// parent transition (e.g. <Link> click) is still pending.
|
|
60
59
|
const optimisticPinnedRef = useRef(false);
|
|
61
60
|
|
|
62
|
-
// useOptimistic allows immediate updates during transitions/actions
|
|
63
61
|
const [value, setOptimisticValue] = useOptimistic(baseValue);
|
|
64
62
|
|
|
65
63
|
// Store selector in a ref so the subscription callback always uses the
|
|
@@ -72,7 +70,6 @@ export function useNavigation<T>(
|
|
|
72
70
|
|
|
73
71
|
// Subscribe to event controller state changes (only runs on client)
|
|
74
72
|
useEffect(() => {
|
|
75
|
-
// Subscribe to updates from event controller
|
|
76
73
|
return ctx.eventController.subscribe(() => {
|
|
77
74
|
const currentState = ctx.eventController.getState();
|
|
78
75
|
const publicState = toPublicState(currentState);
|
|
@@ -50,8 +50,6 @@ export function useParams<T>(
|
|
|
50
50
|
});
|
|
51
51
|
|
|
52
52
|
const prevValue = useRef(value);
|
|
53
|
-
// Ref keeps the latest selector without re-subscribing. Event-driven by
|
|
54
|
-
// design: value updates on store events, not on selector identity change.
|
|
55
53
|
const selectorRef = useRef(selector);
|
|
56
54
|
selectorRef.current = selector;
|
|
57
55
|
|
|
@@ -60,7 +60,7 @@ export function useRouter(): RouterInstance {
|
|
|
60
60
|
return ctx.refresh();
|
|
61
61
|
},
|
|
62
62
|
|
|
63
|
-
prefetch(url: string): void {
|
|
63
|
+
prefetch(url: string, options?: { key?: ":source" }): void {
|
|
64
64
|
const segmentState = ctx.store?.getSegmentState();
|
|
65
65
|
if (segmentState) {
|
|
66
66
|
prefetchDirect(
|
|
@@ -68,6 +68,7 @@ export function useRouter(): RouterInstance {
|
|
|
68
68
|
segmentState.currentSegmentIds,
|
|
69
69
|
getAppVersion(),
|
|
70
70
|
ctx.store?.getRouterId?.(),
|
|
71
|
+
options?.key,
|
|
71
72
|
);
|
|
72
73
|
}
|
|
73
74
|
},
|
|
@@ -24,9 +24,6 @@ import type { ReadonlyURLSearchParams } from "../types.js";
|
|
|
24
24
|
export function useSearchParams(): ReadonlyURLSearchParams {
|
|
25
25
|
const ctx = useContext(NavigationStoreContext);
|
|
26
26
|
|
|
27
|
-
// Always initialize with empty URLSearchParams to match SSR output
|
|
28
|
-
// and avoid hydration mismatch. The useEffect below syncs from
|
|
29
|
-
// the real URL after hydration.
|
|
30
27
|
const [searchParams, setSearchParams] = useState<ReadonlyURLSearchParams>(
|
|
31
28
|
() => new URLSearchParams(),
|
|
32
29
|
);
|
|
@@ -41,12 +38,10 @@ export function useSearchParams(): ReadonlyURLSearchParams {
|
|
|
41
38
|
const nextSearch = location.searchParams.toString();
|
|
42
39
|
if (nextSearch !== prevSearch.current) {
|
|
43
40
|
prevSearch.current = nextSearch;
|
|
44
|
-
// Create a snapshot so callers cannot mutate the source URLSearchParams
|
|
45
41
|
setSearchParams(new URLSearchParams(nextSearch));
|
|
46
42
|
}
|
|
47
43
|
};
|
|
48
44
|
|
|
49
|
-
// Sync on mount (picks up search params from browser URL)
|
|
50
45
|
update();
|
|
51
46
|
|
|
52
47
|
return ctx.eventController.subscribe(update);
|
|
@@ -86,31 +86,20 @@ export function useSegments<T>(
|
|
|
86
86
|
const selectorRef = useRef(selector);
|
|
87
87
|
selectorRef.current = selector;
|
|
88
88
|
|
|
89
|
-
// Track selector identity to detect when the selector function changes.
|
|
90
|
-
// Only then do we eagerly recompute during render to avoid staleness.
|
|
91
|
-
// Without this guard, no-selector mode causes infinite re-renders because
|
|
92
|
-
// buildSegmentsState creates fresh arrays that fail Object.is checks.
|
|
93
89
|
const prevSelectorIdentity = useRef(selector);
|
|
94
90
|
|
|
95
|
-
// Cache SegmentsState to stabilize nested references (path, segmentIds
|
|
96
|
-
// arrays) so selectors returning composite values don't cause spurious
|
|
97
|
-
// render-time setState calls.
|
|
98
91
|
const segmentsCache = useRef<{
|
|
99
92
|
location: URL;
|
|
100
93
|
routeSegmentIds: string[];
|
|
101
94
|
state: SegmentsState;
|
|
102
95
|
} | null>(null);
|
|
103
96
|
|
|
104
|
-
// Recompute selected value from current store state and apply selector.
|
|
105
|
-
// Shared by the render-time eager check and the subscription callback.
|
|
106
97
|
function recompute(
|
|
107
98
|
sel: ((state: SegmentsState) => T) | undefined,
|
|
108
99
|
): T | SegmentsState {
|
|
109
100
|
const location = ctx!.eventController.getLocation();
|
|
110
101
|
const handleState = ctx!.eventController.getHandleState();
|
|
111
102
|
|
|
112
|
-
// Reuse cached state when inputs haven't changed by reference,
|
|
113
|
-
// keeping array/object references stable for composite selectors.
|
|
114
103
|
const cache = segmentsCache.current;
|
|
115
104
|
let segmentsState: SegmentsState;
|
|
116
105
|
if (
|
|
@@ -165,8 +154,6 @@ export function useSegments<T>(
|
|
|
165
154
|
unsubscribeNav();
|
|
166
155
|
unsubscribeHandles();
|
|
167
156
|
};
|
|
168
|
-
// Stable subscription: selector changes are handled via selectorRef,
|
|
169
|
-
// state comparison uses prevState ref. No re-subscribe needed.
|
|
170
157
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
171
158
|
}, []);
|
|
172
159
|
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
generateHistoryKey,
|
|
9
9
|
} from "./navigation-store.js";
|
|
10
10
|
import { createEventController } from "./event-controller.js";
|
|
11
|
+
import { validateRedirectOrigin } from "./validate-redirect-origin.js";
|
|
11
12
|
import { createNavigationClient } from "./navigation-client.js";
|
|
12
13
|
import { createServerActionBridge } from "./server-action-bridge.js";
|
|
13
14
|
import { createNavigationBridge } from "./navigation-bridge.js";
|
|
@@ -178,8 +179,8 @@ export async function initBrowserApp(
|
|
|
178
179
|
|
|
179
180
|
// Register the active store on the module-level handle and wire the
|
|
180
181
|
// jar-divergence observer before any getRangoState() read can detect a
|
|
181
|
-
// cross-tab/server rotation.
|
|
182
|
-
//
|
|
182
|
+
// cross-tab/server rotation. There is no global store singleton, so this
|
|
183
|
+
// handle is the live reference.
|
|
183
184
|
registerNavigationStore(store);
|
|
184
185
|
|
|
185
186
|
// Seed router identity from the initial SSR payload so the first
|
|
@@ -280,7 +281,13 @@ export async function initBrowserApp(
|
|
|
280
281
|
renderSegments,
|
|
281
282
|
onNavigate: (url, options) => {
|
|
282
283
|
if (!navigateFn) {
|
|
283
|
-
|
|
284
|
+
// Navigation bridge not wired yet: hard-navigate, but re-validate
|
|
285
|
+
// same-origin defensively so this init-window fallback cannot become an
|
|
286
|
+
// open redirect (the normal path validates inside the navigation bridge).
|
|
287
|
+
const safe = validateRedirectOrigin(url, window.location.origin);
|
|
288
|
+
if (safe) {
|
|
289
|
+
window.location.href = safe;
|
|
290
|
+
}
|
|
284
291
|
return Promise.resolve();
|
|
285
292
|
}
|
|
286
293
|
return navigateFn(url, options);
|