/** * TanStack Query data fetching layer. * * Handles async data fetching, caching, and invalidation for: * - Lore status (GitLab integration health) * - Bridge status (mapping counts, sync times) * - Sync/reconcile mutations * * Query keys are centralized here for consistent invalidation. */ import { useEffect } from "react"; import { QueryClient, useQuery, useMutation, useQueryClient, type UseQueryResult, type UseMutationResult, } from "@tanstack/react-query"; import { invoke } from "@tauri-apps/api/core"; import type { UnlistenFn } from "@tauri-apps/api/event"; import { events } from "@/lib/bindings"; import type { LoreStatus, BridgeStatus, SyncResult, McError, FocusItem, } from "@/lib/types"; import type { LoreItemsResponse } from "@/lib/bindings"; // --- Query Keys --- export const queryKeys = { loreStatus: ["lore-status"] as const, bridgeStatus: ["bridge-status"] as const, loreItems: ["lore-items"] as const, } as const; // --- QueryClient Factory --- /** * Create a configured QueryClient instance. * * Default options: * - retry: 1 (one retry on failure) * - refetchOnWindowFocus: true (refresh when user returns) */ export function createQueryClient(): QueryClient { return new QueryClient({ defaultOptions: { queries: { retry: 1, refetchOnWindowFocus: true, }, }, }); } // --- Event-Based Invalidation Hook --- /** * Hook to set up query invalidation on Tauri events. * * Listens for: * - loreDataChanged: Invalidates lore and bridge status */ export function useQueryInvalidation(): void { const queryClient = useQueryClient(); useEffect(() => { let cancelled = false; let unlisten: UnlistenFn | undefined; // Invalidate on lore data changes (typed event) events.loreDataChanged .listen(() => { queryClient.invalidateQueries({ queryKey: queryKeys.loreStatus }); queryClient.invalidateQueries({ queryKey: queryKeys.bridgeStatus }); }) .then((fn) => { if (cancelled) { fn(); } else { unlisten = fn; } }); return () => { cancelled = true; if (unlisten) unlisten(); }; }, [queryClient]); } // --- Query Hooks --- /** * Fetch lore integration status. * * Returns health, last sync time, and summary counts. * Stale time: 30 seconds (fresh data is important but not real-time) */ export function useLoreStatus(): UseQueryResult { const queryClient = useQueryClient(); // Set up event-based invalidation (typed event) useEffect(() => { let cancelled = false; let unlisten: UnlistenFn | undefined; events.loreDataChanged .listen(() => { queryClient.invalidateQueries({ queryKey: queryKeys.loreStatus }); }) .then((fn) => { if (cancelled) { fn(); } else { unlisten = fn; } }); return () => { cancelled = true; if (unlisten) unlisten(); }; }, [queryClient]); return useQuery({ queryKey: queryKeys.loreStatus, queryFn: () => invoke("get_lore_status"), staleTime: 30 * 1000, // 30 seconds }); } /** * Fetch bridge status (mapping counts, sync times). * * Stale time: 30 seconds */ export function useBridgeStatus(): UseQueryResult { const queryClient = useQueryClient(); // Set up event-based invalidation (typed event) useEffect(() => { let cancelled = false; let unlisten: UnlistenFn | undefined; events.loreDataChanged .listen(() => { queryClient.invalidateQueries({ queryKey: queryKeys.bridgeStatus }); }) .then((fn) => { if (cancelled) { fn(); } else { unlisten = fn; } }); return () => { cancelled = true; if (unlisten) unlisten(); }; }, [queryClient]); return useQuery({ queryKey: queryKeys.bridgeStatus, queryFn: () => invoke("get_bridge_status"), staleTime: 30 * 1000, // 30 seconds }); } // --- Mutation Hooks --- /** * Trigger an incremental sync (process since_last_check events). * * On success, invalidates lore and bridge status queries. */ export function useSyncNow(): UseMutationResult { const queryClient = useQueryClient(); return useMutation({ mutationFn: () => invoke("sync_now"), onSuccess: () => { queryClient.invalidateQueries({ queryKey: queryKeys.loreStatus }); queryClient.invalidateQueries({ queryKey: queryKeys.bridgeStatus }); }, }); } /** * Trigger a full reconciliation pass. * * On success, invalidates lore and bridge status queries. */ export function useReconcile(): UseMutationResult { const queryClient = useQueryClient(); return useMutation({ mutationFn: () => invoke("reconcile"), onSuccess: () => { queryClient.invalidateQueries({ queryKey: queryKeys.loreStatus }); queryClient.invalidateQueries({ queryKey: queryKeys.bridgeStatus }); }, }); } // --- Lore Items Query --- /** * Transform raw LoreItemsResponse into FocusItem array. */ function transformLoreItems(response: LoreItemsResponse): FocusItem[] { if (!response.success || !response.items) { return []; } return response.items.map((item) => ({ id: item.id, title: item.title, type: item.item_type as FocusItem["type"], project: item.project, url: item.url, iid: item.iid, updatedAt: item.updated_at ?? null, contextQuote: null, requestedBy: item.requested_by ?? null, snoozedUntil: null, })); } /** * Fetch lore items and transform to FocusItem format. * * Returns work items from GitLab (reviews, issues, authored MRs). * Stale time: 30 seconds */ export function useLoreItems(): UseQueryResult { const queryClient = useQueryClient(); // Set up event-based invalidation (typed event) useEffect(() => { let cancelled = false; let unlisten: UnlistenFn | undefined; events.loreDataChanged .listen(() => { queryClient.invalidateQueries({ queryKey: queryKeys.loreItems }); }) .then((fn) => { if (cancelled) { fn(); } else { unlisten = fn; } }); return () => { cancelled = true; if (unlisten) unlisten(); }; }, [queryClient]); return useQuery({ queryKey: queryKeys.loreItems, queryFn: async () => { const response = await invoke("get_lore_items"); return transformLoreItems(response); }, staleTime: 30 * 1000, // 30 seconds }); }