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
+6 -54
View File
@@ -23,14 +23,6 @@ type AuthHandlers = {
refresh: () => Promise<boolean>;
};
const inFlight = new Set<{ invalidated: boolean; pathname: string }>();
const latestReads = new Map<string, { pending: number; sequence: number }>();
function invalidateApiRequests(isMatches: (pathname: string) => boolean) {
for (const request of inFlight)
if (isMatches(request.pathname)) request.invalidated = true;
}
const sessionState: { handlers?: AuthHandlers; hasSession: boolean } = {
hasSession: false,
};
@@ -38,48 +30,6 @@ const sessionState: { handlers?: AuthHandlers; hasSession: boolean } = {
const authSessionListeners = new Set<() => void>();
async function authenticatedFetch(request: Request): Promise<Response> {
const url = new URL(request.url);
url.searchParams.delete("sync");
url.searchParams.sort();
const key = `${request.method} ${url}`;
const ticket = { invalidated: false, pathname: url.pathname };
const read
= request.method === "GET"
? (latestReads.get(key) ?? { pending: 0, sequence: 0 })
: undefined;
if (read) {
read.sequence += 1;
read.pending += 1;
latestReads.set(key, read);
}
const sequence = read?.sequence;
inFlight.add(ticket);
try {
return await fetchCurrent(
request,
() => !ticket.invalidated && (!read || sequence === read.sequence),
);
}
finally {
inFlight.delete(ticket);
if (read && --read.pending === 0) latestReads.delete(key);
}
}
function configureAuthClient(handlers?: AuthHandlers) {
if (!handlers) invalidateApiRequests(() => true);
sessionState.handlers = handlers;
setAuthenticatedSession(Boolean(sessionState.handlers?.accessToken()));
}
async function fetchCurrent(
request: Request,
isLatest: () => boolean,
): Promise<Response> {
const isAuthRequest = new URL(request.url).pathname.startsWith(
"/api/v1/auth/",
);
@@ -91,7 +41,7 @@ async function fetchCurrent(
(session?.generation
? generation === session.generation()
&& generation === sessionState.handlers?.generation?.()
: session === sessionState.handlers) && isLatest();
: session === sessionState.handlers);
const token = session?.accessToken();
@@ -107,7 +57,6 @@ async function fetchCurrent(
const retryRequest = authorized.clone();
const response = await fetch(authorized);
await response.clone().arrayBuffer();
if (!isAuthRequest && !isCurrent())
throw new DOMException("Session changed", "AbortError");
if (
@@ -139,12 +88,16 @@ async function fetchCurrent(
const refreshedToken = sessionState.handlers.accessToken();
if (!refreshedToken) return response;
const retried = await fetch(withToken(retryRequest, refreshedToken));
await retried.clone().arrayBuffer();
if (!isCurrent()) throw new DOMException("Session changed", "AbortError");
return retried;
}
function configureAuthClient(handlers?: AuthHandlers) {
sessionState.handlers = handlers;
setAuthenticatedSession(Boolean(sessionState.handlers?.accessToken()));
}
function hasAuthenticatedSession() {
return sessionState.hasSession;
}
@@ -174,6 +127,5 @@ export {
apiQueryClient,
configureAuthClient,
hasAuthenticatedSession as getAuthenticatedSessionSnapshot,
invalidateApiRequests,
subscribeAuthenticatedSession,
};