Files
cloudlysis/control/ui/src/api/client.ts
Vlad Durnea 1298d9a3df
Some checks failed
ci / ui (push) Failing after 30s
ci / rust (push) Failing after 2m34s
Monorepo consolidation: workspace, shared types, transport plans, docker/swam assets
2026-03-30 11:40:42 +03:00

123 lines
3.3 KiB
TypeScript

type RequestIds = {
requestId: string
correlationId?: string
traceparent?: string
}
const LAST_IDS_STORAGE_KEY = 'control:last_request_ids'
export class ApiError extends Error {
status: number
requestId: string
correlationId?: string
traceparent?: string
constructor(args: {
status: number
message: string
requestId: string
correlationId?: string
traceparent?: string
}) {
super(args.message)
this.name = 'ApiError'
this.status = args.status
this.requestId = args.requestId
this.correlationId = args.correlationId
this.traceparent = args.traceparent
}
}
const state: {
last?: RequestIds
} = {}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null
}
function loadLastIds(): RequestIds | undefined {
try {
const raw = localStorage.getItem(LAST_IDS_STORAGE_KEY)
if (!raw) return undefined
const parsed = JSON.parse(raw) as unknown
if (isRecord(parsed) && typeof parsed.requestId === 'string') {
const correlationId =
typeof parsed.correlationId === 'string' ? parsed.correlationId : undefined
const traceparent =
typeof parsed.traceparent === 'string' ? parsed.traceparent : undefined
return { requestId: parsed.requestId, correlationId, traceparent }
}
} catch {
return undefined
}
return undefined
}
function persistLastIds(ids: RequestIds) {
try {
localStorage.setItem(LAST_IDS_STORAGE_KEY, JSON.stringify(ids))
} catch {
return
}
}
function newRequestId(): string {
if (typeof crypto !== 'undefined' && 'randomUUID' in crypto) {
return crypto.randomUUID()
}
return `${Date.now()}-${Math.random().toString(16).slice(2)}`
}
export function getLastRequestIds(): RequestIds | undefined {
return state.last ?? loadLastIds()
}
type ApiRequestInit = RequestInit & {
correlationId?: string
traceparent?: string
useLastCorrelationId?: boolean
useLastTraceparent?: boolean
}
export async function apiFetch(
input: RequestInfo | URL,
init?: ApiRequestInit,
) {
const requestId = newRequestId()
const headers = new Headers(init?.headers)
headers.set('x-request-id', requestId)
const last = getLastRequestIds()
const correlationId =
init?.correlationId ?? (init?.useLastCorrelationId ? last?.correlationId : undefined)
const traceparent =
init?.traceparent ?? (init?.useLastTraceparent ? last?.traceparent : undefined)
if (correlationId) headers.set('x-correlation-id', correlationId)
if (traceparent) headers.set('traceparent', traceparent)
const res = await fetch(input, { ...init, headers })
const resCorrelationId = res.headers.get('x-correlation-id') ?? correlationId ?? undefined
const resTraceparent = res.headers.get('traceparent') ?? traceparent ?? undefined
const ids = { requestId, correlationId: resCorrelationId, traceparent: resTraceparent }
state.last = ids
persistLastIds(ids)
if (!res.ok) {
const text = await res.text().catch(() => '')
const err = new ApiError({
status: res.status,
requestId,
correlationId: resCorrelationId,
traceparent: resTraceparent,
message: `API error ${res.status}${text ? `: ${text}` : ''} (request_id=${requestId}${
resCorrelationId ? ` correlation_id=${resCorrelationId}` : ''
})`,
})
throw err
}
return res
}