UNPKG

synapse-storage

Version:

Набор инструментов для управления состоянием и апи-запросами

278 lines (269 loc) 12.3 kB
import { handleCallbackError, logError } from "../../_utils/error-handling.util.js"; import { ResponseFormat } from "../types/api.interface.js"; import { getFileMetadataFromHeaders, getResponseFormatForMimeType, isFileResponse } from "./file-utils.js"; /** * Извлекает данные из response в зависимости от формата * @param response Объект Response * @param format Формат ответа * @returns Объект с данными или ошибкой */ async function getResponseData(response, format) { let responseFormat = format; const contentType = response.headers.get('content-type') || ''; // Если формат не указан, пытаемся определить его из MIME-типа if (!responseFormat && contentType) { // Проверка, является ли ответ файлом на основе заголовков if (isFileResponse(response.headers)) { responseFormat = ResponseFormat.Blob; } else { responseFormat = getResponseFormatForMimeType(contentType); } } // Если формат всё ещё не определен, используем JSON по умолчанию if (!responseFormat) { responseFormat = ResponseFormat.Json; } try { // Получение метаданных файла, если формат указывает на файл let fileMetadata; if (responseFormat === ResponseFormat.Blob || responseFormat === ResponseFormat.ArrayBuffer) { fileMetadata = getFileMetadataFromHeaders(response.headers); } // Обработка данных в зависимости от формата switch(responseFormat){ case ResponseFormat.Json: { // Пробуем получить JSON-данные try { const data = await response.json(); return response.ok ? { data: data, fileMetadata } : { error: data, fileMetadata }; } catch (error) { // Если не удалось разобрать JSON, возвращаем текст const text = await response.text(); return response.ok ? { data: text, fileMetadata } : { error: text, fileMetadata }; } } case ResponseFormat.Text: { const text = await response.text(); return response.ok ? { data: text, fileMetadata } : { error: text, fileMetadata }; } case ResponseFormat.Blob: { const blob = await response.blob(); return response.ok ? { data: blob, fileMetadata } : { error: blob, fileMetadata }; } case ResponseFormat.ArrayBuffer: { const buffer = await response.arrayBuffer(); return response.ok ? { data: buffer, fileMetadata } : { error: buffer, fileMetadata }; } case ResponseFormat.FormData: { const formData = await response.formData(); return response.ok ? { data: formData, fileMetadata } : { error: formData, fileMetadata }; } case ResponseFormat.Raw: { return response.ok ? { data: response, fileMetadata } : { error: response, fileMetadata }; } default: // Если формат неизвестен, возвращаем blob как наиболее универсальный // eslint-disable-next-line no-case-declarations const blob = await response.blob(); return response.ok ? { data: blob, fileMetadata } : { error: blob, fileMetadata }; } } catch (err) { handleCallbackError(`fetchBaseQuery: error extracting response data (format: ${responseFormat})`, err); return response.ok ? { data: undefined } : { error: err }; } } /** * Собирает абсолютный URL запроса из `path` и `baseUrl`. * * Важно: сохраняется поведение конкатенации (`baseUrl + path`), а не * резолвинг через `new URL(path, baseUrl)`. Резолвинг отбросил бы префикс * базы для абсолютных путей (например, `new URL('/api/x', 'http://host/_api')` * даёт `http://host/api/x`, теряя `/_api`), что сломало бы проксирование. * * Покрываемые кейсы: * - абсолютный `path` (`http(s)://...`) → используется как есть; * - относительный `path` + абсолютный `baseUrl` → конкатенация даёт абсолютный URL; * - относительный `path` + относительный `baseUrl` (браузер, напр. `/_api`) → * результат резолвится относительно `window.location.origin`, чтобы * `new URL(...)` не падал с `Invalid URL` (same-origin сохраняется). * * @param path Путь запроса * @param baseUrl Базовый URL клиента (может быть абсолютным или относительным) * @returns Готовый объект URL */ function buildRequestUrl(path, baseUrl) { // Путь уже абсолютный — база не нужна. if (/^https?:\/\//i.test(path)) { return new URL(path); } const combined = `${baseUrl}${path}`; // Конкатенация уже дала абсолютный URL (абсолютный baseUrl). if (/^https?:\/\//i.test(combined)) { return new URL(combined); } // Относительный результат — резолвим относительно origin'а в браузере. if (typeof window !== 'undefined' && window.location?.origin) { return new URL(combined, window.location.origin); } // Сервер с относительной базой: явный resolve невозможен. Пробуем как есть — // даст понятную ошибку, если база действительно относительна. return new URL(combined); } /** * Создает базовый fetch-клиент для запросов с поддержкой файлов * @param options Настройки базового клиента * @returns Функция для выполнения запросов */ function fetchBaseQuery(options) { const { baseUrl, timeout = 30000, fetchFn = fetch, credentials = 'same-origin' } = options; return async (args, queryOptions = {}, headers)=>{ const { path, method, body, query, responseFormat: reqResponseFormat } = args; const { signal, timeout: requestTimeout = timeout, responseFormat: optResponseFormat } = queryOptions; // Определяем формат ответа с приоритетом от options const responseFormat = optResponseFormat || reqResponseFormat; // Строим URL с учетом api параметров const url = buildRequestUrl(path, baseUrl); // Добавляем query-параметры в URL if (query) { Object.entries(query).forEach(([key, value])=>{ if (value !== undefined && value !== null) { if (Array.isArray(value)) { value.forEach((item)=>url.searchParams.append(key, String(item))); } else { url.searchParams.append(key, String(value)); } } }); } // Если body это объект, конвертируем в JSON и устанавливаем Content-Type let serializedBody; if (body !== undefined) { if (body instanceof FormData || body instanceof Blob) { serializedBody = body; } else if (typeof body === 'object' && body !== null) { try { serializedBody = JSON.stringify(body); if (!headers.has('Content-Type')) { headers.set('Content-Type', 'application/json'); } } catch (error) { logError('fetchBaseQuery: error serializing request body', error); serializedBody = String(body); } } else { serializedBody = String(body); } } // Создаем таймаут если указан let timeoutId; const timeoutPromise = new Promise((_, reject)=>{ if (requestTimeout) { timeoutId = globalThis.setTimeout(()=>{ reject(new Error(`Превышено время ожидания запроса (${requestTimeout}мс)`)); }, requestTimeout); } }); try { // Выполняем запрос const fetchPromise = fetchFn(url.toString(), { method, headers, body: serializedBody, signal, credentials }); // Используем Promise.race для обработки таймаута const response = await Promise.race([ fetchPromise, timeoutPromise ]); // Обрабатываем ответ const { data, error, fileMetadata } = await getResponseData(response, responseFormat); // Формируем результат запроса const result = { data, error, ok: response.ok, status: response.status, statusText: response.statusText, headers: response.headers, fileDownloadResult: fileMetadata }; return result; } catch (err) { // Обрабатываем ошибки сети или таймаута const error = err; handleCallbackError('fetchBaseQuery: request execution error', error); // Формируем результат с ошибкой return { error: error, ok: false, status: 0, statusText: error.message, headers: new Headers() }; } finally{ // Очищаем таймер в любом случае if (timeoutId) { globalThis.clearTimeout(timeoutId); } } }; } export { buildRequestUrl, fetchBaseQuery }; //# sourceMappingURL=fetch-base-query.js.map