88 lines
2.9 KiB
TypeScript
88 lines
2.9 KiB
TypeScript
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<string, string> = {};
|
|
if (pb.authStore.isValid && pb.authStore.token) {
|
|
headers['Authorization'] = `Bearer ${pb.authStore.token}`;
|
|
}
|
|
if (!init) return { headers };
|
|
const existing = init.headers as Record<string, string> | 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<T>(
|
|
url: string,
|
|
onSuccess: (data: T) => void,
|
|
signal: AbortSignal
|
|
): Promise<void> {
|
|
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<string> {
|
|
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 maxStr = meta?.absolute && max === meta.max ? 'inf' : String(max);
|
|
return `${name}:${min}:${maxStr}`;
|
|
})
|
|
.join(';;');
|
|
}
|