diff options
Diffstat (limited to 'packages/meshbay-hub/tests/harness/window_leak.mjs')
| -rw-r--r-- | packages/meshbay-hub/tests/harness/window_leak.mjs | 73 |
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(); |