aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests/harness/mse_harness.mjs
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 /packages/meshbay-hub/tests/harness/mse_harness.mjs
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 'packages/meshbay-hub/tests/harness/mse_harness.mjs')
-rw-r--r--packages/meshbay-hub/tests/harness/mse_harness.mjs37
1 files changed, 30 insertions, 7 deletions
diff --git a/packages/meshbay-hub/tests/harness/mse_harness.mjs b/packages/meshbay-hub/tests/harness/mse_harness.mjs
index 0daacfb..437a542 100644
--- a/packages/meshbay-hub/tests/harness/mse_harness.mjs
+++ b/packages/meshbay-hub/tests/harness/mse_harness.mjs
@@ -31,7 +31,13 @@ const grab = (name) => {
};
const useCallback = (fn) => fn;
-const src = ['bufferedAhead', 'evictBehind', 'flushQueue', 'pump']
+// Sorted by where they appear in app.js, not by the order this list happens to
+// be written in. Extracting into an order of our own would quietly repair a
+// hook declared before its own dependency — a real fault, which reached
+// production once, and which the harness is otherwise well placed to catch.
+const src = ['currentRange', 'bufferedAhead', 'evictBehind', 'flushQueue', 'pump']
+ .sort((a, b) => app.indexOf(`const ${a} = useCallback(`)
+ - app.indexOf(`const ${b} = useCallback(`))
.map(grab).join('\n');
const SEG = 256 * 1024;
@@ -68,17 +74,34 @@ const sb = {
const e = new Error('quota'); e.name = 'QuotaExceededError'; throw e;
}
const at = ranges.length ? ranges[ranges.length - 1][1] : 0;
- ranges.push([at, at + chunk.byteLength / BITRATE]);
+ const end = at + chunk.byteLength / BITRATE;
+ // A real SourceBuffer coalesces contiguous ranges: `buffered` reports the
+ // spans of media it holds, not the appends that built them. Pushing one
+ // range per segment made every range a couple of seconds long, which is
+ // invisible to code that reads `end(length - 1)` and fatal to code that
+ // looks for the range around the playhead.
+ if (ranges.length && Math.abs(ranges[ranges.length - 1][1] - at) < 0.001) {
+ ranges[ranges.length - 1][1] = end;
+ } else {
+ ranges.push([at, end]);
+ }
bytes += chunk.byteLength;
appended++;
},
remove(a, b) {
removes++;
+ // remove(a, b) takes a span out of whatever it overlaps, trimming a range
+ // rather than only dropping whole ones — otherwise a coalesced range is
+ // never evicted at all and the buffer grows without limit.
let dropped = 0;
- ranges = ranges.filter(([s, e]) => {
- if (e <= b && s >= a) { dropped += (e - s) * BITRATE; return false; }
- return true;
- });
+ const kept = [];
+ for (const [s, e] of ranges) {
+ if (e <= a || s >= b) { kept.push([s, e]); continue; }
+ if (s < a) kept.push([s, a]);
+ if (e > b) kept.push([b, e]);
+ dropped += (Math.min(e, b) - Math.max(s, a)) * BITRATE;
+ }
+ ranges = kept;
bytes -= dropped;
// A real remove() is asynchronous and fires updateend when it lands. That
// event is indistinguishable from an append's unless the player kept track.
@@ -108,7 +131,7 @@ const fns = new Function(
'sbRef,videoRef,msRef,queueRef,appendingRef,endedRef,outstandingRef,' +
'lastPokeRef,transportRef,BUFFER_BEHIND_S,BUFFER_AHEAD_S,QUEUE_HIGH_WATER,' +
'CREDIT_KEEPALIVE_MS,STREAM_WINDOW,quotaRef,stalledRef,console,useCallback',
- src + '\n return {bufferedAhead, evictBehind, flushQueue, pump};'
+ src + '\n return {currentRange, bufferedAhead, evictBehind, flushQueue, pump};'
)(sbRef, videoRef, msRef, queueRef, appendingRef, endedRef, outstandingRef,
lastPokeRef, transportRef, BUFFER_BEHIND_S, BUFFER_AHEAD_S, QUEUE_HIGH_WATER,
CREDIT_KEEPALIVE_MS, STREAM_WINDOW, quotaRef, stalledRef, console, useCallback);