chore: finish frontend ESLint migration #18
Frontend security / security (push) Successful in 9s
Frontend verify / verify (push) Failing after 37m50s
Frontend security / security (pull_request) Successful in 9s
Frontend verify / verify (pull_request) Failing after 42m10s
Conventional commits / commits (pull_request) Successful in 29s
Frontend security / security (push) Successful in 9s
Frontend verify / verify (push) Failing after 37m50s
Frontend security / security (pull_request) Successful in 9s
Frontend verify / verify (pull_request) Failing after 42m10s
Conventional commits / commits (pull_request) Successful in 29s
Enable every plugin recommended preset and replace Biome with ESLint formatting. Preserve API contracts and JSX whitespace; verify source, tests, builds and tooling.
This commit is contained in:
+58
-59
@@ -2,65 +2,44 @@ import createClient from "openapi-fetch";
|
||||
import createQueryClient from "openapi-react-query";
|
||||
|
||||
import type { paths } from "./generated/schema";
|
||||
|
||||
import { apiErrorCode } from "./problem-details";
|
||||
|
||||
const HTTP_UNAUTHORIZED = 401;
|
||||
|
||||
const configuredBaseUrl: unknown = import.meta.env.VITE_API_BASE_URL;
|
||||
const apiBaseUrl =
|
||||
typeof configuredBaseUrl === "string"
|
||||
const apiBaseUrl
|
||||
= typeof configuredBaseUrl === "string"
|
||||
? configuredBaseUrl
|
||||
: "http://localhost:3000";
|
||||
|
||||
type AuthHandlers = {
|
||||
generation?: () => number;
|
||||
accessToken: () => string | null;
|
||||
refresh: () => Promise<boolean>;
|
||||
accessToken: () => null | string | undefined;
|
||||
clear: () => void;
|
||||
generation?: () => number;
|
||||
refresh: () => Promise<boolean>;
|
||||
};
|
||||
|
||||
const inFlight = new Set<{ pathname: string; invalidated: boolean }>();
|
||||
const latestReads = new Map<string, { sequence: number; pending: number }>();
|
||||
function invalidateApiRequests(matches: (pathname: string) => 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 (matches(request.pathname)) request.invalidated = true;
|
||||
if (isMatches(request.pathname)) request.invalidated = true;
|
||||
}
|
||||
|
||||
let auth: AuthHandlers | null = null;
|
||||
let hasAuthenticatedSession = false;
|
||||
const sessionState: { handlers?: AuthHandlers; hasSession: boolean } = { hasSession: false };
|
||||
const authSessionListeners = new Set<() => void>();
|
||||
|
||||
function setAuthenticatedSession(next: boolean) {
|
||||
if (next === hasAuthenticatedSession) return;
|
||||
hasAuthenticatedSession = next;
|
||||
for (const listener of authSessionListeners) listener();
|
||||
}
|
||||
|
||||
function configureAuthClient(handlers: AuthHandlers | null) {
|
||||
if (!handlers) invalidateApiRequests(() => true);
|
||||
auth = handlers;
|
||||
setAuthenticatedSession(Boolean(auth?.accessToken()));
|
||||
}
|
||||
|
||||
function getAuthenticatedSessionSnapshot() {
|
||||
return hasAuthenticatedSession;
|
||||
}
|
||||
|
||||
function subscribeAuthenticatedSession(listener: () => void) {
|
||||
authSessionListeners.add(listener);
|
||||
return () => authSessionListeners.delete(listener);
|
||||
}
|
||||
|
||||
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 = { pathname: url.pathname, invalidated: false };
|
||||
const read =
|
||||
request.method === "GET"
|
||||
? (latestReads.get(key) ?? { sequence: 0, pending: 0 })
|
||||
: null;
|
||||
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;
|
||||
@@ -73,12 +52,19 @@ async function authenticatedFetch(request: Request): Promise<Response> {
|
||||
request,
|
||||
() => !ticket.invalidated && (!read || sequence === read.sequence),
|
||||
);
|
||||
} finally {
|
||||
}
|
||||
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,
|
||||
@@ -86,45 +72,43 @@ async function fetchCurrent(
|
||||
const isAuthRequest = new URL(request.url).pathname.startsWith(
|
||||
"/api/v1/auth/",
|
||||
);
|
||||
const session = auth;
|
||||
const session = sessionState.handlers;
|
||||
const generation = session?.generation?.();
|
||||
const isCurrent = () =>
|
||||
(session?.generation
|
||||
? generation === session.generation() &&
|
||||
generation === auth?.generation?.()
|
||||
: session === auth) && isLatest();
|
||||
? generation === session.generation()
|
||||
&& generation === sessionState.handlers?.generation?.()
|
||||
: session === sessionState.handlers) && isLatest();
|
||||
const token = session?.accessToken();
|
||||
const withToken = (source: Request, value: string) => {
|
||||
const headers = new Headers(source.headers);
|
||||
headers.set("Authorization", `Bearer ${value}`);
|
||||
return new Request(source, { headers });
|
||||
};
|
||||
const authorized =
|
||||
token && !isAuthRequest ? withToken(request, token) : request;
|
||||
const authorized
|
||||
= token && !isAuthRequest ? withToken(request, token) : request;
|
||||
const retryRequest = authorized.clone();
|
||||
const response = await fetch(authorized);
|
||||
await response.clone().arrayBuffer();
|
||||
if (!isAuthRequest && !isCurrent())
|
||||
throw new DOMException("Session changed", "AbortError");
|
||||
if (
|
||||
response.status !== HTTP_UNAUTHORIZED ||
|
||||
isAuthRequest ||
|
||||
!auth ||
|
||||
apiErrorCode(
|
||||
await response
|
||||
.clone()
|
||||
.json()
|
||||
.catch(() => null),
|
||||
) !== "UNAUTHORIZED"
|
||||
)
|
||||
if (isAuthRequest || !sessionState.handlers || response.status !== HTTP_UNAUTHORIZED)
|
||||
return response;
|
||||
let problem: unknown;
|
||||
try {
|
||||
problem = await response.clone().json();
|
||||
}
|
||||
catch {
|
||||
return response;
|
||||
}
|
||||
if (apiErrorCode(problem) !== "UNAUTHORIZED") return response;
|
||||
if (!isCurrent()) throw new DOMException("Session changed", "AbortError");
|
||||
if (!(await auth.refresh())) {
|
||||
if (isCurrent()) auth.clear();
|
||||
if (!(await sessionState.handlers.refresh())) {
|
||||
if (isCurrent()) sessionState.handlers.clear();
|
||||
return response;
|
||||
}
|
||||
if (!isCurrent()) throw new DOMException("Session changed", "AbortError");
|
||||
const refreshedToken = auth.accessToken();
|
||||
const refreshedToken = sessionState.handlers.accessToken();
|
||||
if (!refreshedToken) return response;
|
||||
const retried = await fetch(withToken(retryRequest, refreshedToken));
|
||||
await retried.clone().arrayBuffer();
|
||||
@@ -132,6 +116,21 @@ async function fetchCurrent(
|
||||
return retried;
|
||||
}
|
||||
|
||||
function hasAuthenticatedSession() {
|
||||
return sessionState.hasSession;
|
||||
}
|
||||
|
||||
function setAuthenticatedSession(hasSession: boolean) {
|
||||
if (hasSession === sessionState.hasSession) return;
|
||||
sessionState.hasSession = hasSession;
|
||||
for (const listener of authSessionListeners) listener();
|
||||
}
|
||||
|
||||
function subscribeAuthenticatedSession(listener: () => void) {
|
||||
authSessionListeners.add(listener);
|
||||
return () => authSessionListeners.delete(listener);
|
||||
}
|
||||
|
||||
const apiClient = createClient<paths>({
|
||||
baseUrl: apiBaseUrl,
|
||||
credentials: "include",
|
||||
@@ -144,7 +143,7 @@ export {
|
||||
apiClient,
|
||||
apiQueryClient,
|
||||
configureAuthClient,
|
||||
getAuthenticatedSessionSnapshot,
|
||||
hasAuthenticatedSession as getAuthenticatedSessionSnapshot,
|
||||
invalidateApiRequests,
|
||||
subscribeAuthenticatedSession,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user