diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-02 17:31:50 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-02 17:31:50 +0200 |
| commit | 7b25f1c09ba1b8692988d9616f3c33c97af9f3ca (patch) | |
| tree | 1a7d346ea6a3847269df4aeff3c6b41d515b5d7b /docs/apps.md | |
| parent | 10f8266e7152d7dc38dbfe2449327829bf020ad1 (diff) | |
| download | meshbay-7b25f1c09ba1b8692988d9616f3c33c97af9f3ca.tar.gz | |
fix(hub): stop the maintenance loop racing the tests, and pin _ASSETS
Two defects found while closing out the Search merge, neither of them in
that feature.
The maintenance loop. create_app's lifespan starts cleanup_loop as an
asyncio task, so every test — each entering that lifespan — ran a purge
pass concurrently with its own requests. On SQLite :memory: that is not
merely noisy: the engine uses a StaticPool, one connection for the whole
process, so the request's session and the cleanup task's session
interleave transactions on the same connection. A registration could
commit and then be invisible to the login three lines later, surfacing
as 401 Invalid credentials for an account created moments before, in
roughly one run of test_node_ws_auth.py in four.
The purge itself is not at fault and this is not a production condition.
A passive SQL listener caught the DELETE removing 0 rows, and the INSERT
carrying status='active' — so neither the pending-account mechanism nor
the purge filter is involved, and PostgreSQL gives every session its own
connection. What the fixture removes is the second user of the shared
one. 60 runs of the previously flaky file, 0 failures; reproductions
before the fix landed on attempts 4, 6, 13 and 29 of separate loops, so
a clean run of 60 has about a 1% chance of being luck.
_ASSETS. source-merge.js shipped missing from webapp._ASSETS, the
cache-busting hash's input list — exactly the silent failure docs/apps.md
§4 step 5 warns about: the file changes, the asset URL does not, and a
browser holding the old page keeps the old copy. Harmless this time only
because search-page.js changed in the same commit and is listed, which is
the worst way for it to go unnoticed. Found by re-reading that checklist
for the doc pass, not by any test — so there is a test now, holding
_ASSETS to every .js in static/ (sw.js excepted, unversioned on purpose).
It was the only one missing.
Phase 9 of docs/refactoring-search.md also lands here: mediacenter.md
§10.6, musicbay.md §9b, photos.md §10b, apps.md §2b and step 5.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AbwJDbNTkiRUh7HTWEoyss
Diffstat (limited to 'docs/apps.md')
| -rw-r--r-- | docs/apps.md | 40 |
1 files changed, 38 insertions, 2 deletions
diff --git a/docs/apps.md b/docs/apps.md index 1cd6339..ef8cc1a 100644 --- a/docs/apps.md +++ b/docs/apps.md @@ -80,6 +80,39 @@ needs — a new app does not get a bespoke prop list. Notable ones: | `transportRef`, `gekRef` | refs to the live MNP transport and the imported group key | never state — a ref, so reconnects don't force a re-render of every app | | `mayUpload` | `memberUpload || isNodeAdmin`, computed once | Files' toolbar and Chat's composer both gate on it; a second derivation would eventually disagree with the first | +### 2b. The same app, rendered by the Search page + +Videos, Music and Photos are mounted twice: by `group-page.js` for one group, +and by `search-page.js` across every group the reader belongs to. The second +caller passes the same prop shape, and the difference lives entirely on the +**entries**, in underscore-prefixed fields the group page never sets: + +| Field | What it is | +|---|---| +| `groupId`, `groupName`, `groupOwner` | which group serves this entry | +| `_tRef`, `_gRef` | that group's transport and key — read as `entry._tRef \|\| transportRef`, which is why a single-group mount needs no special case | +| `_connGen` | bumped when that group reconnects; use it as a refetch key so a tile recovers instead of staying a spinner | +| `_sources` | every group that has this file, after the de-duplication below | + +**A file shared by two groups is one entry, not two** (`source-merge.js`, +`docs/refactoring-search.md`). Entries are folded on their content hash and one +source is resolved per *unit* — a film, a show, an album — using each app's own +grouping function to decide what a unit is. Two consequences for a new app: + +- if it renders a group name, use **`SourceTag`** from `group-name.js` rather + than `entry.groupName`: a merged entry has several groups and must say + `N sources` instead of naming one. Pass it the **whole unit** (a show's + episodes, an album's tracks), not the entry the card was drawn from — that + entry is usually chosen for its thumbnail, and would under-report; +- if it needs a merge unit key of its own, add a `<name>Units()` helper to + `search-page.js` that calls the app's **exported** grouping function. Never + re-derive the keys there: a copy keeps agreeing until one of them changes, + and the symptom is a show whose episodes stream from two different nodes. + +The Files explorer is deliberately **not** merged — there each group is a +top-level folder, and merging would remove a file from one of them. +`test_search_files_unmerged.py` refuses a build that changes this. + An app that needs **local** state (Files' `selecting`/`sortKey`/`currentPath`, for instance) owns it itself with `useState`, same as before the split. One thing worth keeping if you add a tab with a notion of "current location within @@ -160,8 +193,11 @@ registry, so a newly-registered app gets a checkbox for free. 5. **`webapp.py`'s `_ASSETS`** tuple: add the new file. This is the cache-busting hash's input list — a file imported by the page but missing here can change without the served URL changing, which is the exact bug - class `test_asset_versioning.py` exists for. Forgetting this step is - silent: nothing errors, a browser just keeps an old copy. + class `test_asset_versioning.py` exists for. Forgetting this step used to be + silent: nothing errors, a browser just keeps an old copy. It is now caught — + `test_every_static_script_participates_in_the_fingerprint` holds `_ASSETS` + to every `.js` in `static/` (`sw.js` excepted, unversioned on purpose). + Written after `source-merge.js` shipped missing from the list. 6. **Test coverage that scans the file set**: `test_hook_ordering.py` (`STATIC_FILES`) and `test_transport_contracts.py` (`test_no_setter_survives_the_state_it_belonged_to`, `SPLIT_FILES`) walk a |