import { useEffect, useState } from 'react'; import { MatrixError, Method } from 'matrix-js-sdk'; import { useMatrixClient } from './useMatrixClient'; const PROFILE_FIELD = 'io.lotus.avatar_decoration'; // Module-level cache — survives re-renders, lives for the app session. // userId → slug | null (null = fetched, no decoration set) const cache = new Map(); // Callbacks waiting for a userId's result const pending = new Map void>>(); // Transient-failure attempt counts (userId → n) so a flaky federated lookup // can retry a couple of times, then gives up for the session. const failures = new Map(); function fetchDecoration( authedRequest: (method: Method, path: string) => Promise>, userId: string, ): Promise { if (cache.has(userId)) return Promise.resolve(cache.get(userId) ?? null); // De-duplicate in-flight requests for the same userId if (pending.has(userId)) { return new Promise((resolve) => { pending.get(userId)!.push(resolve); }); } const waiters: Array<(val: string | null) => void> = []; pending.set(userId, waiters); return authedRequest(Method.Get, `/profile/${encodeURIComponent(userId)}/${PROFILE_FIELD}`) .then((res) => { const val = (res[PROFILE_FIELD] as string | undefined) ?? null; cache.set(userId, val); return val; }) .catch((err: unknown) => { const status = err instanceof MatrixError ? err.httpStatus : undefined; // Definitive rejections — the field is unset (404) or the server won't // serve it (400/403). This is the common case for FEDERATED users whose // homeserver doesn't support extended profiles / rejects the field. Cache // "no decoration" so we never refetch: otherwise every avatar mount // re-requests and floods our homeserver with failing federated profile // lookups (the 403/502 console storm + real HS load). if (status === 404 || status === 403 || status === 400) { cache.set(userId, null); } else { // Transient (429 rate-limit / 5xx / network). Allow a couple of retries // — a single 429 in a member-list burst shouldn't permanently hide a // decoration — then give up for the session so a persistently-failing // federated link (e.g. a 502'ing remote server) can't loop forever. const attempts = (failures.get(userId) ?? 0) + 1; failures.set(userId, attempts); if (attempts >= 2) cache.set(userId, null); } return null; }) .finally(() => { const v = cache.get(userId) ?? null; pending.delete(userId); waiters.forEach((cb) => cb(v)); }); } export function invalidateDecorationCache(userId: string): void { cache.delete(userId); } export function useAvatarDecoration(userId: string): string | null { const mx = useMatrixClient(); const [slug, setSlug] = useState(() => cache.get(userId) ?? null); useEffect(() => { let cancelled = false; fetchDecoration( (method, path) => mx.http.authedRequest>(method, path), userId, ).then((val) => { if (!cancelled) setSlug(val); }); return () => { cancelled = true; }; }, [mx, userId]); return slug; }