import type { FeatureMeta, FeatureFilters } from '../types'; import { INITIAL_RETRY_MS, MAX_RETRY_MS } from './consts'; import pb from './pocketbase'; export function logNonAbortError(label: string, error: unknown): void { if (error instanceof Error && error.name === 'AbortError') { return; } console.error(`${label}:`, error); } export function isAbortError(error: unknown): boolean { return error instanceof Error && error.name === 'AbortError'; } /** Throw if response is not 2xx. Call before `.json()`. */ export function assertOk(res: Response, label: string): void { if (!res.ok) { throw new Error(`${label}: HTTP ${res.status} ${res.statusText}`); } } export function authHeaders(init?: RequestInit): RequestInit { const headers: Record = {}; if (pb.authStore.isValid && pb.authStore.token) { headers['Authorization'] = `Bearer ${pb.authStore.token}`; } if (!init) return { headers }; const existing = init.headers as Record | undefined; return { ...init, headers: { ...existing, ...headers } }; } export function apiUrl(endpoint: string, params?: URLSearchParams): string { const path = endpoint.startsWith('/') ? endpoint : `/api/${endpoint}`; const query = params?.toString(); return query ? `${path}?${query}` : path; } export async function fetchWithRetry( url: string, onSuccess: (data: T) => void, signal: AbortSignal ): Promise { let delay = INITIAL_RETRY_MS; while (!signal.aborted) { try { const res = await fetch(url, authHeaders({ signal })); if (!res.ok) throw new Error(`HTTP ${res.status}`); const json = await res.json(); onSuccess(json); return; } catch (err) { if (signal.aborted) return; console.error(`Failed to fetch ${url}, retrying in ${delay}ms:`, err); await new Promise((resolve) => setTimeout(resolve, delay)); delay = Math.min(delay * 2, MAX_RETRY_MS); } } } export async function shortenUrl(params: string): Promise { const res = await fetch(apiUrl('shorten'), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ params }), }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); return `${window.location.origin}${data.url}`; } export function buildFilterString( filters: FeatureFilters, features: FeatureMeta[], exclude?: string ): string { const entries = Object.entries(filters); if (entries.length === 0) return ''; return entries .filter(([name]) => name !== exclude) .map(([name, value]) => { const meta = features.find((f) => f.name === name); if (meta?.type === 'enum') { return `${name}:${(value as string[]).join('|')}`; } const [min, max] = value as [number, number]; const isAtMax = meta?.histogram ? max >= meta.histogram.max : max === meta?.max; const maxStr = meta?.absolute && isAtMax ? 'inf' : String(max); return `${name}:${min}:${maxStr}`; }) .join(';;'); }