tanstack-router-cache 0.1.3 → 0.1.5

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/index.cjs CHANGED
@@ -787,12 +787,50 @@ const LIVE_ROUTER_METHODS = [
787
787
  "navigate",
788
788
  "preloadRoute"
789
789
  ];
790
- function createStaticStore(value) {
790
+ const routerSnapshotUpdaters = /* @__PURE__ */ new WeakMap();
791
+ function createSnapshotStore(value) {
792
+ let currentValue = value;
793
+ const listeners = /* @__PURE__ */ new Set();
791
794
  return {
792
- get: () => value,
793
- subscribe: () => ({ unsubscribe: () => void 0 })
795
+ get: () => currentValue,
796
+ set: (nextValue) => {
797
+ if (Object.is(currentValue, nextValue)) return;
798
+ currentValue = nextValue;
799
+ for (const listener of listeners) listener(currentValue);
800
+ },
801
+ subscribe: (listener) => {
802
+ if (listener) listeners.add(listener);
803
+ return { unsubscribe: listener ? () => {
804
+ listeners.delete(listener);
805
+ } : () => void 0 };
806
+ }
807
+ };
808
+ }
809
+ function createRouterSnapshotData({ matches, router, routerLocation, routerResolvedLocation }) {
810
+ const snapshotMatches = matches.reduce((snapshot, match) => {
811
+ const nextMatch = snapshotMatch(match, routerLocation);
812
+ if (isRouterMatch(nextMatch)) snapshot.push(nextMatch);
813
+ return snapshot;
814
+ }, []);
815
+ return {
816
+ matches: snapshotMatches,
817
+ state: {
818
+ ...router.stores.__store.get(),
819
+ matches: snapshotMatches,
820
+ location: routerLocation,
821
+ resolvedLocation: routerResolvedLocation
822
+ }
794
823
  };
795
824
  }
825
+ function createMatchStore(match) {
826
+ return Object.assign(createSnapshotStore(match), { routeId: match.routeId });
827
+ }
828
+ function syncRouterSnapshot(routerSnapshot, input) {
829
+ const update = routerSnapshotUpdaters.get(routerSnapshot);
830
+ if (!update) return false;
831
+ update(input);
832
+ return true;
833
+ }
796
834
  function getLiveRouterMethodDescriptors(router) {
797
835
  return Object.fromEntries(LIVE_ROUTER_METHODS.flatMap((methodName) => {
798
836
  const method = router[methodName];
@@ -828,29 +866,40 @@ function hasCurrentRouteError({ erroredRouteCounts, matches, resolvedPathname, r
828
866
  function isRouterMatch(match) {
829
867
  return Boolean(match?.id && match.routeId);
830
868
  }
831
- function snapshotMatch(match) {
869
+ function snapshotMatch(match, routerLocation) {
832
870
  if (!isRouterMatch(match)) return;
871
+ const isLocationMatch = match.pathname ? normalizeCachedRoutePathname(match.pathname) === normalizeCachedRoutePathname(routerLocation.pathname) : false;
833
872
  return {
834
873
  ...match,
874
+ ...isLocationMatch ? {
875
+ _strictSearch: routerLocation.search,
876
+ search: routerLocation.search
877
+ } : {},
835
878
  _nonReactive: { ...match._nonReactive }
836
879
  };
837
880
  }
838
- function shouldRefreshCachedRoute(route, routerHref) {
839
- return !(isRouteCacheEnabled(route?.staticData) && route.ready) || route.href !== routerHref;
881
+ function isCurrentUnmanagedCachedRoute(route, routerHref) {
882
+ return Boolean(route && isRouteCacheEnabled(route.staticData) && route.ready && route.routerSnapshot && route.href === routerHref);
840
883
  }
841
884
  function syncReadyCachedRoute({ matchId, matches, routeId, route, routerLocation, router, routerResolvedLocation, routerHref, routerPathname, setCachedRoutes, staticData }) {
842
- if (!shouldRefreshCachedRoute(route, routerHref)) return;
885
+ const routerSnapshotInput = {
886
+ matches,
887
+ router,
888
+ routerLocation,
889
+ routerResolvedLocation
890
+ };
891
+ let routerSnapshot = matchId ? route?.routerSnapshot : void 0;
892
+ if (routerSnapshot && !syncRouterSnapshot(routerSnapshot, routerSnapshotInput)) {
893
+ if (isCurrentUnmanagedCachedRoute(route, routerHref)) return;
894
+ routerSnapshot = void 0;
895
+ }
896
+ if (matchId && !routerSnapshot) routerSnapshot = createRouterSnapshot(routerSnapshotInput);
843
897
  setCachedRoutes(routerPathname, {
844
898
  href: routerHref,
845
899
  matchId,
846
900
  routeId,
847
901
  ready: true,
848
- routerSnapshot: matchId ? createRouterSnapshot({
849
- matches,
850
- router,
851
- routerLocation,
852
- routerResolvedLocation
853
- }) : void 0,
902
+ routerSnapshot,
854
903
  staticData
855
904
  });
856
905
  }
@@ -879,54 +928,43 @@ function syncCachedRouteState({ isCurrentMatchReady, isCurrentMatchResolved, del
879
928
  }
880
929
  if (route) deleteCachedRoutes([routerPathname]);
881
930
  }
882
- function createRouterSnapshot({ matches, router, routerLocation, routerResolvedLocation }) {
883
- const snapshotMatches = matches.reduce((snapshot, match) => {
884
- const nextMatch = snapshotMatch(match);
885
- if (isRouterMatch(nextMatch)) snapshot.push(nextMatch);
886
- return snapshot;
887
- }, []);
888
- const matchStores = new Map(snapshotMatches.map((match) => {
889
- const store = Object.assign(createStaticStore(match), { routeId: match.routeId });
890
- return [match.id, store];
891
- }));
892
- const snapshotState = {
893
- ...router.stores.__store.get(),
894
- matches: snapshotMatches,
895
- location: routerLocation,
896
- resolvedLocation: routerResolvedLocation
897
- };
931
+ function createRouterSnapshot(input) {
932
+ const { router, routerLocation, routerResolvedLocation } = input;
933
+ const snapshotData = createRouterSnapshotData(input);
934
+ const snapshotMatches = snapshotData.matches;
935
+ const matchStores = new Map(snapshotMatches.map((match) => [match.id, createMatchStore(match)]));
898
936
  const routeMatchStoreCache = /* @__PURE__ */ new Map();
899
937
  const stores = {
900
938
  ...router.stores,
901
- status: createStaticStore(router.stores.status.get()),
902
- loadedAt: createStaticStore(router.stores.loadedAt.get()),
903
- isLoading: createStaticStore(router.stores.isLoading.get()),
904
- isTransitioning: createStaticStore(router.stores.isTransitioning.get()),
905
- location: createStaticStore(routerLocation),
906
- resolvedLocation: createStaticStore(routerResolvedLocation),
907
- statusCode: createStaticStore(router.stores.statusCode.get()),
908
- redirect: createStaticStore(router.stores.redirect.get()),
909
- matchesId: createStaticStore(snapshotMatches.map((match) => match.id)),
910
- pendingIds: createStaticStore([]),
911
- cachedIds: createStaticStore([]),
912
- matches: createStaticStore(snapshotMatches),
913
- pendingMatches: createStaticStore([]),
914
- cachedMatches: createStaticStore([]),
915
- firstId: createStaticStore(snapshotMatches[0]?.id),
916
- hasPending: createStaticStore(snapshotMatches.some((match) => match.status === "pending")),
917
- matchRouteDeps: createStaticStore({
939
+ status: createSnapshotStore(router.stores.status.get()),
940
+ loadedAt: createSnapshotStore(router.stores.loadedAt.get()),
941
+ isLoading: createSnapshotStore(router.stores.isLoading.get()),
942
+ isTransitioning: createSnapshotStore(router.stores.isTransitioning.get()),
943
+ location: createSnapshotStore(routerLocation),
944
+ resolvedLocation: createSnapshotStore(routerResolvedLocation),
945
+ statusCode: createSnapshotStore(router.stores.statusCode.get()),
946
+ redirect: createSnapshotStore(router.stores.redirect.get()),
947
+ matchesId: createSnapshotStore(snapshotMatches.map((match) => match.id)),
948
+ pendingIds: createSnapshotStore([]),
949
+ cachedIds: createSnapshotStore([]),
950
+ matches: createSnapshotStore(snapshotMatches),
951
+ pendingMatches: createSnapshotStore([]),
952
+ cachedMatches: createSnapshotStore([]),
953
+ firstId: createSnapshotStore(snapshotMatches[0]?.id),
954
+ hasPending: createSnapshotStore(snapshotMatches.some((match) => match.status === "pending")),
955
+ matchRouteDeps: createSnapshotStore({
918
956
  locationHref: routerLocation.href,
919
957
  resolvedLocationHref: routerResolvedLocation?.href,
920
- status: snapshotState.status
958
+ status: snapshotData.state.status
921
959
  }),
922
- __store: createStaticStore(snapshotState),
960
+ __store: createSnapshotStore(snapshotData.state),
923
961
  matchStores,
924
962
  pendingMatchStores: /* @__PURE__ */ new Map(),
925
963
  cachedMatchStores: /* @__PURE__ */ new Map(),
926
964
  getRouteMatchStore: (routeId) => {
927
965
  let cached = routeMatchStoreCache.get(routeId);
928
966
  if (!cached) {
929
- cached = createStaticStore(snapshotMatches.find((match) => match.routeId === routeId));
967
+ cached = createSnapshotStore(snapshotMatches.find((match) => match.routeId === routeId));
930
968
  routeMatchStoreCache.set(routeId, cached);
931
969
  }
932
970
  return cached;
@@ -935,14 +973,53 @@ function createRouterSnapshot({ matches, router, routerLocation, routerResolvedL
935
973
  setPending: () => void 0,
936
974
  setCached: () => void 0
937
975
  };
976
+ const updateSnapshot = (nextInput) => {
977
+ const nextData = createRouterSnapshotData(nextInput);
978
+ const nextMatches = nextData.matches;
979
+ const nextMatchIds = new Set(nextMatches.map((match) => match.id));
980
+ const nextMatchesByRouteId = new Map(nextMatches.map((match) => [match.routeId, match]));
981
+ for (const match of nextMatches) {
982
+ const store = matchStores.get(match.id);
983
+ if (store) {
984
+ store.set(match);
985
+ continue;
986
+ }
987
+ matchStores.set(match.id, createMatchStore(match));
988
+ }
989
+ for (const [matchId] of matchStores) if (!nextMatchIds.has(matchId)) matchStores.delete(matchId);
990
+ stores.status.set(nextInput.router.stores.status.get());
991
+ stores.loadedAt.set(nextInput.router.stores.loadedAt.get());
992
+ stores.isLoading.set(nextInput.router.stores.isLoading.get());
993
+ stores.isTransitioning.set(nextInput.router.stores.isTransitioning.get());
994
+ stores.location.set(nextInput.routerLocation);
995
+ stores.resolvedLocation.set(nextInput.routerResolvedLocation);
996
+ stores.statusCode.set(nextInput.router.stores.statusCode.get());
997
+ stores.redirect.set(nextInput.router.stores.redirect.get());
998
+ stores.matchesId.set(nextMatches.map((match) => match.id));
999
+ stores.pendingIds.set([]);
1000
+ stores.cachedIds.set([]);
1001
+ stores.matches.set(nextMatches);
1002
+ stores.pendingMatches.set([]);
1003
+ stores.cachedMatches.set([]);
1004
+ stores.firstId.set(nextMatches[0]?.id);
1005
+ stores.hasPending.set(nextMatches.some((match) => match.status === "pending"));
1006
+ stores.matchRouteDeps.set({
1007
+ locationHref: nextInput.routerLocation.href,
1008
+ resolvedLocationHref: nextInput.routerResolvedLocation?.href,
1009
+ status: nextData.state.status
1010
+ });
1011
+ stores.__store.set(nextData.state);
1012
+ for (const [routeId, store] of routeMatchStoreCache) store.set(nextMatchesByRouteId.get(routeId));
1013
+ };
938
1014
  const routerSnapshot = Object.create(router);
939
1015
  Object.defineProperties(routerSnapshot, {
940
1016
  stores: { value: stores },
941
- latestLocation: { value: routerLocation },
1017
+ latestLocation: { get: () => stores.location.get() },
942
1018
  getMatch: { value: (matchId) => matchStores.get(matchId)?.get() },
943
1019
  updateMatch: { value: () => void 0 },
944
1020
  ...getLiveRouterMethodDescriptors(router)
945
1021
  });
1022
+ routerSnapshotUpdaters.set(routerSnapshot, updateSnapshot);
946
1023
  return routerSnapshot;
947
1024
  }
948
1025
  function getRouterCacheStaticData(childMatches, isCurrentMatchResolved) {
@@ -959,17 +1036,7 @@ function restoreCachedHref(router, href) {
959
1036
  resetScroll: false
960
1037
  }).catch(() => void 0);
961
1038
  }
962
- function isAncestorPathname(ancestorPathname, pathname) {
963
- if (ancestorPathname === pathname) return false;
964
- if (ancestorPathname === "/") return pathname.startsWith("/");
965
- return pathname.startsWith(`${ancestorPathname}/`);
966
- }
967
- function getShouldRenderLiveOutlet({ cachedRoutes, bypassCachedPathname, visiblePathname }) {
968
- if (visiblePathname === bypassCachedPathname) return true;
969
- const visibleRoute = cachedRoutes[visiblePathname];
970
- return !isReadyCachedRoute(visibleRoute);
971
- }
972
- function renderCachedRoute({ bypassCachedPathname, pathname, route, visiblePathname }) {
1039
+ function renderCachedRoute({ bypassCachedPathname, pathname, route, routerPathname }) {
973
1040
  if (pathname === bypassCachedPathname) return null;
974
1041
  const content = route.matchId && route.routerSnapshot ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CachedOutlet, {
975
1042
  matchId: route.matchId,
@@ -977,18 +1044,18 @@ function renderCachedRoute({ bypassCachedPathname, pathname, route, visiblePathn
977
1044
  }) : null;
978
1045
  if (!content) return null;
979
1046
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OffScreen, {
980
- mode: visiblePathname === pathname ? "visible" : "hidden",
1047
+ mode: routerPathname === pathname ? "visible" : "hidden",
981
1048
  pathname,
982
1049
  children: content
983
1050
  }, pathname);
984
1051
  }
985
- function buildRouteCacheModes(cachedRoutes, visiblePathname) {
1052
+ function buildRouteCacheModes(cachedRoutes, routerPathname) {
986
1053
  const nextModes = /* @__PURE__ */ new Map();
987
- for (const pathname of Object.keys(cachedRoutes)) nextModes.set(pathname, visiblePathname === pathname ? "visible" : "hidden");
1054
+ for (const pathname of Object.keys(cachedRoutes)) nextModes.set(pathname, routerPathname === pathname ? "visible" : "hidden");
988
1055
  return nextModes;
989
1056
  }
990
1057
  function syncCachedRouteActivityEvents(params) {
991
- const nextModes = buildRouteCacheModes(params.cachedRoutes, params.visiblePathname);
1058
+ const nextModes = buildRouteCacheModes(params.cachedRoutes, params.routerPathname);
992
1059
  for (const [pathname, mode] of nextModes) {
993
1060
  const previousMode = params.previousRouteCacheModes.get(pathname);
994
1061
  if (previousMode === void 0 && mode === "hidden") continue;
@@ -1014,7 +1081,7 @@ function RouteCacheManager() {
1014
1081
  const previousPathnameRef = (0, react.useRef)(void 0);
1015
1082
  const previousHrefRef = (0, react.useRef)(void 0);
1016
1083
  const previousRouteCacheModesRef = (0, react.useRef)(null);
1017
- if (previousRouteCacheModesRef.current === null) previousRouteCacheModesRef.current = /* @__PURE__ */ new Map();
1084
+ previousRouteCacheModesRef.current ??= /* @__PURE__ */ new Map();
1018
1085
  const previousVisiblePathnameRef = (0, react.useRef)(void 0);
1019
1086
  const routerLocation = (0, _tanstack_react_router.useRouterState)({ select: (state) => toRouterLocation(state.location) });
1020
1087
  const routerHref = routerLocation.href;
@@ -1022,7 +1089,6 @@ function RouteCacheManager() {
1022
1089
  const routerResolvedLocation = (0, _tanstack_react_router.useRouterState)({ select: (state) => state.resolvedLocation ? toRouterLocation(state.resolvedLocation) : void 0 });
1023
1090
  const resolvedPathname = normalizeCachedRoutePathname(routerResolvedLocation?.pathname ?? routerPathname);
1024
1091
  const destinationRoute = cachedRoutes[routerPathname];
1025
- const visiblePathname = routerPathname !== resolvedPathname && (isReadyCachedRoute(destinationRoute) || isAncestorPathname(routerPathname, resolvedPathname)) ? routerPathname : resolvedPathname;
1026
1092
  const matches = (0, _tanstack_react_router.useMatches)();
1027
1093
  const childMatches = (0, _tanstack_react_router.useChildMatches)();
1028
1094
  const router = (0, _tanstack_react_router.useRouter)();
@@ -1090,7 +1156,7 @@ function RouteCacheManager() {
1090
1156
  (0, react.useLayoutEffect)(() => {
1091
1157
  const lastVisitedPathname = previousPathnameRef.current;
1092
1158
  const pendingNavigation = pendingCachedNavigationRef.current;
1093
- if (pendingNavigation && pendingNavigation.pathname !== routerPathname && visiblePathname !== pendingNavigation.pathname) {
1159
+ if (pendingNavigation && pendingNavigation.pathname !== routerPathname) {
1094
1160
  eventListener.emit("cachedNavigationCancel", pendingNavigation);
1095
1161
  pendingCachedNavigationRef.current = null;
1096
1162
  }
@@ -1106,37 +1172,36 @@ function RouteCacheManager() {
1106
1172
  }, [
1107
1173
  destinationRoute,
1108
1174
  eventListener,
1109
- routerPathname,
1110
- visiblePathname
1175
+ routerPathname
1111
1176
  ]);
1112
1177
  (0, react.useLayoutEffect)(() => {
1113
- if (previousRouteCacheModesRef.current === null) previousRouteCacheModesRef.current = /* @__PURE__ */ new Map();
1178
+ previousRouteCacheModesRef.current ??= /* @__PURE__ */ new Map();
1114
1179
  previousRouteCacheModesRef.current = syncCachedRouteActivityEvents({
1115
1180
  cachedRoutes,
1116
1181
  eventListener,
1117
1182
  previousRouteCacheModes: previousRouteCacheModesRef.current,
1118
- visiblePathname
1183
+ routerPathname
1119
1184
  });
1120
1185
  }, [
1121
1186
  cachedRoutes,
1122
1187
  eventListener,
1123
- visiblePathname
1188
+ routerPathname
1124
1189
  ]);
1125
1190
  (0, react.useLayoutEffect)(() => {
1126
- if (previousVisiblePathnameRef.current === visiblePathname || !cachedRoutes[visiblePathname]) {
1127
- previousVisiblePathnameRef.current = visiblePathname;
1191
+ if (previousVisiblePathnameRef.current === routerPathname || !cachedRoutes[routerPathname]) {
1192
+ previousVisiblePathnameRef.current = routerPathname;
1128
1193
  return;
1129
1194
  }
1130
- previousVisiblePathnameRef.current = visiblePathname;
1131
- touchCachedRoutes([visiblePathname]);
1195
+ previousVisiblePathnameRef.current = routerPathname;
1196
+ touchCachedRoutes([routerPathname]);
1132
1197
  }, [
1133
1198
  cachedRoutes,
1134
1199
  touchCachedRoutes,
1135
- visiblePathname
1200
+ routerPathname
1136
1201
  ]);
1137
1202
  (0, react.useEffect)(() => {
1138
1203
  const pendingNavigation = pendingCachedNavigationRef.current;
1139
- if (visiblePathname !== pendingNavigation?.pathname) return;
1204
+ if (routerPathname !== pendingNavigation?.pathname) return;
1140
1205
  let firstFrameId = 0;
1141
1206
  let secondFrameId = 0;
1142
1207
  firstFrameId = globalThis.requestAnimationFrame(() => {
@@ -1157,7 +1222,7 @@ function RouteCacheManager() {
1157
1222
  globalThis.cancelAnimationFrame(firstFrameId);
1158
1223
  globalThis.cancelAnimationFrame(secondFrameId);
1159
1224
  };
1160
- }, [eventListener, visiblePathname]);
1225
+ }, [eventListener, routerPathname]);
1161
1226
  (0, react.useLayoutEffect)(() => {
1162
1227
  if (!(shouldRestoreDestinationHref && destinationRoute?.href)) return;
1163
1228
  restoreCachedHref(router, destinationRoute.href);
@@ -1170,17 +1235,13 @@ function RouteCacheManager() {
1170
1235
  previousPathnameRef.current = routerPathname;
1171
1236
  previousHrefRef.current = routerHref;
1172
1237
  }, [routerHref, routerPathname]);
1173
- const shouldRenderLiveOutlet = getShouldRenderLiveOutlet({
1174
- cachedRoutes,
1175
- bypassCachedPathname,
1176
- visiblePathname
1177
- });
1178
- useRouterCacheDebug(cachedRoutes, visiblePathname);
1238
+ const shouldRenderLiveOutlet = routerPathname === bypassCachedPathname || !isReadyCachedRoute(destinationRoute);
1239
+ useRouterCacheDebug(cachedRoutes, routerPathname);
1179
1240
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [Object.entries(cachedRoutes).map(([pathname, route]) => renderCachedRoute({
1180
1241
  bypassCachedPathname,
1181
1242
  pathname,
1182
1243
  route,
1183
- visiblePathname
1244
+ routerPathname
1184
1245
  })), shouldRenderLiveOutlet ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_tanstack_react_router.Outlet, {}, `live-${routerPathname}`) : null] });
1185
1246
  }
1186
1247
  //#endregion
package/dist/index.js CHANGED
@@ -786,12 +786,50 @@ const LIVE_ROUTER_METHODS = [
786
786
  "navigate",
787
787
  "preloadRoute"
788
788
  ];
789
- function createStaticStore(value) {
789
+ const routerSnapshotUpdaters = /* @__PURE__ */ new WeakMap();
790
+ function createSnapshotStore(value) {
791
+ let currentValue = value;
792
+ const listeners = /* @__PURE__ */ new Set();
790
793
  return {
791
- get: () => value,
792
- subscribe: () => ({ unsubscribe: () => void 0 })
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
+ }
793
822
  };
794
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
+ }
795
833
  function getLiveRouterMethodDescriptors(router) {
796
834
  return Object.fromEntries(LIVE_ROUTER_METHODS.flatMap((methodName) => {
797
835
  const method = router[methodName];
@@ -827,29 +865,40 @@ function hasCurrentRouteError({ erroredRouteCounts, matches, resolvedPathname, r
827
865
  function isRouterMatch(match) {
828
866
  return Boolean(match?.id && match.routeId);
829
867
  }
830
- function snapshotMatch(match) {
868
+ function snapshotMatch(match, routerLocation) {
831
869
  if (!isRouterMatch(match)) return;
870
+ const isLocationMatch = match.pathname ? normalizeCachedRoutePathname(match.pathname) === normalizeCachedRoutePathname(routerLocation.pathname) : false;
832
871
  return {
833
872
  ...match,
873
+ ...isLocationMatch ? {
874
+ _strictSearch: routerLocation.search,
875
+ search: routerLocation.search
876
+ } : {},
834
877
  _nonReactive: { ...match._nonReactive }
835
878
  };
836
879
  }
837
- function shouldRefreshCachedRoute(route, routerHref) {
838
- return !(isRouteCacheEnabled(route?.staticData) && route.ready) || route.href !== routerHref;
880
+ function isCurrentUnmanagedCachedRoute(route, routerHref) {
881
+ return Boolean(route && isRouteCacheEnabled(route.staticData) && route.ready && route.routerSnapshot && route.href === routerHref);
839
882
  }
840
883
  function syncReadyCachedRoute({ matchId, matches, routeId, route, routerLocation, router, routerResolvedLocation, routerHref, routerPathname, setCachedRoutes, staticData }) {
841
- if (!shouldRefreshCachedRoute(route, routerHref)) return;
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);
842
896
  setCachedRoutes(routerPathname, {
843
897
  href: routerHref,
844
898
  matchId,
845
899
  routeId,
846
900
  ready: true,
847
- routerSnapshot: matchId ? createRouterSnapshot({
848
- matches,
849
- router,
850
- routerLocation,
851
- routerResolvedLocation
852
- }) : void 0,
901
+ routerSnapshot,
853
902
  staticData
854
903
  });
855
904
  }
@@ -878,54 +927,43 @@ function syncCachedRouteState({ isCurrentMatchReady, isCurrentMatchResolved, del
878
927
  }
879
928
  if (route) deleteCachedRoutes([routerPathname]);
880
929
  }
881
- function createRouterSnapshot({ matches, router, routerLocation, routerResolvedLocation }) {
882
- const snapshotMatches = matches.reduce((snapshot, match) => {
883
- const nextMatch = snapshotMatch(match);
884
- if (isRouterMatch(nextMatch)) snapshot.push(nextMatch);
885
- return snapshot;
886
- }, []);
887
- const matchStores = new Map(snapshotMatches.map((match) => {
888
- const store = Object.assign(createStaticStore(match), { routeId: match.routeId });
889
- return [match.id, store];
890
- }));
891
- const snapshotState = {
892
- ...router.stores.__store.get(),
893
- matches: snapshotMatches,
894
- location: routerLocation,
895
- resolvedLocation: routerResolvedLocation
896
- };
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)]));
897
935
  const routeMatchStoreCache = /* @__PURE__ */ new Map();
898
936
  const stores = {
899
937
  ...router.stores,
900
- status: createStaticStore(router.stores.status.get()),
901
- loadedAt: createStaticStore(router.stores.loadedAt.get()),
902
- isLoading: createStaticStore(router.stores.isLoading.get()),
903
- isTransitioning: createStaticStore(router.stores.isTransitioning.get()),
904
- location: createStaticStore(routerLocation),
905
- resolvedLocation: createStaticStore(routerResolvedLocation),
906
- statusCode: createStaticStore(router.stores.statusCode.get()),
907
- redirect: createStaticStore(router.stores.redirect.get()),
908
- matchesId: createStaticStore(snapshotMatches.map((match) => match.id)),
909
- pendingIds: createStaticStore([]),
910
- cachedIds: createStaticStore([]),
911
- matches: createStaticStore(snapshotMatches),
912
- pendingMatches: createStaticStore([]),
913
- cachedMatches: createStaticStore([]),
914
- firstId: createStaticStore(snapshotMatches[0]?.id),
915
- hasPending: createStaticStore(snapshotMatches.some((match) => match.status === "pending")),
916
- matchRouteDeps: createStaticStore({
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({
917
955
  locationHref: routerLocation.href,
918
956
  resolvedLocationHref: routerResolvedLocation?.href,
919
- status: snapshotState.status
957
+ status: snapshotData.state.status
920
958
  }),
921
- __store: createStaticStore(snapshotState),
959
+ __store: createSnapshotStore(snapshotData.state),
922
960
  matchStores,
923
961
  pendingMatchStores: /* @__PURE__ */ new Map(),
924
962
  cachedMatchStores: /* @__PURE__ */ new Map(),
925
963
  getRouteMatchStore: (routeId) => {
926
964
  let cached = routeMatchStoreCache.get(routeId);
927
965
  if (!cached) {
928
- cached = createStaticStore(snapshotMatches.find((match) => match.routeId === routeId));
966
+ cached = createSnapshotStore(snapshotMatches.find((match) => match.routeId === routeId));
929
967
  routeMatchStoreCache.set(routeId, cached);
930
968
  }
931
969
  return cached;
@@ -934,14 +972,53 @@ function createRouterSnapshot({ matches, router, routerLocation, routerResolvedL
934
972
  setPending: () => void 0,
935
973
  setCached: () => void 0
936
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
+ };
937
1013
  const routerSnapshot = Object.create(router);
938
1014
  Object.defineProperties(routerSnapshot, {
939
1015
  stores: { value: stores },
940
- latestLocation: { value: routerLocation },
1016
+ latestLocation: { get: () => stores.location.get() },
941
1017
  getMatch: { value: (matchId) => matchStores.get(matchId)?.get() },
942
1018
  updateMatch: { value: () => void 0 },
943
1019
  ...getLiveRouterMethodDescriptors(router)
944
1020
  });
1021
+ routerSnapshotUpdaters.set(routerSnapshot, updateSnapshot);
945
1022
  return routerSnapshot;
946
1023
  }
947
1024
  function getRouterCacheStaticData(childMatches, isCurrentMatchResolved) {
@@ -958,17 +1035,7 @@ function restoreCachedHref(router, href) {
958
1035
  resetScroll: false
959
1036
  }).catch(() => void 0);
960
1037
  }
961
- function isAncestorPathname(ancestorPathname, pathname) {
962
- if (ancestorPathname === pathname) return false;
963
- if (ancestorPathname === "/") return pathname.startsWith("/");
964
- return pathname.startsWith(`${ancestorPathname}/`);
965
- }
966
- function getShouldRenderLiveOutlet({ cachedRoutes, bypassCachedPathname, visiblePathname }) {
967
- if (visiblePathname === bypassCachedPathname) return true;
968
- const visibleRoute = cachedRoutes[visiblePathname];
969
- return !isReadyCachedRoute(visibleRoute);
970
- }
971
- function renderCachedRoute({ bypassCachedPathname, pathname, route, visiblePathname }) {
1038
+ function renderCachedRoute({ bypassCachedPathname, pathname, route, routerPathname }) {
972
1039
  if (pathname === bypassCachedPathname) return null;
973
1040
  const content = route.matchId && route.routerSnapshot ? /* @__PURE__ */ jsx(CachedOutlet, {
974
1041
  matchId: route.matchId,
@@ -976,18 +1043,18 @@ function renderCachedRoute({ bypassCachedPathname, pathname, route, visiblePathn
976
1043
  }) : null;
977
1044
  if (!content) return null;
978
1045
  return /* @__PURE__ */ jsx(OffScreen, {
979
- mode: visiblePathname === pathname ? "visible" : "hidden",
1046
+ mode: routerPathname === pathname ? "visible" : "hidden",
980
1047
  pathname,
981
1048
  children: content
982
1049
  }, pathname);
983
1050
  }
984
- function buildRouteCacheModes(cachedRoutes, visiblePathname) {
1051
+ function buildRouteCacheModes(cachedRoutes, routerPathname) {
985
1052
  const nextModes = /* @__PURE__ */ new Map();
986
- for (const pathname of Object.keys(cachedRoutes)) nextModes.set(pathname, visiblePathname === pathname ? "visible" : "hidden");
1053
+ for (const pathname of Object.keys(cachedRoutes)) nextModes.set(pathname, routerPathname === pathname ? "visible" : "hidden");
987
1054
  return nextModes;
988
1055
  }
989
1056
  function syncCachedRouteActivityEvents(params) {
990
- const nextModes = buildRouteCacheModes(params.cachedRoutes, params.visiblePathname);
1057
+ const nextModes = buildRouteCacheModes(params.cachedRoutes, params.routerPathname);
991
1058
  for (const [pathname, mode] of nextModes) {
992
1059
  const previousMode = params.previousRouteCacheModes.get(pathname);
993
1060
  if (previousMode === void 0 && mode === "hidden") continue;
@@ -1013,7 +1080,7 @@ function RouteCacheManager() {
1013
1080
  const previousPathnameRef = useRef(void 0);
1014
1081
  const previousHrefRef = useRef(void 0);
1015
1082
  const previousRouteCacheModesRef = useRef(null);
1016
- if (previousRouteCacheModesRef.current === null) previousRouteCacheModesRef.current = /* @__PURE__ */ new Map();
1083
+ previousRouteCacheModesRef.current ??= /* @__PURE__ */ new Map();
1017
1084
  const previousVisiblePathnameRef = useRef(void 0);
1018
1085
  const routerLocation = useRouterState({ select: (state) => toRouterLocation(state.location) });
1019
1086
  const routerHref = routerLocation.href;
@@ -1021,7 +1088,6 @@ function RouteCacheManager() {
1021
1088
  const routerResolvedLocation = useRouterState({ select: (state) => state.resolvedLocation ? toRouterLocation(state.resolvedLocation) : void 0 });
1022
1089
  const resolvedPathname = normalizeCachedRoutePathname(routerResolvedLocation?.pathname ?? routerPathname);
1023
1090
  const destinationRoute = cachedRoutes[routerPathname];
1024
- const visiblePathname = routerPathname !== resolvedPathname && (isReadyCachedRoute(destinationRoute) || isAncestorPathname(routerPathname, resolvedPathname)) ? routerPathname : resolvedPathname;
1025
1091
  const matches = useMatches();
1026
1092
  const childMatches = useChildMatches();
1027
1093
  const router = useRouter();
@@ -1089,7 +1155,7 @@ function RouteCacheManager() {
1089
1155
  useLayoutEffect(() => {
1090
1156
  const lastVisitedPathname = previousPathnameRef.current;
1091
1157
  const pendingNavigation = pendingCachedNavigationRef.current;
1092
- if (pendingNavigation && pendingNavigation.pathname !== routerPathname && visiblePathname !== pendingNavigation.pathname) {
1158
+ if (pendingNavigation && pendingNavigation.pathname !== routerPathname) {
1093
1159
  eventListener.emit("cachedNavigationCancel", pendingNavigation);
1094
1160
  pendingCachedNavigationRef.current = null;
1095
1161
  }
@@ -1105,37 +1171,36 @@ function RouteCacheManager() {
1105
1171
  }, [
1106
1172
  destinationRoute,
1107
1173
  eventListener,
1108
- routerPathname,
1109
- visiblePathname
1174
+ routerPathname
1110
1175
  ]);
1111
1176
  useLayoutEffect(() => {
1112
- if (previousRouteCacheModesRef.current === null) previousRouteCacheModesRef.current = /* @__PURE__ */ new Map();
1177
+ previousRouteCacheModesRef.current ??= /* @__PURE__ */ new Map();
1113
1178
  previousRouteCacheModesRef.current = syncCachedRouteActivityEvents({
1114
1179
  cachedRoutes,
1115
1180
  eventListener,
1116
1181
  previousRouteCacheModes: previousRouteCacheModesRef.current,
1117
- visiblePathname
1182
+ routerPathname
1118
1183
  });
1119
1184
  }, [
1120
1185
  cachedRoutes,
1121
1186
  eventListener,
1122
- visiblePathname
1187
+ routerPathname
1123
1188
  ]);
1124
1189
  useLayoutEffect(() => {
1125
- if (previousVisiblePathnameRef.current === visiblePathname || !cachedRoutes[visiblePathname]) {
1126
- previousVisiblePathnameRef.current = visiblePathname;
1190
+ if (previousVisiblePathnameRef.current === routerPathname || !cachedRoutes[routerPathname]) {
1191
+ previousVisiblePathnameRef.current = routerPathname;
1127
1192
  return;
1128
1193
  }
1129
- previousVisiblePathnameRef.current = visiblePathname;
1130
- touchCachedRoutes([visiblePathname]);
1194
+ previousVisiblePathnameRef.current = routerPathname;
1195
+ touchCachedRoutes([routerPathname]);
1131
1196
  }, [
1132
1197
  cachedRoutes,
1133
1198
  touchCachedRoutes,
1134
- visiblePathname
1199
+ routerPathname
1135
1200
  ]);
1136
1201
  useEffect(() => {
1137
1202
  const pendingNavigation = pendingCachedNavigationRef.current;
1138
- if (visiblePathname !== pendingNavigation?.pathname) return;
1203
+ if (routerPathname !== pendingNavigation?.pathname) return;
1139
1204
  let firstFrameId = 0;
1140
1205
  let secondFrameId = 0;
1141
1206
  firstFrameId = globalThis.requestAnimationFrame(() => {
@@ -1156,7 +1221,7 @@ function RouteCacheManager() {
1156
1221
  globalThis.cancelAnimationFrame(firstFrameId);
1157
1222
  globalThis.cancelAnimationFrame(secondFrameId);
1158
1223
  };
1159
- }, [eventListener, visiblePathname]);
1224
+ }, [eventListener, routerPathname]);
1160
1225
  useLayoutEffect(() => {
1161
1226
  if (!(shouldRestoreDestinationHref && destinationRoute?.href)) return;
1162
1227
  restoreCachedHref(router, destinationRoute.href);
@@ -1169,17 +1234,13 @@ function RouteCacheManager() {
1169
1234
  previousPathnameRef.current = routerPathname;
1170
1235
  previousHrefRef.current = routerHref;
1171
1236
  }, [routerHref, routerPathname]);
1172
- const shouldRenderLiveOutlet = getShouldRenderLiveOutlet({
1173
- cachedRoutes,
1174
- bypassCachedPathname,
1175
- visiblePathname
1176
- });
1177
- useRouterCacheDebug(cachedRoutes, visiblePathname);
1237
+ const shouldRenderLiveOutlet = routerPathname === bypassCachedPathname || !isReadyCachedRoute(destinationRoute);
1238
+ useRouterCacheDebug(cachedRoutes, routerPathname);
1178
1239
  return /* @__PURE__ */ jsxs(Fragment, { children: [Object.entries(cachedRoutes).map(([pathname, route]) => renderCachedRoute({
1179
1240
  bypassCachedPathname,
1180
1241
  pathname,
1181
1242
  route,
1182
- visiblePathname
1243
+ routerPathname
1183
1244
  })), shouldRenderLiveOutlet ? /* @__PURE__ */ jsx(Outlet, {}, `live-${routerPathname}`) : null] });
1184
1245
  }
1185
1246
  //#endregion
@@ -131,8 +131,8 @@ classDiagram
131
131
  }
132
132
 
133
133
  class RouterSnapshot {
134
- static stores
135
- static matches
134
+ snapshot stores
135
+ snapshot matches
136
136
  live navigate
137
137
  live invalidate
138
138
  live preloadRoute
@@ -149,21 +149,21 @@ classDiagram
149
149
  | `routeId` | TanStack route id. Used by `maxEntriesPerRouteId`. |
150
150
  | `staticData` | Route static data. The route is cacheable when `routeCache` is `true`. |
151
151
  | `matchId` | Match id used to render the cached route with TanStack Router's `Match`. |
152
- | `routerSnapshot` | Frozen router-like object used by the cached route tree. |
152
+ | `routerSnapshot` | Router-like object with isolated snapshot stores used by the cached route tree. |
153
153
  | `ready` | Marks that the route has a complete snapshot and can be rendered from cache. |
154
154
 
155
155
  Pathnames are normalized by removing trailing slashes except for `/`, so `/customers/` and `/customers` share one cache key.
156
156
 
157
157
  ## Router snapshot
158
158
 
159
- Cached route trees still expect TanStack Router context. Instead of keeping every cached tree connected to the live router stores, the package creates a router snapshot when a route becomes ready.
159
+ Cached route trees still expect TanStack Router context. Instead of connecting every cached tree directly to the live router stores, the package creates a router-like snapshot when a route becomes ready.
160
160
 
161
161
  ```mermaid
162
162
  flowchart TD
163
163
  liveRouter["Live router"]
164
164
  matches["Current matches"]
165
165
  location["Current location"]
166
- staticStores["Static stores"]
166
+ snapshotStores["Snapshot stores"]
167
167
  liveMethods["Bound live methods"]
168
168
  routerSnapshot["Router snapshot"]
169
169
  cachedOutlet["CachedOutlet"]
@@ -171,18 +171,18 @@ flowchart TD
171
171
 
172
172
  liveRouter --> matches
173
173
  liveRouter --> location
174
- matches --> staticStores
175
- location --> staticStores
174
+ matches --> snapshotStores
175
+ location --> snapshotStores
176
176
  liveRouter --> liveMethods
177
- staticStores --> routerSnapshot
177
+ snapshotStores --> routerSnapshot
178
178
  liveMethods --> routerSnapshot
179
179
  routerSnapshot --> cachedOutlet
180
180
  cachedOutlet --> match
181
181
  ```
182
182
 
183
- The snapshot copies the current matches, location, resolved location, and match stores. It also keeps selected live router methods bound to the real router, including `navigate`, `invalidate`, `preloadRoute`, and location builders.
183
+ The snapshot owns isolated stores for the current matches, location, resolved location, and match stores. It also keeps selected live router methods bound to the real router, including `navigate`, `invalidate`, `preloadRoute`, and location builders.
184
184
 
185
- This gives hidden cached routes a stable route view while still allowing imperative router actions to call through to the real router.
185
+ When the cached entry is refreshed, the cache manager updates those snapshot stores in place. This lets route hooks read current match and search data without remounting the retained route tree. Imperative router actions still call through to the real router.
186
186
 
187
187
  `CachedOutlet` renders that snapshot like this:
188
188
 
package/docs/releases.md CHANGED
@@ -34,15 +34,23 @@ That updates GitHub and runs CI, but it does not publish to npm.
34
34
  For a release, start from a clean `main` branch after your changes are already committed:
35
35
 
36
36
  ```sh
37
- bun run release:prepare patch
38
- bun run release:push
37
+ bun run release patch
39
38
  ```
40
39
 
41
40
  Use `minor`, `major`, or an exact version when needed:
42
41
 
43
42
  ```sh
44
- bun run release:prepare minor
45
- bun run release:prepare 0.2.0
43
+ bun run release minor
44
+ bun run release 0.2.0
45
+ ```
46
+
47
+ `release` updates `package.json`, runs the package checks, commits the version bump, creates the matching tag, pushes `main` and the tag, creates the GitHub Release, waits for the publish workflow, and verifies that npm has the new package version.
48
+
49
+ The lower-level commands are still available when you need to split the release into two steps:
50
+
51
+ ```sh
52
+ bun run release:prepare patch
53
+ bun run release:push
46
54
  ```
47
55
 
48
56
  `release:prepare` updates `package.json`, runs the package checks, commits the version bump, and creates the matching tag locally. `release:push` pushes `main` and that exact tag. The tag starts the npm publish workflow.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tanstack-router-cache",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Route view caching for TanStack Router.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -50,6 +50,7 @@
50
50
  "lint:fix": "biome lint src scripts .github/scripts rolldown.config.ts --write --skip=lint/performance/noBarrelFile --skip=lint/style/useConsistentTypeDefinitions --skip=lint/suspicious/noConsole",
51
51
  "check": "bun run lint && bun run typecheck && bun run build && bun run pack:dry-run",
52
52
  "pack:dry-run": "npm pack --dry-run",
53
+ "release": "node scripts/release.mjs release",
53
54
  "release:prepare": "node scripts/release.mjs prepare",
54
55
  "release:push": "node scripts/release.mjs push",
55
56
  "prepack": "bun run build",