Files
beavergram/frontend/src/lib/api/avatars.ts
T

145 lines
3.6 KiB
TypeScript

import { accounts } from "$lib/stores/accounts.svelte";
import { auth } from "$lib/stores/auth.svelte";
const BASE = import.meta.env.VITE_API_BASE ?? "/api";
const RETRY_DELAY = 2500;
export type AvatarKind = "peer" | "chat";
const MAX_CACHED = 240;
const ready = new Map<string, string>();
const missing = new Set<string>();
const inflight = new Map<string, Promise<string | null>>();
function cacheKey(account: number, kind: AvatarKind, id: number): string {
return `${account}:${kind}:${id}`;
}
function remember(key: string, url: string) {
ready.set(key, url);
while (ready.size > MAX_CACHED) {
const oldest = ready.keys().next();
if (oldest.done) {
return;
}
const stale = ready.get(oldest.value);
ready.delete(oldest.value);
if (stale) {
URL.revokeObjectURL(stale);
}
}
}
function authHeaders(): Record<string, string> {
return auth.token ? { Authorization: `Bearer ${auth.token}` } : {};
}
function delay(ms: number): Promise<void> {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
async function fetchAvatar(
account: number,
kind: AvatarKind,
id: number,
key: string,
retry: boolean
): Promise<string | null> {
const url = `${BASE}/avatars/${kind}/${id}?account_id=${account}`;
const response = await fetch(url, { headers: authHeaders() });
if (response.ok) {
const objectUrl = URL.createObjectURL(await response.blob());
remember(key, objectUrl);
return objectUrl;
}
if (response.status === 409 && retry) {
await delay(RETRY_DELAY);
return fetchAvatar(account, kind, id, key, false);
}
missing.add(key);
return null;
}
export function loadAvatar(
kind: AvatarKind,
id: number
): Promise<string | null> {
const account = accounts.selectedId;
if (account === null) {
return Promise.resolve(null);
}
const key = cacheKey(account, kind, id);
const cached = ready.get(key);
if (cached) {
return Promise.resolve(cached);
}
if (missing.has(key)) {
return Promise.resolve(null);
}
const existing = inflight.get(key);
if (existing) {
return existing;
}
const promise = fetchAvatar(account, kind, id, key, true).finally(() => {
inflight.delete(key);
});
inflight.set(key, promise);
return promise;
}
async function fetchVariant(
account: number,
kind: AvatarKind,
id: number,
uniqueId: string,
key: string,
retry: boolean
): Promise<string | null> {
const url = `${BASE}/avatars/${kind}/${id}?account_id=${account}&unique_id=${encodeURIComponent(uniqueId)}`;
const response = await fetch(url, { headers: authHeaders() });
if (response.ok) {
const objectUrl = URL.createObjectURL(await response.blob());
remember(key, objectUrl);
return objectUrl;
}
if (response.status === 409 && retry) {
await delay(RETRY_DELAY);
return fetchVariant(account, kind, id, uniqueId, key, false);
}
missing.add(key);
return null;
}
export function loadAvatarVariant(
kind: AvatarKind,
id: number,
uniqueId: string
): Promise<string | null> {
const account = accounts.selectedId;
if (account === null) {
return Promise.resolve(null);
}
const key = `${cacheKey(account, kind, id)}:${uniqueId}`;
const cached = ready.get(key);
if (cached) {
return Promise.resolve(cached);
}
if (missing.has(key)) {
return Promise.resolve(null);
}
const existing = inflight.get(key);
if (existing) {
return existing;
}
const promise = fetchVariant(account, kind, id, uniqueId, key, true).finally(
() => {
inflight.delete(key);
}
);
inflight.set(key, promise);
return promise;
}