Files
frontend/src/shared/api/resource-query.ts
T
ksv741 3c226f8c29
Frontend verify / commits (push) Skipped
Frontend verify / security (push) Successful in 12s
Frontend verify / verify (push) Successful in 7m41s
feat: render hourly investment states
2026-10-09 21:13:34 +03:00

156 lines
4.1 KiB
TypeScript

import {
type QueryClient,
type QueryFunctionContext,
useQueryClient,
type UseQueryOptions,
} from "@tanstack/react-query";
import { useEffect } from "react";
import type { components, paths } from "./generated/schema";
import { useAuthenticatedQuery } from "./authenticated-query";
import { getAuthenticatedSessionSnapshot } from "./client";
import { ApiErrorCode, apiErrorCode, isApiErrorCode } from "./problem-details";
type InvestmentResourceMeta = components["schemas"]["InvestmentResourceMetaDto"];
type Resource = {
[key: string]: unknown;
data: unknown;
meta: InvestmentResourceMeta;
sync?: unknown;
};
type ResourcePath = {
[P in keyof paths]: paths[P]["get"] extends {
// eslint-disable-next-line @typescript-eslint/no-magic-numbers -- HTTP success response in the generated contract.
responses: { 200: { content: { "application/json": Resource } } };
} ? P : never;
}[keyof paths];
type ResourceQueryKey = readonly [
"get",
ResourcePath,
{ params?: { path?: object; query?: { sync?: "observe" } } }?,
];
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 affected = {
predicate: (cached: { meta?: Record<string, unknown> }) =>
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",
});
}
// Keep endpoint and parameters intact. The generated queryFn accepts the
// optional sync mode even for a cache key originally created without init.
function observationKey<K extends readonly unknown[]>(key: K): K {
const [method, path, init] = key as unknown as [
string,
string,
{ params?: { path?: object; query?: object } }?,
];
return [method, path, {
...init,
params: { ...init?.params, query: { ...init?.params?.query, sync: "observe" } },
}] as unknown as K;
}
// Observation changes transport mode, never the generated cache identity.
function useResourceQuery<
T extends Resource,
E = unknown,
K extends readonly unknown[] = ResourceQueryKey,
>(
options: UseQueryOptions<T, E, T, K>,
) {
const client = useQueryClient();
const group
= typeof options.meta?.accessGroup === "string"
? options.meta.accessGroup
: undefined;
const query = useAuthenticatedQuery(options);
useEffect(() => {
if (group) invalidateResourceAccess(client, group, query.error);
}, [client, group, query.error]);
const observe = async () => {
const queryFunction = options.queryFn;
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<K>) => {
const queryKey = observationKey(context.queryKey);
return queryFunction({ ...context, queryKey });
},
retry: false,
// Keep the cache envelope independent of observer projection.
select: undefined,
staleTime: 0,
});
};
const data: T["data"] | undefined = query.data?.data;
const meta: T["meta"] | undefined = query.data?.meta;
// Project resource fields; refetch still returns the transport envelope.
return {
data: data ?? undefined,
error: query.error,
fetchStatus: query.fetchStatus,
isEnabled: query.isEnabled,
isError: query.isError,
isFetching: query.isFetching,
isLoading: query.isLoading,
isPending: query.isPending,
isSuccess: query.isSuccess,
meta,
observe,
queryKey: options.queryKey,
refetch: query.refetch,
status: query.status,
sync: query.data && "sync" in query.data ? query.data.sync : undefined,
};
}
export {
type InvestmentResourceMeta,
useResourceQuery,
};