aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests/harness/window_leak.mjs
blob: d5185de84e6740ce7889f0c0f4c6f39f1023f94f (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
// 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 silentConsole = { log() {}, warn() {}, error() {} };

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, silentConsole, 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();