summaryrefslogtreecommitdiffstats
path: root/CLAUDE.md
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-08-17 02:16:28 +0200
committerChristophe Besson <cbesson@gmail.com>2026-08-17 02:16:28 +0200
commit42047dac4041e72e09499e3adf145f1c0f83b284 (patch)
treeb816f20fd8b3b190faa69473b7a2892f3bfc9706 /CLAUDE.md
parentf5c4c058aa7e91fbdbbdd8cf34042535a05df433 (diff)
downloadmeshbay-42047dac4041e72e09499e3adf145f1c0f83b284.tar.gz
test(hub): a hook that depends on one declared below it never runs
`const a = useCallback(fn, [b])` evaluates `[b]` where it is written, so a `b` further down the component is still in its temporal dead zone. ReferenceError on every render, before anything the component does can run — and the symptom is the component simply not appearing. Clicking a video did nothing at all: no picture, no error on screen, nothing in the node's log because nothing was ever requested. It reached production. Nothing caught it. `node --check` passes, the code is well-formed. Worse, the MSE harness extracts the player functions into an order of its own and therefore *reordered* them before running — quietly repairing the one class of defect it was best placed to catch. It sorts by position in the file now, and test_hook_ordering.py checks the property directly across the whole SPA. Both the rule and the harness are checked against the layout that actually shipped. test_video_seek.py covers the rest of seeking, and window_leak.mjs forces the race that made the third seek hang: the whole in-flight window arriving while `reinitAt` is still awaiting. Before, the player is left believing eight segments are in flight and grants nothing; after, the window comes back. A run that happens to work proves nothing about a race, which is the point of forcing the worst case rather than trusting a longer session.
Diffstat (limited to 'CLAUDE.md')
-rw-r--r--CLAUDE.md34
1 files changed, 33 insertions, 1 deletions
diff --git a/CLAUDE.md b/CLAUDE.md
index 42dd8b2..babbd86 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -302,6 +302,35 @@ keypair bundle, or anything that looks like a user's public key.
reproduced the defect in one run. Model the environment, never the code under
test
+- **A hook cannot depend on one declared below it.** `const a = useCallback(fn,
+ [b])` evaluates `[b]` where it is written, so a `b` further down the component
+ is still in its temporal dead zone: `ReferenceError` on every render, before
+ anything the component does can run. The component simply does not appear —
+ clicking a video did nothing at all, with no error on screen and nothing in
+ the node's log because nothing was ever requested. It reached production.
+ `node --check` passes; the code is well-formed. Worse, the MSE harness
+ extracted the player functions in a list order of its own and therefore
+ *reordered* them, quietly repairing the one class of defect it was best placed
+ to catch — it now sorts by position in the file. `test_hook_ordering.py`
+ checks the whole SPA
+
+- **Flow-control accounting comes before every early return.** A segment that
+ arrived is no longer in flight, whatever is then done with it. Discarding one
+ before decrementing the in-flight window leaked a slot per discard, and
+ `reinitAt` is asynchronous, so a seek's whole window could arrive while it
+ was still awaiting its `updateend`s — the player then believed a full window
+ was in flight, granted no further credit, and the node waited for ever while
+ logging a stream it had fed perfectly. A race, so it worked twice and hung on
+ the third try; "it works now" is not evidence against a race, and
+ `tests/harness/window_leak.mjs` forces the worst case instead
+
+- **A new stream starts from a known state, and that list grows.** `appendingRef`
+ and `endedRef` were the first two; `awaitingInitRef` and `seekTargetRef`
+ repeated the same bug a session later, and worse — only `reinitAt` lowers
+ `awaitingInit`, and the next film never reaches it, so every one of its
+ segments would have been discarded. Reset at the *start* of the stream, never
+ in the teardown of the one before, which is skippable
+
- **`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
@@ -462,7 +491,10 @@ SFR residential Fedora 44 → meshbay.org OVH VPS:
| 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) |
+| Stream probe (no browser) | — | `QE/deploy/stream_probe.py` — pulls a real film over real MNP, `--start` to seek. Answers "is it the node or the browser" in one run (not versioned) |
+| Seeking (browser) | `static/app.js` | `requestSeek` → node restarts ffmpeg with `-ss`; `reinitAt` clears the buffer and sets `timestampOffset`. `-copyts` does *not* preserve position — measured — so the offset comes from the client |
+| Seeking (node) | `webrtc_server.py` | `start` on `stream_req`; `-ss` **before** `-i` (index seek, not decode-and-discard), clamped away from the end, echoed in `stream_init` |
+| Resume position | `static/app.js` | `readResumePosition` / `writeResumePosition` — localStorage, per file, per browser. No protocol, and nothing new learns what you watch |
## meshbay.org server (état cible)