/** * 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 { listen, type UnlistenFn } from "@tauri-apps/api/event"; import type { LoreStatus, BridgeStatus, SyncResult, McError, } from "@/lib/types"; // --- Query Keys --- export const queryKeys = { loreStatus: ["lore-status"] as const, bridgeStatus: ["bridge-status"] 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: * - lore-data-changed: Invalidates lore and bridge status * - sync-status (completed): Invalidates lore and bridge status */ export function useQueryInvalidation(): void { const queryClient = useQueryClient(); useEffect(() => { let cancelled = false; const unlisteners: Promise[] = []; // Invalidate on lore data changes const loreUnlisten = listen("lore-data-changed", () => { queryClient.invalidateQueries({ queryKey: queryKeys.loreStatus }); queryClient.invalidateQueries({ queryKey: queryKeys.bridgeStatus }); }); unlisteners.push(loreUnlisten); // Invalidate on sync completion const syncUnlisten = listen<{ status: string; message?: string }>( "sync-status", (event) => { if (event.payload.status === "completed") { queryClient.invalidateQueries({ queryKey: queryKeys.loreStatus }); queryClient.invalidateQueries({ queryKey: queryKeys.bridgeStatus }); } } ); unlisteners.push(syncUnlisten); return () => { cancelled = true; // Cleanup all listeners Promise.all(unlisteners).then((fns) => { if (!cancelled) return; for (const fn of fns) { fn(); } }); }; }, [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 useEffect(() => { let cancelled = false; let unlisten: UnlistenFn | undefined; listen("lore-data-changed", () => { queryClient.invalidateQueries({ queryKey: queryKeys.loreStatus }); }).then((fn) => { if (cancelled) { fn(); } else { unlisten = fn; } }); // Also listen for sync completion let syncUnlisten: UnlistenFn | undefined; listen<{ status: string }>("sync-status", (event) => { if (event.payload.status === "completed") { queryClient.invalidateQueries({ queryKey: queryKeys.loreStatus }); } }).then((fn) => { if (cancelled) { fn(); } else { syncUnlisten = fn; } }); return () => { cancelled = true; if (unlisten) unlisten(); if (syncUnlisten) syncUnlisten(); }; }, [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 useEffect(() => { let cancelled = false; let unlisten: UnlistenFn | undefined; listen("lore-data-changed", () => { 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 }); }, }); }