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 +1 -2
- package/dist/index.cjs +130 -56
- package/dist/index.js +130 -56
- package/docs/architecture.md +10 -10
- package/docs/releases.md +31 -7
- package/package.json +7 -4
package/README.md
CHANGED
|
@@ -55,8 +55,7 @@ For the full API, see [docs](./docs).
|
|
|
55
55
|
|
|
56
56
|
## Examples
|
|
57
57
|
|
|
58
|
-
- [
|
|
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
|
-
|
|
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: () =>
|
|
796
|
-
|
|
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
|
|
842
|
-
return
|
|
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
|
-
|
|
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
|
|
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(
|
|
886
|
-
const
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
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:
|
|
905
|
-
loadedAt:
|
|
906
|
-
isLoading:
|
|
907
|
-
isTransitioning:
|
|
908
|
-
location:
|
|
909
|
-
resolvedLocation:
|
|
910
|
-
statusCode:
|
|
911
|
-
redirect:
|
|
912
|
-
matchesId:
|
|
913
|
-
pendingIds:
|
|
914
|
-
cachedIds:
|
|
915
|
-
matches:
|
|
916
|
-
pendingMatches:
|
|
917
|
-
cachedMatches:
|
|
918
|
-
firstId:
|
|
919
|
-
hasPending:
|
|
920
|
-
matchRouteDeps:
|
|
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:
|
|
958
|
+
status: snapshotData.state.status
|
|
924
959
|
}),
|
|
925
|
-
__store:
|
|
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 =
|
|
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: {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: () =>
|
|
795
|
-
|
|
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
|
|
841
|
-
return
|
|
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
|
-
|
|
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
|
|
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(
|
|
885
|
-
const
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
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:
|
|
904
|
-
loadedAt:
|
|
905
|
-
isLoading:
|
|
906
|
-
isTransitioning:
|
|
907
|
-
location:
|
|
908
|
-
resolvedLocation:
|
|
909
|
-
statusCode:
|
|
910
|
-
redirect:
|
|
911
|
-
matchesId:
|
|
912
|
-
pendingIds:
|
|
913
|
-
cachedIds:
|
|
914
|
-
matches:
|
|
915
|
-
pendingMatches:
|
|
916
|
-
cachedMatches:
|
|
917
|
-
firstId:
|
|
918
|
-
hasPending:
|
|
919
|
-
matchRouteDeps:
|
|
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:
|
|
957
|
+
status: snapshotData.state.status
|
|
923
958
|
}),
|
|
924
|
-
__store:
|
|
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 =
|
|
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: {
|
|
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
|
-
|
|
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
|
-
|
|
1189
|
+
previousRouteCacheModesRef.current ??= /* @__PURE__ */ new Map();
|
|
1116
1190
|
previousRouteCacheModesRef.current = syncCachedRouteActivityEvents({
|
|
1117
1191
|
cachedRoutes,
|
|
1118
1192
|
eventListener,
|
package/docs/architecture.md
CHANGED
|
@@ -131,8 +131,8 @@ classDiagram
|
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
class RouterSnapshot {
|
|
134
|
-
|
|
135
|
-
|
|
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` |
|
|
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
|
|
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
|
-
|
|
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 -->
|
|
175
|
-
location -->
|
|
174
|
+
matches --> snapshotStores
|
|
175
|
+
location --> snapshotStores
|
|
176
176
|
liveRouter --> liveMethods
|
|
177
|
-
|
|
177
|
+
snapshotStores --> routerSnapshot
|
|
178
178
|
liveMethods --> routerSnapshot
|
|
179
179
|
routerSnapshot --> cachedOutlet
|
|
180
180
|
cachedOutlet --> match
|
|
181
181
|
```
|
|
182
182
|
|
|
183
|
-
The snapshot
|
|
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
|
-
|
|
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
|
-
|
|
18
|
-
2. Commit the change.
|
|
19
|
-
3. Tag the same version:
|
|
17
|
+
Work in the package repo:
|
|
20
18
|
|
|
21
19
|
```sh
|
|
22
|
-
|
|
23
|
-
git push origin main --tags
|
|
20
|
+
cd packages/tanstack-router-cache
|
|
24
21
|
```
|
|
25
22
|
|
|
26
|
-
|
|
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.
|
|
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
|
-
"
|
|
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
|
},
|