From 42047dac4041e72e09499e3adf145f1c0f83b284 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Mon, 17 Aug 2026 02:16:28 +0200 Subject: test(hub): a hook that depends on one declared below it never runs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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. --- CLAUDE.md | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) (limited to 'CLAUDE.md') 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//` 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) -- cgit v1.2.3