tanstack-router-cache 0.1.7 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/cached-outlet.cjs +12 -0
- package/dist/components/cached-outlet.js +12 -0
- package/dist/components/off-screen-in.cjs +130 -0
- package/dist/components/off-screen-in.js +130 -0
- package/dist/components/off-screen.cjs +8 -0
- package/dist/components/off-screen.js +8 -0
- package/dist/components/restore-cached-href.cjs +28 -0
- package/dist/components/restore-cached-href.js +28 -0
- package/dist/components/route-cache-manager.cjs +485 -0
- package/dist/components/route-cache-manager.js +485 -0
- package/dist/components/router-cache-outlet.cjs +9 -0
- package/dist/components/router-cache-outlet.js +9 -0
- package/dist/contexts/router-cache.cjs +237 -0
- package/dist/contexts/router-cache.d.ts +40 -0
- package/dist/contexts/router-cache.js +235 -0
- package/dist/dom/dismiss-transient-ui.cjs +230 -0
- package/dist/dom/dismiss-transient-ui.js +228 -0
- package/dist/hooks/use-event-listener.cjs +76 -0
- package/dist/hooks/use-event-listener.js +76 -0
- package/dist/hooks/use-route-cache-active.cjs +19 -0
- package/dist/hooks/use-route-cache-active.js +19 -0
- package/dist/hooks/use-route-cache-activity.cjs +12 -0
- package/dist/hooks/use-route-cache-activity.js +12 -0
- package/dist/hooks/use-route-cache-effect.cjs +38 -0
- package/dist/hooks/use-route-cache-effect.js +38 -0
- package/dist/hooks/use-route-cache-error-boundary.cjs +23 -0
- package/dist/hooks/use-route-cache-error-boundary.js +23 -0
- package/dist/hooks/use-route-cache-navigation.cjs +36 -0
- package/dist/hooks/use-route-cache-navigation.js +36 -0
- package/dist/hooks/use-router-cache-debug.cjs +85 -0
- package/dist/hooks/use-router-cache-debug.js +85 -0
- package/dist/hooks/use-router-cache.cjs +32 -0
- package/dist/hooks/use-router-cache.js +32 -0
- package/dist/hooks/use-update.cjs +8 -0
- package/dist/hooks/use-update.js +8 -0
- package/dist/index.cjs +18 -1402
- package/dist/index.d.ts +9 -1
- package/dist/index.js +10 -1395
- package/dist/pathname.cjs +8 -0
- package/dist/pathname.js +8 -0
- package/dist/route-cache-static-data.cjs +43 -0
- package/dist/route-cache-static-data.d.ts +25 -0
- package/dist/route-cache-static-data.js +41 -0
- package/dist/types.d.ts +50 -0
- package/docs/architecture.md +8 -5
- package/docs/cache-behavior.md +17 -0
- package/docs/components.md +1 -2
- package/docs/getting-started.md +38 -3
- package/docs/types.md +12 -0
- package/package.json +2 -2
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
const require_pathname = require("../pathname.cjs");
|
|
2
|
+
const require_route_cache_static_data = require("../route-cache-static-data.cjs");
|
|
3
|
+
let react = require("react");
|
|
4
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
5
|
+
//#region src/contexts/router-cache.tsx
|
|
6
|
+
const DEFAULT_MAX_ENTRIES = Number.POSITIVE_INFINITY;
|
|
7
|
+
const EMPTY_CACHED_ROUTES = {};
|
|
8
|
+
const RouterCacheContext = (0, react.createContext)(null);
|
|
9
|
+
function shallowEqualRouteStaticData(current, next) {
|
|
10
|
+
if (current === next) return true;
|
|
11
|
+
const currentEntries = Object.entries({ ...current });
|
|
12
|
+
const nextEntries = Object.entries({ ...next });
|
|
13
|
+
if (currentEntries.length !== nextEntries.length) return false;
|
|
14
|
+
const nextValuesByKey = new Map(nextEntries);
|
|
15
|
+
for (const [key, value] of currentEntries) if (!Object.is(value, nextValuesByKey.get(key))) return false;
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
function filterRouterCacheRoutes(routes) {
|
|
19
|
+
const now = Date.now();
|
|
20
|
+
return Object.fromEntries(Object.entries(routes).flatMap(([pathname, route]) => require_route_cache_static_data.isRouteCacheEnabled(route.staticData) && !require_route_cache_static_data.isCachedRouteStale(route, now) ? [[require_pathname.normalizeCachedRoutePathname(pathname), route]] : []));
|
|
21
|
+
}
|
|
22
|
+
function pruneStaleCachedRoutes(routes) {
|
|
23
|
+
const now = Date.now();
|
|
24
|
+
const nextRoutes = {};
|
|
25
|
+
let changed = false;
|
|
26
|
+
for (const [pathname, route] of Object.entries(routes)) {
|
|
27
|
+
if (require_route_cache_static_data.isCachedRouteStale(route, now)) {
|
|
28
|
+
changed = true;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
nextRoutes[pathname] = route;
|
|
32
|
+
}
|
|
33
|
+
return changed ? nextRoutes : routes;
|
|
34
|
+
}
|
|
35
|
+
function isSameCachedRouteData(current, next) {
|
|
36
|
+
if (!current) return false;
|
|
37
|
+
return current.routeId === next.routeId && current.href === next.href && current.matchId === next.matchId && current.ready === next.ready && current.routerSnapshot === next.routerSnapshot && shallowEqualRouteStaticData(current.staticData, next.staticData);
|
|
38
|
+
}
|
|
39
|
+
function normalizeLimit(limit) {
|
|
40
|
+
if (typeof limit !== "number" || Number.isNaN(limit)) return DEFAULT_MAX_ENTRIES;
|
|
41
|
+
if (!Number.isFinite(limit)) return DEFAULT_MAX_ENTRIES;
|
|
42
|
+
return Math.max(Math.trunc(limit), 0);
|
|
43
|
+
}
|
|
44
|
+
function getCachedRouteTimestamp(route) {
|
|
45
|
+
return route.lastVisibleAt ?? route.createdAt ?? 0;
|
|
46
|
+
}
|
|
47
|
+
function sortCachedRouteEntries([leftPathname, leftRoute], [rightPathname, rightRoute]) {
|
|
48
|
+
const timestampDifference = getCachedRouteTimestamp(leftRoute) - getCachedRouteTimestamp(rightRoute);
|
|
49
|
+
if (timestampDifference !== 0) return timestampDifference;
|
|
50
|
+
return leftPathname.localeCompare(rightPathname);
|
|
51
|
+
}
|
|
52
|
+
function getUnmarkedCachedRouteEntries(routes, keysToDelete) {
|
|
53
|
+
return Object.entries(routes).filter(([pathname]) => !keysToDelete.has(pathname));
|
|
54
|
+
}
|
|
55
|
+
function getEntriesByRouteId(entries) {
|
|
56
|
+
const entriesByRouteId = /* @__PURE__ */ new Map();
|
|
57
|
+
for (const entry of entries) {
|
|
58
|
+
const routeId = entry[1].routeId;
|
|
59
|
+
if (!routeId) continue;
|
|
60
|
+
const existingEntries = entriesByRouteId.get(routeId);
|
|
61
|
+
if (existingEntries) {
|
|
62
|
+
existingEntries.push(entry);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
entriesByRouteId.set(routeId, [entry]);
|
|
66
|
+
}
|
|
67
|
+
return entriesByRouteId;
|
|
68
|
+
}
|
|
69
|
+
function markEntriesForDeletion(entries, keysToDelete, protectedKeys, excessEntryCount) {
|
|
70
|
+
if (excessEntryCount <= 0) return;
|
|
71
|
+
const evictableEntries = entries.filter(([pathname]) => !protectedKeys.has(pathname)).sort(sortCachedRouteEntries);
|
|
72
|
+
let remainingExcess = excessEntryCount;
|
|
73
|
+
for (const [pathname] of evictableEntries) {
|
|
74
|
+
if (remainingExcess <= 0) break;
|
|
75
|
+
if (!keysToDelete.has(pathname)) {
|
|
76
|
+
keysToDelete.add(pathname);
|
|
77
|
+
remainingExcess -= 1;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function applyPerRouteEntryLimit(routes, maxEntriesPerRouteId, keysToDelete, protectedKeys) {
|
|
82
|
+
if (maxEntriesPerRouteId === DEFAULT_MAX_ENTRIES) return;
|
|
83
|
+
const entriesByRouteId = getEntriesByRouteId(getUnmarkedCachedRouteEntries(routes, keysToDelete));
|
|
84
|
+
for (const entries of entriesByRouteId.values()) markEntriesForDeletion(entries, keysToDelete, protectedKeys, entries.length - maxEntriesPerRouteId);
|
|
85
|
+
}
|
|
86
|
+
function applyGlobalEntryLimit(routes, maxEntries, keysToDelete, protectedKeys) {
|
|
87
|
+
if (maxEntries === DEFAULT_MAX_ENTRIES) return;
|
|
88
|
+
const remainingEntries = getUnmarkedCachedRouteEntries(routes, keysToDelete);
|
|
89
|
+
markEntriesForDeletion(remainingEntries, keysToDelete, protectedKeys, remainingEntries.length - maxEntries);
|
|
90
|
+
}
|
|
91
|
+
function applyCachedRouteLimits(routes, config, protectedKeys) {
|
|
92
|
+
if (config.maxEntries === DEFAULT_MAX_ENTRIES && config.maxEntriesPerRouteId === DEFAULT_MAX_ENTRIES) return routes;
|
|
93
|
+
const keysToDelete = /* @__PURE__ */ new Set();
|
|
94
|
+
applyPerRouteEntryLimit(routes, config.maxEntriesPerRouteId, keysToDelete, protectedKeys);
|
|
95
|
+
applyGlobalEntryLimit(routes, config.maxEntries, keysToDelete, protectedKeys);
|
|
96
|
+
if (keysToDelete.size === 0) return routes;
|
|
97
|
+
const nextRoutes = { ...routes };
|
|
98
|
+
for (const pathname of keysToDelete) delete nextRoutes[pathname];
|
|
99
|
+
return nextRoutes;
|
|
100
|
+
}
|
|
101
|
+
function createCachedRouteData(current, next) {
|
|
102
|
+
const now = Date.now();
|
|
103
|
+
return {
|
|
104
|
+
...next,
|
|
105
|
+
createdAt: current?.createdAt ?? next.createdAt ?? now,
|
|
106
|
+
lastVisibleAt: next.lastVisibleAt ?? now
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
function getNextCachedRoutesState(params) {
|
|
110
|
+
const normalizedKey = require_pathname.normalizeCachedRoutePathname(params.key);
|
|
111
|
+
const state = pruneStaleCachedRoutes(params.state);
|
|
112
|
+
if (params.cacheConfig.maxEntries === 0) return state === EMPTY_CACHED_ROUTES ? state : EMPTY_CACHED_ROUTES;
|
|
113
|
+
if (!require_route_cache_static_data.isRouteCacheEnabled(params.value.staticData)) {
|
|
114
|
+
if (!Object.hasOwn(state, normalizedKey)) return state;
|
|
115
|
+
const nextState = { ...state };
|
|
116
|
+
delete nextState[normalizedKey];
|
|
117
|
+
return nextState;
|
|
118
|
+
}
|
|
119
|
+
const nextRouteData = createCachedRouteData(state[normalizedKey], params.value);
|
|
120
|
+
if (isSameCachedRouteData(state[normalizedKey], nextRouteData)) return state;
|
|
121
|
+
return applyCachedRouteLimits({
|
|
122
|
+
...state,
|
|
123
|
+
[normalizedKey]: nextRouteData
|
|
124
|
+
}, params.cacheConfig, /* @__PURE__ */ new Set([normalizedKey]));
|
|
125
|
+
}
|
|
126
|
+
function RouterCacheProvider({ cacheScopeKey = "__default__", children, defaultCachedRoutes = EMPTY_CACHED_ROUTES, maxEntries = DEFAULT_MAX_ENTRIES, maxEntriesPerRouteId = DEFAULT_MAX_ENTRIES }) {
|
|
127
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RouterCacheProviderScope, {
|
|
128
|
+
defaultCachedRoutes,
|
|
129
|
+
maxEntries,
|
|
130
|
+
maxEntriesPerRouteId,
|
|
131
|
+
children
|
|
132
|
+
}, cacheScopeKey ?? "__default__");
|
|
133
|
+
}
|
|
134
|
+
function RouterCacheProviderScope({ children, defaultCachedRoutes = EMPTY_CACHED_ROUTES, maxEntries, maxEntriesPerRouteId }) {
|
|
135
|
+
const cacheConfigRef = (0, react.useRef)({
|
|
136
|
+
maxEntries: normalizeLimit(maxEntries),
|
|
137
|
+
maxEntriesPerRouteId: normalizeLimit(maxEntriesPerRouteId)
|
|
138
|
+
});
|
|
139
|
+
const initialCachedRoutesRef = (0, react.useRef)(null);
|
|
140
|
+
if (initialCachedRoutesRef.current === null) initialCachedRoutesRef.current = applyCachedRouteLimits(filterRouterCacheRoutes(defaultCachedRoutes), cacheConfigRef.current, /* @__PURE__ */ new Set());
|
|
141
|
+
const [cachedRoutes, setCachedRoutes] = (0, react.useState)(() => initialCachedRoutesRef.current ?? EMPTY_CACHED_ROUTES);
|
|
142
|
+
const [erroredRouteCounts, setErroredRouteCounts] = (0, react.useState)({});
|
|
143
|
+
const updateCachedRoutes = (key, value) => {
|
|
144
|
+
setCachedRoutes((state) => getNextCachedRoutesState({
|
|
145
|
+
cacheConfig: cacheConfigRef.current,
|
|
146
|
+
key,
|
|
147
|
+
state,
|
|
148
|
+
value
|
|
149
|
+
}));
|
|
150
|
+
};
|
|
151
|
+
const deleteCachedRoutes = (keys) => {
|
|
152
|
+
setCachedRoutes((state) => {
|
|
153
|
+
let changed = false;
|
|
154
|
+
const newState = { ...state };
|
|
155
|
+
for (const key of keys) {
|
|
156
|
+
const normalizedKey = require_pathname.normalizeCachedRoutePathname(key);
|
|
157
|
+
if (Object.hasOwn(newState, normalizedKey)) {
|
|
158
|
+
delete newState[normalizedKey];
|
|
159
|
+
changed = true;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return changed ? newState : state;
|
|
163
|
+
});
|
|
164
|
+
};
|
|
165
|
+
const touchCachedRoutes = (keys) => {
|
|
166
|
+
setCachedRoutes((state) => {
|
|
167
|
+
let changed = false;
|
|
168
|
+
const touchedAt = Date.now();
|
|
169
|
+
const nextState = { ...state };
|
|
170
|
+
for (const key of keys) {
|
|
171
|
+
const normalizedKey = require_pathname.normalizeCachedRoutePathname(key);
|
|
172
|
+
const route = nextState[normalizedKey];
|
|
173
|
+
if (!route || route.lastVisibleAt === touchedAt) continue;
|
|
174
|
+
nextState[normalizedKey] = {
|
|
175
|
+
...route,
|
|
176
|
+
lastVisibleAt: touchedAt
|
|
177
|
+
};
|
|
178
|
+
changed = true;
|
|
179
|
+
}
|
|
180
|
+
return changed ? nextState : state;
|
|
181
|
+
});
|
|
182
|
+
};
|
|
183
|
+
const retainErroredRoute = (pathname) => {
|
|
184
|
+
const normalizedPathname = require_pathname.normalizeCachedRoutePathname(pathname);
|
|
185
|
+
setErroredRouteCounts((state) => ({
|
|
186
|
+
...state,
|
|
187
|
+
[normalizedPathname]: (state[normalizedPathname] ?? 0) + 1
|
|
188
|
+
}));
|
|
189
|
+
setCachedRoutes((state) => {
|
|
190
|
+
if (!Object.hasOwn(state, normalizedPathname)) return state;
|
|
191
|
+
const nextState = { ...state };
|
|
192
|
+
delete nextState[normalizedPathname];
|
|
193
|
+
return nextState;
|
|
194
|
+
});
|
|
195
|
+
};
|
|
196
|
+
const releaseErroredRoute = (pathname) => {
|
|
197
|
+
const normalizedPathname = require_pathname.normalizeCachedRoutePathname(pathname);
|
|
198
|
+
setErroredRouteCounts((state) => {
|
|
199
|
+
const currentCount = state[normalizedPathname] ?? 0;
|
|
200
|
+
if (currentCount <= 1) {
|
|
201
|
+
if (!Object.hasOwn(state, normalizedPathname)) return state;
|
|
202
|
+
const nextState = { ...state };
|
|
203
|
+
delete nextState[normalizedPathname];
|
|
204
|
+
return nextState;
|
|
205
|
+
}
|
|
206
|
+
return {
|
|
207
|
+
...state,
|
|
208
|
+
[normalizedPathname]: currentCount - 1
|
|
209
|
+
};
|
|
210
|
+
});
|
|
211
|
+
};
|
|
212
|
+
const contextValue = {
|
|
213
|
+
cachedRoutes,
|
|
214
|
+
erroredRouteCounts,
|
|
215
|
+
setCachedRoutes: updateCachedRoutes,
|
|
216
|
+
deleteCachedRoutes,
|
|
217
|
+
touchCachedRoutes,
|
|
218
|
+
retainErroredRoute,
|
|
219
|
+
releaseErroredRoute
|
|
220
|
+
};
|
|
221
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RouterCacheContext.Provider, {
|
|
222
|
+
value: contextValue,
|
|
223
|
+
children
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
function useRouterCacheContext() {
|
|
227
|
+
const context = (0, react.use)(RouterCacheContext);
|
|
228
|
+
if (!context) throw new Error("RouterCacheContext is not available");
|
|
229
|
+
return context;
|
|
230
|
+
}
|
|
231
|
+
function useOptionalRouterCacheContext() {
|
|
232
|
+
return (0, react.use)(RouterCacheContext);
|
|
233
|
+
}
|
|
234
|
+
//#endregion
|
|
235
|
+
exports.RouterCacheProvider = RouterCacheProvider;
|
|
236
|
+
exports.useOptionalRouterCacheContext = useOptionalRouterCacheContext;
|
|
237
|
+
exports.useRouterCacheContext = useRouterCacheContext;
|
|
@@ -1,23 +1,63 @@
|
|
|
1
1
|
import type { RouterContextProvider, StaticDataRouteOption } from "@tanstack/react-router";
|
|
2
2
|
import type { ComponentProps, ReactNode } from "react";
|
|
3
3
|
type RouterCacheProviderProps = {
|
|
4
|
+
/**
|
|
5
|
+
* Clears all cached routes when the key changes.
|
|
6
|
+
*
|
|
7
|
+
* Use this for user, tenant, workspace, locale, or environment boundaries.
|
|
8
|
+
*
|
|
9
|
+
* @defaultValue `"__default__"`
|
|
10
|
+
*/
|
|
4
11
|
cacheScopeKey?: string | number | null;
|
|
12
|
+
/** Outlet and surrounding UI that can access the route cache. */
|
|
5
13
|
children: ReactNode;
|
|
14
|
+
/**
|
|
15
|
+
* Initial cached route entries.
|
|
16
|
+
*
|
|
17
|
+
* Most apps do not need this. Entries without enabled `staticData.routeCache`
|
|
18
|
+
* or entries older than their `maxAge` are ignored.
|
|
19
|
+
*
|
|
20
|
+
* @defaultValue `{}`
|
|
21
|
+
*/
|
|
6
22
|
defaultCachedRoutes?: CachedRoutes;
|
|
23
|
+
/**
|
|
24
|
+
* Maximum cached route entries across this provider.
|
|
25
|
+
*
|
|
26
|
+
* Set to `0` to disable caching.
|
|
27
|
+
*
|
|
28
|
+
* @defaultValue `Infinity`
|
|
29
|
+
*/
|
|
7
30
|
maxEntries?: number;
|
|
31
|
+
/**
|
|
32
|
+
* Maximum cached entries for the same TanStack route id.
|
|
33
|
+
*
|
|
34
|
+
* Useful for dynamic routes where each pathname can create a separate cached
|
|
35
|
+
* view.
|
|
36
|
+
*
|
|
37
|
+
* @defaultValue `Infinity`
|
|
38
|
+
*/
|
|
8
39
|
maxEntriesPerRouteId?: number;
|
|
9
40
|
};
|
|
10
41
|
type RouterSnapshot = ComponentProps<typeof RouterContextProvider>["router"];
|
|
11
42
|
export type CachedRouteData = {
|
|
43
|
+
/** First time this cache entry was stored, as `Date.now()`. */
|
|
12
44
|
createdAt?: number;
|
|
45
|
+
/** Full cached href, including search and hash when available. */
|
|
13
46
|
href?: string;
|
|
47
|
+
/** Last time this cached route became visible, as `Date.now()`. */
|
|
14
48
|
lastVisibleAt?: number;
|
|
49
|
+
/** TanStack Router route id used for per-route entry limits. */
|
|
15
50
|
routeId?: string;
|
|
51
|
+
/** Static data from the deepest cache-enabled route match. */
|
|
16
52
|
staticData: StaticDataRouteOption;
|
|
53
|
+
/** TanStack Router match id rendered by the cached outlet. */
|
|
17
54
|
matchId?: string;
|
|
55
|
+
/** Router snapshot used to keep the cached route tree isolated while hidden. */
|
|
18
56
|
routerSnapshot?: RouterSnapshot;
|
|
57
|
+
/** Whether the cached route has a complete snapshot and can be restored. */
|
|
19
58
|
ready?: boolean;
|
|
20
59
|
};
|
|
60
|
+
/** Cached route entries keyed by normalized pathname. */
|
|
21
61
|
export type CachedRoutes = {
|
|
22
62
|
[key: string]: CachedRouteData;
|
|
23
63
|
};
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { normalizeCachedRoutePathname } from "../pathname.js";
|
|
2
|
+
import { isCachedRouteStale, isRouteCacheEnabled } from "../route-cache-static-data.js";
|
|
3
|
+
import { createContext, use, useRef, useState } from "react";
|
|
4
|
+
import { jsx } from "react/jsx-runtime";
|
|
5
|
+
//#region src/contexts/router-cache.tsx
|
|
6
|
+
const DEFAULT_MAX_ENTRIES = Number.POSITIVE_INFINITY;
|
|
7
|
+
const EMPTY_CACHED_ROUTES = {};
|
|
8
|
+
const RouterCacheContext = createContext(null);
|
|
9
|
+
function shallowEqualRouteStaticData(current, next) {
|
|
10
|
+
if (current === next) return true;
|
|
11
|
+
const currentEntries = Object.entries({ ...current });
|
|
12
|
+
const nextEntries = Object.entries({ ...next });
|
|
13
|
+
if (currentEntries.length !== nextEntries.length) return false;
|
|
14
|
+
const nextValuesByKey = new Map(nextEntries);
|
|
15
|
+
for (const [key, value] of currentEntries) if (!Object.is(value, nextValuesByKey.get(key))) return false;
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
function filterRouterCacheRoutes(routes) {
|
|
19
|
+
const now = Date.now();
|
|
20
|
+
return Object.fromEntries(Object.entries(routes).flatMap(([pathname, route]) => isRouteCacheEnabled(route.staticData) && !isCachedRouteStale(route, now) ? [[normalizeCachedRoutePathname(pathname), route]] : []));
|
|
21
|
+
}
|
|
22
|
+
function pruneStaleCachedRoutes(routes) {
|
|
23
|
+
const now = Date.now();
|
|
24
|
+
const nextRoutes = {};
|
|
25
|
+
let changed = false;
|
|
26
|
+
for (const [pathname, route] of Object.entries(routes)) {
|
|
27
|
+
if (isCachedRouteStale(route, now)) {
|
|
28
|
+
changed = true;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
nextRoutes[pathname] = route;
|
|
32
|
+
}
|
|
33
|
+
return changed ? nextRoutes : routes;
|
|
34
|
+
}
|
|
35
|
+
function isSameCachedRouteData(current, next) {
|
|
36
|
+
if (!current) return false;
|
|
37
|
+
return current.routeId === next.routeId && current.href === next.href && current.matchId === next.matchId && current.ready === next.ready && current.routerSnapshot === next.routerSnapshot && shallowEqualRouteStaticData(current.staticData, next.staticData);
|
|
38
|
+
}
|
|
39
|
+
function normalizeLimit(limit) {
|
|
40
|
+
if (typeof limit !== "number" || Number.isNaN(limit)) return DEFAULT_MAX_ENTRIES;
|
|
41
|
+
if (!Number.isFinite(limit)) return DEFAULT_MAX_ENTRIES;
|
|
42
|
+
return Math.max(Math.trunc(limit), 0);
|
|
43
|
+
}
|
|
44
|
+
function getCachedRouteTimestamp(route) {
|
|
45
|
+
return route.lastVisibleAt ?? route.createdAt ?? 0;
|
|
46
|
+
}
|
|
47
|
+
function sortCachedRouteEntries([leftPathname, leftRoute], [rightPathname, rightRoute]) {
|
|
48
|
+
const timestampDifference = getCachedRouteTimestamp(leftRoute) - getCachedRouteTimestamp(rightRoute);
|
|
49
|
+
if (timestampDifference !== 0) return timestampDifference;
|
|
50
|
+
return leftPathname.localeCompare(rightPathname);
|
|
51
|
+
}
|
|
52
|
+
function getUnmarkedCachedRouteEntries(routes, keysToDelete) {
|
|
53
|
+
return Object.entries(routes).filter(([pathname]) => !keysToDelete.has(pathname));
|
|
54
|
+
}
|
|
55
|
+
function getEntriesByRouteId(entries) {
|
|
56
|
+
const entriesByRouteId = /* @__PURE__ */ new Map();
|
|
57
|
+
for (const entry of entries) {
|
|
58
|
+
const routeId = entry[1].routeId;
|
|
59
|
+
if (!routeId) continue;
|
|
60
|
+
const existingEntries = entriesByRouteId.get(routeId);
|
|
61
|
+
if (existingEntries) {
|
|
62
|
+
existingEntries.push(entry);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
entriesByRouteId.set(routeId, [entry]);
|
|
66
|
+
}
|
|
67
|
+
return entriesByRouteId;
|
|
68
|
+
}
|
|
69
|
+
function markEntriesForDeletion(entries, keysToDelete, protectedKeys, excessEntryCount) {
|
|
70
|
+
if (excessEntryCount <= 0) return;
|
|
71
|
+
const evictableEntries = entries.filter(([pathname]) => !protectedKeys.has(pathname)).sort(sortCachedRouteEntries);
|
|
72
|
+
let remainingExcess = excessEntryCount;
|
|
73
|
+
for (const [pathname] of evictableEntries) {
|
|
74
|
+
if (remainingExcess <= 0) break;
|
|
75
|
+
if (!keysToDelete.has(pathname)) {
|
|
76
|
+
keysToDelete.add(pathname);
|
|
77
|
+
remainingExcess -= 1;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function applyPerRouteEntryLimit(routes, maxEntriesPerRouteId, keysToDelete, protectedKeys) {
|
|
82
|
+
if (maxEntriesPerRouteId === DEFAULT_MAX_ENTRIES) return;
|
|
83
|
+
const entriesByRouteId = getEntriesByRouteId(getUnmarkedCachedRouteEntries(routes, keysToDelete));
|
|
84
|
+
for (const entries of entriesByRouteId.values()) markEntriesForDeletion(entries, keysToDelete, protectedKeys, entries.length - maxEntriesPerRouteId);
|
|
85
|
+
}
|
|
86
|
+
function applyGlobalEntryLimit(routes, maxEntries, keysToDelete, protectedKeys) {
|
|
87
|
+
if (maxEntries === DEFAULT_MAX_ENTRIES) return;
|
|
88
|
+
const remainingEntries = getUnmarkedCachedRouteEntries(routes, keysToDelete);
|
|
89
|
+
markEntriesForDeletion(remainingEntries, keysToDelete, protectedKeys, remainingEntries.length - maxEntries);
|
|
90
|
+
}
|
|
91
|
+
function applyCachedRouteLimits(routes, config, protectedKeys) {
|
|
92
|
+
if (config.maxEntries === DEFAULT_MAX_ENTRIES && config.maxEntriesPerRouteId === DEFAULT_MAX_ENTRIES) return routes;
|
|
93
|
+
const keysToDelete = /* @__PURE__ */ new Set();
|
|
94
|
+
applyPerRouteEntryLimit(routes, config.maxEntriesPerRouteId, keysToDelete, protectedKeys);
|
|
95
|
+
applyGlobalEntryLimit(routes, config.maxEntries, keysToDelete, protectedKeys);
|
|
96
|
+
if (keysToDelete.size === 0) return routes;
|
|
97
|
+
const nextRoutes = { ...routes };
|
|
98
|
+
for (const pathname of keysToDelete) delete nextRoutes[pathname];
|
|
99
|
+
return nextRoutes;
|
|
100
|
+
}
|
|
101
|
+
function createCachedRouteData(current, next) {
|
|
102
|
+
const now = Date.now();
|
|
103
|
+
return {
|
|
104
|
+
...next,
|
|
105
|
+
createdAt: current?.createdAt ?? next.createdAt ?? now,
|
|
106
|
+
lastVisibleAt: next.lastVisibleAt ?? now
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
function getNextCachedRoutesState(params) {
|
|
110
|
+
const normalizedKey = normalizeCachedRoutePathname(params.key);
|
|
111
|
+
const state = pruneStaleCachedRoutes(params.state);
|
|
112
|
+
if (params.cacheConfig.maxEntries === 0) return state === EMPTY_CACHED_ROUTES ? state : EMPTY_CACHED_ROUTES;
|
|
113
|
+
if (!isRouteCacheEnabled(params.value.staticData)) {
|
|
114
|
+
if (!Object.hasOwn(state, normalizedKey)) return state;
|
|
115
|
+
const nextState = { ...state };
|
|
116
|
+
delete nextState[normalizedKey];
|
|
117
|
+
return nextState;
|
|
118
|
+
}
|
|
119
|
+
const nextRouteData = createCachedRouteData(state[normalizedKey], params.value);
|
|
120
|
+
if (isSameCachedRouteData(state[normalizedKey], nextRouteData)) return state;
|
|
121
|
+
return applyCachedRouteLimits({
|
|
122
|
+
...state,
|
|
123
|
+
[normalizedKey]: nextRouteData
|
|
124
|
+
}, params.cacheConfig, /* @__PURE__ */ new Set([normalizedKey]));
|
|
125
|
+
}
|
|
126
|
+
function RouterCacheProvider({ cacheScopeKey = "__default__", children, defaultCachedRoutes = EMPTY_CACHED_ROUTES, maxEntries = DEFAULT_MAX_ENTRIES, maxEntriesPerRouteId = DEFAULT_MAX_ENTRIES }) {
|
|
127
|
+
return /* @__PURE__ */ jsx(RouterCacheProviderScope, {
|
|
128
|
+
defaultCachedRoutes,
|
|
129
|
+
maxEntries,
|
|
130
|
+
maxEntriesPerRouteId,
|
|
131
|
+
children
|
|
132
|
+
}, cacheScopeKey ?? "__default__");
|
|
133
|
+
}
|
|
134
|
+
function RouterCacheProviderScope({ children, defaultCachedRoutes = EMPTY_CACHED_ROUTES, maxEntries, maxEntriesPerRouteId }) {
|
|
135
|
+
const cacheConfigRef = useRef({
|
|
136
|
+
maxEntries: normalizeLimit(maxEntries),
|
|
137
|
+
maxEntriesPerRouteId: normalizeLimit(maxEntriesPerRouteId)
|
|
138
|
+
});
|
|
139
|
+
const initialCachedRoutesRef = useRef(null);
|
|
140
|
+
if (initialCachedRoutesRef.current === null) initialCachedRoutesRef.current = applyCachedRouteLimits(filterRouterCacheRoutes(defaultCachedRoutes), cacheConfigRef.current, /* @__PURE__ */ new Set());
|
|
141
|
+
const [cachedRoutes, setCachedRoutes] = useState(() => initialCachedRoutesRef.current ?? EMPTY_CACHED_ROUTES);
|
|
142
|
+
const [erroredRouteCounts, setErroredRouteCounts] = useState({});
|
|
143
|
+
const updateCachedRoutes = (key, value) => {
|
|
144
|
+
setCachedRoutes((state) => getNextCachedRoutesState({
|
|
145
|
+
cacheConfig: cacheConfigRef.current,
|
|
146
|
+
key,
|
|
147
|
+
state,
|
|
148
|
+
value
|
|
149
|
+
}));
|
|
150
|
+
};
|
|
151
|
+
const deleteCachedRoutes = (keys) => {
|
|
152
|
+
setCachedRoutes((state) => {
|
|
153
|
+
let changed = false;
|
|
154
|
+
const newState = { ...state };
|
|
155
|
+
for (const key of keys) {
|
|
156
|
+
const normalizedKey = normalizeCachedRoutePathname(key);
|
|
157
|
+
if (Object.hasOwn(newState, normalizedKey)) {
|
|
158
|
+
delete newState[normalizedKey];
|
|
159
|
+
changed = true;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return changed ? newState : state;
|
|
163
|
+
});
|
|
164
|
+
};
|
|
165
|
+
const touchCachedRoutes = (keys) => {
|
|
166
|
+
setCachedRoutes((state) => {
|
|
167
|
+
let changed = false;
|
|
168
|
+
const touchedAt = Date.now();
|
|
169
|
+
const nextState = { ...state };
|
|
170
|
+
for (const key of keys) {
|
|
171
|
+
const normalizedKey = normalizeCachedRoutePathname(key);
|
|
172
|
+
const route = nextState[normalizedKey];
|
|
173
|
+
if (!route || route.lastVisibleAt === touchedAt) continue;
|
|
174
|
+
nextState[normalizedKey] = {
|
|
175
|
+
...route,
|
|
176
|
+
lastVisibleAt: touchedAt
|
|
177
|
+
};
|
|
178
|
+
changed = true;
|
|
179
|
+
}
|
|
180
|
+
return changed ? nextState : state;
|
|
181
|
+
});
|
|
182
|
+
};
|
|
183
|
+
const retainErroredRoute = (pathname) => {
|
|
184
|
+
const normalizedPathname = normalizeCachedRoutePathname(pathname);
|
|
185
|
+
setErroredRouteCounts((state) => ({
|
|
186
|
+
...state,
|
|
187
|
+
[normalizedPathname]: (state[normalizedPathname] ?? 0) + 1
|
|
188
|
+
}));
|
|
189
|
+
setCachedRoutes((state) => {
|
|
190
|
+
if (!Object.hasOwn(state, normalizedPathname)) return state;
|
|
191
|
+
const nextState = { ...state };
|
|
192
|
+
delete nextState[normalizedPathname];
|
|
193
|
+
return nextState;
|
|
194
|
+
});
|
|
195
|
+
};
|
|
196
|
+
const releaseErroredRoute = (pathname) => {
|
|
197
|
+
const normalizedPathname = normalizeCachedRoutePathname(pathname);
|
|
198
|
+
setErroredRouteCounts((state) => {
|
|
199
|
+
const currentCount = state[normalizedPathname] ?? 0;
|
|
200
|
+
if (currentCount <= 1) {
|
|
201
|
+
if (!Object.hasOwn(state, normalizedPathname)) return state;
|
|
202
|
+
const nextState = { ...state };
|
|
203
|
+
delete nextState[normalizedPathname];
|
|
204
|
+
return nextState;
|
|
205
|
+
}
|
|
206
|
+
return {
|
|
207
|
+
...state,
|
|
208
|
+
[normalizedPathname]: currentCount - 1
|
|
209
|
+
};
|
|
210
|
+
});
|
|
211
|
+
};
|
|
212
|
+
const contextValue = {
|
|
213
|
+
cachedRoutes,
|
|
214
|
+
erroredRouteCounts,
|
|
215
|
+
setCachedRoutes: updateCachedRoutes,
|
|
216
|
+
deleteCachedRoutes,
|
|
217
|
+
touchCachedRoutes,
|
|
218
|
+
retainErroredRoute,
|
|
219
|
+
releaseErroredRoute
|
|
220
|
+
};
|
|
221
|
+
return /* @__PURE__ */ jsx(RouterCacheContext.Provider, {
|
|
222
|
+
value: contextValue,
|
|
223
|
+
children
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
function useRouterCacheContext() {
|
|
227
|
+
const context = use(RouterCacheContext);
|
|
228
|
+
if (!context) throw new Error("RouterCacheContext is not available");
|
|
229
|
+
return context;
|
|
230
|
+
}
|
|
231
|
+
function useOptionalRouterCacheContext() {
|
|
232
|
+
return use(RouterCacheContext);
|
|
233
|
+
}
|
|
234
|
+
//#endregion
|
|
235
|
+
export { RouterCacheProvider, useOptionalRouterCacheContext, useRouterCacheContext };
|