fix(recent-projects): scope client cache by context
This commit is contained in:
parent
72633daa6e
commit
b46b53d667
4 changed files with 178 additions and 69 deletions
|
|
@ -47,7 +47,9 @@ export function createRecentProjectsFeature(deps: {
|
||||||
return {
|
return {
|
||||||
listDashboardRecentProjects: async () => {
|
listDashboardRecentProjects: async () => {
|
||||||
const activeContext = deps.getActiveContext();
|
const activeContext = deps.getActiveContext();
|
||||||
const payload = await useCase.execute(`dashboard-recent-projects:${activeContext.id}`);
|
const payload = await useCase.execute(
|
||||||
|
`dashboard-recent-projects:${activeContext.type}:${activeContext.id}`
|
||||||
|
);
|
||||||
return normalizeDashboardRecentProjectsPayload(payload) ?? { projects: [], degraded: true };
|
return normalizeDashboardRecentProjectsPayload(payload) ?? { projects: [], degraded: true };
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,6 @@ import {
|
||||||
import { useOpenRecentProject } from './useOpenRecentProject';
|
import { useOpenRecentProject } from './useOpenRecentProject';
|
||||||
|
|
||||||
import type { RecentProjectCardModel } from '../adapters/RecentProjectsSectionAdapter';
|
import type { RecentProjectCardModel } from '../adapters/RecentProjectsSectionAdapter';
|
||||||
import type { TeamSummary } from '@shared/types';
|
|
||||||
|
|
||||||
const INITIAL_RECENT_PROJECTS = 11;
|
const INITIAL_RECENT_PROJECTS = 11;
|
||||||
const LOAD_MORE_STEP = 8;
|
const LOAD_MORE_STEP = 8;
|
||||||
|
|
@ -70,6 +69,7 @@ export function useRecentProjectsSection(
|
||||||
globalTasksLoading,
|
globalTasksLoading,
|
||||||
fetchAllTasks,
|
fetchAllTasks,
|
||||||
teams,
|
teams,
|
||||||
|
activeContextId,
|
||||||
provisioningRuns,
|
provisioningRuns,
|
||||||
currentProvisioningRunIdByTeam,
|
currentProvisioningRunIdByTeam,
|
||||||
provisioningSnapshotByTeam,
|
provisioningSnapshotByTeam,
|
||||||
|
|
@ -80,12 +80,16 @@ export function useRecentProjectsSection(
|
||||||
globalTasksLoading: state.globalTasksLoading,
|
globalTasksLoading: state.globalTasksLoading,
|
||||||
fetchAllTasks: state.fetchAllTasks,
|
fetchAllTasks: state.fetchAllTasks,
|
||||||
teams: state.teams,
|
teams: state.teams,
|
||||||
|
activeContextId: state.activeContextId,
|
||||||
provisioningRuns: state.provisioningRuns,
|
provisioningRuns: state.provisioningRuns,
|
||||||
currentProvisioningRunIdByTeam: state.currentProvisioningRunIdByTeam,
|
currentProvisioningRunIdByTeam: state.currentProvisioningRunIdByTeam,
|
||||||
provisioningSnapshotByTeam: state.provisioningSnapshotByTeam,
|
provisioningSnapshotByTeam: state.provisioningSnapshotByTeam,
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
const initialSnapshot = useMemo(() => getRecentProjectsClientSnapshot(), []);
|
const initialSnapshot = useMemo(
|
||||||
|
() => getRecentProjectsClientSnapshot(activeContextId),
|
||||||
|
[activeContextId]
|
||||||
|
);
|
||||||
const { openRecentProject, openProjectPath, selectProjectFolder } = useOpenRecentProject();
|
const { openRecentProject, openProjectPath, selectProjectFolder } = useOpenRecentProject();
|
||||||
const [recentProjects, setRecentProjects] = useState<DashboardRecentProject[]>(
|
const [recentProjects, setRecentProjects] = useState<DashboardRecentProject[]>(
|
||||||
initialSnapshot?.payload.projects ?? []
|
initialSnapshot?.payload.projects ?? []
|
||||||
|
|
@ -105,6 +109,7 @@ export function useRecentProjectsSection(
|
||||||
const recentProjectsRef = useRef<DashboardRecentProject[]>(
|
const recentProjectsRef = useRef<DashboardRecentProject[]>(
|
||||||
initialSnapshot?.payload.projects ?? []
|
initialSnapshot?.payload.projects ?? []
|
||||||
);
|
);
|
||||||
|
const activeContextIdRef = useRef(activeContextId);
|
||||||
const provisioningState = useMemo(
|
const provisioningState = useMemo(
|
||||||
() => ({ currentProvisioningRunIdByTeam, provisioningRuns }),
|
() => ({ currentProvisioningRunIdByTeam, provisioningRuns }),
|
||||||
[currentProvisioningRunIdByTeam, provisioningRuns]
|
[currentProvisioningRunIdByTeam, provisioningRuns]
|
||||||
|
|
@ -125,37 +130,67 @@ export function useRecentProjectsSection(
|
||||||
recentProjectsRef.current = recentProjects;
|
recentProjectsRef.current = recentProjects;
|
||||||
}, [recentProjects]);
|
}, [recentProjects]);
|
||||||
|
|
||||||
const reload = useCallback(async (options?: { force?: boolean }): Promise<void> => {
|
useEffect(() => {
|
||||||
const hasVisibleProjects =
|
activeContextIdRef.current = activeContextId;
|
||||||
recentProjectsRef.current.length > 0 || getRecentProjectsClientSnapshot() != null;
|
}, [activeContextId]);
|
||||||
|
|
||||||
if (!hasVisibleProjects) {
|
const reload = useCallback(
|
||||||
setLoading(true);
|
async (options?: { force?: boolean }): Promise<void> => {
|
||||||
}
|
const requestContextId = activeContextId;
|
||||||
setError(null);
|
const hasVisibleProjects =
|
||||||
try {
|
recentProjectsRef.current.length > 0 ||
|
||||||
const payload = await loadRecentProjectsWithClientCache(
|
getRecentProjectsClientSnapshot(requestContextId) != null;
|
||||||
() => api.getDashboardRecentProjects(),
|
|
||||||
options
|
if (!hasVisibleProjects) {
|
||||||
);
|
setLoading(true);
|
||||||
setRecentProjects(payload.projects);
|
}
|
||||||
setRecentProjectsDegraded(payload.degraded);
|
setError(null);
|
||||||
setDegradedRefreshCount((current) => (payload.degraded ? current + 1 : 0));
|
try {
|
||||||
} catch (nextError) {
|
const payload = await loadRecentProjectsWithClientCache(
|
||||||
setError(nextError instanceof Error ? nextError.message : 'Failed to load recent projects');
|
requestContextId,
|
||||||
} finally {
|
() => api.getDashboardRecentProjects(),
|
||||||
setLoading(false);
|
options
|
||||||
}
|
);
|
||||||
}, []);
|
if (activeContextIdRef.current !== requestContextId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setRecentProjects(payload.projects);
|
||||||
|
setRecentProjectsDegraded(payload.degraded);
|
||||||
|
setDegradedRefreshCount((current) => (payload.degraded ? current + 1 : 0));
|
||||||
|
} catch (nextError) {
|
||||||
|
if (activeContextIdRef.current !== requestContextId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setError(nextError instanceof Error ? nextError.message : 'Failed to load recent projects');
|
||||||
|
} finally {
|
||||||
|
if (activeContextIdRef.current === requestContextId) {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[activeContextId]
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const snapshot = getRecentProjectsClientSnapshot();
|
const snapshot = getRecentProjectsClientSnapshot(activeContextId);
|
||||||
|
if (snapshot) {
|
||||||
|
setRecentProjects(snapshot.payload.projects);
|
||||||
|
setRecentProjectsDegraded(snapshot.payload.degraded);
|
||||||
|
setDegradedRefreshCount(snapshot.payload.degraded ? 1 : 0);
|
||||||
|
setLoading(false);
|
||||||
|
} else {
|
||||||
|
setRecentProjects([]);
|
||||||
|
setRecentProjectsDegraded(false);
|
||||||
|
setDegradedRefreshCount(0);
|
||||||
|
setLoading(true);
|
||||||
|
}
|
||||||
|
|
||||||
if (snapshot && !snapshot.isStale) {
|
if (snapshot && !snapshot.isStale) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
void reload({ force: snapshot != null });
|
void reload({ force: snapshot != null });
|
||||||
}, [reload]);
|
}, [activeContextId, reload]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!recentProjectsDegraded) {
|
if (!recentProjectsDegraded) {
|
||||||
|
|
@ -225,22 +260,21 @@ export function useRecentProjectsSection(
|
||||||
});
|
});
|
||||||
}, [aliveTeams, provisioningSnapshotByTeam, provisioningTeamNames, teams]);
|
}, [aliveTeams, provisioningSnapshotByTeam, provisioningTeamNames, teams]);
|
||||||
|
|
||||||
const decoratedCards = useMemo(
|
const decoratedCards = useMemo(() => {
|
||||||
() =>
|
void openHistoryVersion;
|
||||||
adaptRecentProjectsSection({
|
return adaptRecentProjectsSection({
|
||||||
projects: sortRecentProjectsByDisplayPriority(recentProjects),
|
projects: sortRecentProjectsByDisplayPriority(recentProjects),
|
||||||
taskCountsByProject,
|
|
||||||
activeTeamsByProject,
|
|
||||||
tasksLoading: globalTasksLoading,
|
|
||||||
}),
|
|
||||||
[
|
|
||||||
activeTeamsByProject,
|
|
||||||
globalTasksLoading,
|
|
||||||
openHistoryVersion,
|
|
||||||
recentProjects,
|
|
||||||
taskCountsByProject,
|
taskCountsByProject,
|
||||||
]
|
activeTeamsByProject,
|
||||||
);
|
tasksLoading: globalTasksLoading,
|
||||||
|
});
|
||||||
|
}, [
|
||||||
|
activeTeamsByProject,
|
||||||
|
globalTasksLoading,
|
||||||
|
openHistoryVersion,
|
||||||
|
recentProjects,
|
||||||
|
taskCountsByProject,
|
||||||
|
]);
|
||||||
|
|
||||||
const filteredCards = useMemo(
|
const filteredCards = useMemo(
|
||||||
() => decoratedCards.filter((card) => matchesSearch(card.project, searchQuery)),
|
() => decoratedCards.filter((card) => matchesSearch(card.project, searchQuery)),
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,9 @@ const RECENT_PROJECTS_CLIENT_CACHE_TTL_MS = 15_000;
|
||||||
const RECENT_PROJECTS_CLIENT_DEGRADED_CACHE_TTL_MS = 30_000;
|
const RECENT_PROJECTS_CLIENT_DEGRADED_CACHE_TTL_MS = 30_000;
|
||||||
|
|
||||||
let cachedPayload: DashboardRecentProjectsPayloadLike = null;
|
let cachedPayload: DashboardRecentProjectsPayloadLike = null;
|
||||||
|
let cachedKey: string | null = null;
|
||||||
let cachedAt = 0;
|
let cachedAt = 0;
|
||||||
let inFlightLoad: Promise<DashboardRecentProjectsPayload> | null = null;
|
let inFlightLoad: { key: string; promise: Promise<DashboardRecentProjectsPayload> } | null = null;
|
||||||
|
|
||||||
export interface RecentProjectsClientSnapshot {
|
export interface RecentProjectsClientSnapshot {
|
||||||
payload: DashboardRecentProjectsPayload;
|
payload: DashboardRecentProjectsPayload;
|
||||||
|
|
@ -18,7 +19,13 @@ export interface RecentProjectsClientSnapshot {
|
||||||
isStale: boolean;
|
isStale: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getRecentProjectsClientSnapshot(): RecentProjectsClientSnapshot | null {
|
export function getRecentProjectsClientSnapshot(
|
||||||
|
cacheKey: string
|
||||||
|
): RecentProjectsClientSnapshot | null {
|
||||||
|
if (cachedKey !== cacheKey) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const normalizedPayload = normalizeDashboardRecentProjectsPayload(cachedPayload);
|
const normalizedPayload = normalizeDashboardRecentProjectsPayload(cachedPayload);
|
||||||
if (!normalizedPayload) {
|
if (!normalizedPayload) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -40,39 +47,44 @@ export function getRecentProjectsClientSnapshot(): RecentProjectsClientSnapshot
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadRecentProjectsWithClientCache(
|
export async function loadRecentProjectsWithClientCache(
|
||||||
|
cacheKey: string,
|
||||||
loader: () => Promise<DashboardRecentProjectsPayloadLike>,
|
loader: () => Promise<DashboardRecentProjectsPayloadLike>,
|
||||||
options?: { force?: boolean }
|
options?: { force?: boolean }
|
||||||
): Promise<DashboardRecentProjectsPayload> {
|
): Promise<DashboardRecentProjectsPayload> {
|
||||||
const force = options?.force ?? false;
|
const force = options?.force ?? false;
|
||||||
const snapshot = getRecentProjectsClientSnapshot();
|
const snapshot = getRecentProjectsClientSnapshot(cacheKey);
|
||||||
|
|
||||||
if (!force && snapshot && !snapshot.isStale) {
|
if (!force && snapshot && !snapshot.isStale) {
|
||||||
return snapshot.payload;
|
return snapshot.payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (inFlightLoad) {
|
if (inFlightLoad?.key === cacheKey) {
|
||||||
return inFlightLoad;
|
return inFlightLoad.promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
const request = loader()
|
const request = loader()
|
||||||
.then((payloadLike) => {
|
.then((payloadLike) => {
|
||||||
const normalizedPayload = normalizeDashboardRecentProjectsPayload(payloadLike);
|
const normalizedPayload = normalizeDashboardRecentProjectsPayload(payloadLike);
|
||||||
cachedPayload = normalizedPayload;
|
if (inFlightLoad?.key === cacheKey && inFlightLoad.promise === request) {
|
||||||
cachedAt = Date.now();
|
cachedKey = normalizedPayload ? cacheKey : null;
|
||||||
|
cachedPayload = normalizedPayload;
|
||||||
|
cachedAt = Date.now();
|
||||||
|
}
|
||||||
return normalizedPayload ?? { projects: [], degraded: true };
|
return normalizedPayload ?? { projects: [], degraded: true };
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
if (inFlightLoad === request) {
|
if (inFlightLoad?.promise === request) {
|
||||||
inFlightLoad = null;
|
inFlightLoad = null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
inFlightLoad = request;
|
inFlightLoad = { key: cacheKey, promise: request };
|
||||||
return request;
|
return request;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function __resetRecentProjectsClientCacheForTests(): void {
|
export function __resetRecentProjectsClientCacheForTests(): void {
|
||||||
cachedPayload = null;
|
cachedPayload = null;
|
||||||
|
cachedKey = null;
|
||||||
cachedAt = 0;
|
cachedAt = 0;
|
||||||
inFlightLoad = null;
|
inFlightLoad = null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
__resetRecentProjectsClientCacheForTests,
|
__resetRecentProjectsClientCacheForTests,
|
||||||
getRecentProjectsClientSnapshot,
|
getRecentProjectsClientSnapshot,
|
||||||
loadRecentProjectsWithClientCache,
|
loadRecentProjectsWithClientCache,
|
||||||
} from '@features/recent-projects/renderer/utils/recentProjectsClientCache';
|
} from '@features/recent-projects/renderer/utils/recentProjectsClientCache';
|
||||||
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
DashboardRecentProject,
|
DashboardRecentProject,
|
||||||
|
|
@ -33,6 +32,19 @@ const payload = (
|
||||||
degraded: false,
|
degraded: false,
|
||||||
...overrides,
|
...overrides,
|
||||||
});
|
});
|
||||||
|
const LOCAL_CACHE_KEY = 'local';
|
||||||
|
const SSH_CACHE_KEY = 'ssh-dev';
|
||||||
|
|
||||||
|
function deferred<T>(): {
|
||||||
|
promise: Promise<T>;
|
||||||
|
resolve: (value: T) => void;
|
||||||
|
} {
|
||||||
|
let resolve!: (value: T) => void;
|
||||||
|
const promise = new Promise<T>((promiseResolve) => {
|
||||||
|
resolve = promiseResolve;
|
||||||
|
});
|
||||||
|
return { promise, resolve };
|
||||||
|
}
|
||||||
|
|
||||||
describe('recentProjectsClientCache', () => {
|
describe('recentProjectsClientCache', () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
|
@ -44,11 +56,15 @@ describe('recentProjectsClientCache', () => {
|
||||||
it('returns cached projects while the client cache is fresh', async () => {
|
it('returns cached projects while the client cache is fresh', async () => {
|
||||||
const loader = vi.fn().mockResolvedValue(payload('alpha'));
|
const loader = vi.fn().mockResolvedValue(payload('alpha'));
|
||||||
|
|
||||||
await expect(loadRecentProjectsWithClientCache(loader)).resolves.toEqual(payload('alpha'));
|
await expect(loadRecentProjectsWithClientCache(LOCAL_CACHE_KEY, loader)).resolves.toEqual(
|
||||||
await expect(loadRecentProjectsWithClientCache(loader)).resolves.toEqual(payload('alpha'));
|
payload('alpha')
|
||||||
|
);
|
||||||
|
await expect(loadRecentProjectsWithClientCache(LOCAL_CACHE_KEY, loader)).resolves.toEqual(
|
||||||
|
payload('alpha')
|
||||||
|
);
|
||||||
|
|
||||||
expect(loader).toHaveBeenCalledTimes(1);
|
expect(loader).toHaveBeenCalledTimes(1);
|
||||||
expect(getRecentProjectsClientSnapshot()?.payload).toEqual(payload('alpha'));
|
expect(getRecentProjectsClientSnapshot(LOCAL_CACHE_KEY)?.payload).toEqual(payload('alpha'));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('revalidates stale cache without dropping the previous snapshot', async () => {
|
it('revalidates stale cache without dropping the previous snapshot', async () => {
|
||||||
|
|
@ -60,20 +76,20 @@ describe('recentProjectsClientCache', () => {
|
||||||
.mockResolvedValueOnce(payload('alpha'))
|
.mockResolvedValueOnce(payload('alpha'))
|
||||||
.mockResolvedValueOnce(payload('beta'));
|
.mockResolvedValueOnce(payload('beta'));
|
||||||
|
|
||||||
await loadRecentProjectsWithClientCache(loader);
|
await loadRecentProjectsWithClientCache(LOCAL_CACHE_KEY, loader);
|
||||||
vi.setSystemTime(new Date('2026-04-14T12:00:16.000Z'));
|
vi.setSystemTime(new Date('2026-04-14T12:00:16.000Z'));
|
||||||
|
|
||||||
expect(getRecentProjectsClientSnapshot()).toMatchObject({
|
expect(getRecentProjectsClientSnapshot(LOCAL_CACHE_KEY)).toMatchObject({
|
||||||
payload: payload('alpha'),
|
payload: payload('alpha'),
|
||||||
isStale: true,
|
isStale: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect(loadRecentProjectsWithClientCache(loader, { force: true })).resolves.toEqual(
|
await expect(
|
||||||
payload('beta')
|
loadRecentProjectsWithClientCache(LOCAL_CACHE_KEY, loader, { force: true })
|
||||||
);
|
).resolves.toEqual(payload('beta'));
|
||||||
|
|
||||||
expect(loader).toHaveBeenCalledTimes(2);
|
expect(loader).toHaveBeenCalledTimes(2);
|
||||||
expect(getRecentProjectsClientSnapshot()).toMatchObject({
|
expect(getRecentProjectsClientSnapshot(LOCAL_CACHE_KEY)).toMatchObject({
|
||||||
payload: payload('beta'),
|
payload: payload('beta'),
|
||||||
isStale: false,
|
isStale: false,
|
||||||
});
|
});
|
||||||
|
|
@ -92,8 +108,8 @@ describe('recentProjectsClientCache', () => {
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const first = loadRecentProjectsWithClientCache(loader, { force: true });
|
const first = loadRecentProjectsWithClientCache(LOCAL_CACHE_KEY, loader, { force: true });
|
||||||
const second = loadRecentProjectsWithClientCache(loader, { force: true });
|
const second = loadRecentProjectsWithClientCache(LOCAL_CACHE_KEY, loader, { force: true });
|
||||||
|
|
||||||
expect(loader).toHaveBeenCalledTimes(1);
|
expect(loader).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
|
@ -111,24 +127,24 @@ describe('recentProjectsClientCache', () => {
|
||||||
.fn<() => Promise<DashboardRecentProjectsPayload>>()
|
.fn<() => Promise<DashboardRecentProjectsPayload>>()
|
||||||
.mockResolvedValueOnce(payload('alpha', { degraded: true }));
|
.mockResolvedValueOnce(payload('alpha', { degraded: true }));
|
||||||
|
|
||||||
await expect(loadRecentProjectsWithClientCache(loader)).resolves.toEqual(
|
await expect(loadRecentProjectsWithClientCache(LOCAL_CACHE_KEY, loader)).resolves.toEqual(
|
||||||
payload('alpha', { degraded: true })
|
payload('alpha', { degraded: true })
|
||||||
);
|
);
|
||||||
|
|
||||||
vi.setSystemTime(new Date('2026-04-14T12:00:01.000Z'));
|
vi.setSystemTime(new Date('2026-04-14T12:00:01.000Z'));
|
||||||
expect(getRecentProjectsClientSnapshot()).toMatchObject({
|
expect(getRecentProjectsClientSnapshot(LOCAL_CACHE_KEY)).toMatchObject({
|
||||||
payload: payload('alpha', { degraded: true }),
|
payload: payload('alpha', { degraded: true }),
|
||||||
isStale: false,
|
isStale: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
vi.setSystemTime(new Date('2026-04-14T12:00:20.000Z'));
|
vi.setSystemTime(new Date('2026-04-14T12:00:20.000Z'));
|
||||||
expect(getRecentProjectsClientSnapshot()).toMatchObject({
|
expect(getRecentProjectsClientSnapshot(LOCAL_CACHE_KEY)).toMatchObject({
|
||||||
payload: payload('alpha', { degraded: true }),
|
payload: payload('alpha', { degraded: true }),
|
||||||
isStale: false,
|
isStale: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
vi.setSystemTime(new Date('2026-04-14T12:00:31.000Z'));
|
vi.setSystemTime(new Date('2026-04-14T12:00:31.000Z'));
|
||||||
expect(getRecentProjectsClientSnapshot()).toMatchObject({
|
expect(getRecentProjectsClientSnapshot(LOCAL_CACHE_KEY)).toMatchObject({
|
||||||
payload: payload('alpha', { degraded: true }),
|
payload: payload('alpha', { degraded: true }),
|
||||||
isStale: true,
|
isStale: true,
|
||||||
});
|
});
|
||||||
|
|
@ -139,7 +155,52 @@ describe('recentProjectsClientCache', () => {
|
||||||
.fn<() => Promise<DashboardRecentProject[]>>()
|
.fn<() => Promise<DashboardRecentProject[]>>()
|
||||||
.mockResolvedValue([project('alpha')]);
|
.mockResolvedValue([project('alpha')]);
|
||||||
|
|
||||||
await expect(loadRecentProjectsWithClientCache(loader)).resolves.toEqual(payload('alpha'));
|
await expect(loadRecentProjectsWithClientCache(LOCAL_CACHE_KEY, loader)).resolves.toEqual(
|
||||||
expect(getRecentProjectsClientSnapshot()?.payload).toEqual(payload('alpha'));
|
payload('alpha')
|
||||||
|
);
|
||||||
|
expect(getRecentProjectsClientSnapshot(LOCAL_CACHE_KEY)?.payload).toEqual(payload('alpha'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not serve a cached payload across active context keys', async () => {
|
||||||
|
const loader = vi
|
||||||
|
.fn<() => Promise<DashboardRecentProjectsPayload>>()
|
||||||
|
.mockResolvedValueOnce(payload('local-alpha'))
|
||||||
|
.mockResolvedValueOnce(payload('ssh-beta'));
|
||||||
|
|
||||||
|
await expect(loadRecentProjectsWithClientCache(LOCAL_CACHE_KEY, loader)).resolves.toEqual(
|
||||||
|
payload('local-alpha')
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(getRecentProjectsClientSnapshot(SSH_CACHE_KEY)).toBeNull();
|
||||||
|
await expect(loadRecentProjectsWithClientCache(SSH_CACHE_KEY, loader)).resolves.toEqual(
|
||||||
|
payload('ssh-beta')
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(loader).toHaveBeenCalledTimes(2);
|
||||||
|
expect(getRecentProjectsClientSnapshot(LOCAL_CACHE_KEY)).toBeNull();
|
||||||
|
expect(getRecentProjectsClientSnapshot(SSH_CACHE_KEY)?.payload).toEqual(payload('ssh-beta'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not reuse or cache an in-flight payload for a different context key', async () => {
|
||||||
|
const localRequest = deferred<DashboardRecentProjectsPayload>();
|
||||||
|
const sshRequest = deferred<DashboardRecentProjectsPayload>();
|
||||||
|
const loader = vi
|
||||||
|
.fn<() => Promise<DashboardRecentProjectsPayload>>()
|
||||||
|
.mockReturnValueOnce(localRequest.promise)
|
||||||
|
.mockReturnValueOnce(sshRequest.promise);
|
||||||
|
|
||||||
|
const localLoad = loadRecentProjectsWithClientCache(LOCAL_CACHE_KEY, loader, { force: true });
|
||||||
|
const sshLoad = loadRecentProjectsWithClientCache(SSH_CACHE_KEY, loader, { force: true });
|
||||||
|
|
||||||
|
expect(loader).toHaveBeenCalledTimes(2);
|
||||||
|
|
||||||
|
sshRequest.resolve(payload('ssh-beta'));
|
||||||
|
await expect(sshLoad).resolves.toEqual(payload('ssh-beta'));
|
||||||
|
expect(getRecentProjectsClientSnapshot(SSH_CACHE_KEY)?.payload).toEqual(payload('ssh-beta'));
|
||||||
|
|
||||||
|
localRequest.resolve(payload('local-alpha'));
|
||||||
|
await expect(localLoad).resolves.toEqual(payload('local-alpha'));
|
||||||
|
expect(getRecentProjectsClientSnapshot(LOCAL_CACHE_KEY)).toBeNull();
|
||||||
|
expect(getRecentProjectsClientSnapshot(SSH_CACHE_KEY)?.payload).toEqual(payload('ssh-beta'));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue