import { useQueryClient } from "@tanstack/react-query"; import { useEffect, useRef, useState } from "react"; import { apiErrorCode, apiQueryClient, invalidateResourceAccess, type paths, useAuthenticatedQueries, } from "@/shared/api"; import { type InvestmentScreen, useTbankSync } from "./sync"; type ResourceMeta = paths["/api/v1/tbank-sync/{id}"]["get"]["responses"][200]["content"]["application/json"]["resources"][number]; type ScreenResource = { meta?: ResourceMeta; observe: (version?: string) => Promise; }; const STATUS_INTERVAL_MS = 1000; function useScreenSync(scope: InvestmentScreen, resources: ScreenResource[]) { const mutation = useTbankSync(); const client = useQueryClient(); const denied = useRef(new Set()); const scopeKey = JSON.stringify(scope); const [requested, setRequested] = useState<{ scope: string; id: string; } | null>(null); const requestInFlight = useRef(null); const currentScope = useRef(scopeKey); currentScope.current = scopeKey; const currentMutation = JSON.stringify(mutation.variables?.body) === scopeKey; const observed = useRef(new Map()); const ids = [ ...new Set([ ...(requested?.scope === scopeKey ? [requested.id] : []), ...resources .flatMap(({ meta }) => meta ? [meta, ...(meta.dependencies ?? [])] : [], ) .flatMap((meta) => meta?.sync.id && (meta.sync.state === "running" || meta.sync.state === "queued") ? [meta.sync.id] : [], ), ]), ]; const statuses = useAuthenticatedQueries( ids.map((id) => ({ ...apiQueryClient.queryOptions("get", "/api/v1/tbank-sync/{id}", { params: { path: { id } }, }), retry: false, refetchOnMount: false, refetchOnWindowFocus: false, refetchInterval: (query: { state: { error: unknown; data?: { state: string } }; }) => query.state.error || (query.state.data && query.state.data.state !== "running") ? false : STATUS_INTERVAL_MS, })), ); useEffect(() => { for (const status of statuses) { if (status.error && !denied.current.has(status.error)) { denied.current.add(status.error); if (apiErrorCode(status.error) !== "NOT_FOUND") invalidateResourceAccess(client, "brokerage", status.error); } } const latest = new Map(); for (const status of [...statuses].sort( (a, b) => a.dataUpdatedAt - b.dataUpdatedAt, )) for (const meta of status.data?.resources ?? []) { latest.set(meta.resourceKey, meta); for (const dependency of meta.dependencies ?? []) latest.set(dependency.resourceKey, dependency); } for (const resource of resources) { if (!resource.meta) continue; const changed = [ resource.meta, ...(resource.meta.dependencies ?? []), ].flatMap((current) => { const notified = latest.get(current.resourceKey); return notified && notified.stateVersion !== current.stateVersion ? [notified] : []; }); if (!changed.length) continue; const notification = JSON.stringify( changed.map((meta) => [meta.resourceKey, meta.stateVersion]), ); if (observed.current.get(resource.meta.resourceKey) === notification) continue; observed.current.set(resource.meta.resourceKey, notification); const own = changed.find( (meta) => meta.resourceKey === resource.meta?.resourceKey, ); void resource.observe(own?.stateVersion).catch(() => undefined); } }, [client, statuses, resources]); const refreshing = (currentMutation && mutation.isPending) || statuses.some( (status) => !status.isError && status.data?.state === "running", ) || resources.some(({ meta }) => { const status = statuses[ids.indexOf(meta?.sync.id ?? "")]; return ( !status?.isError && (!status?.data || status.data.state === "running") && (meta?.sync.state === "running" || meta?.sync.state === "queued") ); }); const refresh = async () => { if (requestInFlight.current === scopeKey || refreshing) return; requestInFlight.current = scopeKey; try { const accepted = await mutation.mutateAsync({ body: scope }); if ( currentScope.current === scopeKey && requestInFlight.current === scopeKey ) setRequested({ scope: scopeKey, id: accepted.id }); } catch { /* The mutation retains the transport error for presentation. */ } finally { if (requestInFlight.current === scopeKey) requestInFlight.current = null; } }; return { refresh, refreshing, transportError: (currentMutation && mutation.isError) || statuses.some((status) => status.isError), sourceError: statuses.some((status) => status.data?.state === "failed") || resources.some(({ meta }) => meta?.sync.state === "failed"), }; } export { useScreenSync };