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.
Files changed (50) hide show
  1. package/dist/components/cached-outlet.cjs +12 -0
  2. package/dist/components/cached-outlet.js +12 -0
  3. package/dist/components/off-screen-in.cjs +130 -0
  4. package/dist/components/off-screen-in.js +130 -0
  5. package/dist/components/off-screen.cjs +8 -0
  6. package/dist/components/off-screen.js +8 -0
  7. package/dist/components/restore-cached-href.cjs +28 -0
  8. package/dist/components/restore-cached-href.js +28 -0
  9. package/dist/components/route-cache-manager.cjs +485 -0
  10. package/dist/components/route-cache-manager.js +485 -0
  11. package/dist/components/router-cache-outlet.cjs +9 -0
  12. package/dist/components/router-cache-outlet.js +9 -0
  13. package/dist/contexts/router-cache.cjs +237 -0
  14. package/dist/contexts/router-cache.d.ts +40 -0
  15. package/dist/contexts/router-cache.js +235 -0
  16. package/dist/dom/dismiss-transient-ui.cjs +230 -0
  17. package/dist/dom/dismiss-transient-ui.js +228 -0
  18. package/dist/hooks/use-event-listener.cjs +76 -0
  19. package/dist/hooks/use-event-listener.js +76 -0
  20. package/dist/hooks/use-route-cache-active.cjs +19 -0
  21. package/dist/hooks/use-route-cache-active.js +19 -0
  22. package/dist/hooks/use-route-cache-activity.cjs +12 -0
  23. package/dist/hooks/use-route-cache-activity.js +12 -0
  24. package/dist/hooks/use-route-cache-effect.cjs +38 -0
  25. package/dist/hooks/use-route-cache-effect.js +38 -0
  26. package/dist/hooks/use-route-cache-error-boundary.cjs +23 -0
  27. package/dist/hooks/use-route-cache-error-boundary.js +23 -0
  28. package/dist/hooks/use-route-cache-navigation.cjs +36 -0
  29. package/dist/hooks/use-route-cache-navigation.js +36 -0
  30. package/dist/hooks/use-router-cache-debug.cjs +85 -0
  31. package/dist/hooks/use-router-cache-debug.js +85 -0
  32. package/dist/hooks/use-router-cache.cjs +32 -0
  33. package/dist/hooks/use-router-cache.js +32 -0
  34. package/dist/hooks/use-update.cjs +8 -0
  35. package/dist/hooks/use-update.js +8 -0
  36. package/dist/index.cjs +18 -1402
  37. package/dist/index.d.ts +9 -1
  38. package/dist/index.js +10 -1395
  39. package/dist/pathname.cjs +8 -0
  40. package/dist/pathname.js +8 -0
  41. package/dist/route-cache-static-data.cjs +43 -0
  42. package/dist/route-cache-static-data.d.ts +25 -0
  43. package/dist/route-cache-static-data.js +41 -0
  44. package/dist/types.d.ts +50 -0
  45. package/docs/architecture.md +8 -5
  46. package/docs/cache-behavior.md +17 -0
  47. package/docs/components.md +1 -2
  48. package/docs/getting-started.md +38 -3
  49. package/docs/types.md +12 -0
  50. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -1,1395 +1,10 @@
1
- import { Match, Outlet, RouterContextProvider, useChildMatches, useLocation, useMatches, useRouter, useRouterState } from "@tanstack/react-router";
2
- import { Activity, createContext, use, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
3
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
4
- //#region src/pathname.ts
5
- const TRAILING_SLASHES_REGEX = /\/+$/u;
6
- function normalizeCachedRoutePathname(pathname) {
7
- if (pathname === "/") return pathname;
8
- return pathname.replace(TRAILING_SLASHES_REGEX, "");
9
- }
10
- //#endregion
11
- //#region src/contexts/router-cache.tsx
12
- const DEFAULT_MAX_ENTRIES = Number.POSITIVE_INFINITY;
13
- const EMPTY_CACHED_ROUTES = {};
14
- const RouterCacheContext = createContext(null);
15
- function shallowEqualRouteStaticData(current, next) {
16
- if (current === next) return true;
17
- const currentEntries = Object.entries({ ...current });
18
- const nextEntries = Object.entries({ ...next });
19
- if (currentEntries.length !== nextEntries.length) return false;
20
- const nextValuesByKey = new Map(nextEntries);
21
- for (const [key, value] of currentEntries) if (!Object.is(value, nextValuesByKey.get(key))) return false;
22
- return true;
23
- }
24
- function isCacheEnabledRouteData(route) {
25
- return route?.staticData.routeCache === true;
26
- }
27
- function filterRouterCacheRoutes(routes) {
28
- return Object.fromEntries(Object.entries(routes).flatMap(([pathname, route]) => isCacheEnabledRouteData(route) ? [[normalizeCachedRoutePathname(pathname), route]] : []));
29
- }
30
- function isSameCachedRouteData(current, next) {
31
- if (!current) return false;
32
- 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);
33
- }
34
- function normalizeLimit(limit) {
35
- if (typeof limit !== "number" || Number.isNaN(limit)) return DEFAULT_MAX_ENTRIES;
36
- if (!Number.isFinite(limit)) return DEFAULT_MAX_ENTRIES;
37
- return Math.max(Math.trunc(limit), 0);
38
- }
39
- function getCachedRouteTimestamp(route) {
40
- return route.lastVisibleAt ?? route.createdAt ?? 0;
41
- }
42
- function sortCachedRouteEntries([leftPathname, leftRoute], [rightPathname, rightRoute]) {
43
- const timestampDifference = getCachedRouteTimestamp(leftRoute) - getCachedRouteTimestamp(rightRoute);
44
- if (timestampDifference !== 0) return timestampDifference;
45
- return leftPathname.localeCompare(rightPathname);
46
- }
47
- function getUnmarkedCachedRouteEntries(routes, keysToDelete) {
48
- return Object.entries(routes).filter(([pathname]) => !keysToDelete.has(pathname));
49
- }
50
- function getEntriesByRouteId(entries) {
51
- const entriesByRouteId = /* @__PURE__ */ new Map();
52
- for (const entry of entries) {
53
- const routeId = entry[1].routeId;
54
- if (!routeId) continue;
55
- const existingEntries = entriesByRouteId.get(routeId);
56
- if (existingEntries) {
57
- existingEntries.push(entry);
58
- continue;
59
- }
60
- entriesByRouteId.set(routeId, [entry]);
61
- }
62
- return entriesByRouteId;
63
- }
64
- function markEntriesForDeletion(entries, keysToDelete, protectedKeys, excessEntryCount) {
65
- if (excessEntryCount <= 0) return;
66
- const evictableEntries = entries.filter(([pathname]) => !protectedKeys.has(pathname)).sort(sortCachedRouteEntries);
67
- let remainingExcess = excessEntryCount;
68
- for (const [pathname] of evictableEntries) {
69
- if (remainingExcess <= 0) break;
70
- if (!keysToDelete.has(pathname)) {
71
- keysToDelete.add(pathname);
72
- remainingExcess -= 1;
73
- }
74
- }
75
- }
76
- function applyPerRouteEntryLimit(routes, maxEntriesPerRouteId, keysToDelete, protectedKeys) {
77
- if (maxEntriesPerRouteId === DEFAULT_MAX_ENTRIES) return;
78
- const entriesByRouteId = getEntriesByRouteId(getUnmarkedCachedRouteEntries(routes, keysToDelete));
79
- for (const entries of entriesByRouteId.values()) markEntriesForDeletion(entries, keysToDelete, protectedKeys, entries.length - maxEntriesPerRouteId);
80
- }
81
- function applyGlobalEntryLimit(routes, maxEntries, keysToDelete, protectedKeys) {
82
- if (maxEntries === DEFAULT_MAX_ENTRIES) return;
83
- const remainingEntries = getUnmarkedCachedRouteEntries(routes, keysToDelete);
84
- markEntriesForDeletion(remainingEntries, keysToDelete, protectedKeys, remainingEntries.length - maxEntries);
85
- }
86
- function applyCachedRouteLimits(routes, config, protectedKeys) {
87
- if (config.maxEntries === DEFAULT_MAX_ENTRIES && config.maxEntriesPerRouteId === DEFAULT_MAX_ENTRIES) return routes;
88
- const keysToDelete = /* @__PURE__ */ new Set();
89
- applyPerRouteEntryLimit(routes, config.maxEntriesPerRouteId, keysToDelete, protectedKeys);
90
- applyGlobalEntryLimit(routes, config.maxEntries, keysToDelete, protectedKeys);
91
- if (keysToDelete.size === 0) return routes;
92
- const nextRoutes = { ...routes };
93
- for (const pathname of keysToDelete) delete nextRoutes[pathname];
94
- return nextRoutes;
95
- }
96
- function createCachedRouteData(current, next) {
97
- const now = Date.now();
98
- return {
99
- ...next,
100
- createdAt: current?.createdAt ?? next.createdAt ?? now,
101
- lastVisibleAt: next.lastVisibleAt ?? now
102
- };
103
- }
104
- function getNextCachedRoutesState(params) {
105
- const normalizedKey = normalizeCachedRoutePathname(params.key);
106
- if (params.cacheConfig.maxEntries === 0) return params.state === EMPTY_CACHED_ROUTES ? params.state : EMPTY_CACHED_ROUTES;
107
- if (!isCacheEnabledRouteData(params.value)) {
108
- if (!Object.hasOwn(params.state, normalizedKey)) return params.state;
109
- const nextState = { ...params.state };
110
- delete nextState[normalizedKey];
111
- return nextState;
112
- }
113
- const nextRouteData = createCachedRouteData(params.state[normalizedKey], params.value);
114
- if (isSameCachedRouteData(params.state[normalizedKey], nextRouteData)) return params.state;
115
- return applyCachedRouteLimits({
116
- ...params.state,
117
- [normalizedKey]: nextRouteData
118
- }, params.cacheConfig, new Set([normalizedKey]));
119
- }
120
- function RouterCacheProvider({ cacheScopeKey = "__default__", children, defaultCachedRoutes = EMPTY_CACHED_ROUTES, maxEntries = DEFAULT_MAX_ENTRIES, maxEntriesPerRouteId = DEFAULT_MAX_ENTRIES }) {
121
- return /* @__PURE__ */ jsx(RouterCacheProviderScope, {
122
- defaultCachedRoutes,
123
- maxEntries,
124
- maxEntriesPerRouteId,
125
- children
126
- }, cacheScopeKey ?? "__default__");
127
- }
128
- function RouterCacheProviderScope({ children, defaultCachedRoutes = EMPTY_CACHED_ROUTES, maxEntries, maxEntriesPerRouteId }) {
129
- const cacheConfigRef = useRef({
130
- maxEntries: normalizeLimit(maxEntries),
131
- maxEntriesPerRouteId: normalizeLimit(maxEntriesPerRouteId)
132
- });
133
- const initialCachedRoutesRef = useRef(null);
134
- if (initialCachedRoutesRef.current === null) initialCachedRoutesRef.current = applyCachedRouteLimits(filterRouterCacheRoutes(defaultCachedRoutes), cacheConfigRef.current, /* @__PURE__ */ new Set());
135
- const [cachedRoutes, setCachedRoutes] = useState(() => initialCachedRoutesRef.current ?? EMPTY_CACHED_ROUTES);
136
- const [erroredRouteCounts, setErroredRouteCounts] = useState({});
137
- const updateCachedRoutes = (key, value) => {
138
- setCachedRoutes((state) => getNextCachedRoutesState({
139
- cacheConfig: cacheConfigRef.current,
140
- key,
141
- state,
142
- value
143
- }));
144
- };
145
- const deleteCachedRoutes = (keys) => {
146
- setCachedRoutes((state) => {
147
- let changed = false;
148
- const newState = { ...state };
149
- for (const key of keys) {
150
- const normalizedKey = normalizeCachedRoutePathname(key);
151
- if (Object.hasOwn(newState, normalizedKey)) {
152
- delete newState[normalizedKey];
153
- changed = true;
154
- }
155
- }
156
- return changed ? newState : state;
157
- });
158
- };
159
- const touchCachedRoutes = (keys) => {
160
- setCachedRoutes((state) => {
161
- let changed = false;
162
- const touchedAt = Date.now();
163
- const nextState = { ...state };
164
- for (const key of keys) {
165
- const normalizedKey = normalizeCachedRoutePathname(key);
166
- const route = nextState[normalizedKey];
167
- if (!route || route.lastVisibleAt === touchedAt) continue;
168
- nextState[normalizedKey] = {
169
- ...route,
170
- lastVisibleAt: touchedAt
171
- };
172
- changed = true;
173
- }
174
- return changed ? nextState : state;
175
- });
176
- };
177
- const retainErroredRoute = (pathname) => {
178
- const normalizedPathname = normalizeCachedRoutePathname(pathname);
179
- setErroredRouteCounts((state) => ({
180
- ...state,
181
- [normalizedPathname]: (state[normalizedPathname] ?? 0) + 1
182
- }));
183
- setCachedRoutes((state) => {
184
- if (!Object.hasOwn(state, normalizedPathname)) return state;
185
- const nextState = { ...state };
186
- delete nextState[normalizedPathname];
187
- return nextState;
188
- });
189
- };
190
- const releaseErroredRoute = (pathname) => {
191
- const normalizedPathname = normalizeCachedRoutePathname(pathname);
192
- setErroredRouteCounts((state) => {
193
- const currentCount = state[normalizedPathname] ?? 0;
194
- if (currentCount <= 1) {
195
- if (!Object.hasOwn(state, normalizedPathname)) return state;
196
- const nextState = { ...state };
197
- delete nextState[normalizedPathname];
198
- return nextState;
199
- }
200
- return {
201
- ...state,
202
- [normalizedPathname]: currentCount - 1
203
- };
204
- });
205
- };
206
- const contextValue = {
207
- cachedRoutes,
208
- erroredRouteCounts,
209
- setCachedRoutes: updateCachedRoutes,
210
- deleteCachedRoutes,
211
- touchCachedRoutes,
212
- retainErroredRoute,
213
- releaseErroredRoute
214
- };
215
- return /* @__PURE__ */ jsx(RouterCacheContext.Provider, {
216
- value: contextValue,
217
- children
218
- });
219
- }
220
- function useRouterCacheContext() {
221
- const context = use(RouterCacheContext);
222
- if (!context) throw new Error("RouterCacheContext is not available");
223
- return context;
224
- }
225
- function useOptionalRouterCacheContext() {
226
- return use(RouterCacheContext);
227
- }
228
- //#endregion
229
- //#region src/hooks/use-event-listener.ts
230
- const EVENT_BUCKET_KEYS = ["on", "once"];
231
- const instance = class RouterCacheEvent {
232
- static instance;
233
- listeners = {
234
- activeChange: /* @__PURE__ */ new Set(),
235
- cachedNavigationCancel: /* @__PURE__ */ new Set(),
236
- cachedNavigationComplete: /* @__PURE__ */ new Set(),
237
- cachedNavigationStart: /* @__PURE__ */ new Set()
238
- };
239
- constructor() {}
240
- static getInstance() {
241
- if (!RouterCacheEvent.instance) RouterCacheEvent.instance = new RouterCacheEvent();
242
- return RouterCacheEvent.instance;
243
- }
244
- on(eventName, handler) {
245
- this.listeners[eventName].add(handler);
246
- return this;
247
- }
248
- once(eventName, handler) {
249
- const onceHandler = (...args) => {
250
- this.off(eventName, onceHandler);
251
- handler(...args);
252
- };
253
- this.listeners[eventName].add(onceHandler);
254
- return this;
255
- }
256
- off(eventName, handler) {
257
- this.listeners[eventName].delete(handler);
258
- return this;
259
- }
260
- emit(eventName, ...args) {
261
- const handlers = this.listeners[eventName];
262
- for (const handler of handlers) handler(...args);
263
- return handlers.size > 0;
264
- }
265
- }.getInstance();
266
- function syncEventHandler(type, eventName, handler) {
267
- if (type === "on") {
268
- instance.on(eventName, handler);
269
- return;
270
- }
271
- instance.off(eventName, handler);
272
- }
273
- function syncEventBucket(type, bucket) {
274
- if (bucket.activeChange) syncEventHandler(type, "activeChange", bucket.activeChange);
275
- if (bucket.cachedNavigationCancel) syncEventHandler(type, "cachedNavigationCancel", bucket.cachedNavigationCancel);
276
- if (bucket.cachedNavigationComplete) syncEventHandler(type, "cachedNavigationComplete", bucket.cachedNavigationComplete);
277
- if (bucket.cachedNavigationStart) syncEventHandler(type, "cachedNavigationStart", bucket.cachedNavigationStart);
278
- }
279
- function syncEventHandlers(type, events) {
280
- if (!events) return;
281
- for (const key of EVENT_BUCKET_KEYS) {
282
- const bucket = events[key];
283
- if (!bucket) continue;
284
- syncEventBucket(type, bucket);
285
- }
286
- }
287
- function useEventListener(events) {
288
- useEffect(() => {
289
- const registerEvent = () => {
290
- syncEventHandlers("on", events);
291
- return () => {
292
- syncEventHandlers("off", events);
293
- };
294
- };
295
- const unregister = registerEvent();
296
- return () => {
297
- unregister?.();
298
- };
299
- }, [events]);
300
- return { eventListener: instance };
301
- }
302
- //#endregion
303
- //#region src/hooks/use-router-cache-debug.ts
304
- const DYNAMIC_SEGMENT_PATTERNS = [
305
- /^\d{4,}$/u,
306
- /^[0-9a-f]{8,}$/iu,
307
- /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu
308
- ];
309
- function isDynamicLookingSegment(segment) {
310
- for (const pattern of DYNAMIC_SEGMENT_PATTERNS) if (pattern.test(segment)) return true;
311
- return false;
312
- }
313
- function isDynamicLookingPathname(pathname) {
314
- return pathname.split("/").filter(Boolean).some(isDynamicLookingSegment);
315
- }
316
- function countHiddenContainers() {
317
- if (typeof document === "undefined") return 0;
318
- return document.querySelectorAll("[data-router-cache-container=\"true\"][data-router-cache-mode=\"hidden\"]").length;
319
- }
320
- function buildSnapshot(cachedRoutes, visiblePathname) {
321
- const cachedRoutePathnames = Object.keys(cachedRoutes).sort((a, b) => a.localeCompare(b));
322
- const dynamicLookingRoutePathnames = cachedRoutePathnames.filter(isDynamicLookingPathname);
323
- const hiddenCachedRoutePathnames = cachedRoutePathnames.filter((pathname) => pathname !== visiblePathname);
324
- return {
325
- cachedRoutePathnames,
326
- dynamicLookingRouteCount: dynamicLookingRoutePathnames.length,
327
- dynamicLookingRoutePathnames,
328
- hiddenCachedRouteCount: hiddenCachedRoutePathnames.length,
329
- hiddenContainerCount: countHiddenContainers(),
330
- totalCachedRouteCount: cachedRoutePathnames.length,
331
- visiblePathname
332
- };
333
- }
334
- function getDebugWindow() {
335
- return globalThis.window;
336
- }
337
- function isProduction() {
338
- return globalThis.process?.env?.NODE_ENV === "production";
339
- }
340
- function useRouterCacheDebug(cachedRoutes, visiblePathname) {
341
- const warningThresholdRef = useRef(null);
342
- const lastWarnedCountRef = useRef(null);
343
- const lastSnapshotRef = useRef(null);
344
- if (lastSnapshotRef.current === null) lastSnapshotRef.current = buildSnapshot(cachedRoutes, visiblePathname);
345
- const snapshot = useMemo(() => buildSnapshot(cachedRoutes, visiblePathname), [cachedRoutes, visiblePathname]);
346
- useEffect(() => {
347
- if (isProduction()) return;
348
- const debugWindow = getDebugWindow();
349
- if (!debugWindow) return;
350
- const refresh = () => {
351
- lastSnapshotRef.current = buildSnapshot(cachedRoutes, visiblePathname);
352
- return lastSnapshotRef.current;
353
- };
354
- const api = {
355
- getSnapshot: () => lastSnapshotRef.current ?? snapshot,
356
- lastSnapshot: snapshot,
357
- refresh,
358
- setWarningThreshold: (nextThreshold) => {
359
- warningThresholdRef.current = typeof nextThreshold === "number" ? nextThreshold : null;
360
- lastWarnedCountRef.current = null;
361
- },
362
- warningThreshold: warningThresholdRef.current
363
- };
364
- debugWindow.__TANSTACK_ROUTER_CACHE_DEBUG__ = api;
365
- const rafId = globalThis.requestAnimationFrame(() => {
366
- const nextSnapshot = refresh();
367
- debugWindow.__TANSTACK_ROUTER_CACHE_DEBUG__ = {
368
- ...api,
369
- lastSnapshot: nextSnapshot,
370
- warningThreshold: warningThresholdRef.current
371
- };
372
- const warningThreshold = warningThresholdRef.current;
373
- if (typeof warningThreshold === "number" && nextSnapshot.totalCachedRouteCount > warningThreshold && lastWarnedCountRef.current !== nextSnapshot.totalCachedRouteCount) lastWarnedCountRef.current = nextSnapshot.totalCachedRouteCount;
374
- });
375
- return () => {
376
- globalThis.cancelAnimationFrame(rafId);
377
- if (debugWindow.__TANSTACK_ROUTER_CACHE_DEBUG__ === api) debugWindow.__TANSTACK_ROUTER_CACHE_DEBUG__ = void 0;
378
- };
379
- }, [
380
- cachedRoutes,
381
- snapshot,
382
- visiblePathname
383
- ]);
384
- }
385
- //#endregion
386
- //#region src/components/cached-outlet.tsx
387
- function CachedOutlet(props) {
388
- const { matchId, routerSnapshot } = props;
389
- return /* @__PURE__ */ jsx(RouterContextProvider, {
390
- router: routerSnapshot,
391
- children: /* @__PURE__ */ jsx(Match, { matchId })
392
- });
393
- }
394
- //#endregion
395
- //#region src/dom/dismiss-transient-ui.ts
396
- const documentStates = /* @__PURE__ */ new WeakMap();
397
- const PERSISTENT_EXTERNAL_ATTRIBUTE = "data-router-cache-persistent-external";
398
- function isBlurTarget(element) {
399
- return element instanceof HTMLElement;
400
- }
401
- function getOrCreateDocumentState(documentObject) {
402
- const existingState = documentStates.get(documentObject);
403
- if (existingState) return existingState;
404
- const state = {
405
- observer: null,
406
- ownerByElement: /* @__PURE__ */ new WeakMap(),
407
- routes: /* @__PURE__ */ new Map(),
408
- visiblePathname: null
409
- };
410
- if (typeof MutationObserver !== "undefined") {
411
- state.observer = new MutationObserver((mutations) => {
412
- handleDomMutations(mutations, state);
413
- });
414
- state.observer.observe(documentObject.body ?? documentObject.documentElement, {
415
- childList: true,
416
- subtree: true
417
- });
418
- }
419
- documentStates.set(documentObject, state);
420
- return state;
421
- }
422
- function getOrCreateRouteState(state, pathname) {
423
- const existingRouteState = state.routes.get(pathname);
424
- if (existingRouteState) return existingRouteState;
425
- const routeState = {
426
- hiddenElements: /* @__PURE__ */ new Map(),
427
- ownedElements: /* @__PURE__ */ new Set()
428
- };
429
- state.routes.set(pathname, routeState);
430
- return routeState;
431
- }
432
- function getHoveredElements(documentObject) {
433
- try {
434
- return Array.from(documentObject.querySelectorAll(":hover")).reverse();
435
- } catch {
436
- return [];
437
- }
438
- }
439
- function getOwnedExternalElements(documentObject, pathname) {
440
- const routeState = getOrCreateRouteState(getOrCreateDocumentState(documentObject), pathname);
441
- return Array.from(routeState.ownedElements).filter((element) => element.isConnected);
442
- }
443
- function dispatchEscapeKeyboardEvent(target, documentObject, type) {
444
- const keyboardEventConstructor = documentObject.defaultView?.KeyboardEvent ?? KeyboardEvent;
445
- target.dispatchEvent(new keyboardEventConstructor(type, {
446
- bubbles: true,
447
- cancelable: true,
448
- code: "Escape",
449
- key: "Escape",
450
- keyCode: 27,
451
- which: 27
452
- }));
453
- }
454
- function dispatchHoverExitEvent(target, documentObject, type) {
455
- const eventConstructor = type.startsWith("pointer") ? documentObject.defaultView?.PointerEvent ?? documentObject.defaultView?.MouseEvent ?? MouseEvent : documentObject.defaultView?.MouseEvent ?? MouseEvent;
456
- target.dispatchEvent(new eventConstructor(type, {
457
- bubbles: type.endsWith("out"),
458
- cancelable: true,
459
- composed: true,
460
- relatedTarget: documentObject.body
461
- }));
462
- }
463
- function dispatchHoverExitEvents(hoveredElements, documentObject) {
464
- for (const hoveredElement of hoveredElements) {
465
- dispatchHoverExitEvent(hoveredElement, documentObject, "pointerout");
466
- dispatchHoverExitEvent(hoveredElement, documentObject, "pointerleave");
467
- dispatchHoverExitEvent(hoveredElement, documentObject, "mouseout");
468
- dispatchHoverExitEvent(hoveredElement, documentObject, "mouseleave");
469
- }
470
- }
471
- function focusDocumentBody(documentObject) {
472
- const { body } = documentObject;
473
- const previousTabIndex = body.getAttribute("tabindex");
474
- if (previousTabIndex === null) body.setAttribute("tabindex", "-1");
475
- body.focus();
476
- if (previousTabIndex === null) body.removeAttribute("tabindex");
477
- }
478
- function getRouterCacheContainerAncestor(element) {
479
- const container = element?.closest("[data-router-cache-container=\"true\"]");
480
- return container instanceof HTMLElement ? container : null;
481
- }
482
- function isElementInsideAnyRouterCacheContainer(element) {
483
- return getRouterCacheContainerAncestor(element) !== null;
484
- }
485
- function containsRouterCacheContainer(element) {
486
- return element.querySelector("[data-router-cache-container=\"true\"]") instanceof HTMLElement;
487
- }
488
- function isElementConnectedOutsideRouterCacheContainer(element) {
489
- return element.isConnected && !isElementInsideAnyRouterCacheContainer(element);
490
- }
491
- function isPersistentExternalElement(element) {
492
- return element.closest(`[${PERSISTENT_EXTERNAL_ATTRIBUTE}="true"]`) instanceof HTMLElement;
493
- }
494
- function getTrackableElementsFromNode(node) {
495
- if (node instanceof HTMLElement) return [node];
496
- if (node instanceof DocumentFragment) return Array.from(node.children).flatMap((child) => child instanceof HTMLElement ? [child] : []);
497
- return [];
498
- }
499
- function forEachRemovedNode(mutations, callback) {
500
- for (const mutation of mutations) for (const removedNode of mutation.removedNodes) callback(removedNode);
501
- }
502
- function forEachAddedNode(mutations, callback) {
503
- for (const mutation of mutations) for (const addedNode of mutation.addedNodes) callback(addedNode);
504
- }
505
- function handleDomMutations(mutations, state) {
506
- forEachRemovedNode(mutations, (removedNode) => {
507
- untrackRemovedNode(removedNode, state);
508
- });
509
- if (!state.visiblePathname) return;
510
- forEachAddedNode(mutations, (addedNode) => {
511
- trackAddedNode(addedNode, state.visiblePathname, state);
512
- });
513
- }
514
- function hasTrackedAncestor(element, state) {
515
- let ancestor = element.parentElement;
516
- while (ancestor) {
517
- if (state.ownerByElement.get(ancestor)) return true;
518
- ancestor = ancestor.parentElement;
519
- }
520
- return false;
521
- }
522
- function restoreOwnedExternalElement(routeState, element) {
523
- const snapshot = routeState.hiddenElements.get(element);
524
- if (!snapshot) return;
525
- element.style.setProperty("display", snapshot.display, snapshot.displayPriority);
526
- if (snapshot.ariaHidden === null) element.removeAttribute("aria-hidden");
527
- else element.setAttribute("aria-hidden", snapshot.ariaHidden);
528
- element.inert = snapshot.inert;
529
- routeState.hiddenElements.delete(element);
530
- }
531
- function trackExternalElement(pathname, element, state) {
532
- if (!isElementConnectedOutsideRouterCacheContainer(element) || containsRouterCacheContainer(element) || isPersistentExternalElement(element) || hasTrackedAncestor(element, state)) return;
533
- const currentOwner = state.ownerByElement.get(element);
534
- if (currentOwner === pathname) return;
535
- if (currentOwner) {
536
- const previousRouteState = state.routes.get(currentOwner);
537
- if (previousRouteState) {
538
- restoreOwnedExternalElement(previousRouteState, element);
539
- previousRouteState.ownedElements.delete(element);
540
- }
541
- }
542
- getOrCreateRouteState(state, pathname).ownedElements.add(element);
543
- state.ownerByElement.set(element, pathname);
544
- }
545
- function trackAddedNode(node, pathname, state) {
546
- const elements = getTrackableElementsFromNode(node);
547
- for (const element of elements) trackExternalElement(pathname, element, state);
548
- }
549
- function untrackElement(routeState, state, element) {
550
- routeState.hiddenElements.delete(element);
551
- routeState.ownedElements.delete(element);
552
- state.ownerByElement.delete(element);
553
- }
554
- function untrackRemovedNode(node, state) {
555
- if (!(node instanceof HTMLElement)) return;
556
- for (const routeState of state.routes.values()) for (const element of Array.from(routeState.ownedElements)) if (node === element || node.contains(element)) untrackElement(routeState, state, element);
557
- }
558
- function hideOwnedExternalElements(documentObject, pathname) {
559
- const routeState = getOrCreateRouteState(getOrCreateDocumentState(documentObject), pathname);
560
- for (const element of Array.from(routeState.ownedElements)) {
561
- if (!isElementConnectedOutsideRouterCacheContainer(element)) {
562
- routeState.ownedElements.delete(element);
563
- routeState.hiddenElements.delete(element);
564
- continue;
565
- }
566
- if (routeState.hiddenElements.has(element)) continue;
567
- routeState.hiddenElements.set(element, {
568
- ariaHidden: element.getAttribute("aria-hidden"),
569
- display: element.style.getPropertyValue("display"),
570
- displayPriority: element.style.getPropertyPriority("display"),
571
- inert: element.inert
572
- });
573
- element.style.setProperty("display", "none", "important");
574
- element.setAttribute("aria-hidden", "true");
575
- element.inert = true;
576
- }
577
- }
578
- function showOwnedExternalElements(documentObject, pathname) {
579
- const routeState = getOrCreateRouteState(getOrCreateDocumentState(documentObject), pathname);
580
- for (const [element] of Array.from(routeState.hiddenElements)) {
581
- if (!element.isConnected) {
582
- routeState.hiddenElements.delete(element);
583
- routeState.ownedElements.delete(element);
584
- continue;
585
- }
586
- restoreOwnedExternalElement(routeState, element);
587
- }
588
- }
589
- function initializeTransientUiTracking(documentObject) {
590
- getOrCreateDocumentState(documentObject);
591
- }
592
- function syncTransientUiRouteActivity(pathname, mode) {
593
- if (typeof document === "undefined") return;
594
- const documentState = getOrCreateDocumentState(document);
595
- getOrCreateRouteState(documentState, pathname);
596
- if (mode === "visible") {
597
- documentState.visiblePathname = pathname;
598
- showOwnedExternalElements(document, pathname);
599
- return;
600
- }
601
- if (documentState.visiblePathname === pathname) documentState.visiblePathname = null;
602
- dispatchHoverExitEvents(getOwnedExternalElements(document, pathname), document);
603
- hideOwnedExternalElements(document, pathname);
604
- }
605
- function dismissTransientUi(container, pathname) {
606
- if (typeof document === "undefined" || !container) return;
607
- initializeTransientUiTracking(document);
608
- const hoveredElements = getHoveredElements(document);
609
- const hoverExitTargets = Array.from(new Set([...hoveredElements, ...getOwnedExternalElements(document, pathname)]));
610
- const activeElement = document.activeElement;
611
- const activeElementBelongsToHoveredTree = activeElement instanceof Element && hoverExitTargets.some((hoveredElement) => hoveredElement === activeElement || hoveredElement.contains(activeElement));
612
- if (activeElement instanceof Element && (container.contains(activeElement) || activeElementBelongsToHoveredTree) && isBlurTarget(activeElement)) activeElement.blur();
613
- dispatchHoverExitEvents(hoverExitTargets, document);
614
- dispatchEscapeKeyboardEvent(document, document, "keydown");
615
- dispatchEscapeKeyboardEvent(document, document, "keyup");
616
- hideOwnedExternalElements(document, pathname);
617
- const nextActiveElement = document.activeElement;
618
- const nextActiveElementBelongsToHoveredTree = nextActiveElement instanceof Element && hoverExitTargets.some((hoveredElement) => hoveredElement === nextActiveElement || hoveredElement.contains(nextActiveElement));
619
- if (nextActiveElement instanceof Element && (container.contains(nextActiveElement) || nextActiveElementBelongsToHoveredTree)) focusDocumentBody(document);
620
- }
621
- //#endregion
622
- //#region src/components/off-screen-in.tsx
623
- const windowScrollPositions = /* @__PURE__ */ new Map();
624
- const IMMEDIATE_SCROLL_RESTORE_DELAY = 0;
625
- const EARLY_SCROLL_RESTORE_DELAY = 80;
626
- const MIDDLE_SCROLL_RESTORE_DELAY = 240;
627
- const LATE_SCROLL_RESTORE_DELAY = 600;
628
- const FINAL_SCROLL_RESTORE_DELAY = 1e3;
629
- const SCROLL_TRACKING_INTERVAL_MS = 200;
630
- const SCROLL_KEYS = new Set([
631
- " ",
632
- "ArrowDown",
633
- "ArrowLeft",
634
- "ArrowRight",
635
- "ArrowUp",
636
- "End",
637
- "Home",
638
- "PageDown",
639
- "PageUp"
640
- ]);
641
- const SCROLL_RESTORE_DELAYS = [
642
- IMMEDIATE_SCROLL_RESTORE_DELAY,
643
- EARLY_SCROLL_RESTORE_DELAY,
644
- MIDDLE_SCROLL_RESTORE_DELAY,
645
- LATE_SCROLL_RESTORE_DELAY,
646
- FINAL_SCROLL_RESTORE_DELAY
647
- ];
648
- function OffScreenIn(props) {
649
- const { mode, children, containerRef, pathname } = props;
650
- const localContainerRef = useRef(null);
651
- const attachContainerRef = (node) => {
652
- localContainerRef.current = node;
653
- if (containerRef) containerRef.current = node;
654
- };
655
- useLayoutEffect(() => {
656
- if (typeof document === "undefined") return;
657
- initializeTransientUiTracking(document);
658
- }, []);
659
- useLayoutEffect(() => {
660
- if (!pathname) return;
661
- syncTransientUiRouteActivity(pathname, mode);
662
- }, [mode, pathname]);
663
- useLayoutEffect(() => {
664
- const visibleContainer = localContainerRef.current;
665
- return () => {
666
- if (!pathname || mode !== "visible") return;
667
- dismissTransientUi(visibleContainer, pathname);
668
- };
669
- }, [mode, pathname]);
670
- useLayoutEffect(() => {
671
- if (typeof globalThis === "undefined" || !pathname) return;
672
- const saveScrollPosition = () => {
673
- windowScrollPositions.set(pathname, {
674
- x: globalThis.scrollX,
675
- y: globalThis.scrollY
676
- });
677
- };
678
- if (mode === "hidden") return;
679
- const savedPosition = windowScrollPositions.get(pathname);
680
- let userRequestedScroll = false;
681
- let scrollTrackingIntervalId;
682
- const startScrollTracking = () => {
683
- scrollTrackingIntervalId ??= globalThis.setInterval(saveScrollPosition, SCROLL_TRACKING_INTERVAL_MS);
684
- };
685
- const handleScroll = () => {
686
- if (!savedPosition || userRequestedScroll) saveScrollPosition();
687
- };
688
- const markUserRequestedScroll = () => {
689
- userRequestedScroll = true;
690
- };
691
- const handleKeyDown = (event) => {
692
- if (SCROLL_KEYS.has(event.key)) markUserRequestedScroll();
693
- };
694
- globalThis.addEventListener("scroll", handleScroll, { passive: true });
695
- globalThis.addEventListener("wheel", markUserRequestedScroll, { passive: true });
696
- globalThis.addEventListener("touchstart", markUserRequestedScroll, { passive: true });
697
- globalThis.addEventListener("pointerdown", markUserRequestedScroll, { passive: true });
698
- globalThis.addEventListener("keydown", handleKeyDown);
699
- if (!savedPosition) {
700
- startScrollTracking();
701
- saveScrollPosition();
702
- return () => {
703
- if (scrollTrackingIntervalId !== void 0) globalThis.clearInterval(scrollTrackingIntervalId);
704
- globalThis.removeEventListener("scroll", handleScroll);
705
- globalThis.removeEventListener("wheel", markUserRequestedScroll);
706
- globalThis.removeEventListener("touchstart", markUserRequestedScroll);
707
- globalThis.removeEventListener("pointerdown", markUserRequestedScroll);
708
- globalThis.removeEventListener("keydown", handleKeyDown);
709
- };
710
- }
711
- const restoreScrollPosition = () => {
712
- if (userRequestedScroll) return;
713
- globalThis.scrollTo(savedPosition.x, savedPosition.y);
714
- saveScrollPosition();
715
- startScrollTracking();
716
- };
717
- const timeoutIds = [];
718
- let rafId = globalThis.requestAnimationFrame(() => {
719
- rafId = globalThis.requestAnimationFrame(() => {
720
- restoreScrollPosition();
721
- for (const delay of SCROLL_RESTORE_DELAYS) timeoutIds.push(globalThis.setTimeout(restoreScrollPosition, delay));
722
- });
723
- });
724
- return () => {
725
- globalThis.cancelAnimationFrame(rafId);
726
- for (const timeoutId of timeoutIds) globalThis.clearTimeout(timeoutId);
727
- if (scrollTrackingIntervalId !== void 0) globalThis.clearInterval(scrollTrackingIntervalId);
728
- globalThis.removeEventListener("scroll", handleScroll);
729
- globalThis.removeEventListener("wheel", markUserRequestedScroll);
730
- globalThis.removeEventListener("touchstart", markUserRequestedScroll);
731
- globalThis.removeEventListener("pointerdown", markUserRequestedScroll);
732
- globalThis.removeEventListener("keydown", handleKeyDown);
733
- };
734
- }, [mode, pathname]);
735
- return /* @__PURE__ */ jsx(Activity, {
736
- mode,
737
- children: /* @__PURE__ */ jsx("div", {
738
- className: "flex min-h-full w-full flex-1 flex-col",
739
- "data-router-cache-container": "true",
740
- "data-router-cache-mode": mode,
741
- "data-router-cache-pathname": pathname,
742
- ref: attachContainerRef,
743
- children
744
- })
745
- });
746
- }
747
- //#endregion
748
- //#region src/components/off-screen.tsx
749
- function OffScreen(props) {
750
- return /* @__PURE__ */ jsx(OffScreenIn, { ...props });
751
- }
752
- //#endregion
753
- //#region src/components/restore-cached-href.ts
754
- const FALLBACK_ORIGIN = "http://tanstack-router-cache.local";
755
- function parseHref(href) {
756
- try {
757
- const url = new URL(href, FALLBACK_ORIGIN);
758
- return {
759
- hasHash: url.hash.length > 0,
760
- hasSearch: url.search.length > 0,
761
- pathname: url.pathname
762
- };
763
- } catch {
764
- return null;
765
- }
766
- }
767
- function isBareRouteHref(href, currentPathname) {
768
- const candidateHref = href ?? currentPathname;
769
- const parsedHref = parseHref(candidateHref);
770
- if (!parsedHref) return candidateHref === currentPathname;
771
- return parsedHref.pathname === currentPathname && !parsedHref.hasSearch && !parsedHref.hasHash;
772
- }
773
- function shouldRestoreCachedHref({ cachedHref, currentHref, currentPathname, isRouteCacheEnabled, previousPathname }) {
774
- if (!(isRouteCacheEnabled && cachedHref)) return false;
775
- if (!previousPathname || previousPathname === currentPathname) return false;
776
- if (!isBareRouteHref(currentHref, currentPathname)) return false;
777
- return cachedHref !== (currentHref ?? currentPathname);
778
- }
779
- //#endregion
780
- //#region src/components/route-cache-manager.tsx
781
- const LIVE_ROUTER_METHODS = [
782
- "buildLocation",
783
- "commitLocation",
784
- "invalidate",
785
- "loadRouteChunk",
786
- "navigate",
787
- "preloadRoute"
788
- ];
789
- const routerSnapshotUpdaters = /* @__PURE__ */ new WeakMap();
790
- function createSnapshotStore(value) {
791
- let currentValue = value;
792
- const listeners = /* @__PURE__ */ new Set();
793
- return {
794
- get: () => currentValue,
795
- set: (nextValue) => {
796
- if (Object.is(currentValue, nextValue)) return;
797
- currentValue = nextValue;
798
- for (const listener of listeners) listener(currentValue);
799
- },
800
- subscribe: (listener) => {
801
- if (listener) listeners.add(listener);
802
- return { unsubscribe: listener ? () => {
803
- listeners.delete(listener);
804
- } : () => void 0 };
805
- }
806
- };
807
- }
808
- function createRouterSnapshotData({ matches, router, routerLocation, routerResolvedLocation }) {
809
- const snapshotMatches = matches.reduce((snapshot, match) => {
810
- const nextMatch = snapshotMatch(match, routerLocation);
811
- if (isRouterMatch(nextMatch)) snapshot.push(nextMatch);
812
- return snapshot;
813
- }, []);
814
- return {
815
- matches: snapshotMatches,
816
- state: {
817
- ...router.stores.__store.get(),
818
- matches: snapshotMatches,
819
- location: routerLocation,
820
- resolvedLocation: routerResolvedLocation
821
- }
822
- };
823
- }
824
- function createMatchStore(match) {
825
- return Object.assign(createSnapshotStore(match), { routeId: match.routeId });
826
- }
827
- function syncRouterSnapshot(routerSnapshot, input) {
828
- const update = routerSnapshotUpdaters.get(routerSnapshot);
829
- if (!update) return false;
830
- update(input);
831
- return true;
832
- }
833
- function getLiveRouterMethodDescriptors(router) {
834
- return Object.fromEntries(LIVE_ROUTER_METHODS.flatMap((methodName) => {
835
- const method = router[methodName];
836
- if (typeof method !== "function") return [];
837
- return [[methodName, { value: method.bind(router) }]];
838
- }));
839
- }
840
- function toRouterLocation(location) {
841
- return { ...location };
842
- }
843
- function isReadyCachedRoute(route) {
844
- return Boolean(route && isRouteCacheEnabled(route.staticData) && route.ready && route.matchId && route.routerSnapshot);
845
- }
846
- function isRouteCacheEnabled(staticData) {
847
- return staticData?.routeCache === true;
848
- }
849
- function hasErroredRouteMatch(matches) {
850
- return matches.some((match) => match?.status === "error");
851
- }
852
- function hasRetainedRouteError(erroredRouteCounts, pathname) {
853
- return (erroredRouteCounts[pathname] ?? 0) > 0;
854
- }
855
- function hasCurrentRouterMatchError({ matches, resolvedPathname, routerPathname }) {
856
- return routerPathname === resolvedPathname && hasErroredRouteMatch(matches);
857
- }
858
- function hasCurrentRouteError({ erroredRouteCounts, matches, resolvedPathname, routerPathname }) {
859
- return hasRetainedRouteError(erroredRouteCounts, routerPathname) || hasCurrentRouterMatchError({
860
- matches,
861
- resolvedPathname,
862
- routerPathname
863
- });
864
- }
865
- function isRouterMatch(match) {
866
- return Boolean(match?.id && match.routeId);
867
- }
868
- function snapshotMatch(match, routerLocation) {
869
- if (!isRouterMatch(match)) return;
870
- const isLocationMatch = match.pathname ? normalizeCachedRoutePathname(match.pathname) === normalizeCachedRoutePathname(routerLocation.pathname) : false;
871
- return {
872
- ...match,
873
- ...isLocationMatch ? {
874
- _strictSearch: routerLocation.search,
875
- search: routerLocation.search
876
- } : {},
877
- _nonReactive: { ...match._nonReactive }
878
- };
879
- }
880
- function isCurrentUnmanagedCachedRoute(route, routerHref) {
881
- return Boolean(route && isRouteCacheEnabled(route.staticData) && route.ready && route.routerSnapshot && route.href === routerHref);
882
- }
883
- function syncReadyCachedRoute({ matchId, matches, routeId, route, routerLocation, router, routerResolvedLocation, routerHref, routerPathname, setCachedRoutes, staticData }) {
884
- const routerSnapshotInput = {
885
- matches,
886
- router,
887
- routerLocation,
888
- routerResolvedLocation
889
- };
890
- let routerSnapshot = matchId ? route?.routerSnapshot : void 0;
891
- if (routerSnapshot && !syncRouterSnapshot(routerSnapshot, routerSnapshotInput)) {
892
- if (isCurrentUnmanagedCachedRoute(route, routerHref)) return;
893
- routerSnapshot = void 0;
894
- }
895
- if (matchId && !routerSnapshot) routerSnapshot = createRouterSnapshot(routerSnapshotInput);
896
- setCachedRoutes(routerPathname, {
897
- href: routerHref,
898
- matchId,
899
- routeId,
900
- ready: true,
901
- routerSnapshot,
902
- staticData
903
- });
904
- }
905
- function syncCachedRouteState({ isCurrentMatchReady, isCurrentMatchResolved, deleteCachedRoutes, isCurrentRouteErrored, matchId, matches, route, routeId, router, routerHref, routerLocation, routerPathname, routerResolvedLocation, setCachedRoutes, staticData }) {
906
- if (isCurrentRouteErrored) {
907
- if (route) deleteCachedRoutes([routerPathname]);
908
- return;
909
- }
910
- if (!isCurrentMatchResolved) return;
911
- if (staticData && isRouteCacheEnabled(staticData)) {
912
- if (!isCurrentMatchReady) return;
913
- syncReadyCachedRoute({
914
- matchId,
915
- matches,
916
- routeId,
917
- route,
918
- routerLocation,
919
- router,
920
- routerResolvedLocation,
921
- routerHref,
922
- routerPathname,
923
- setCachedRoutes,
924
- staticData
925
- });
926
- return;
927
- }
928
- if (route) deleteCachedRoutes([routerPathname]);
929
- }
930
- function createRouterSnapshot(input) {
931
- const { router, routerLocation, routerResolvedLocation } = input;
932
- const snapshotData = createRouterSnapshotData(input);
933
- const snapshotMatches = snapshotData.matches;
934
- const matchStores = new Map(snapshotMatches.map((match) => [match.id, createMatchStore(match)]));
935
- const routeMatchStoreCache = /* @__PURE__ */ new Map();
936
- const stores = {
937
- ...router.stores,
938
- status: createSnapshotStore(router.stores.status.get()),
939
- loadedAt: createSnapshotStore(router.stores.loadedAt.get()),
940
- isLoading: createSnapshotStore(router.stores.isLoading.get()),
941
- isTransitioning: createSnapshotStore(router.stores.isTransitioning.get()),
942
- location: createSnapshotStore(routerLocation),
943
- resolvedLocation: createSnapshotStore(routerResolvedLocation),
944
- statusCode: createSnapshotStore(router.stores.statusCode.get()),
945
- redirect: createSnapshotStore(router.stores.redirect.get()),
946
- matchesId: createSnapshotStore(snapshotMatches.map((match) => match.id)),
947
- pendingIds: createSnapshotStore([]),
948
- cachedIds: createSnapshotStore([]),
949
- matches: createSnapshotStore(snapshotMatches),
950
- pendingMatches: createSnapshotStore([]),
951
- cachedMatches: createSnapshotStore([]),
952
- firstId: createSnapshotStore(snapshotMatches[0]?.id),
953
- hasPending: createSnapshotStore(snapshotMatches.some((match) => match.status === "pending")),
954
- matchRouteDeps: createSnapshotStore({
955
- locationHref: routerLocation.href,
956
- resolvedLocationHref: routerResolvedLocation?.href,
957
- status: snapshotData.state.status
958
- }),
959
- __store: createSnapshotStore(snapshotData.state),
960
- matchStores,
961
- pendingMatchStores: /* @__PURE__ */ new Map(),
962
- cachedMatchStores: /* @__PURE__ */ new Map(),
963
- getRouteMatchStore: (routeId) => {
964
- let cached = routeMatchStoreCache.get(routeId);
965
- if (!cached) {
966
- cached = createSnapshotStore(snapshotMatches.find((match) => match.routeId === routeId));
967
- routeMatchStoreCache.set(routeId, cached);
968
- }
969
- return cached;
970
- },
971
- setMatches: () => void 0,
972
- setPending: () => void 0,
973
- setCached: () => void 0
974
- };
975
- const updateSnapshot = (nextInput) => {
976
- const nextData = createRouterSnapshotData(nextInput);
977
- const nextMatches = nextData.matches;
978
- const nextMatchIds = new Set(nextMatches.map((match) => match.id));
979
- const nextMatchesByRouteId = new Map(nextMatches.map((match) => [match.routeId, match]));
980
- for (const match of nextMatches) {
981
- const store = matchStores.get(match.id);
982
- if (store) {
983
- store.set(match);
984
- continue;
985
- }
986
- matchStores.set(match.id, createMatchStore(match));
987
- }
988
- for (const [matchId] of matchStores) if (!nextMatchIds.has(matchId)) matchStores.delete(matchId);
989
- stores.status.set(nextInput.router.stores.status.get());
990
- stores.loadedAt.set(nextInput.router.stores.loadedAt.get());
991
- stores.isLoading.set(nextInput.router.stores.isLoading.get());
992
- stores.isTransitioning.set(nextInput.router.stores.isTransitioning.get());
993
- stores.location.set(nextInput.routerLocation);
994
- stores.resolvedLocation.set(nextInput.routerResolvedLocation);
995
- stores.statusCode.set(nextInput.router.stores.statusCode.get());
996
- stores.redirect.set(nextInput.router.stores.redirect.get());
997
- stores.matchesId.set(nextMatches.map((match) => match.id));
998
- stores.pendingIds.set([]);
999
- stores.cachedIds.set([]);
1000
- stores.matches.set(nextMatches);
1001
- stores.pendingMatches.set([]);
1002
- stores.cachedMatches.set([]);
1003
- stores.firstId.set(nextMatches[0]?.id);
1004
- stores.hasPending.set(nextMatches.some((match) => match.status === "pending"));
1005
- stores.matchRouteDeps.set({
1006
- locationHref: nextInput.routerLocation.href,
1007
- resolvedLocationHref: nextInput.routerResolvedLocation?.href,
1008
- status: nextData.state.status
1009
- });
1010
- stores.__store.set(nextData.state);
1011
- for (const [routeId, store] of routeMatchStoreCache) store.set(nextMatchesByRouteId.get(routeId));
1012
- };
1013
- const routerSnapshot = Object.create(router);
1014
- Object.defineProperties(routerSnapshot, {
1015
- stores: { value: stores },
1016
- latestLocation: { get: () => stores.location.get() },
1017
- getMatch: { value: (matchId) => matchStores.get(matchId)?.get() },
1018
- updateMatch: { value: () => void 0 },
1019
- ...getLiveRouterMethodDescriptors(router)
1020
- });
1021
- routerSnapshotUpdaters.set(routerSnapshot, updateSnapshot);
1022
- return routerSnapshot;
1023
- }
1024
- function getRouterCacheStaticData(childMatches, isCurrentMatchResolved) {
1025
- if (!isCurrentMatchResolved) return;
1026
- for (let i = childMatches.length - 1; i >= 0; i--) {
1027
- const match = childMatches[i];
1028
- if (match?.staticData && isRouteCacheEnabled(match.staticData)) return match.staticData;
1029
- }
1030
- }
1031
- function restoreCachedHref(router, href) {
1032
- router.navigate({
1033
- href,
1034
- replace: true,
1035
- resetScroll: false
1036
- }).catch(() => void 0);
1037
- }
1038
- function renderCachedRoute({ bypassCachedPathname, pathname, route, routerPathname }) {
1039
- if (pathname === bypassCachedPathname) return null;
1040
- const content = route.matchId && route.routerSnapshot ? /* @__PURE__ */ jsx(CachedOutlet, {
1041
- matchId: route.matchId,
1042
- routerSnapshot: route.routerSnapshot
1043
- }) : null;
1044
- if (!content) return null;
1045
- return /* @__PURE__ */ jsx(OffScreen, {
1046
- mode: routerPathname === pathname ? "visible" : "hidden",
1047
- pathname,
1048
- children: content
1049
- }, pathname);
1050
- }
1051
- function buildRouteCacheModes(cachedRoutes, routerPathname) {
1052
- const nextModes = /* @__PURE__ */ new Map();
1053
- for (const pathname of Object.keys(cachedRoutes)) nextModes.set(pathname, routerPathname === pathname ? "visible" : "hidden");
1054
- return nextModes;
1055
- }
1056
- function syncCachedRouteActivityEvents(params) {
1057
- const nextModes = buildRouteCacheModes(params.cachedRoutes, params.routerPathname);
1058
- for (const [pathname, mode] of nextModes) {
1059
- const previousMode = params.previousRouteCacheModes.get(pathname);
1060
- if (previousMode === void 0 && mode === "hidden") continue;
1061
- if (previousMode === mode) continue;
1062
- params.eventListener.emit("activeChange", {
1063
- pathname,
1064
- mode
1065
- });
1066
- }
1067
- for (const [pathname] of params.previousRouteCacheModes) {
1068
- if (nextModes.has(pathname)) continue;
1069
- params.eventListener.emit("activeChange", {
1070
- pathname,
1071
- mode: "hidden"
1072
- });
1073
- }
1074
- return nextModes;
1075
- }
1076
- function RouteCacheManager() {
1077
- const { cachedRoutes, deleteCachedRoutes, erroredRouteCounts, setCachedRoutes, touchCachedRoutes } = useRouterCacheContext();
1078
- const { eventListener } = useEventListener();
1079
- const pendingCachedNavigationRef = useRef(null);
1080
- const previousPathnameRef = useRef(void 0);
1081
- const previousHrefRef = useRef(void 0);
1082
- const previousRouteCacheModesRef = useRef(null);
1083
- previousRouteCacheModesRef.current ??= /* @__PURE__ */ new Map();
1084
- const previousVisiblePathnameRef = useRef(void 0);
1085
- const routerLocation = useRouterState({ select: (state) => toRouterLocation(state.location) });
1086
- const routerHref = routerLocation.href;
1087
- const routerPathname = normalizeCachedRoutePathname(routerLocation.pathname);
1088
- const routerResolvedLocation = useRouterState({ select: (state) => state.resolvedLocation ? toRouterLocation(state.resolvedLocation) : void 0 });
1089
- const resolvedPathname = normalizeCachedRoutePathname(routerResolvedLocation?.pathname ?? routerPathname);
1090
- const destinationRoute = cachedRoutes[routerPathname];
1091
- const matches = useMatches();
1092
- const childMatches = useChildMatches();
1093
- const router = useRouter();
1094
- const currentMatch = matches.length ? matches.at(-1) : void 0;
1095
- const outletRootMatch = childMatches.length ? childMatches[0] : currentMatch;
1096
- const isCurrentMatchResolved = (currentMatch?.pathname ? normalizeCachedRoutePathname(currentMatch.pathname) : void 0) === routerPathname;
1097
- const isCurrentMatchReady = isCurrentMatchResolved && currentMatch?.status === "success";
1098
- const staticData = getRouterCacheStaticData(childMatches, isCurrentMatchResolved);
1099
- const matchId = isCurrentMatchResolved && outletRootMatch ? outletRootMatch.id : void 0;
1100
- const routeId = isCurrentMatchResolved && outletRootMatch ? outletRootMatch.routeId : void 0;
1101
- const currentCachedRoute = cachedRoutes[routerPathname];
1102
- const isCurrentRouteErrored = hasCurrentRouteError({
1103
- erroredRouteCounts,
1104
- matches,
1105
- resolvedPathname,
1106
- routerPathname
1107
- });
1108
- const bypassCachedPathname = isCurrentRouteErrored ? routerPathname : void 0;
1109
- const previousPathname = previousPathnameRef.current;
1110
- previousHrefRef.current;
1111
- const shouldRestoreDestinationHref = shouldRestoreCachedHref({
1112
- cachedHref: destinationRoute?.href,
1113
- currentHref: routerHref,
1114
- currentPathname: routerPathname,
1115
- isRouteCacheEnabled: isRouteCacheEnabled(destinationRoute?.staticData),
1116
- previousPathname
1117
- });
1118
- useLayoutEffect(() => {
1119
- if (shouldRestoreDestinationHref) return;
1120
- syncCachedRouteState({
1121
- isCurrentMatchReady,
1122
- isCurrentMatchResolved,
1123
- isCurrentRouteErrored,
1124
- matchId,
1125
- matches,
1126
- routeId,
1127
- route: currentCachedRoute,
1128
- routerLocation,
1129
- router,
1130
- routerResolvedLocation,
1131
- routerHref,
1132
- routerPathname,
1133
- deleteCachedRoutes,
1134
- setCachedRoutes,
1135
- staticData
1136
- });
1137
- }, [
1138
- currentCachedRoute,
1139
- deleteCachedRoutes,
1140
- isCurrentRouteErrored,
1141
- isCurrentMatchReady,
1142
- isCurrentMatchResolved,
1143
- matchId,
1144
- matches,
1145
- routeId,
1146
- router,
1147
- routerLocation,
1148
- routerResolvedLocation,
1149
- routerHref,
1150
- routerPathname,
1151
- staticData,
1152
- setCachedRoutes,
1153
- shouldRestoreDestinationHref
1154
- ]);
1155
- useLayoutEffect(() => {
1156
- const lastVisitedPathname = previousPathnameRef.current;
1157
- const pendingNavigation = pendingCachedNavigationRef.current;
1158
- if (pendingNavigation && pendingNavigation.pathname !== routerPathname) {
1159
- eventListener.emit("cachedNavigationCancel", pendingNavigation);
1160
- pendingCachedNavigationRef.current = null;
1161
- }
1162
- if (!lastVisitedPathname || lastVisitedPathname === routerPathname) return;
1163
- if (!isReadyCachedRoute(destinationRoute)) return;
1164
- if (pendingCachedNavigationRef.current?.pathname === routerPathname) return;
1165
- const nextNavigation = {
1166
- pathname: routerPathname,
1167
- startedAt: performance.now()
1168
- };
1169
- pendingCachedNavigationRef.current = nextNavigation;
1170
- eventListener.emit("cachedNavigationStart", nextNavigation);
1171
- }, [
1172
- destinationRoute,
1173
- eventListener,
1174
- routerPathname
1175
- ]);
1176
- useLayoutEffect(() => {
1177
- previousRouteCacheModesRef.current ??= /* @__PURE__ */ new Map();
1178
- previousRouteCacheModesRef.current = syncCachedRouteActivityEvents({
1179
- cachedRoutes,
1180
- eventListener,
1181
- previousRouteCacheModes: previousRouteCacheModesRef.current,
1182
- routerPathname
1183
- });
1184
- }, [
1185
- cachedRoutes,
1186
- eventListener,
1187
- routerPathname
1188
- ]);
1189
- useLayoutEffect(() => {
1190
- if (previousVisiblePathnameRef.current === routerPathname || !cachedRoutes[routerPathname]) {
1191
- previousVisiblePathnameRef.current = routerPathname;
1192
- return;
1193
- }
1194
- previousVisiblePathnameRef.current = routerPathname;
1195
- touchCachedRoutes([routerPathname]);
1196
- }, [
1197
- cachedRoutes,
1198
- touchCachedRoutes,
1199
- routerPathname
1200
- ]);
1201
- useEffect(() => {
1202
- const pendingNavigation = pendingCachedNavigationRef.current;
1203
- if (routerPathname !== pendingNavigation?.pathname) return;
1204
- let firstFrameId = 0;
1205
- let secondFrameId = 0;
1206
- firstFrameId = globalThis.requestAnimationFrame(() => {
1207
- const visibleAt = performance.now();
1208
- secondFrameId = globalThis.requestAnimationFrame(() => {
1209
- const paintedAt = performance.now();
1210
- if (pendingCachedNavigationRef.current?.pathname !== pendingNavigation.pathname) return;
1211
- pendingCachedNavigationRef.current = null;
1212
- eventListener.emit("cachedNavigationComplete", {
1213
- ...pendingNavigation,
1214
- duration: paintedAt - pendingNavigation.startedAt,
1215
- paintedAt,
1216
- visibleAt
1217
- });
1218
- });
1219
- });
1220
- return () => {
1221
- globalThis.cancelAnimationFrame(firstFrameId);
1222
- globalThis.cancelAnimationFrame(secondFrameId);
1223
- };
1224
- }, [eventListener, routerPathname]);
1225
- useLayoutEffect(() => {
1226
- if (!(shouldRestoreDestinationHref && destinationRoute?.href)) return;
1227
- restoreCachedHref(router, destinationRoute.href);
1228
- }, [
1229
- destinationRoute?.href,
1230
- router,
1231
- shouldRestoreDestinationHref
1232
- ]);
1233
- useEffect(() => {
1234
- previousPathnameRef.current = routerPathname;
1235
- previousHrefRef.current = routerHref;
1236
- }, [routerHref, routerPathname]);
1237
- const shouldRenderLiveOutlet = routerPathname === bypassCachedPathname || !isReadyCachedRoute(destinationRoute);
1238
- useRouterCacheDebug(cachedRoutes, routerPathname);
1239
- return /* @__PURE__ */ jsxs(Fragment, { children: [Object.entries(cachedRoutes).map(([pathname, route]) => renderCachedRoute({
1240
- bypassCachedPathname,
1241
- pathname,
1242
- route,
1243
- routerPathname
1244
- })), shouldRenderLiveOutlet ? /* @__PURE__ */ jsx(Outlet, {}, `live-${routerPathname}`) : null] });
1245
- }
1246
- //#endregion
1247
- //#region src/components/router-cache-outlet.tsx
1248
- function RouterCacheOutlet(props) {
1249
- const { children } = props;
1250
- return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(RouteCacheManager, {}), children] });
1251
- }
1252
- //#endregion
1253
- //#region src/hooks/use-route-cache-active.ts
1254
- function useRoutePathname(pathname) {
1255
- const locationPathname = useLocation({ select: (location) => location.pathname });
1256
- return normalizeCachedRoutePathname(pathname ?? locationPathname);
1257
- }
1258
- function useRouteCacheActive(pathname) {
1259
- const routePathname = useRoutePathname(pathname);
1260
- const [isActive, setIsActive] = useState(true);
1261
- useEventListener({ on: { activeChange: ({ pathname: changedPathname, mode }) => {
1262
- if (changedPathname === routePathname) setIsActive(mode === "visible");
1263
- } } });
1264
- return isActive;
1265
- }
1266
- //#endregion
1267
- //#region src/hooks/use-route-cache-activity.ts
1268
- function useRouteCacheActivity(fn) {
1269
- const routePathname = useLocation({ select: (location) => location.pathname });
1270
- useEventListener({ on: { activeChange: ({ pathname, mode, callback }) => {
1271
- if (pathname === routePathname) fn(mode === "visible");
1272
- callback?.();
1273
- } } });
1274
- }
1275
- //#endregion
1276
- //#region src/hooks/use-route-cache-effect.ts
1277
- function areDependenciesEqual(previousDeps, nextDeps) {
1278
- if (previousDeps?.length !== nextDeps.length) return false;
1279
- for (let index = 0; index < nextDeps.length; index += 1) if (!Object.is(previousDeps[index], nextDeps[index])) return false;
1280
- return true;
1281
- }
1282
- function useRouteCacheEffect(activeCallback, deps = []) {
1283
- const returnValue = useRef(void 0);
1284
- const isActiveRef = useRef(false);
1285
- const previousCallbackRef = useRef(void 0);
1286
- const previousDepsRef = useRef(void 0);
1287
- useEffect(() => {
1288
- const callbackChanged = previousCallbackRef.current !== activeCallback;
1289
- const dependenciesChanged = !areDependenciesEqual(previousDepsRef.current, deps);
1290
- if (!(callbackChanged || dependenciesChanged)) return;
1291
- previousCallbackRef.current = activeCallback;
1292
- previousDepsRef.current = deps;
1293
- if (!isActiveRef.current) return;
1294
- returnValue.current?.();
1295
- returnValue.current = activeCallback();
1296
- });
1297
- useEffect(() => () => {
1298
- returnValue.current?.();
1299
- }, []);
1300
- useRouteCacheActivity((active) => {
1301
- if (active) {
1302
- isActiveRef.current = true;
1303
- returnValue.current = activeCallback();
1304
- return;
1305
- }
1306
- isActiveRef.current = false;
1307
- returnValue.current?.();
1308
- });
1309
- }
1310
- //#endregion
1311
- //#region src/hooks/use-route-cache-error-boundary.ts
1312
- function useRouteCacheErrorBoundary(pathname) {
1313
- const context = useOptionalRouterCacheContext();
1314
- const routePathname = useLocation({ select: (location) => location.pathname });
1315
- useEffect(() => {
1316
- if (!context) return;
1317
- const targetPathname = normalizeCachedRoutePathname(pathname ?? routePathname);
1318
- context.retainErroredRoute(targetPathname);
1319
- return () => {
1320
- context.releaseErroredRoute(targetPathname);
1321
- };
1322
- }, [
1323
- context,
1324
- pathname,
1325
- routePathname
1326
- ]);
1327
- }
1328
- //#endregion
1329
- //#region src/hooks/use-route-cache-navigation.ts
1330
- const INITIAL_STATE = {
1331
- activeNavigation: null,
1332
- lastCompletedNavigation: null
1333
- };
1334
- function useRouteCacheNavigation() {
1335
- const [state, setState] = useState(INITIAL_STATE);
1336
- useEventListener({ on: {
1337
- cachedNavigationStart: (navigation) => {
1338
- setState((current) => ({
1339
- ...current,
1340
- activeNavigation: navigation
1341
- }));
1342
- },
1343
- cachedNavigationCancel: (navigation) => {
1344
- setState((current) => {
1345
- if (current.activeNavigation?.pathname !== navigation.pathname) return current;
1346
- return {
1347
- ...current,
1348
- activeNavigation: null
1349
- };
1350
- });
1351
- },
1352
- cachedNavigationComplete: (navigation) => {
1353
- setState((current) => ({
1354
- activeNavigation: current.activeNavigation?.pathname === navigation.pathname ? null : current.activeNavigation,
1355
- lastCompletedNavigation: navigation
1356
- }));
1357
- }
1358
- } });
1359
- return state;
1360
- }
1361
- //#endregion
1362
- //#region src/hooks/use-update.ts
1363
- function useUpdate() {
1364
- const [, setState] = useState({});
1365
- return useCallback(() => setState({}), []);
1366
- }
1367
- //#endregion
1368
- //#region src/hooks/use-router-cache.ts
1369
- function useRouterCache() {
1370
- const { cachedRoutes, deleteCachedRoutes } = useRouterCacheContext();
1371
- const update = useUpdate();
1372
- useEventListener({ on: { activeChange: () => {
1373
- update();
1374
- } } });
1375
- const destroy = (pathname) => {
1376
- deleteCachedRoutes(Array.isArray(pathname) ? pathname : [pathname]);
1377
- };
1378
- const destroyAll = () => {
1379
- deleteCachedRoutes(Object.keys(cachedRoutes));
1380
- };
1381
- const invalidateWhere = (predicate) => {
1382
- const pathnames = Object.entries(cachedRoutes).flatMap(([pathname, route]) => predicate(pathname, route) ? [pathname] : []);
1383
- if (pathnames.length > 0) deleteCachedRoutes(pathnames);
1384
- return pathnames;
1385
- };
1386
- return {
1387
- cachedRoutes,
1388
- destroy,
1389
- destroyAll,
1390
- invalidateWhere,
1391
- isCached: (pathname) => Object.hasOwn(cachedRoutes, normalizeCachedRoutePathname(pathname))
1392
- };
1393
- }
1394
- //#endregion
1395
- export { RouterCacheOutlet, RouterCacheProvider, useRouteCacheActive, useRouteCacheActivity, useRouteCacheEffect, useRouteCacheErrorBoundary, useRouteCacheNavigation, useRouterCache };
1
+ import { defineRouteCache } from "./route-cache-static-data.js";
2
+ import { RouterCacheProvider } from "./contexts/router-cache.js";
3
+ import { RouterCacheOutlet } from "./components/router-cache-outlet.js";
4
+ import { useRouteCacheActive } from "./hooks/use-route-cache-active.js";
5
+ import { useRouteCacheActivity } from "./hooks/use-route-cache-activity.js";
6
+ import { useRouteCacheEffect } from "./hooks/use-route-cache-effect.js";
7
+ import { useRouteCacheErrorBoundary } from "./hooks/use-route-cache-error-boundary.js";
8
+ import { useRouteCacheNavigation } from "./hooks/use-route-cache-navigation.js";
9
+ import { useRouterCache } from "./hooks/use-router-cache.js";
10
+ export { RouterCacheOutlet, RouterCacheProvider, defineRouteCache, useRouteCacheActive, useRouteCacheActivity, useRouteCacheEffect, useRouteCacheErrorBoundary, useRouteCacheNavigation, useRouterCache };