summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests/harness/window_leak.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/tests/harness/window_leak.mjs')
-rw-r--r--packages/meshbay-hub/tests/harness/window_leak.mjs73
1 files changed, 73 insertions, 0 deletions
diff --git a/packages/meshbay-hub/tests/harness/window_leak.mjs b/packages/meshbay-hub/tests/harness/window_leak.mjs
new file mode 100644
index 0000000..ae6d46e
--- /dev/null
+++ b/packages/meshbay-hub/tests/harness/window_leak.mjs
@@ -0,0 +1,73 @@
+// Run the shipped onStreamData against a seek that lands badly.
+//
+// The defect was a race: `reinitAt` waits for two `updateend` events, and the
+// segments the node sends in that gap are discarded. Whether the player
+// survived depended on how many arrived before the gap closed — which is why
+// the same seek worked twice and hung on the third, and why "it works now" is
+// not on its own evidence that it is fixed.
+//
+// So force the worst case. Every in-flight segment arrives during the gap. The
+// question is only whether the window comes back.
+//
+// The handler is lifted out of app.js as text, like the rest of the harness.
+// What is modelled is the transport and the clock.
+import { readFileSync } from 'fs';
+
+const app = readFileSync(process.argv[2], 'utf8');
+const cfg = JSON.parse(process.argv[3] || '{}');
+const { decrementFirst = true } = cfg; // false reproduces the shipped defect
+
+const STREAM_WINDOW = Number(app.match(/const STREAM_WINDOW = (\d+)/)[1]);
+
+// The body of `transport.onStreamData = async (msg) => { ... }`.
+const start = app.indexOf('transport.onStreamData = async (msg) => {');
+const body = app.slice(app.indexOf('{', start) + 1,
+ app.indexOf('\n };', start));
+
+// The A/B: put the decrement back after the guards, which is where it was.
+const DECREMENT = 'outstandingRef.current = Math.max(0, outstandingRef.current - 1);';
+let source = body;
+if (!decrementFirst) {
+ source = source.replace(DECREMENT, '');
+ source = source.replace('if (msg.file_id && msg.file_id !== entry.id) return;',
+ 'if (msg.file_id && msg.file_id !== entry.id) return;\n' + DECREMENT);
+}
+
+const outstandingRef = { current: STREAM_WINDOW };
+const awaitingInitRef = { current: true }; // mid-reinit, as after a seek
+const queueRef = { current: [] };
+const entry = { id: 'abc' };
+let cancelled = false;
+const pump = () => {};
+const flushQueue = () => {};
+const gekRef = { current: null };
+const window_ = { MeshBayCrypto: { decryptChunkBin: async () => new Uint8Array(4) } };
+
+const handler = new Function(
+ 'msg', 'cancelled', 'awaitingInitRef', 'outstandingRef', 'queueRef',
+ 'entry', 'gekRef', 'pump', 'flushQueue', 'console', 'window',
+ `return (async () => {${source}})();`);
+
+const deliver = (n) => Promise.all(
+ Array.from({ length: n }, (_, i) => handler(
+ { file_id: entry.id, segment_index: i, nonce: 'n', ct: 'c' },
+ cancelled, awaitingInitRef, outstandingRef, queueRef, entry, gekRef,
+ pump, flushQueue, console, window_)));
+
+const run = async () => {
+ // The whole window arrives while reinitAt is still awaiting its updateends.
+ await deliver(STREAM_WINDOW);
+ const duringGap = outstandingRef.current;
+ // reinitAt finishes and lowers the flag.
+ awaitingInitRef.current = false;
+ // pump() would now top the window up to STREAM_WINDOW - outstanding.
+ const roomAfterwards = STREAM_WINDOW - outstandingRef.current;
+ console.log(JSON.stringify({
+ decrementFirst,
+ windowAfterDiscards: duringGap,
+ creditPumpWouldGrant: roomAfterwards,
+ deadlocked: roomAfterwards <= 0,
+ }));
+};
+
+run();