Files
EvoBGP/web/src/lib/api/client.ts
T
Denozordec 54eaad3f78
CI / changes (push) Successful in 5s
CI / openapi (push) Has been skipped
CI / go (push) Successful in 19s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Successful in 1m6s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Successful in 1m1s
CI / docker-bird (push) Successful in 38s
CI / bird2 (push) Successful in 16s
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Successful in 1m1s
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Successful in 1m32s
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Has been cancelled
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Has been cancelled
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Has been cancelled
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Has been cancelled
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Has been cancelled
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Has been cancelled
refactor: enhance CI workflow for granular change detection by module and improve idempotency key generation in API client. Update job flags and conditions for better handling of file changes across different components.
2026-04-05 22:17:29 +07:00

106 lines
3.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { browser } from '$app/environment';
export const TOKEN_STORAGE_KEY = 'evobgp_api_token';
export type Problem = {
type?: string;
title?: string;
status?: number;
detail?: string;
};
function getToken(): string | null {
if (!browser) return null;
return localStorage.getItem(TOKEN_STORAGE_KEY);
}
function mergeHeaders(init?: RequestInit, extraHeaders?: Record<string, string>): Headers {
const h = new Headers(init?.headers);
if (!h.has('Accept')) h.set('Accept', 'application/json');
const t = getToken();
if (t && !h.has('Authorization')) h.set('Authorization', `Bearer ${t}`);
if (extraHeaders) {
for (const [k, v] of Object.entries(extraHeaders)) {
if (!h.has(k)) h.set(k, v);
}
}
return h;
}
/**
* Idempotency keys: `crypto.randomUUID()` exists only in secure contexts (HTTPS / localhost).
* Over plain HTTP to a LAN IP it is often undefined — use getRandomValues or a fallback.
*/
function newIdempotencyKey(): string {
const c = typeof globalThis !== 'undefined' ? globalThis.crypto : undefined;
if (c?.randomUUID) return c.randomUUID();
if (c?.getRandomValues) {
const buf = new Uint8Array(16);
c.getRandomValues(buf);
buf[6] = (buf[6]! & 0x0f) | 0x40;
buf[8] = (buf[8]! & 0x3f) | 0x80;
const hex = [...buf].map((b) => b.toString(16).padStart(2, '0')).join('');
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
}
return `idem-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 14)}`;
}
export class ApiError extends Error {
constructor(
public readonly status: number,
message: string,
public readonly problem?: Problem
) {
super(message);
}
}
export async function apiFetch(path: string, init?: RequestInit): Promise<Response> {
if (!browser) throw new Error('API is only available in the browser');
return fetch(path, { ...init, headers: mergeHeaders(init) });
}
/** GET / DELETE без тела */
export async function apiJSON<T>(path: string, init?: RequestInit): Promise<T> {
const res = await apiFetch(path, init);
return parseResponse<T>(res);
}
/** POST / PATCH / PUT с JSON-телом и автоматическим Idempotency-Key */
export async function apiMutate<T = void>(
path: string,
method: 'POST' | 'PATCH' | 'PUT' | 'DELETE',
body?: unknown,
opts?: { idempotent?: boolean }
): Promise<T> {
const headers: Record<string, string> = {};
if (body !== undefined) headers['Content-Type'] = 'application/json';
if (opts?.idempotent !== false) {
headers['Idempotency-Key'] = newIdempotencyKey();
}
const res = await fetch(path, {
method,
headers: mergeHeaders({ headers }, headers),
body: body !== undefined ? JSON.stringify(body) : undefined
});
return parseResponse<T>(res);
}
async function parseResponse<T>(res: Response): Promise<T> {
if (res.status === 204 || res.status === 205) return undefined as T;
const text = await res.text();
if (!res.ok) {
let problem: Problem | undefined;
let detail = `HTTP ${res.status}`;
try {
problem = JSON.parse(text) as Problem;
detail = problem.detail ?? problem.title ?? detail;
} catch {
if (text) detail = text;
}
throw new ApiError(res.status, detail, problem);
}
if (!text) return undefined as T;
return JSON.parse(text) as T;
}