diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-08-17 02:16:28 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-08-17 02:16:28 +0200 |
| commit | 42047dac4041e72e09499e3adf145f1c0f83b284 (patch) | |
| tree | b816f20fd8b3b190faa69473b7a2892f3bfc9706 /CLAUDE.md | |
| parent | f5c4c058aa7e91fbdbbdd8cf34042535a05df433 (diff) | |
| download | meshbay-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.md | 34 |
1 files changed, 33 insertions, 1 deletions
@@ -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) |