// Svelte 5 reactive mirror of peer-profile-store.ts. Components read // `peerProfiles.byPrimary[peer_primary]?.picture` and re-render // automatically when a fetch completes. // // This is a separate file from peer-profile-store.ts because // .svelte.ts files get the runes transform applied, and we want the // store layer (which is also imported by non-component code) to stay // rune-free. import { fetchPeerProfile, getCachedPeerProfile, hydratePeerProfileCache, type CachedPeerProfile, } from "./peer-profile-store.js"; import type { Identity } from "./kez.js"; class PeerProfilesCell { /** primary → CachedPeerProfile. Reactive: avatar consumers read * `peerProfiles.byPrimary[primary]?.picture`. */ byPrimary = $state>({}); /** True once IDB-backed mirror has loaded once. */ hydrated = $state(false); async hydrate() { if (this.hydrated) return; await hydratePeerProfileCache(); // Pull every entry into the reactive map. Cheap — at most ~hundreds // of profiles per active user. this.hydrated = true; } /** Trigger a fetch + cache update for a single peer. Idempotent * within the staleness window. Returns the resolved profile if * one was found (cached or fresh). */ async refresh(opts: { peer_primary: Identity; peer_nostr_pubkey: string; my_handle: string; my_seed: Uint8Array; forceRefresh?: boolean; }): Promise { const result = await fetchPeerProfile(opts); if (result) { // Mutate the record so Svelte 5's deep reactivity ticks. this.byPrimary = { ...this.byPrimary, [opts.peer_primary]: result }; } else { // Maybe an in-memory cache entry from a previous run that // wasn't surfaced yet — bring it through. const cached = getCachedPeerProfile(opts.peer_primary); if (cached && !this.byPrimary[opts.peer_primary]) { this.byPrimary = { ...this.byPrimary, [opts.peer_primary]: cached }; } } return result; } } export const peerProfiles = new PeerProfilesCell();