import { type QueryClient, type QueryFunctionContext, type QueryKey, useQueryClient, type UseQueryOptions, type UseQueryResult, } from "@tanstack/react-query"; import { useEffect } from "react"; import type { components } from "./generated/schema"; import { useAuthenticatedQuery } from "./authenticated-query"; import { getAuthenticatedSessionSnapshot } from "./client"; import { ApiErrorCode, apiErrorCode, isApiErrorCode } from "./problem-details"; const accessEpochs = new WeakMap>(); type InvestmentResourceMeta = components["schemas"]["InvestmentResourceMetaDto"]; type Resource = { data: unknown; meta: InvestmentResourceMeta; }; function invalidateResourceAccess( client: QueryClient, group: string, error: unknown, ) { const errorCode = apiErrorCode(error); if ( [ ApiErrorCode.Forbidden, ApiErrorCode.InvalidTbankToken, ApiErrorCode.NotFound, ApiErrorCode.TbankNotConnected, ].every(code => !isApiErrorCode(errorCode, code)) ) return; const epochs = accessEpochs.get(client) ?? new Map(); accessEpochs.set(client, epochs); epochs.set(group, (epochs.get(group) ?? 0) + 1); const affected = { predicate: (cached: { meta?: Record }) => cached.meta?.accessGroup === group, }; void client.cancelQueries(affected); for (const cached of client.getQueryCache().findAll(affected)) cached.setState({ data: undefined, dataUpdatedAt: 0, error: error as Error, fetchStatus: "idle", status: "error", }); } function unwrapResourceResult( result: UseQueryResult, ) { const isMissing = result.isSuccess && result.data.data === null; const data: T["data"] | undefined = result.data?.data; const meta: T["meta"] | undefined = result.data?.meta; const status: UseQueryResult["status"] = isMissing ? "pending" : result.status; // Project resource fields; refetch still returns the transport envelope. return { ...result, data: data ?? undefined, isPending: result.isPending || isMissing, isSuccess: result.isSuccess && !isMissing, meta, status, transport: { error: result.error, fetchStatus: result.fetchStatus, status: result.status, }, }; } // Observation changes transport mode, never the generated cache identity. function useResourceQuery( options: UseQueryOptions, ) { const client = useQueryClient(); const group = typeof options.meta?.accessGroup === "string" ? options.meta.accessGroup : undefined; let epochs = accessEpochs.get(client); if (!epochs) { epochs = new Map(); accessEpochs.set(client, epochs); } const groupEpochs = epochs; const queryFunction = options.queryFn; const guarded = typeof queryFunction === "function" ? async (context: QueryFunctionContext) => { const epoch = group ? (groupEpochs.get(group) ?? 0) : 0; const result = await queryFunction(context); if (group && epoch !== (groupEpochs.get(group) ?? 0)) throw new DOMException("Access changed", "AbortError"); return result; } : queryFunction; const query = useAuthenticatedQuery({ ...options, queryFn: guarded }); useEffect(() => { if (group) invalidateResourceAccess(client, group, query.error); }, [client, group, query.error]); const observe = async (expectedStateVersion?: string) => { const queryFunction = guarded; if ( typeof queryFunction !== "function" || !query.isEnabled || !getAuthenticatedSessionSnapshot() ) return; // Cancellation is synchronous; start the replacement in the same boundary. void client.cancelQueries({ exact: true, queryKey: options.queryKey }); return client.query({ ...options, queryFn: async (context: QueryFunctionContext) => { const [method, path, input] = context.queryKey; const init = input as undefined | { params?: { query?: Record } }; const queryKey = [ method, path, { ...init, params: { ...init?.params, query: { ...init?.params?.query, sync: "observe" }, }, }, ] as unknown as K; const response = await queryFunction({ ...context, queryKey }); if ( expectedStateVersion && response.meta.stateVersion !== expectedStateVersion ) { // One more observational read; never turn a stale notification into an import. const current = await queryFunction({ ...context, queryKey }); if (current.meta.stateVersion !== expectedStateVersion) throw new DOMException("Resource changed", "AbortError"); return current; } return response; }, retry: false, // Observation validates raw resource metadata, before any observer projection. select: undefined, staleTime: 0, }); }; return { ...unwrapResourceResult(query), observe, queryKey: options.queryKey, }; } export { invalidateResourceAccess, type InvestmentResourceMeta, unwrapResourceResult, useResourceQuery, };