summaryrefslogtreecommitdiffstats
path: root/docs/apps.md
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-02 17:31:50 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-02 17:31:50 +0200
commit7b25f1c09ba1b8692988d9616f3c33c97af9f3ca (patch)
tree1a7d346ea6a3847269df4aeff3c6b41d515b5d7b /docs/apps.md
parent10f8266e7152d7dc38dbfe2449327829bf020ad1 (diff)
downloadmeshbay-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.md40
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