improve api structure
This commit is contained in:
parent
29eb856a45
commit
fb1c015d9a
10
README.md
10
README.md
|
|
@ -2,6 +2,16 @@
|
||||||
|
|
||||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||||
|
|
||||||
|
## Environment variables
|
||||||
|
|
||||||
|
Copy `.env.example` to `.env` (or provide the same variables through your deployment system) and adjust the URLs if needed. All frontend traffic (админка, загрузка, просмотр) завязан на `VITE_API_BASE_URL`, поэтому указывайте полный путь до `/api/v1` нужной ноды.
|
||||||
|
|
||||||
|
```
|
||||||
|
VITE_API_BASE_URL=https://my-public-node-8.projscale.dev/api/v1
|
||||||
|
```
|
||||||
|
|
||||||
|
При таком значении фронт автоматически отправляет tus-запросы на `https://my-public-node-8.projscale.dev/tus/files`, а прогрессивные загрузки (обложки, метаданные) — на `https://my-public-node-8.projscale.dev/api/v1.5/storage`.
|
||||||
|
|
||||||
Currently, two official plugins are available:
|
Currently, two official plugins are available:
|
||||||
|
|
||||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
|
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,8 @@
|
||||||
import axios, { AxiosError, InternalAxiosRequestConfig } from 'axios';
|
import axios, { InternalAxiosRequestConfig } from 'axios';
|
||||||
|
|
||||||
import { getAdminAuthSnapshot } from '~/shared/libs/admin-auth';
|
import { getAdminAuthSnapshot } from '~/shared/libs/admin-auth';
|
||||||
|
|
||||||
const API_BASE_PATH = '/api/v1';
|
const API_BASE_PATH = '/api/v1';
|
||||||
const API_ENDPOINTS = [
|
|
||||||
'https://my-public-node-8.projscale.dev',
|
|
||||||
'http://localhost:3000',
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
const API_BASE_URLS = API_ENDPOINTS.map((endpoint) => {
|
|
||||||
return `${endpoint.replace(/\/$/, '')}${API_BASE_PATH}`;
|
|
||||||
});
|
|
||||||
|
|
||||||
const ENDPOINT_INDEX_KEY = '__apiEndpointIndex__';
|
|
||||||
|
|
||||||
const isBrowser = typeof window !== 'undefined';
|
const isBrowser = typeof window !== 'undefined';
|
||||||
|
|
||||||
|
|
@ -28,10 +18,23 @@ const readStorage = (key: string) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
type RequestConfig = InternalAxiosRequestConfig & {
|
type RequestConfig = InternalAxiosRequestConfig;
|
||||||
[ENDPOINT_INDEX_KEY]?: number;
|
|
||||||
|
const resolveDefaultBaseUrl = () => {
|
||||||
|
const envBase = (import.meta.env.VITE_API_BASE_URL ?? '').trim();
|
||||||
|
if (envBase) {
|
||||||
|
return envBase.replace(/\/$/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isBrowser) {
|
||||||
|
return `${window.location.origin.replace(/\/$/, '')}${API_BASE_PATH}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return API_BASE_PATH;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const DEFAULT_API_BASE_URL = resolveDefaultBaseUrl();
|
||||||
|
|
||||||
export const request = axios.create({ withCredentials: true });
|
export const request = axios.create({ withCredentials: true });
|
||||||
|
|
||||||
request.interceptors.request.use((config: RequestConfig) => {
|
request.interceptors.request.use((config: RequestConfig) => {
|
||||||
|
|
@ -54,49 +57,11 @@ request.interceptors.request.use((config: RequestConfig) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasCustomBaseUrl = Boolean(config.baseURL && !API_BASE_URLS.includes(config.baseURL));
|
if (!config.baseURL) {
|
||||||
if (hasCustomBaseUrl) {
|
config.baseURL = DEFAULT_API_BASE_URL;
|
||||||
return config;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const attempt = typeof config[ENDPOINT_INDEX_KEY] === 'number' ? (config[ENDPOINT_INDEX_KEY] as number) : 0;
|
|
||||||
config[ENDPOINT_INDEX_KEY] = attempt;
|
|
||||||
config.baseURL = API_BASE_URLS[attempt] ?? API_BASE_URLS[0];
|
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
});
|
});
|
||||||
|
|
||||||
request.interceptors.response.use(
|
request.interceptors.response.use((response) => response);
|
||||||
(response) => response,
|
|
||||||
async (error: AxiosError) => {
|
|
||||||
const config = error.config as RequestConfig | undefined;
|
|
||||||
|
|
||||||
if (!config) {
|
|
||||||
return Promise.reject(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasCustomBaseUrl = Boolean(config.baseURL && !API_BASE_URLS.includes(config.baseURL));
|
|
||||||
if (hasCustomBaseUrl) {
|
|
||||||
return Promise.reject(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
const isAbortError = error.code === 'ERR_CANCELED' || error.message === 'canceled';
|
|
||||||
const isNetworkError = !error.response && !isAbortError;
|
|
||||||
|
|
||||||
if (!isNetworkError) {
|
|
||||||
return Promise.reject(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentIndex = typeof config[ENDPOINT_INDEX_KEY] === 'number' ? (config[ENDPOINT_INDEX_KEY] as number) : 0;
|
|
||||||
const nextIndex = currentIndex + 1;
|
|
||||||
|
|
||||||
if (nextIndex >= API_BASE_URLS.length) {
|
|
||||||
return Promise.reject(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
config[ENDPOINT_INDEX_KEY] = nextIndex;
|
|
||||||
config.baseURL = API_BASE_URLS[nextIndex];
|
|
||||||
|
|
||||||
return request(config);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,15 @@ import { useMutation, useQuery } from "react-query";
|
||||||
import { request } from "~/shared/libs";
|
import { request } from "~/shared/libs";
|
||||||
import { Royalty } from "~/shared/stores/root";
|
import { Royalty } from "~/shared/stores/root";
|
||||||
|
|
||||||
|
const VIEW_CONTENT_NODE_ORIGINS = [
|
||||||
|
"https://my-public-node-8.projscale.dev",
|
||||||
|
"https://my-public-node-7.projscale.dev",
|
||||||
|
"https://my-public-node-9.projscale.dev",
|
||||||
|
"https://my-public-node-10.projscale.dev",
|
||||||
|
"http://localhost:3000",
|
||||||
|
] as const;
|
||||||
|
const VIEW_CONTENT_API_PATH = "/api/v1";
|
||||||
|
|
||||||
type UseCreateNewContentPayload = {
|
type UseCreateNewContentPayload = {
|
||||||
title: string;
|
title: string;
|
||||||
content: string;
|
content: string;
|
||||||
|
|
@ -76,12 +85,34 @@ export const useCreateNewContent = () => {
|
||||||
export const useViewContent = (contentId: string | null | undefined) => {
|
export const useViewContent = (contentId: string | null | undefined) => {
|
||||||
return useQuery(
|
return useQuery(
|
||||||
["view", "content", contentId],
|
["view", "content", contentId],
|
||||||
() => {
|
async () => {
|
||||||
return request.get(`/content.view/${contentId}`, {
|
if (!contentId) {
|
||||||
headers: {
|
throw new Error("contentId is required");
|
||||||
Authorization: localStorage.getItem('auth_v1_token') ?? ""
|
}
|
||||||
|
|
||||||
|
let lastError: unknown = null;
|
||||||
|
for (const origin of VIEW_CONTENT_NODE_ORIGINS) {
|
||||||
|
const baseURL = `${origin.replace(/\/$/, "")}${VIEW_CONTENT_API_PATH}`;
|
||||||
|
try {
|
||||||
|
const response = await request.get(`/content.view/${contentId}`, {
|
||||||
|
baseURL,
|
||||||
|
});
|
||||||
|
if (origin !== VIEW_CONTENT_NODE_ORIGINS[0]) {
|
||||||
|
console.info("[App2Client][ContentView] Используем резервную ноду", {
|
||||||
|
origin,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
lastError = error;
|
||||||
|
console.warn("[App2Client][ContentView] Нода недоступна", {
|
||||||
|
origin,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw lastError ?? new Error("All content nodes are unreachable");
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: Boolean(contentId),
|
enabled: Boolean(contentId),
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,50 @@ import { Upload } from "tus-js-client";
|
||||||
|
|
||||||
import { request } from "~/shared/libs";
|
import { request } from "~/shared/libs";
|
||||||
|
|
||||||
const STORAGE_API_URL = '/api/v1.5/storage';
|
const resolveApiBaseUrl = () => {
|
||||||
|
const envBase = (import.meta.env.VITE_API_BASE_URL ?? "").trim();
|
||||||
|
if (envBase) {
|
||||||
|
return envBase.replace(/\/$/, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
return `${window.location.origin.replace(/\/$/, "")}/api/v1`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "/api/v1";
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEFAULT_API_BASE_URL = resolveApiBaseUrl();
|
||||||
|
|
||||||
|
const resolveUploadEndpoints = () => {
|
||||||
|
try {
|
||||||
|
const apiUrl = new URL(DEFAULT_API_BASE_URL);
|
||||||
|
const origin = apiUrl.origin.replace(/\/$/, "");
|
||||||
|
return {
|
||||||
|
storage: `${origin}/api/v1.5/storage`,
|
||||||
|
tus: `${origin}/tus/files`,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
const origin = window.location.origin.replace(/\/$/, "");
|
||||||
|
return {
|
||||||
|
storage: `${origin}/api/v1.5/storage`,
|
||||||
|
tus: `${origin}/tus/files`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
console.warn("[App2Client][Upload] Cannot derive upload endpoints from VITE_API_BASE_URL", {
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
storage: '/api/v1.5/storage',
|
||||||
|
tus: '/tus/files',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const { storage: STORAGE_API_URL, tus: TUS_ENDPOINT } = resolveUploadEndpoints();
|
||||||
const MAX_CHUNK_SIZE = 80 * 1024 * 1024; // 80 MB
|
const MAX_CHUNK_SIZE = 80 * 1024 * 1024; // 80 MB
|
||||||
const TUS_ENDPOINT = '/tus/files';
|
|
||||||
|
|
||||||
const TUS_STATUS_POLL_INTERVAL_MS = 2000;
|
const TUS_STATUS_POLL_INTERVAL_MS = 2000;
|
||||||
const TUS_STATUS_POLL_TIMEOUT_MS = 5 * 60 * 1000;
|
const TUS_STATUS_POLL_TIMEOUT_MS = 5 * 60 * 1000;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue