fix and update

This commit is contained in:
lendry
2026-07-01 00:26:16 +03:00
parent a0c1722a8d
commit 115fc140af
7 changed files with 115 additions and 28 deletions

View File

@@ -598,7 +598,10 @@ async function trySilentTokenRefresh(): Promise<RefreshSessionResponse | null> {
}
}
return refreshed;
} catch {
} catch (error) {
if (error instanceof ApiError && error.status === 401) {
notifyAuthSessionInvalid();
}
return null;
}
}
@@ -685,6 +688,8 @@ const API_BURST_MAX_CONCURRENT = 2;
const API_BURST_WINDOW_MS = 5000;
const API_READY_EVENT = 'idp-api-ready';
const API_NOT_READY_EVENT = 'idp-api-not-ready';
export const AUTH_SESSION_INVALID_EVENT = 'idp-auth-session-invalid';
const GATEWAY_CIRCUIT_COOLDOWN_MS = 8_000;
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
@@ -694,6 +699,7 @@ let gatewayReadyResolved = false;
let gatewayReadyPromise: Promise<void> | null = null;
let stableGatewayPromise: Promise<void> | null = null;
let gatewayReadyAt = 0;
let gatewayCircuitOpenUntil = 0;
let activeApiRequests = 0;
const apiRequestWaiters: Array<() => void> = [];
@@ -701,6 +707,7 @@ export function resetApiGatewayWarmup() {
gatewayReadyResolved = false;
gatewayReadyPromise = null;
stableGatewayPromise = null;
gatewayCircuitOpenUntil = 0;
}
export function invalidateApiGatewayReady() {
@@ -712,6 +719,29 @@ export function invalidateApiGatewayReady() {
}
}
function openGatewayCircuit(): void {
gatewayReadyResolved = false;
gatewayReadyAt = 0;
stableGatewayPromise = null;
gatewayCircuitOpenUntil = Date.now() + GATEWAY_CIRCUIT_COOLDOWN_MS;
if (typeof window !== 'undefined') {
window.dispatchEvent(new CustomEvent(API_NOT_READY_EVENT));
}
}
function assertGatewayCircuitClosed(): void {
if (typeof window === 'undefined') return;
if (Date.now() < gatewayCircuitOpenUntil) {
throw new ApiError('Сервер API временно недоступен. Запрос отложен, чтобы не перегружать консоль ошибками.', 503, 'GATEWAY_UNAVAILABLE');
}
gatewayCircuitOpenUntil = 0;
}
function notifyAuthSessionInvalid(): void {
if (typeof window === 'undefined') return;
window.dispatchEvent(new CustomEvent(AUTH_SESSION_INVALID_EVENT));
}
export function isApiGatewayReady() {
return gatewayReadyResolved;
}
@@ -720,6 +750,7 @@ export function markApiGatewayReady() {
if (gatewayReadyResolved) return;
gatewayReadyResolved = true;
gatewayReadyAt = Date.now();
gatewayCircuitOpenUntil = 0;
if (typeof window !== 'undefined') {
window.dispatchEvent(new CustomEvent(API_READY_EVENT));
}
@@ -816,6 +847,7 @@ export async function ensureApiGatewayReady(force = false): Promise<void> {
gatewayReadyResolved = false;
gatewayReadyAt = 0;
stableGatewayPromise = null;
gatewayCircuitOpenUntil = 0;
}
const probes = force ? 2 : 1;
@@ -835,6 +867,7 @@ async function fetchWithGatewayRetry(url: string, init: RequestInit): Promise<Re
let lastNetworkError: unknown;
for (let attempt = 0; attempt < GATEWAY_RETRY_ATTEMPTS; attempt++) {
try {
assertGatewayCircuitClosed();
const response = await fetch(url, init);
if (response.ok) {
markApiGatewayReady();
@@ -843,16 +876,24 @@ async function fetchWithGatewayRetry(url: string, init: RequestInit): Promise<Re
await sleep(gatewayRetryDelay(attempt));
continue;
}
if (GATEWAY_RETRY_STATUS.has(response.status)) {
openGatewayCircuit();
}
return response;
} catch (error) {
lastNetworkError = error;
if (isGatewayUnavailableError(error)) {
throw error;
}
if (attempt + 1 < GATEWAY_RETRY_ATTEMPTS) {
await sleep(gatewayRetryDelay(attempt));
continue;
}
openGatewayCircuit();
throw mapFetchError(error);
}
}
openGatewayCircuit();
throw mapFetchError(lastNetworkError);
}
@@ -871,6 +912,13 @@ export async function apiFetch<T>(path: string, options: RequestInit = {}, token
};
let response: Response;
let apiSlotReleased = false;
const releaseCurrentApiSlot = () => {
if (apiSlotReleased) return;
apiSlotReleased = true;
releaseApiSlot();
};
try {
try {
response = await fetchWithGatewayRetry(`${getApiUrl()}${path}`, {
@@ -892,10 +940,15 @@ export async function apiFetch<T>(path: string, options: RequestInit = {}, token
throw error;
}
if (resolvedToken && error.status === 401 && error.code !== 'TOKEN_EXPIRED') {
notifyAuthSessionInvalid();
throw error;
}
if (allowRetry && resolvedToken && error.code === 'TOKEN_EXPIRED') {
const refreshed = await trySilentTokenRefresh();
if (refreshed?.accessToken) {
releaseApiSlot();
releaseCurrentApiSlot();
return apiFetch<T>(path, options, refreshed.accessToken, false);
}
if (refreshed?.requiresPin) {
@@ -903,13 +956,17 @@ export async function apiFetch<T>(path: string, options: RequestInit = {}, token
}
}
if (resolvedToken && error.status === 401) {
notifyAuthSessionInvalid();
}
throw error;
}
markApiGatewayReady();
return response.json() as Promise<T>;
} finally {
releaseApiSlot();
releaseCurrentApiSlot();
}
}