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/tests/test_no_index_cache.py | |
| 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/tests/test_no_index_cache.py')
| -rw-r--r-- | packages/meshbay-hub/tests/test_no_index_cache.py | 98 |
1 files changed, 98 insertions, 0 deletions
diff --git a/packages/meshbay-hub/tests/test_no_index_cache.py b/packages/meshbay-hub/tests/test_no_index_cache.py new file mode 100644 index 0000000..c39bf00 --- /dev/null +++ b/packages/meshbay-hub/tests/test_no_index_cache.py @@ -0,0 +1,98 @@ +""" +A group's index is never written to browser storage. + +`group_indexes` was an IndexedDB store holding a decrypted copy of every group's +index — each file's name, path, size, hash and uploader — written on every index +and every delta. The cross-group search of the time read it instead of dialling +anything, which is what it was for. + +Search has dialled the nodes since 2026-08-28. That change removed the reader and +kept the writers, so the browser went 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. + +Showing a group's files while its node is unreachable is the only thing such a +cache buys, and it is not wanted: a listing you cannot open is worse than an +honest absence. So there is nothing left to read it with, and these tests keep it +that way — a writer reintroduced without a reader would be invisible again, and +the second time it would be invisible for the same reason as the first. +""" + +import re +from pathlib import Path + +STATIC = Path(__file__).resolve().parents[1] / "src" / "meshbay_hub" / "static" +HUB_CLIENT = STATIC / "hub-client.js" +APP = STATIC / "app.js" + +# The store name, read from the source rather than written down here: renaming it +# must not quietly take these tests out of the picture. +STORE = re.search(r"const IDB_STORE = '([^']+)';", + HUB_CLIENT.read_text(encoding="utf-8")).group(1) + + +def _functions(src: str) -> dict[str, str]: + """Every top-level function in a module, by name.""" + out = {} + starts = [(m.start(), m.group(1)) for m in + re.finditer(r"^(?:async )?function (\w+)\(", src, re.M)] + for i, (at, name) in enumerate(starts): + end = starts[i + 1][0] if i + 1 < len(starts) else len(src) + out[name] = src[at:end] + return out + + +def test_only_the_purge_touches_the_old_store(): + """ + Creating it and emptying it, and nothing else. + + `openDB` still creates the store because dropping it needs a version bump, + and a version bump is an upgrade another tab can block — which would take + playlists down with it, since they share this database. An empty store costs + nothing; the point is that nothing writes to it. + """ + fns = _functions(HUB_CLIENT.read_text(encoding="utf-8")) + touching = sorted(n for n, body in fns.items() if "IDB_STORE" in body) + assert touching == ["openDB", "purgeGroupIndexCache"], ( + f"{touching} touch the {STORE!r} store; only creating and emptying it " + "are allowed, and a write to it is a file listing kept on disk that " + "nothing will ever read") + + +def test_nothing_writes_a_group_index_to_the_store(): + """Stated on the operation rather than on the callers, so a new one is caught.""" + fns = _functions(HUB_CLIENT.read_text(encoding="utf-8")) + purge = fns["purgeGroupIndexCache"] + assert ".clear()" in purge + for write in (".put(", ".add(", ".putAll("): + assert write not in purge, f"the purge does a {write} — it must only clear" + + +def test_no_module_carries_a_cache_writer_any_more(): + """ + The functions are gone, so the way this comes back is a new one. Any export + of hub-client.js whose name is about caching an index is refused here rather + than discovered months later with a store full of filenames. + """ + src = HUB_CLIENT.read_text(encoding="utf-8") + for name in re.findall(r"^(?:async )?function (\w+)\(", src, re.M): + assert not re.search(r"cache.*index|index.*cache", name, re.I) \ + or name == "purgeGroupIndexCache", ( + f"{name} looks like an index cache again — the store it would write " + "to has no reader, and adding one was decided against") + + +def test_the_purge_is_actually_called(): + """ + The defect being cleaned up was a function nobody called. A purge nobody + calls is the same defect wearing the opposite hat: the data stays on every + machine that already has it, and nothing says so. + """ + app = APP.read_text(encoding="utf-8") + assert "purgeGroupIndexCache" in app, "app.js no longer imports the purge" + call = re.search(r"purgeGroupIndexCache\(\)", app) + assert call, "the purge is imported and never called" + mount = app.index("const mount = () => {") + assert "purgeOnce()" in app[mount:mount + 400], ( + "the purge is no longer run at start-up, so a browser that still holds " + "the old store keeps it") |