import type { InfiniteData, QueryClient, QueryKey, } from "@tanstack/react-query"; import { agentActivityKeys, agentRunCountsKeys, agentTaskSnapshotKeys, agentTasksKeys, } from "../agents/queries"; import { labelKeys } from "../labels/queries"; import type { Issue, ListIssuesCache, ListIssuesResponse, } from "../types"; import { findIssueLocation, removeIssueFromBuckets } from "./cache-helpers"; import { issueKeys } from "./queries"; import { useRecentIssuesStore } from "./stores/recent-issues-store"; export type DeletedIssueCacheMetadata = { parentIssueIds: string[]; }; function collectParentId( parentIssueIds: Set, parentId: string | null | undefined, ) { if (parentId) parentIssueIds.add(parentId); } function collectParentFromListCache( parentIssueIds: Set, data: ListIssuesCache | undefined, issueId: string, ) { const parentId = data ? findIssueLocation(data, issueId)?.issue.parent_issue_id : undefined; collectParentId(parentIssueIds, parentId); } function parentIdFromChildrenKey(key: QueryKey) { const parentId = key[key.length - 1]; return typeof parentId === "string" ? parentId : null; } export function collectDeletedIssueCacheMetadata( qc: QueryClient, wsId: string, issueId: string, ): DeletedIssueCacheMetadata { const parentIssueIds = new Set(); const detail = qc.getQueryData(issueKeys.detail(wsId, issueId)); collectParentId(parentIssueIds, detail?.parent_issue_id); for (const [, data] of qc.getQueriesData({ queryKey: issueKeys.list(wsId), })) { collectParentFromListCache(parentIssueIds, data, issueId); } for (const [, data] of qc.getQueriesData< InfiniteData >({ queryKey: issueKeys.flatAll(wsId) })) { for (const page of data?.pages ?? []) { collectParentId( parentIssueIds, page.issues.find((issue) => issue.id === issueId)?.parent_issue_id, ); } } for (const [, data] of qc.getQueriesData({ queryKey: issueKeys.myAll(wsId), })) { collectParentFromListCache(parentIssueIds, data, issueId); } for (const [key, data] of qc.getQueriesData({ queryKey: [...issueKeys.all(wsId), "children"], })) { const child = data?.find((issue) => issue.id === issueId); if (!child) continue; collectParentId(parentIssueIds, child.parent_issue_id); collectParentId(parentIssueIds, parentIdFromChildrenKey(key)); } return { parentIssueIds: Array.from(parentIssueIds) }; } export function pruneDeletedIssueFromListCaches( qc: QueryClient, wsId: string, issueId: string, ) { for (const [key] of qc.getQueriesData({ queryKey: issueKeys.list(wsId), })) { qc.setQueryData(key, (old) => old ? removeIssueFromBuckets(old, issueId) : old, ); } for (const [key] of qc.getQueriesData({ queryKey: issueKeys.myAll(wsId), })) { qc.setQueryData(key, (old) => old ? removeIssueFromBuckets(old, issueId) : old, ); } for (const [key, data] of qc.getQueriesData< InfiniteData >({ queryKey: issueKeys.flatAll(wsId) })) { if (!data?.pages) continue; const found = data.pages.some((page) => page.issues.some((issue) => issue.id === issueId), ); if (!found) continue; qc.setQueryData>(key, { ...data, pages: data.pages.map((page) => ({ ...page, total: Math.max(0, page.total - 1), issues: page.issues.filter((issue) => issue.id !== issueId), })), }); } } export function pruneDeletedIssueFromParentChildrenCaches( qc: QueryClient, wsId: string, issueId: string, metadata: DeletedIssueCacheMetadata, ) { for (const parentId of metadata.parentIssueIds) { qc.setQueryData(issueKeys.children(wsId, parentId), (old) => old?.filter((issue) => issue.id !== issueId), ); } } export function invalidateDeletedIssueParentCaches( qc: QueryClient, wsId: string, metadata: DeletedIssueCacheMetadata, ) { if (metadata.parentIssueIds.length === 0) return; for (const parentId of metadata.parentIssueIds) { qc.invalidateQueries({ queryKey: issueKeys.children(wsId, parentId) }); } qc.invalidateQueries({ queryKey: issueKeys.childProgress(wsId) }); qc.invalidateQueries({ queryKey: issueKeys.childrenByParentsAll(wsId) }); } export function invalidateDeletedIssueDependentCaches( qc: QueryClient, wsId: string, ) { qc.invalidateQueries({ queryKey: agentTaskSnapshotKeys.list(wsId) }); qc.invalidateQueries({ queryKey: agentActivityKeys.last30d(wsId) }); qc.invalidateQueries({ queryKey: agentRunCountsKeys.last30d(wsId) }); qc.invalidateQueries({ queryKey: agentTasksKeys.all(wsId) }); } export function invalidateIssueScopedCaches( qc: QueryClient, wsId: string, issueId: string, ) { qc.invalidateQueries({ queryKey: issueKeys.timeline(issueId) }); qc.invalidateQueries({ queryKey: issueKeys.reactions(issueId) }); qc.invalidateQueries({ queryKey: issueKeys.subscribers(issueId) }); qc.invalidateQueries({ queryKey: issueKeys.usage(issueId) }); qc.invalidateQueries({ queryKey: issueKeys.attachments(issueId) }); qc.invalidateQueries({ queryKey: issueKeys.tasks(issueId) }); qc.invalidateQueries({ queryKey: issueKeys.children(wsId, issueId) }); qc.invalidateQueries({ queryKey: labelKeys.byIssue(wsId, issueId) }); } export function cleanupDeletedIssueCaches( qc: QueryClient, wsId: string, issueId: string, metadata = collectDeletedIssueCacheMetadata(qc, wsId, issueId), ) { pruneDeletedIssueFromListCaches(qc, wsId, issueId); pruneDeletedIssueFromParentChildrenCaches(qc, wsId, issueId, metadata); invalidateDeletedIssueParentCaches(qc, wsId, metadata); qc.removeQueries({ queryKey: issueKeys.detail(wsId, issueId) }); qc.removeQueries({ queryKey: issueKeys.timeline(issueId) }); qc.removeQueries({ queryKey: issueKeys.reactions(issueId) }); qc.removeQueries({ queryKey: issueKeys.subscribers(issueId) }); qc.removeQueries({ queryKey: issueKeys.usage(issueId) }); qc.removeQueries({ queryKey: issueKeys.attachments(issueId) }); qc.removeQueries({ queryKey: issueKeys.tasks(issueId) }); qc.removeQueries({ queryKey: issueKeys.children(wsId, issueId) }); qc.removeQueries({ queryKey: labelKeys.byIssue(wsId, issueId) }); qc.invalidateQueries({ queryKey: issueKeys.childProgress(wsId) }); qc.invalidateQueries({ queryKey: issueKeys.list(wsId) }); qc.invalidateQueries({ queryKey: issueKeys.myAll(wsId) }); qc.invalidateQueries({ queryKey: issueKeys.flatAll(wsId) }); qc.invalidateQueries({ queryKey: issueKeys.tableAll(wsId) }); // Project Gantt cache lives outside `myAll`, so it needs an explicit // refresh when an issue is removed — the deleted row may have been a // scheduled bar visible right now. qc.invalidateQueries({ queryKey: issueKeys.projectGanttAll(wsId) }); invalidateDeletedIssueDependentCaches(qc, wsId); // Recent Issues store persists to localStorage and survives reloads, so a // deleted id left behind keeps the Cmd+K command bar firing 404s on every // open. Both the delete mutation and the WS delete event flow through here, // so a single call covers self-delete and cross-client delete. useRecentIssuesStore.getState().forgetIssue(wsId, issueId); }