Разработана первая версия приложений
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
import { env } from "./config";
|
||||
import { getAccessToken, refreshTokens } from "./auth";
|
||||
export { sessionMemory } from "./session";
|
||||
import { sessionMemory } from "./session";
|
||||
|
||||
export type ApiErrorEnvelope = {
|
||||
error: { code: string; message: string; request_id?: string; details?: Record<string, unknown> };
|
||||
};
|
||||
|
||||
export class ApiError extends Error {
|
||||
constructor(
|
||||
readonly status: number,
|
||||
readonly code: string,
|
||||
message: string,
|
||||
readonly requestId?: string,
|
||||
readonly retryAfter?: number,
|
||||
) {
|
||||
super(message);
|
||||
this.name = "ApiError";
|
||||
}
|
||||
}
|
||||
|
||||
export type Diagnostic = {
|
||||
at: number;
|
||||
method: string;
|
||||
path: string;
|
||||
status: number;
|
||||
requestId: string;
|
||||
};
|
||||
|
||||
const diagnostics: Diagnostic[] = [];
|
||||
|
||||
export const getDiagnostics = () => [...diagnostics];
|
||||
|
||||
function traceparent() {
|
||||
const traceId = crypto.randomUUID().replaceAll("-", "") + crypto.randomUUID().replaceAll("-", "").slice(0, 16);
|
||||
const spanId = crypto.randomUUID().replaceAll("-", "").slice(0, 16);
|
||||
return `00-${traceId.slice(0, 32)}-${spanId}-01`;
|
||||
}
|
||||
|
||||
function safePath(path: string) {
|
||||
return path.split("?")[0] ?? path;
|
||||
}
|
||||
|
||||
async function parseError(response: Response, requestId: string) {
|
||||
let envelope: ApiErrorEnvelope | undefined;
|
||||
try { envelope = (await response.json()) as ApiErrorEnvelope; } catch { /* intentionally empty */ }
|
||||
const code = envelope?.error?.code ?? `http_${response.status}`;
|
||||
const retry = Number(response.headers.get("Retry-After"));
|
||||
return new ApiError(
|
||||
response.status,
|
||||
code,
|
||||
envelope?.error?.message ?? "Запрос не выполнен",
|
||||
envelope?.error?.request_id ?? requestId,
|
||||
Number.isFinite(retry) ? retry : undefined,
|
||||
);
|
||||
}
|
||||
|
||||
export async function apiRequest<T>(
|
||||
path: string,
|
||||
init: RequestInit & { protected?: boolean } = {},
|
||||
replayed = false,
|
||||
): Promise<T> {
|
||||
const requestId = crypto.randomUUID();
|
||||
const isProtected = init.protected ?? false;
|
||||
const headers = new Headers(init.headers);
|
||||
headers.set("Accept", "application/json");
|
||||
headers.set("X-Request-ID", requestId);
|
||||
headers.set("traceparent", traceparent());
|
||||
if (init.body && !headers.has("Content-Type")) headers.set("Content-Type", "application/json");
|
||||
if (isProtected) {
|
||||
const token = getAccessToken();
|
||||
if (!token) throw new ApiError(401, "unauthorized", "Требуется авторизация", requestId);
|
||||
headers.set("Authorization", `Bearer ${token}`);
|
||||
if (sessionMemory.id) headers.set("X-Ux-Session-Id", sessionMemory.id);
|
||||
}
|
||||
const response = await fetch(`${env.apiBaseUrl}${path}`, { ...init, headers });
|
||||
diagnostics.unshift({
|
||||
at: Date.now(), method: init.method ?? "GET", path: safePath(path),
|
||||
status: response.status, requestId: response.headers.get("X-Request-ID") ?? requestId,
|
||||
});
|
||||
diagnostics.splice(20);
|
||||
if (response.status === 401 && isProtected && !replayed) {
|
||||
await refreshTokens();
|
||||
return apiRequest<T>(path, init, true);
|
||||
}
|
||||
if (!response.ok) throw await parseError(response, requestId);
|
||||
if (response.status === 204) return undefined as T;
|
||||
return response.json() as Promise<T>;
|
||||
}
|
||||
|
||||
export const json = (value: unknown) => JSON.stringify(value);
|
||||
export const idempotencyHeaders = (key: string) => ({ "Idempotency-Key": key });
|
||||
Reference in New Issue
Block a user