tanstack-router-cache 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -55,8 +55,7 @@ For the full API, see [docs](./docs).
55
55
 
56
56
  ## Examples
57
57
 
58
- - [Basic](https://github.com/santiago-ramos-02/tanstack-router-cache/tree/main/examples/basic): the smallest useful setup. Start here; most apps only need this pattern.
59
- - [Power-user demo](https://github.com/santiago-ramos-02/tanstack-router-cache/tree/main/examples/power-user-demo): a larger demo with retained forms, filtered lists, cache controls, route lifecycle state, and window scroll restoration. Use it when you need to inspect edge cases.
58
+ - [Demo app](https://github.com/santiago-ramos-02/tanstack-router-cache/tree/main/examples/demo): one Vite app with a basic flow for most users and a power-user flow for edge cases.
60
59
 
61
60
  ## Acknowledgements
62
61
 
package/dist/index.cjs CHANGED
@@ -371,10 +371,7 @@ function useRouterCacheDebug(cachedRoutes, visiblePathname) {
371
371
  warningThreshold: warningThresholdRef.current
372
372
  };
373
373
  const warningThreshold = warningThresholdRef.current;
374
- if (typeof warningThreshold === "number" && nextSnapshot.totalCachedRouteCount > warningThreshold && lastWarnedCountRef.current !== nextSnapshot.totalCachedRouteCount) {
375
- lastWarnedCountRef.current = nextSnapshot.totalCachedRouteCount;
376
- console.warn("[tanstack-router-cache] cached route count exceeded threshold", nextSnapshot);
377
- }
374
+ if (typeof warningThreshold === "number" && nextSnapshot.totalCachedRouteCount > warningThreshold && lastWarnedCountRef.current !== nextSnapshot.totalCachedRouteCount) lastWarnedCountRef.current = nextSnapshot.totalCachedRouteCount;
378
375
  });
379
376
  return () => {
380
377
  globalThis.cancelAnimationFrame(rafId);
@@ -790,12 +787,50 @@ const LIVE_ROUTER_METHODS = [
790
787
  "navigate",
791
788
  "preloadRoute"
792
789
  ];
793
- function createStaticStore(value) {
790
+ const routerSnapshotUpdaters = /* @__PURE__ */ new WeakMap();
791
+ function createSnapshotStore(value) {
792
+ let currentValue = value;
793
+ const listeners = /* @__PURE__ */ new Set();
794
794
  return {
795
- get: () => value,
796
- 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
+ }
797
807
  };
798
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
+ }
823
+ };
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
+ }
799
834
  function getLiveRouterMethodDescriptors(router) {
800
835
  return Object.fromEntries(LIVE_ROUTER_METHODS.flatMap((methodName) => {
801
836
  const method = router[methodName];
@@ -831,29 +866,40 @@ function hasCurrentRouteError({ erroredRouteCounts, matches, resolvedPathname, r
831
866
  function isRouterMatch(match) {
832
867
  return Boolean(match?.id && match.routeId);
833
868
  }
834
- function snapshotMatch(match) {
869
+ function snapshotMatch(match, routerLocation) {
835
870
  if (!isRouterMatch(match)) return;
871
+ const isLocationMatch = match.pathname ? normalizeCachedRoutePathname(match.pathname) === normalizeCachedRoutePathname(routerLocation.pathname) : false;
836
872
  return {
837
873
  ...match,
874
+ ...isLocationMatch ? {
875
+ _strictSearch: routerLocation.search,
876
+ search: routerLocation.search
877
+ } : {},
838
878
  _nonReactive: { ...match._nonReactive }
839
879
  };
840
880
  }
841
- function shouldRefreshCachedRoute(route, routerHref) {
842
- 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);
843
883
  }
844
884
  function syncReadyCachedRoute({ matchId, matches, routeId, route, routerLocation, router, routerResolvedLocation, routerHref, routerPathname, setCachedRoutes, staticData }) {
845
- 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);
846
897
  setCachedRoutes(routerPathname, {
847
898
  href: routerHref,
848
899
  matchId,
849
900
  routeId,
850
901
  ready: true,
851
- routerSnapshot: matchId ? createRouterSnapshot({
852
- matches,
853
- router,
854
- routerLocation,
855
- routerResolvedLocation
856
- }) : void 0,
902
+ routerSnapshot,
857
903
  staticData
858
904
  });
859
905
  }
@@ -882,54 +928,43 @@ function syncCachedRouteState({ isCurrentMatchReady, isCurrentMatchResolved, del
882
928
  }
883
929
  if (route) deleteCachedRoutes([routerPathname]);
884
930
  }
885
- function createRouterSnapshot({ matches, router, routerLocation, routerResolvedLocation }) {
886
- const snapshotMatches = matches.reduce((snapshot, match) => {
887
- const nextMatch = snapshotMatch(match);
888
- if (isRouterMatch(nextMatch)) snapshot.push(nextMatch);
889
- return snapshot;
890
- }, []);
891
- const matchStores = new Map(snapshotMatches.map((match) => {
892
- const store = Object.assign(createStaticStore(match), { routeId: match.routeId });
893
- return [match.id, store];
894
- }));
895
- const snapshotState = {
896
- ...router.stores.__store.get(),
897
- matches: snapshotMatches,
898
- location: routerLocation,
899
- resolvedLocation: routerResolvedLocation
900
- };
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)]));
901
936
  const routeMatchStoreCache = /* @__PURE__ */ new Map();
902
937
  const stores = {
903
938
  ...router.stores,
904
- status: createStaticStore(router.stores.status.get()),
905
- loadedAt: createStaticStore(router.stores.loadedAt.get()),
906
- isLoading: createStaticStore(router.stores.isLoading.get()),
907
- isTransitioning: createStaticStore(router.stores.isTransitioning.get()),
908
- location: createStaticStore(routerLocation),
909
- resolvedLocation: createStaticStore(routerResolvedLocation),
910
- statusCode: createStaticStore(router.stores.statusCode.get()),
911
- redirect: createStaticStore(router.stores.redirect.get()),
912
- matchesId: createStaticStore(snapshotMatches.map((match) => match.id)),
913
- pendingIds: createStaticStore([]),
914
- cachedIds: createStaticStore([]),
915
- matches: createStaticStore(snapshotMatches),
916
- pendingMatches: createStaticStore([]),
917
- cachedMatches: createStaticStore([]),
918
- firstId: createStaticStore(snapshotMatches[0]?.id),
919
- hasPending: createStaticStore(snapshotMatches.some((match) => match.status === "pending")),
920
- 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({
921
956
  locationHref: routerLocation.href,
922
957
  resolvedLocationHref: routerResolvedLocation?.href,
923
- status: snapshotState.status
958
+ status: snapshotData.state.status
924
959
  }),
925
- __store: createStaticStore(snapshotState),
960
+ __store: createSnapshotStore(snapshotData.state),
926
961
  matchStores,
927
962
  pendingMatchStores: /* @__PURE__ */ new Map(),
928
963
  cachedMatchStores: /* @__PURE__ */ new Map(),
929
964
  getRouteMatchStore: (routeId) => {
930
965
  let cached = routeMatchStoreCache.get(routeId);
931
966
  if (!cached) {
932
- cached = createStaticStore(snapshotMatches.find((match) => match.routeId === routeId));
967
+ cached = createSnapshotStore(snapshotMatches.find((match) => match.routeId === routeId));
933
968
  routeMatchStoreCache.set(routeId, cached);
934
969
  }
935
970
  return cached;
@@ -938,14 +973,53 @@ function createRouterSnapshot({ matches, router, routerLocation, routerResolvedL
938
973
  setPending: () => void 0,
939
974
  setCached: () => void 0
940
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
+ };
941
1014
  const routerSnapshot = Object.create(router);
942
1015
  Object.defineProperties(routerSnapshot, {
943
1016
  stores: { value: stores },
944
- latestLocation: { value: routerLocation },
1017
+ latestLocation: { get: () => stores.location.get() },
945
1018
  getMatch: { value: (matchId) => matchStores.get(matchId)?.get() },
946
1019
  updateMatch: { value: () => void 0 },
947
1020
  ...getLiveRouterMethodDescriptors(router)
948
1021
  });
1022
+ routerSnapshotUpdaters.set(routerSnapshot, updateSnapshot);
949
1023
  return routerSnapshot;
950
1024
  }
951
1025
  function getRouterCacheStaticData(childMatches, isCurrentMatchResolved) {
@@ -1017,7 +1091,7 @@ function RouteCacheManager() {
1017
1091
  const previousPathnameRef = (0, react.useRef)(void 0);
1018
1092
  const previousHrefRef = (0, react.useRef)(void 0);
1019
1093
  const previousRouteCacheModesRef = (0, react.useRef)(null);
1020
- if (previousRouteCacheModesRef.current === null) previousRouteCacheModesRef.current = /* @__PURE__ */ new Map();
1094
+ previousRouteCacheModesRef.current ??= /* @__PURE__ */ new Map();
1021
1095
  const previousVisiblePathnameRef = (0, react.useRef)(void 0);
1022
1096
  const routerLocation = (0, _tanstack_react_router.useRouterState)({ select: (state) => toRouterLocation(state.location) });
1023
1097
  const routerHref = routerLocation.href;
@@ -1113,7 +1187,7 @@ function RouteCacheManager() {
1113
1187
  visiblePathname
1114
1188
  ]);
1115
1189
  (0, react.useLayoutEffect)(() => {
1116
- if (previousRouteCacheModesRef.current === null) previousRouteCacheModesRef.current = /* @__PURE__ */ new Map();
1190
+ previousRouteCacheModesRef.current ??= /* @__PURE__ */ new Map();
1117
1191
  previousRouteCacheModesRef.current = syncCachedRouteActivityEvents({
1118
1192
  cachedRoutes,
1119
1193
  eventListener,
package/dist/index.js CHANGED
@@ -370,10 +370,7 @@ function useRouterCacheDebug(cachedRoutes, visiblePathname) {
370
370
  warningThreshold: warningThresholdRef.current
371
371
  };
372
372
  const warningThreshold = warningThresholdRef.current;
373
- if (typeof warningThreshold === "number" && nextSnapshot.totalCachedRouteCount > warningThreshold && lastWarnedCountRef.current !== nextSnapshot.totalCachedRouteCount) {
374
- lastWarnedCountRef.current = nextSnapshot.totalCachedRouteCount;
375
- console.warn("[tanstack-router-cache] cached route count exceeded threshold", nextSnapshot);
376
- }
373
+ if (typeof warningThreshold === "number" && nextSnapshot.totalCachedRouteCount > warningThreshold && lastWarnedCountRef.current !== nextSnapshot.totalCachedRouteCount) lastWarnedCountRef.current = nextSnapshot.totalCachedRouteCount;
377
374
  });
378
375
  return () => {
379
376
  globalThis.cancelAnimationFrame(rafId);
@@ -789,12 +786,50 @@ const LIVE_ROUTER_METHODS = [
789
786
  "navigate",
790
787
  "preloadRoute"
791
788
  ];
792
- function createStaticStore(value) {
789
+ const routerSnapshotUpdaters = /* @__PURE__ */ new WeakMap();
790
+ function createSnapshotStore(value) {
791
+ let currentValue = value;
792
+ const listeners = /* @__PURE__ */ new Set();
793
793
  return {
794
- get: () => value,
795
- 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
+ }
796
806
  };
797
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
+ }
798
833
  function getLiveRouterMethodDescriptors(router) {
799
834
  return Object.fromEntries(LIVE_ROUTER_METHODS.flatMap((methodName) => {
800
835
  const method = router[methodName];
@@ -830,29 +865,40 @@ function hasCurrentRouteError({ erroredRouteCounts, matches, resolvedPathname, r
830
865
  function isRouterMatch(match) {
831
866
  return Boolean(match?.id && match.routeId);
832
867
  }
833
- function snapshotMatch(match) {
868
+ function snapshotMatch(match, routerLocation) {
834
869
  if (!isRouterMatch(match)) return;
870
+ const isLocationMatch = match.pathname ? normalizeCachedRoutePathname(match.pathname) === normalizeCachedRoutePathname(routerLocation.pathname) : false;
835
871
  return {
836
872
  ...match,
873
+ ...isLocationMatch ? {
874
+ _strictSearch: routerLocation.search,
875
+ search: routerLocation.search
876
+ } : {},
837
877
  _nonReactive: { ...match._nonReactive }
838
878
  };
839
879
  }
840
- function shouldRefreshCachedRoute(route, routerHref) {
841
- 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);
842
882
  }
843
883
  function syncReadyCachedRoute({ matchId, matches, routeId, route, routerLocation, router, routerResolvedLocation, routerHref, routerPathname, setCachedRoutes, staticData }) {
844
- 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);
845
896
  setCachedRoutes(routerPathname, {
846
897
  href: routerHref,
847
898
  matchId,
848
899
  routeId,
849
900
  ready: true,
850
- routerSnapshot: matchId ? createRouterSnapshot({
851
- matches,
852
- router,
853
- routerLocation,
854
- routerResolvedLocation
855
- }) : void 0,
901
+ routerSnapshot,
856
902
  staticData
857
903
  });
858
904
  }
@@ -881,54 +927,43 @@ function syncCachedRouteState({ isCurrentMatchReady, isCurrentMatchResolved, del
881
927
  }
882
928
  if (route) deleteCachedRoutes([routerPathname]);
883
929
  }
884
- function createRouterSnapshot({ matches, router, routerLocation, routerResolvedLocation }) {
885
- const snapshotMatches = matches.reduce((snapshot, match) => {
886
- const nextMatch = snapshotMatch(match);
887
- if (isRouterMatch(nextMatch)) snapshot.push(nextMatch);
888
- return snapshot;
889
- }, []);
890
- const matchStores = new Map(snapshotMatches.map((match) => {
891
- const store = Object.assign(createStaticStore(match), { routeId: match.routeId });
892
- return [match.id, store];
893
- }));
894
- const snapshotState = {
895
- ...router.stores.__store.get(),
896
- matches: snapshotMatches,
897
- location: routerLocation,
898
- resolvedLocation: routerResolvedLocation
899
- };
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)]));
900
935
  const routeMatchStoreCache = /* @__PURE__ */ new Map();
901
936
  const stores = {
902
937
  ...router.stores,
903
- status: createStaticStore(router.stores.status.get()),
904
- loadedAt: createStaticStore(router.stores.loadedAt.get()),
905
- isLoading: createStaticStore(router.stores.isLoading.get()),
906
- isTransitioning: createStaticStore(router.stores.isTransitioning.get()),
907
- location: createStaticStore(routerLocation),
908
- resolvedLocation: createStaticStore(routerResolvedLocation),
909
- statusCode: createStaticStore(router.stores.statusCode.get()),
910
- redirect: createStaticStore(router.stores.redirect.get()),
911
- matchesId: createStaticStore(snapshotMatches.map((match) => match.id)),
912
- pendingIds: createStaticStore([]),
913
- cachedIds: createStaticStore([]),
914
- matches: createStaticStore(snapshotMatches),
915
- pendingMatches: createStaticStore([]),
916
- cachedMatches: createStaticStore([]),
917
- firstId: createStaticStore(snapshotMatches[0]?.id),
918
- hasPending: createStaticStore(snapshotMatches.some((match) => match.status === "pending")),
919
- 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({
920
955
  locationHref: routerLocation.href,
921
956
  resolvedLocationHref: routerResolvedLocation?.href,
922
- status: snapshotState.status
957
+ status: snapshotData.state.status
923
958
  }),
924
- __store: createStaticStore(snapshotState),
959
+ __store: createSnapshotStore(snapshotData.state),
925
960
  matchStores,
926
961
  pendingMatchStores: /* @__PURE__ */ new Map(),
927
962
  cachedMatchStores: /* @__PURE__ */ new Map(),
928
963
  getRouteMatchStore: (routeId) => {
929
964
  let cached = routeMatchStoreCache.get(routeId);
930
965
  if (!cached) {
931
- cached = createStaticStore(snapshotMatches.find((match) => match.routeId === routeId));
966
+ cached = createSnapshotStore(snapshotMatches.find((match) => match.routeId === routeId));
932
967
  routeMatchStoreCache.set(routeId, cached);
933
968
  }
934
969
  return cached;
@@ -937,14 +972,53 @@ function createRouterSnapshot({ matches, router, routerLocation, routerResolvedL
937
972
  setPending: () => void 0,
938
973
  setCached: () => void 0
939
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
+ };
940
1013
  const routerSnapshot = Object.create(router);
941
1014
  Object.defineProperties(routerSnapshot, {
942
1015
  stores: { value: stores },
943
- latestLocation: { value: routerLocation },
1016
+ latestLocation: { get: () => stores.location.get() },
944
1017
  getMatch: { value: (matchId) => matchStores.get(matchId)?.get() },
945
1018
  updateMatch: { value: () => void 0 },
946
1019
  ...getLiveRouterMethodDescriptors(router)
947
1020
  });
1021
+ routerSnapshotUpdaters.set(routerSnapshot, updateSnapshot);
948
1022
  return routerSnapshot;
949
1023
  }
950
1024
  function getRouterCacheStaticData(childMatches, isCurrentMatchResolved) {
@@ -1016,7 +1090,7 @@ function RouteCacheManager() {
1016
1090
  const previousPathnameRef = useRef(void 0);
1017
1091
  const previousHrefRef = useRef(void 0);
1018
1092
  const previousRouteCacheModesRef = useRef(null);
1019
- if (previousRouteCacheModesRef.current === null) previousRouteCacheModesRef.current = /* @__PURE__ */ new Map();
1093
+ previousRouteCacheModesRef.current ??= /* @__PURE__ */ new Map();
1020
1094
  const previousVisiblePathnameRef = useRef(void 0);
1021
1095
  const routerLocation = useRouterState({ select: (state) => toRouterLocation(state.location) });
1022
1096
  const routerHref = routerLocation.href;
@@ -1112,7 +1186,7 @@ function RouteCacheManager() {
1112
1186
  visiblePathname
1113
1187
  ]);
1114
1188
  useLayoutEffect(() => {
1115
- if (previousRouteCacheModesRef.current === null) previousRouteCacheModesRef.current = /* @__PURE__ */ new Map();
1189
+ previousRouteCacheModesRef.current ??= /* @__PURE__ */ new Map();
1116
1190
  previousRouteCacheModesRef.current = syncCachedRouteActivityEvents({
1117
1191
  cachedRoutes,
1118
1192
  eventListener,
@@ -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
@@ -1,6 +1,6 @@
1
1
  # Releases
2
2
 
3
- This package publishes from GitHub Actions after a version tag is pushed.
3
+ This package publishes from GitHub Actions after a version tag is pushed. A regular commit to `main` only runs CI and updates the GitHub repo. It does not publish to npm.
4
4
 
5
5
  ## Trusted publishing
6
6
 
@@ -14,13 +14,37 @@ npm trust github tanstack-router-cache --repo santiago-ramos-02/tanstack-router-
14
14
 
15
15
  ## Publishing a version
16
16
 
17
- 1. Update `version` in `package.json`.
18
- 2. Commit the change.
19
- 3. Tag the same version:
17
+ Work in the package repo:
20
18
 
21
19
  ```sh
22
- git tag v0.2.0
23
- git push origin main --tags
20
+ cd packages/tanstack-router-cache
24
21
  ```
25
22
 
26
- The workflow checks that the tag matches `package.json` before publishing.
23
+ For normal changes:
24
+
25
+ ```sh
26
+ bun run check
27
+ git add .
28
+ git commit -m "Your change"
29
+ git push origin main
30
+ ```
31
+
32
+ That updates GitHub and runs CI, but it does not publish to npm.
33
+
34
+ For a release, start from a clean `main` branch after your changes are already committed:
35
+
36
+ ```sh
37
+ bun run release:prepare patch
38
+ bun run release:push
39
+ ```
40
+
41
+ Use `minor`, `major`, or an exact version when needed:
42
+
43
+ ```sh
44
+ bun run release:prepare minor
45
+ bun run release:prepare 0.2.0
46
+ ```
47
+
48
+ `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.
49
+
50
+ The workflow checks that the tag matches `package.json` before publishing, so a mismatched tag cannot publish accidentally.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tanstack-router-cache",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Route view caching for TanStack Router.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -46,9 +46,12 @@
46
46
  "build": "bun run clean && rolldown -c && tsc",
47
47
  "typecheck": "tsc --noEmit",
48
48
  "clean": "node -e \"fs.rmSync('dist',{recursive:true,force:true})\"",
49
- "lint": "biome lint src rolldown.config.ts --skip=lint/performance/noBarrelFile --skip=lint/style/useConsistentTypeDefinitions --skip=lint/suspicious/noConsole",
50
- "lint:fix": "biome lint src rolldown.config.ts --write --skip=lint/performance/noBarrelFile --skip=lint/style/useConsistentTypeDefinitions --skip=lint/suspicious/noConsole",
51
- "pack:dry-run": "bun pm pack --dry-run",
49
+ "lint": "biome lint src scripts .github/scripts rolldown.config.ts --skip=lint/performance/noBarrelFile --skip=lint/style/useConsistentTypeDefinitions --skip=lint/suspicious/noConsole",
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
+ "check": "bun run lint && bun run typecheck && bun run build && bun run pack:dry-run",
52
+ "pack:dry-run": "npm pack --dry-run",
53
+ "release:prepare": "node scripts/release.mjs prepare",
54
+ "release:push": "node scripts/release.mjs push",
52
55
  "prepack": "bun run build",
53
56
  "prepublishOnly": "bun run lint && bun run typecheck"
54
57
  },