aboutsummaryrefslogtreecommitdiffstats
path: root/CLAUDE.md
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-08-16 20:58:17 +0200
committerChristophe Besson <cbesson@gmail.com>2026-08-16 20:58:17 +0200
commitf57b2f6c0c2770fe0f456128d985e29427d2432f (patch)
tree3f502d00da42c3a82d755b1db8ec7cb29ed2c946 /CLAUDE.md
parente012b7e9ce55079f411943c7a1f6ccbfbd629a5f (diff)
downloadmeshbay-f57b2f6c0c2770fe0f456128d985e29427d2432f.tar.gz
docs: what this session cost to find
Five lessons, and the first is the expensive one: a test that models a fix agrees with it by construction. The buffer-ceiling test passed against a player that still hung, because the model and the fix had the same author and the same misunderstanding. Also: `no-cache` only binds a browser that asks; redeploying during someone else's test kills their session and truncates the log holding the reproduction; `updateend` fires for `remove()`; and flow control on a media stream is a window, not a debt. USERGUIDE section 7 rewritten — it still described 24 segments in flight and two transcode slots, and said "transcode" where ffmpeg does a `-c copy` remux, which is exactly why a slot costs little and why 500 MB really does go on the wire.
Diffstat (limited to 'CLAUDE.md')
-rw-r--r--CLAUDE.md44
1 files changed, 44 insertions, 0 deletions
diff --git a/CLAUDE.md b/CLAUDE.md
index 483ad7c..42dd8b2 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -292,6 +292,44 @@ keypair bundle, or anything that looks like a user's public key.
SPA; prefer ones that re-derive a value from the source over ones that restate
it
+- **A test that models a fix agrees with it by construction.** The first
+ buffer-ceiling test transcribed the player's credit loop into a small model and
+ passed, while the player it was written for still hung on a phone. The model and
+ the fix had the same author and the same misunderstanding. `tests/harness/
+ mse_harness.mjs` lifts `bufferedAhead`, `evictBehind`, `flushQueue` and `pump`
+ out of `app.js` *as text* and executes them; what it models is the browser. When
+ even that was not enough, a headless Chrome driven against real fragmented MP4
+ reproduced the defect in one run. Model the environment, never the code under
+ test
+
+- **`Cache-Control: no-cache` only binds a browser that asks.** One that cached
+ the SPA before that header existed applies heuristic freshness — a fraction of
+ the file's age, days for a file dated weeks ago — and does not ask at all. A
+ fix can be written, tested, deployed, served and still not be what runs, which
+ is indistinguishable from a fix that does not work. The whole module graph is
+ now served under `/a/<content-hash>/` so relative imports inherit the prefix and
+ no cache can serve yesterday's build or half of each. `test_asset_versioning.py`
+
+- **Redeploying during someone else's test destroys the evidence.** A node deploy
+ restarts the daemon, which kills every live WebRTC session — the tester sees
+ "transport not connected" caused by nothing they did — and `deploy-node.sh`
+ truncates `/tmp/meshbay-node.log`, taking the reproduction with it. Ask before
+ deploying while a reproduction is in flight
+
+- **`updateend` fires for `remove()` as well as `appendBuffer()`.** Crediting the
+ node from that event paid it for the player's own evictions: every time room was
+ made, more was asked for to fill it. Credit now follows the buffer, decided in
+ one place, and the append path grants nothing
+
+- **Flow control on a media stream is a window, not a debt.** Granting a credit
+ per append means pulling at network speed, which for a film is far faster than
+ watching it and fills the browser's SourceBuffer ceiling; accumulating those
+ credits and releasing the balance when the buffer finally drains sends the lot
+ in one burst and then says nothing for forty-six seconds. Bound the read-ahead
+ by *time past the playhead* and top a small window up as segments land. A viewer
+ deliberately holding credit must still say so, or the node's stall timeout ends
+ a film that is merely paused
+
- **`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
@@ -419,6 +457,12 @@ SFR residential Fedora 44 → meshbay.org OVH VPS:
| JWT scope enforcement | `meshbay_hub.api.deps` | `require_user_scope` — blocks node-scoped tokens from mutations |
| Node local admin UI | `meshbay_node.ui.app` | Dashboard, peers, groups, audit log (localhost:18000) |
| Demo scripts | — | `QE/demo-v1/*.py`, `QE/demo-v2/*.py`, `QE/demo-v3/*.py` (not versioned) |
+| Video flow control (browser) | `static/app.js` | `pump()` — the only place credit is granted. Read-ahead bounded by `BUFFER_AHEAD_S` of film, `STREAM_WINDOW` segments in flight, driven by a clock and by playback, never by arriving data |
+| Player under test | `tests/harness/mse_harness.mjs` | Runs the real `pump`/`flushQueue`/`evictBehind` against a fake SourceBuffer with a ceiling. Do not write a second model of them |
+| Asset versioning (hub) | `meshbay_hub.api.webapp` | `_asset_version()` — content hash; whole module graph served under `/a/<hash>/` so a cache cannot mix two builds |
+| Stream capacity (node) | `meshbay_node.config` | `[node] max_concurrent_streams` (default 8) — a slot is held for the length of a film, so it counts simultaneous viewers |
+| Stream diagnosis (node) | `webrtc_server.py` | `client_diag` at DEBUG — the player's own view (`ready`, `quota`, `ranges`, `err`) in the node's log. The only window into a phone |
+| Stream probe (no browser) | — | `QE/deploy/stream_probe.py` — pulls a real film over real MNP. Answers "is it the node or the browser" in one run (not versioned) |
## meshbay.org server (état cible)