refactor!: simplify query and session ownership
Frontend verify / commits (pull_request) Failing after 27s
Frontend verify / security (pull_request) Skipped
Frontend verify / verify (pull_request) Skipped

Use Query cancellation and native mutation results instead of HTTP
registries, access epochs and copied scope state.
Keep local auth ordering and mutation identity fences.
Allow staged generated files to remain excluded from ESLint.

Backend contract: WealthLedger/backend@d698ed9

BREAKING CHANGE: drop investment stateVersion/progress/resources
from the client contract and use data/metadata readiness instead of
rewritten query status. Cross-tab auth may require re-login.
This commit is contained in:
2026-10-03 11:45:04 +03:00
parent 6a89b08ee5
commit 69997075e5
25 changed files with 151 additions and 551 deletions
+21 -75
View File
@@ -3,7 +3,6 @@ import {
type QueryFunctionContext,
useQueryClient,
type UseQueryOptions,
type UseQueryResult,
} from "@tanstack/react-query";
import { useEffect } from "react";
@@ -13,8 +12,6 @@ import { useAuthenticatedQuery } from "./authenticated-query";
import { getAuthenticatedSessionSnapshot } from "./client";
import { ApiErrorCode, apiErrorCode, isApiErrorCode } from "./problem-details";
const accessEpochs = new WeakMap<QueryClient, Map<string, number>>();
type InvestmentResourceMeta = components["schemas"]["InvestmentResourceMetaDto"];
type Resource = {
@@ -50,9 +47,6 @@ function invalidateResourceAccess(
].every(code => !isApiErrorCode(errorCode, code))
)
return;
const epochs = accessEpochs.get(client) ?? new Map<string, number>();
accessEpochs.set(client, epochs);
epochs.set(group, (epochs.get(group) ?? 0) + 1);
const affected = {
predicate: (cached: { meta?: Record<string, unknown> }) =>
@@ -81,33 +75,6 @@ function observationKey<K extends ResourceQueryKey>(key: K): K {
}] as unknown as K;
}
function unwrapResourceResult<T extends Resource, E>(
result: UseQueryResult<T, E>,
) {
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<T, E>["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<T extends Resource, E, K extends ResourceQueryKey>(
options: UseQueryOptions<T, E, T, K>,
@@ -119,35 +86,13 @@ function useResourceQuery<T extends Resource, E, K extends ResourceQueryKey>(
? 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<K>) => {
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 });
const query = useAuthenticatedQuery(options);
useEffect(() => {
if (group) invalidateResourceAccess(client, group, query.error);
}, [client, group, query.error]);
const observe = async (expectedStateVersion?: string) => {
const queryFunction = guarded;
const observe = async () => {
const queryFunction = options.queryFn;
if (
typeof queryFunction !== "function"
|| !query.isEnabled
@@ -162,33 +107,34 @@ function useResourceQuery<T extends Resource, E, K extends ResourceQueryKey>(
queryFn: async (context: QueryFunctionContext<K>) => {
const queryKey = observationKey(context.queryKey);
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;
return queryFunction({ ...context, queryKey });
},
retry: false,
// Observation validates raw resource metadata, before any observer projection.
// 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 {
...unwrapResourceResult(query),
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,
};
}