synapse-storage
Version:
Набор инструментов для управления состоянием и апи-запросами
278 lines (269 loc) • 12.3 kB
JavaScript
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