diff options
| author | Adam Soltys <[email protected]> | 2026-10-07 07:24:33 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-10-07 07:24:33 -0700 |
| commit | 78cf670939133fc4f656f0f42b21a59431a53842 (patch) | |
| tree | 745ec53290fe37c91fb607e6013a322b887f9388 /src/features | |
| parent | 5bb7da55232c7a1debed2a505a85b5e7da5ba6db (diff) | |
| download | coinosv3-78cf670939133fc4f656f0f42b21a59431a53842.tar.gz coinosv3-78cf670939133fc4f656f0f42b21a59431a53842.zip | |
Rebuildable caches move from localStorage to IndexedDB (cache-store.js)
localStorage is ~5M characters per site whatever the device, and a phone
had filled it. Profiles, faceless list, reports, follow packs, profile
pages, relay lists, the moderation baseline, community directory, link
previews, video sizes/probes and search profiles (~3M on that phone) now
live in IndexedDB, which shares the site's disk allowance (GBs).
cacheStorage keeps localStorage's string API: reads from memory,
coalesced write-behind, flushed on hide. Copies migrate on open and are
removed once written. Boot waits for it (max 3s) beside the Ark store;
caches read at construction re-merge on ready; an early write can't
replace the stored copy. A copy an older build writes to localStorage
later moves in on first ask. Wallet wipes reach it; private mode stays on
localStorage. Read markers, drafts, follows, note threads and sync
outboxes stay in localStorage on purpose (cross-tab consistency, early
server-rendered seeding). Also asks navigator.storage.persist().
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src/features')
| -rw-r--r-- | src/features/messages.js | 64 |
1 files changed, 46 insertions, 18 deletions
diff --git a/src/features/messages.js b/src/features/messages.js index 6f34d48..f98df3a 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -44,6 +44,7 @@ import { t, getLang } from '../i18n.js'; import { inLanguage, isMachinePost } from '../lang-guess.js'; import { animateZap, warmZapSound } from '../zap-animation.js'; import { SIGNER_SILENT } from '../dm.js'; +import { cacheStorage, onCacheReady, cacheIsReady } from '../cache-store.js'; import { dlog } from '../debug.js'; import { qrSvg } from '../qr.js'; @@ -114,6 +115,19 @@ export function messagesFeature(ctx) { const communities = () => [COMMUNITY, ...st().communities]; + // A [key, value] list cache folded into its Map. What this session already + // learned stays (and stays newest); stored rows fill in around it — the + // same call serves the first read and the re-read once IndexedDB opens. + function mergeStored(map, key) { + let rows; + try { rows = JSON.parse(cacheStorage.getItem(key) || '[]'); } catch { return; } + if (!Array.isArray(rows) || !rows.length) return; + const mine = [...map]; + map.clear(); + for (const [k, v] of rows) map.set(k, v); + for (const [k, v] of mine) { map.delete(k); map.set(k, v); } + } + // ---- custom emoji (NIP-30 tags, NIP-51 emoji sets) ---------------------- // A message that says :pika_wave: carries an ["emoji", code, url] tag for // it, so RENDERING never needs anything held locally. Two sources feed @@ -1954,13 +1968,16 @@ export function messagesFeature(ctx) { .sort((a, b) => b.members - a.members); } const hub = { list: normalizeHub(COMMUNITY_DIRECTORY), at: 0, attempted: false, loading: false }; - try { - const cached = JSON.parse(localStorage.getItem(HUB_CACHE_KEY) || 'null'); - if (cached && Array.isArray(cached.list) && Number.isFinite(cached.at)) { - const list = normalizeHub(cached.list); - if (list.length || !cached.list.length) { hub.list = list; hub.at = cached.at; } - } - } catch {} + function loadHubCache() { + try { + const cached = JSON.parse(cacheStorage.getItem(HUB_CACHE_KEY) || 'null'); + if (cached && Array.isArray(cached.list) && Number.isFinite(cached.at) && cached.at > hub.at) { + const list = normalizeHub(cached.list); + if (list.length || !cached.list.length) { hub.list = list; hub.at = cached.at; } + } + } catch {} + } + loadHubCache(); async function loadHub() { if (hub.loading || hub.attempted || (hub.at > 0 && Date.now() - hub.at < HUB_REFRESH_MS)) return; hub.attempted = true; hub.loading = true; @@ -1976,7 +1993,7 @@ export function messagesFeature(ctx) { // Bound icon sizes so public suggestions do not crowd out wallet data. const cached = list.map((c) => ({ name: c.name, description: c.description, category: c.category, invite_url: c.invite, member_count: c.members, icon_url: c.icon })); - localStorage.setItem(HUB_CACHE_KEY, JSON.stringify({ at: hub.at, list: cached })); + cacheStorage.setItem(HUB_CACHE_KEY, JSON.stringify({ at: hub.at, list: cached })); } catch {} scheduleRepaint(); } catch { /* Keep the usable directory if refresh fails or we're offline. */ } @@ -5681,7 +5698,8 @@ export function messagesFeature(ctx) { const VIDEO_PROBE_MS = 3000, VIDEO_DIMS_KEEP = 400, VIDEO_DIMS_KEY = 'coinos-video-dims'; const videoDims = new Map(); // url -> { width, height } const videoUnknown = new Set(); // probed this session and no answer: paint 16:9, don't ask again - try { for (const [u, d] of JSON.parse(localStorage.getItem(VIDEO_DIMS_KEY) || '[]')) videoDims.set(u, d); } catch {} + const loadVideoDims = () => mergeStored(videoDims, VIDEO_DIMS_KEY); + loadVideoDims(); let videoDimsFlush = null; const rememberVideo = (url, width, height) => { if (!(width > 0 && height > 0)) return; @@ -5692,7 +5710,7 @@ export function messagesFeature(ctx) { if (videoDimsFlush) return; videoDimsFlush = setTimeout(() => { videoDimsFlush = null; - try { localStorage.setItem(VIDEO_DIMS_KEY, JSON.stringify([...videoDims].slice(-VIDEO_DIMS_KEEP))); } catch {} + try { cacheStorage.setItem(VIDEO_DIMS_KEY, JSON.stringify([...videoDims].slice(-VIDEO_DIMS_KEEP))); } catch {} }, 1000); }; const imetaDim = (ev, url) => { @@ -5714,13 +5732,14 @@ export function messagesFeature(ctx) { const VIDEO_INFO_KEY = 'coinos-video-info', VIDEO_INFO_KEEP = 2000, VIDEO_PROBE_BYTES = 65536; const VIDEO_LARGE_KEY = 'coinos-video-large'; const videoInfo = new Map(); // url -> { size, w, h, image, faststart, ranges, moovEnd, probed, failedAt } - try { for (const [u, d] of JSON.parse(localStorage.getItem(VIDEO_INFO_KEY) || '[]')) videoInfo.set(u, d); } catch {} + const loadVideoInfo = () => mergeStored(videoInfo, VIDEO_INFO_KEY); + loadVideoInfo(); let videoInfoFlush = null; const saveVideoInfo = () => { if (videoInfoFlush) return; videoInfoFlush = setTimeout(() => { videoInfoFlush = null; - try { localStorage.setItem(VIDEO_INFO_KEY, JSON.stringify([...videoInfo].slice(-VIDEO_INFO_KEEP))); } catch {} + try { cacheStorage.setItem(VIDEO_INFO_KEY, JSON.stringify([...videoInfo].slice(-VIDEO_INFO_KEEP))); } catch {} }, 1000); }; const noteVideoInfo = (url, d) => { videoInfo.set(url, { ...(videoInfo.get(url) || {}), ...d }); saveVideoInfo(); }; @@ -5913,16 +5932,15 @@ export function messagesFeature(ctx) { const PREVIEW_KEEP = 400; const previews = new Map(); // url -> { title, description, image, site } | null (nothing there) const previewWarming = new Map(); - try { - for (const [u, m] of JSON.parse(localStorage.getItem('coinos-link-previews') || '[]')) previews.set(u, m); - } catch {} + const loadPreviews = () => mergeStored(previews, 'coinos-link-previews'); + loadPreviews(); let previewFlush = null; const savePreviews = () => { if (previewFlush) return; previewFlush = setTimeout(() => { previewFlush = null; const rows = [...previews].filter(([, m]) => m).slice(-PREVIEW_KEEP); - try { localStorage.setItem('coinos-link-previews', JSON.stringify(rows)); } catch {} + try { cacheStorage.setItem('coinos-link-previews', JSON.stringify(rows)); } catch {} }, 1000); }; const previewable = (url) => /^https?:\/\//i.test(url) && !youtubeId(url) @@ -8022,7 +8040,7 @@ export function messagesFeature(ctx) { function baselineNow() { if (!baseline) { let b = null; - try { b = JSON.parse(localStorage.getItem(BASELINE_KEY) || 'null'); } catch {} + try { b = JSON.parse(cacheStorage.getItem(BASELINE_KEY) || 'null'); } catch {} const toMap = (o) => new Map(Object.entries(o || {}).map(([k, v]) => [k, new Set(v)])); baseline = { follows: new Set((b && b.follows) || []), mutes: withSets({ tags: (b && b.mutes) || [], priv: [], content: '', at: 0 }), ev: toMap(b && b.ev), pk: toMap(b && b.pk), mine: new Set(), at: (b && b.at) || 0 }; @@ -8057,7 +8075,7 @@ export function messagesFeature(ctx) { for (const ev of got.flat()) if (ev && ev.kind === 1984 && from.has(ev.pubkey)) indexReport(r, ev, false); baseline = { follows: new Set(follows), mutes: withSets({ tags: muteTags, priv: [], content: '', at: k10 ? k10.created_at : 0 }), ev: r.ev, pk: r.pk, mine: new Set(), at: Date.now() }; const toObj = (m) => Object.fromEntries([...m.entries()].slice(-1500).map(([k, v]) => [k, [...v].slice(0, 50)])); - try { localStorage.setItem(BASELINE_KEY, JSON.stringify({ follows, mutes: muteTags, ev: toObj(r.ev), pk: toObj(r.pk), at: baseline.at })); } catch {} + try { cacheStorage.setItem(BASELINE_KEY, JSON.stringify({ follows, mutes: muteTags, ev: toObj(r.ev), pk: toObj(r.pk), at: baseline.at })); } catch {} scheduleRepaint(); } catch {} finally { baselineSync = null; } })(); @@ -12699,6 +12717,16 @@ export function messagesFeature(ctx) { return homeView(); } + // The caches above live in IndexedDB (cache-store.js), which opens a few + // milliseconds into boot. Anything read before then came from what was + // left in localStorage — nothing, once migrated — so re-read it all. + if (!cacheIsReady()) onCacheReady(() => { + profBlob = null; facelessBlob = null; relayLists = null; packMembers = null; reportsCache = null; baseline = null; + profilesWarmed = false; warmProfiles(); + loadHubCache(); loadVideoDims(); loadVideoInfo(); loadPreviews(); + scheduleRepaint(); + }); + return { id: 'messages', // group-chat state is device state in IndexedDB: it goes with the account |
