From 3a37063aac59b8faaae72cd71c1990231a898144 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Sun, 16 Aug 2026 15:29:30 +0200 Subject: docs: record what this session cost to find MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Six lessons, each of which took a deployment and a user's patience to locate. Five are the same shape: something that fails silently and looks like something else. The asyncio task nobody holds, `proc.wait()` deadlocking on a pipe it will not read, a peer whose loss is noted but not acted on, a service worker that is active without controlling the page, and a React setter that outlives its state. The sixth is about method: `node --check` validates syntax and not names, and `e2e.py` is a second implementation of the client rather than a test of this one. Neither caught any of the above. Source-reading tests are weak evidence and are the only evidence available for the SPA — so prefer ones that re-derive a value from the source over ones that restate it. Co-Authored-By: Claude Opus 5 --- CLAUDE.md | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) (limited to 'CLAUDE.md') diff --git a/CLAUDE.md b/CLAUDE.md index 1fae8a0..483ad7c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -70,7 +70,10 @@ dans le bon `pyproject.toml`. - All three packages share the same version number (released together) ### Protocol versions (independent) -- MNP: `0.1` → bumped independently of package version +- MNP: `0.2` → bumped independently of package version + - 0.2 added `PING`/`PONG` and backward chat paging (`before` / `has_more`). + Additive, so an 0.1 peer still works: it sends no `before` and is answered + with the newest page, which is what it wanted - MHP: `0.1` → bumped independently of package version - Every wire message carries a `v` field - Breaking change → MAJOR bump; backward-compatible → MINOR bump @@ -247,6 +250,48 @@ keypair bundle, or anything that looks like a user's public key. the task for good. Symptom is always the same — daemon running, logging nothing, `connected_nodes: 0`, socket in CLOSE-WAIT. Look there first +- **A background task nobody holds can be collected mid-flight.** asyncio keeps + only a *weak* reference to a task, so `asyncio.ensure_future(coro)` with the + result thrown away may be garbage-collected while still running — the loop + logs "Task was destroyed but it is pending!" and nothing else happens. For + `_stream_video` that meant its `async with sem` never reached `__aexit__` and + a transcode slot was lost for good. `WebRTCPeerSession._spawn()` exists for + this; never call `ensure_future` there directly, and `test_task_lifetime.py` + fails the build if anyone does + +- **`await proc.wait()` after `kill()` still deadlocks on a full pipe.** ffmpeg + outruns a credit-paced viewer; stop reading its stdout — which is what closing + the player does — and the transport cannot finish closing, SIGKILL or not. + Measured 2026-08-16: closing a viewer after 99 segments held a slot past the + 15 s handover timeout, so the next video hung and the one after was refused. + Drain the pipes, then wait with a timeout, and release the slot regardless + +- **Losing a peer must *stop* its work, not merely forget it.** The WebRTC + `connectionstatechange` handler popped the session from a dict and nothing + else, so a closed tab went on transcoding for the full 120 s credit timeout. + Anything holding a resource needs `shutdown_tasks()` on the way out + +- **A service worker being *active* is not the page being *controlled*.** An + uncontrolled page's requests never reach the worker's fetch handler, so the + streamed-download path handed over its stream and was never asked for it — + `writer.write()` then blocks on backpressure that will never lift, and the + download freezes after exactly one chunk. Require + `navigator.serviceWorker.controller`, and have the worker confirm it actually + served the request before trusting the sink + +- **A removed `useState` leaves its setter behind and nothing complains.** + `setActionsOpen` outlived `actionsOpen` and shipped: every action in the Files + panel threw ReferenceError on click. `grep actionsOpen` does not find + `setActionsOpen` — the capital breaks the match, which is exactly how it got + through. `test_transport_contracts.py` compares called setters against + declared ones + +- **`node --check` validates syntax, not names.** It caught none of the above. + Neither can `e2e.py`, which is a second implementation of the client. Source- + reading tests are weak evidence and are the only evidence available for the + SPA; prefer ones that re-derive a value from the source over ones that restate + it + - **`create_all()` is not a migration.** It creates missing *tables* and never a missing *column*, so a new column reaches the tests (fresh DB every run) and never reaches the deployed hub. Symptom: one endpoint answering 500 with an HTML body @@ -327,6 +372,16 @@ SFR residential Fedora 44 → meshbay.org OVH VPS: | Browser transport client | `static/transport.js` | Phase 9.4 — WebRTC DataChannel | | Web SPA | `static/app.js` | Phase 9.6 — Preact + preact-router | | File download (large) | `static/app.js` | File System Access API (`showSaveFilePicker`) — stream to disk | +| Background tasks (node) | `meshbay_node.transport.webrtc_server` | `_spawn()` — the only way to start one; a bare `ensure_future` can be collected | +| Stream handover (node) | `meshbay_node.transport.webrtc_server` | `_replace_stream` + `shutdown_tasks` — one viewer, one film, and the slot comes back when they leave | +| Download backpressure (node) | `meshbay_node.transport.webrtc_server` | `DOWNLOAD_BUFFER_HIGH` — 8 × 1 MB answered blind queues 8 MB on the channel | +| Streamed download (browser) | `static/downloads.js` + `static/sw.js` | Needs the page *controlled*, and the worker confirms it served the request | +| Leave a group (hub) | `meshbay_hub.api.groups` | `POST /v1/groups/{id}/leave` — self only; the owner is refused | +| Public group cap (hub) | `meshbay_hub.api.groups` | `_check_public_group_quota` — 10 live public groups per owner, staff exempt. **Checked at creation only, because PATCH refuses to change visibility** | +| Node presence (hub) | `meshbay_hub.api.groups` | `node_online` on `/v1/groups/mine`, read from the signaling registry — no poll, no timer | +| Chat paging (node) | `meshbay_node.chat.store` | `get_recent` / `get_before` / `has_before`. `get_messages` pages *forwards* and is not what a chat opens with | +| Liveness (MNP) | `meshbay_common.protocol` | `PING`/`PONG` on an **already-open** channel; never for discovery — a handshake costs 0.6-7 s | +| Profile page (browser) | `static/app.js` | `ProfilePage` — identity, node link, pins, account deletion. Settings keeps behaviour | | i18n (browser) | `static/i18n.js` | `t()` lookup + `Intl.PluralRules`, region-aware resolution, localStorage lang selection | | Translation catalogues | `static/locales/*.js` | One per language, fetched on demand. `en.js` is the source; `test_locales.py` holds the other nine to its key set | | Admin API (hub) | `meshbay_hub.api.admin` | Phase 10.2 — user/group mgmt, audit logs, stats | -- cgit v1.2.3