diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-19 10:14:27 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-19 10:14:27 +0200 |
| commit | e1bce3b8d5835c3c70208d39b5b2c66787625e15 (patch) | |
| tree | 0541ee26df124093d964a92502e96cd17ade2548 /packages/meshbay-hub/src | |
| parent | 20a824118c09af15d6c338db4c9480ffe5cbcdb6 (diff) | |
| download | meshbay-e1bce3b8d5835c3c70208d39b5b2c66787625e15.tar.gz | |
fix(hub): stop keeping a copy of every group's file listing in the browser
`group_indexes` was an IndexedDB store holding a decrypted copy of each group's
index — every file's name, path, size, hash and uploader — written on every index
and on every delta, from three call sites.
It was the cross-group search of Phase 10b: `doSearch` read `getAllCachedIndexes`
and searched those records instead of dialling anything. On 2026-08-28 Search
began dialling the nodes, and that commit removed the reader and left the writers.
Since then the browser has gone on building a cleartext file listing that nothing
consulted, that no sign-out removed — the key database is a different one — and
that grew with every group ever opened. L7, at rest: kept code that nothing calls
does not sit still.
Drawing a group's files while its node is unreachable is the only thing such a
cache buys, and it is not wanted: a listing that cannot be opened is worse than an
honest absence. So there is nothing to read it with, and the writers go.
The store stays in the schema and is emptied instead. Dropping it needs a version
bump, a version bump is an upgrade another tab can block, and playlists share this
database — so the tidier change is the one with a failure mode. `purgeGroupIndexCache`
runs once per browser behind a flag, which clears what is already on people's
machines; a browser that refuses storage simply runs it again, which is harmless
because it is idempotent.
Three guards, each checked by reintroducing the fault: only `openDB` and the purge
may touch the store, the purge may only clear it, and the purge must actually be
called at start-up — a purge nobody calls is the same defect wearing the opposite
hat.
`test_sticky_header.py[firefox]` reports twelve setup errors in a full run here.
A Firefox instance is open on this machine, which is the trap CLAUDE.md describes;
the same twelve appear with these changes stashed, and the `[chrome]` half of the
same file, covering the same geometry, is clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-hub/src')
4 files changed, 40 insertions, 49 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/app.js b/packages/meshbay-hub/src/meshbay_hub/static/app.js index 92dcb28..fbfb942 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/app.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/app.js @@ -10,7 +10,7 @@ import * as downloads from './downloads.js'; import { Icon } from './icon.js'; import { formatSize } from './file-utils.js'; import { - HUB, navigate, session, getCachedGroupIndex, + HUB, navigate, session, purgeGroupIndexCache, _storeBundleKey, _loadBundleKey, _clearKeyDB, loadAuth, saveAuth, setAuth, setAuthChangeListener, ensureFreshToken, hubFetch, refreshAccessToken, logoutOnHub, @@ -1252,7 +1252,23 @@ const trayLabels = () => ({ // Catalogues are fetched, so the first render waits for one: mounting earlier // would paint the interface in English and then swap every string. initLocale() // falls back to English rather than rejecting, so this cannot strand the page. +// One sweep, once per browser, to remove what the cross-group search of 2026-08 +// left behind: a cleartext copy of every group's file listing that nothing has +// read since, and that no sign-out removed. Guarded by a flag so it costs one +// transaction ever rather than one per load; a browser that refuses storage +// simply does it again, which is harmless. +const PURGED_KEY = 'meshbay.indexcache.purged'; +const purgeOnce = () => { + try { + if (localStorage.getItem(PURGED_KEY)) return; + } catch { /* no storage: purge anyway, it is idempotent */ } + purgeGroupIndexCache().then(() => { + try { localStorage.setItem(PURGED_KEY, '1'); } catch { /* nothing to remember with */ } + }); +}; + const mount = () => { + purgeOnce(); render(html`<${App} />`, document.getElementById('app')); // Get the download worker registered and this page under its control now, // rather than inside the first click on Download. On Firefox and Safari it is diff --git a/packages/meshbay-hub/src/meshbay_hub/static/group-page.js b/packages/meshbay-hub/src/meshbay_hub/static/group-page.js index 9a86f2a..26519da 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/group-page.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/group-page.js @@ -6,7 +6,7 @@ import { Icon } from './icon.js'; import { transfers } from './transfers.js'; import { downloadEntry } from './file-utils.js'; import { - HUB, session, cacheGroupIndex, hubFetch, ensureFreshToken, + HUB, session, hubFetch, ensureFreshToken, _loadBundleKey, _loadRecoveryKey, _storeBundleKey, } from './hub-client.js'; import { APPS, visibleApps } from './apps.js'; @@ -265,10 +265,6 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, setEntries(fresh); if (indexMsg.dirs) setNodeDirs(indexMsg.dirs); if (indexMsg.roots) setNodeRoots(indexMsg.roots); - cacheGroupIndex(groupId, group ? group.name : groupId, - group ? group.owner_username : null, fresh, - { video: appDirs('video'), music: appDirs('music'), - photo: appDirs('photo') }); }, [groupId, group, appDirs]); // additions/deletions/updates (daemon.py _broadcast_index_change, once @@ -296,10 +292,6 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, const keptIds = new Set(updated.map((e) => e.id)); const additions = (deltaMsg.additions || []).filter((e) => !keptIds.has(e.id)); const fresh = updated.concat(additions); - cacheGroupIndex(groupId, group ? group.name : groupId, - group ? group.owner_username : null, fresh, - { video: appDirs('video'), music: appDirs('music'), - photo: appDirs('photo') }); return fresh; }); }, [groupId, group, appDirs]); diff --git a/packages/meshbay-hub/src/meshbay_hub/static/hub-client.js b/packages/meshbay-hub/src/meshbay_hub/static/hub-client.js index f467720..ba91f00 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/hub-client.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/hub-client.js @@ -81,42 +81,26 @@ function openDB() { }); } -async function cacheGroupIndex(groupId, groupName, groupOwner, entries, roots) { - try { - const db = await openDB(); - const tx = db.transaction(IDB_STORE, 'readwrite'); - tx.objectStore(IDB_STORE).put({ - groupId, groupName, groupOwner, entries, roots: roots || {}, - cachedAt: Date.now(), - }); - await new Promise((r, rej) => { tx.oncomplete = r; tx.onerror = rej; }); - db.close(); - } catch { /* best-effort */ } -} - -async function getCachedGroupIndex(groupId) { - try { - const db = await openDB(); - const tx = db.transaction(IDB_STORE, 'readonly'); - const req = tx.objectStore(IDB_STORE).get(groupId); - const result = await new Promise((r, rej) => { req.onsuccess = () => r(req.result); req.onerror = rej; }); - db.close(); - return result || null; - } catch { return null; } -} - -async function getAllCachedIndexes() { - try { - const db = await openDB(); - const tx = db.transaction(IDB_STORE, 'readonly'); - const req = tx.objectStore(IDB_STORE).getAll(); - const result = await new Promise((r, rej) => { req.onsuccess = () => r(req.result); req.onerror = rej; }); - db.close(); - return result || []; - } catch { return []; } -} - -async function clearAllCachedIndexes() { +// `group_indexes` held a decrypted copy of every group's index — each file's +// name, path, size, hash and uploader — written on every index and every delta, +// and read by the cross-group search of the time, which searched those records +// instead of dialling anything. +// +// Search has dialled the nodes since 2026-08-28. The reader went with that +// change and the writers stayed, so for weeks the browser kept building a +// cleartext file listing that nothing consulted and no sign-out removed: the key +// database is a different one. It is **L7** — code nothing calls does not sit +// still, it accumulates. +// +// Showing a group's files while its node is unreachable was the only use left +// for such a cache, and it is not wanted: a listing you cannot open is worse +// than an honest absence. +// +// The store itself is left in the schema. Dropping it means a version bump, and +// a version bump means an upgrade another tab can block — which would take +// playlists down with it, since they share this database. Emptying it costs +// nothing and leaves nothing behind. +async function purgeGroupIndexCache() { try { const db = await openDB(); const tx = db.transaction(IDB_STORE, 'readwrite'); @@ -378,7 +362,7 @@ async function hubFetch(path, { method = 'GET', body, token, _retried } = {}) { export { HUB, navigate, session, openDB, IDB_PLAYLISTS, - cacheGroupIndex, getCachedGroupIndex, getAllCachedIndexes, clearAllCachedIndexes, + purgeGroupIndexCache, _storeBundleKey, _loadBundleKey, _storeRecoveryKey, _loadRecoveryKey, _clearKeyDB, loadAuth, saveAuth, setAuth, setAuthChangeListener, tokenLifeLeft, refreshAccessToken, ensureFreshToken, logoutOnHub, hubFetch, diff --git a/packages/meshbay-hub/src/meshbay_hub/static/search-page.js b/packages/meshbay-hub/src/meshbay_hub/static/search-page.js index a096414..0a96237 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/search-page.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/search-page.js @@ -5,7 +5,7 @@ import { t } from './i18n.js'; import { Icon } from './icon.js'; import { canPreview, downloadEntry } from './file-utils.js'; import { - HUB, session, cacheGroupIndex, hubFetch, ensureFreshToken, _loadBundleKey, + HUB, session, hubFetch, ensureFreshToken, _loadBundleKey, } from './hub-client.js'; import { FilesPanel, FilePreview } from './files-app.js'; import { VideoApp, groupVideoEntries } from './video-app.js'; @@ -295,7 +295,6 @@ async function fetchAllIndexes(groups, token, username, userId, onProgress, onRe groupName: g.name, groupOwner: g.owner_username, }); - cacheGroupIndex(g.id, g.name, g.owner_username, result.entries, result.roots); // Drawn now, not when this group's neighbours are done. Its index is // already in hand; holding it back until a group that is not answering // has finished not answering is ten seconds of blank page for work that |