Files
hertzbeat/web-next/lib/api-client.ts
T
Logic 2de48d2caa feat: complete HertzBeat 2.0 novice workflows
Refine entity, monitor, alert, settings, observability, and shared operator workflows; add scale guards, route compatibility, release validation, and Product Design coverage.

AI assistance: used for draft implementation and test iteration.\nHuman validation: Node 22 production build; 426 frontend files and 3,398 tests; 59-route matrix; release budget 7.84 MiB / 8.00 MiB; focused backend tests and Browser QA.\nRisk notes: local scale proof covers 2,005 entities; sustained concurrent-ingestion SLA remains deployment-specific.
2026-07-10 14:54:43 +08:00

223 lines
6.9 KiB
TypeScript

import { SUPPLEMENTAL_MESSAGES } from './i18n-runtime-messages';
import { clearClientSessionMarker } from './session-client';
const API_MESSAGE_NON_ZERO_FALLBACK = SUPPLEMENTAL_MESSAGES['en-US']?.['common.api.message-nonzero'] ?? 'common.api.message-nonzero';
const API_REQUEST_FAILED_STATUS_FALLBACK = SUPPLEMENTAL_MESSAGES['en-US']?.['common.api.request-failed-status'] ?? 'common.api.request-failed-status';
function formatApiRequestFailedStatus(status: number): string {
return API_REQUEST_FAILED_STATUS_FALLBACK.replace('{{status}}', String(status));
}
export type ApiClientError = Error & {
code?: number;
status?: number;
};
type ApiErrorPayload = {
code?: number;
msg?: unknown;
message?: unknown;
error?: unknown;
data?: unknown;
};
function normalizeErrorMessage(value: unknown): string | null {
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null;
}
function readNestedErrorMessage(data: unknown): string | null {
if (!data || typeof data !== 'object') return null;
const nested = data as { msg?: unknown; message?: unknown; error?: unknown };
return normalizeErrorMessage(nested.msg) || normalizeErrorMessage(nested.message) || normalizeErrorMessage(nested.error);
}
function parseApiErrorPayload(text: string): { code?: number; message?: string } {
if (!text.trim()) return {};
try {
const payload = JSON.parse(text) as ApiErrorPayload;
return {
code: typeof payload.code === 'number' ? payload.code : undefined,
message:
normalizeErrorMessage(payload.msg)
|| normalizeErrorMessage(payload.message)
|| normalizeErrorMessage(payload.error)
|| readNestedErrorMessage(payload.data)
|| undefined
};
} catch {
return {};
}
}
async function readApiErrorResponse(response: Response): Promise<{ code?: number; message?: string }> {
try {
return parseApiErrorPayload(await response.clone().text());
} catch {
return {};
}
}
export function getAuthorizationToken(): string | null {
return null;
}
export function getCurrentLocale(): string | null {
if (typeof window === 'undefined') return null;
return window.localStorage.getItem('hb.lang') || window.localStorage.getItem('layout.lang');
}
function buildHeaders(extra?: Record<string, string>) {
const locale = getCurrentLocale();
return {
...(locale ? { 'Accept-Language': locale } : {}),
...(extra || {})
};
}
async function refreshAuthorizationToken(): Promise<boolean> {
try {
const response = await fetch('/api/account/auth/refresh', {
method: 'POST',
credentials: 'same-origin',
cache: 'no-store'
});
if (!response.ok) {
clearClientSessionMarker();
return false;
}
const payload = (await response.json()) as {
code: number;
msg?: string;
data?: { authenticated?: boolean };
};
if (payload.code !== 0 || !payload.data?.authenticated) {
clearClientSessionMarker();
return false;
}
return true;
} catch {
clearClientSessionMarker();
return false;
}
}
async function apiFetch(path: string, init: RequestInit = {}, retryOn401 = true): Promise<Response> {
const response = await fetch(path.startsWith('/api') ? path : `/api${path}`, {
...init,
headers: buildHeaders(init.headers as Record<string, string> | undefined),
credentials: 'same-origin',
cache: 'no-store'
});
if (response.status === 401 && retryOn401) {
const refreshed = await refreshAuthorizationToken();
if (refreshed) {
return apiFetch(path, init, false);
}
}
if (!response.ok) {
const apiError = await readApiErrorResponse(response);
const error = new Error(apiError.message || formatApiRequestFailedStatus(response.status)) as ApiClientError;
error.status = response.status;
if (typeof apiError.code === 'number') {
error.code = apiError.code;
}
throw error;
}
return response;
}
export async function apiDownload(path: string, init: RequestInit = {}, retryOn401 = true): Promise<Response> {
const response = await fetch(path.startsWith('/api') ? path : `/api${path}`, {
...init,
headers: buildHeaders(init.headers as Record<string, string> | undefined),
credentials: 'same-origin',
cache: 'no-store'
});
if (response.status === 401 && retryOn401) {
const refreshed = await refreshAuthorizationToken();
if (refreshed) {
return apiDownload(path, init, false);
}
}
return response;
}
export async function apiGet<T>(path: string, init: RequestInit = {}): Promise<T> {
const response = await apiFetch(path, init);
return response.json() as Promise<T>;
}
export async function apiPost<T>(path: string, body: unknown): Promise<T> {
const isFormDataBody = typeof FormData !== 'undefined' && body instanceof FormData;
const response = await apiFetch(path, {
method: 'POST',
...(isFormDataBody
? { body }
: {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
}),
});
return response.json() as Promise<T>;
}
export async function apiPut<T>(path: string, body: unknown): Promise<T> {
const response = await apiFetch(path, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
return response.json() as Promise<T>;
}
export async function apiDelete<T>(path: string): Promise<T> {
const response = await apiFetch(path, {
method: 'DELETE',
});
return response.json() as Promise<T>;
}
export async function apiMessageGet<T>(path: string, init: RequestInit = {}): Promise<T> {
const payload = await apiGet<{ code: number; msg?: string; data: T }>(path, init);
if (payload.code !== 0) {
const error = new Error(payload.msg || API_MESSAGE_NON_ZERO_FALLBACK) as ApiClientError;
error.code = payload.code;
throw error;
}
return payload.data;
}
export async function apiMessagePost<T>(path: string, body: unknown): Promise<T> {
const payload = await apiPost<{ code: number; msg?: string; data: T }>(path, body);
if (payload.code !== 0) {
const error = new Error(payload.msg || API_MESSAGE_NON_ZERO_FALLBACK) as ApiClientError;
error.code = payload.code;
throw error;
}
return payload.data;
}
export async function apiMessagePut<T>(path: string, body: unknown): Promise<T> {
const payload = await apiPut<{ code: number; msg?: string; data: T }>(path, body);
if (payload.code !== 0) {
const error = new Error(payload.msg || API_MESSAGE_NON_ZERO_FALLBACK) as ApiClientError;
error.code = payload.code;
throw error;
}
return payload.data;
}
export async function apiMessageDelete<T>(path: string): Promise<T> {
const payload = await apiDelete<{ code: number; msg?: string; data: T }>(path);
if (payload.code !== 0) {
const error = new Error(payload.msg || API_MESSAGE_NON_ZERO_FALLBACK) as ApiClientError;
error.code = payload.code;
throw error;
}
return payload.data;
}