const GATEWAY_RETRY_STATUS = new Set([502, 503, 504]); const MEDIA_FETCH_MAX_ATTEMPTS = 3; const MEDIA_SLOW_LOAD_WARN_MS = 8_000; const MEDIA_STREAM_PATH = '/media/stream/'; const SAME_ORIGIN_API_PREFIX = '/idp-api'; function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } function warnSlowMediaLoad(label: string, startedAt: number, error: unknown) { if (Date.now() - startedAt < MEDIA_SLOW_LOAD_WARN_MS) return; const detail = error instanceof Error ? error.message : 'неизвестная ошибка'; console.warn(`[media] ${label}: не удалось загрузить за ${Math.round((Date.now() - startedAt) / 1000)} с (${detail})`); } export function resolveBrowserMediaUrl(accessUrl: string): string { if (typeof window === 'undefined') return accessUrl; try { const url = new URL(accessUrl, window.location.origin); const streamIndex = url.pathname.indexOf(MEDIA_STREAM_PATH); if (streamIndex === -1) { return accessUrl; } const streamPath = url.pathname.slice(streamIndex); return `${window.location.origin}${SAME_ORIGIN_API_PREFIX}${streamPath}${url.search}`; } catch { return accessUrl; } } export async function fetchMediaBlobWithRetry( accessUrl: string, token: string, options?: { mimeType?: string; label?: string } ): Promise { const startedAt = Date.now(); const label = options?.label ?? 'изображение'; const mediaUrl = resolveBrowserMediaUrl(accessUrl); let lastError: unknown; for (let attempt = 0; attempt < MEDIA_FETCH_MAX_ATTEMPTS; attempt += 1) { try { const response = await fetch(mediaUrl, { headers: { Authorization: `Bearer ${token}` }, cache: 'no-store' }); if (GATEWAY_RETRY_STATUS.has(response.status) && attempt + 1 < MEDIA_FETCH_MAX_ATTEMPTS) { await sleep(350 * (attempt + 1)); continue; } if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const raw = await response.blob(); const mimeType = options?.mimeType; if (mimeType && (!raw.type || raw.type === 'application/octet-stream')) { return new Blob([await raw.arrayBuffer()], { type: mimeType }); } return raw; } catch (error) { lastError = error; if (attempt + 1 < MEDIA_FETCH_MAX_ATTEMPTS) { await sleep(350 * (attempt + 1)); } } } warnSlowMediaLoad(label, startedAt, lastError); throw lastError instanceof Error ? lastError : new Error('Не удалось загрузить файл'); }