124 lines
5.8 KiB
JavaScript
124 lines
5.8 KiB
JavaScript
/*
|
|||
|
|
* Does the stream delay hold back the INTERNET without holding back the ROOM?
|
||
|
|
*
|
||
|
|
* Starts the real server on a spare port with a throwaway DATA_DIR, connects
|
||
|
|
* two listeners — one plain (the room), one carrying x-forwarded-for (through
|
||
|
|
* the tunnel) — plus a fake engine, and times when each one is told things.
|
||
|
|
*/
|
||
|
|
import { spawn } from 'child_process';
|
||
|
|
import { WebSocket } from 'ws';
|
||
|
|
import fs from 'fs'; import os from 'os'; import path from 'path';
|
||
|
|
import { fileURLToPath } from 'url';
|
||
|
|
|
||
|
|
const DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'ct-delay-'));
|
||
|
|
const PORT = 3199;
|
||
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||
|
|
|
||
|
|
const srv = spawn(process.execPath, [path.join(__dirname, 'server', 'server.js')],
|
||
|
|
{ env: { ...process.env, PORT: String(PORT), DATA_DIR: DIR }, stdio: ['ignore', 'pipe', 'pipe'] });
|
||
|
|
const logs = [];
|
||
|
|
srv.stdout.on('data', (d) => logs.push(String(d)));
|
||
|
|
srv.stderr.on('data', (d) => logs.push('ERR ' + d));
|
||
|
|
|
||
|
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||
|
|
const B = 'http://127.0.0.1:' + PORT;
|
||
|
|
/* Wait for OUR server, identified by the config it writes into OUR dir — a
|
||
|
|
stray copy left on this port by an earlier run would answer /healthz and
|
||
|
|
then be tested instead. */
|
||
|
|
const CFG = path.join(DIR, 'config.json');
|
||
|
|
for (let i = 0; i < 80; i++) {
|
||
|
|
if (fs.existsSync(CFG)) { try { await fetch(B + '/healthz'); break; } catch (_) {} }
|
||
|
|
await sleep(150);
|
||
|
|
}
|
||
|
|
if (!fs.existsSync(CFG)) {
|
||
|
|
console.error('server never started. log:\n' + logs.join(''));
|
||
|
|
srv.kill(); process.exit(1);
|
||
|
|
}
|
||
|
|
const cfg = JSON.parse(fs.readFileSync(CFG, 'utf8'));
|
||
|
|
const KEY = cfg.controlToken, PIN = cfg.operatorPin, EKEY = cfg.engineKey;
|
||
|
|
console.log('server up on ' + PORT + '; new PIN is ' + PIN.length + ' digits');
|
||
|
|
|
||
|
|
const DELAY = 4;
|
||
|
|
await fetch(B + '/api/control/delay-set?s=' + DELAY + '&key=' + KEY);
|
||
|
|
console.log('stream delay set to ' + DELAY + 's\n');
|
||
|
|
|
||
|
|
function listener(label, headers) {
|
||
|
|
const ws = new WebSocket(B.replace('http', 'ws') + '/ws?role=listener&lang=eng_Latn', { headers });
|
||
|
|
const got = [];
|
||
|
|
ws.on('message', (raw) => {
|
||
|
|
const m = JSON.parse(raw);
|
||
|
|
got.push({ t: m.t, at: Date.now(), text: m.text, delaySec: m.delaySec });
|
||
|
|
});
|
||
|
|
return { label, ws, got, ready: new Promise((r) => ws.on('open', r)) };
|
||
|
|
}
|
||
|
|
const room = listener('room ', {});
|
||
|
|
const remote = listener('remote', { 'x-forwarded-for': '203.0.113.7' });
|
||
|
|
await Promise.all([room.ready, remote.ready]);
|
||
|
|
|
||
|
|
const eng = new WebSocket(B.replace('http', 'ws') + '/ws?role=engine&key=' + encodeURIComponent(EKEY));
|
||
|
|
await new Promise((r) => eng.on('open', r));
|
||
|
|
await fetch(B + '/api/control/start?key=' + KEY);
|
||
|
|
await sleep(300);
|
||
|
|
|
||
|
|
const hello = (l) => l.got.find((g) => g.t === 'hello');
|
||
|
|
console.log('hello.delaySec room=' + hello(room).delaySec + ' remote=' + hello(remote).delaySec);
|
||
|
|
|
||
|
|
const t0 = Date.now();
|
||
|
|
eng.send(JSON.stringify({ t: 'final', text: 'God has a plan for your life', cid: 'c1', tr: {} }));
|
||
|
|
await sleep(DELAY * 1000 + 1200);
|
||
|
|
|
||
|
|
let pass = 0, fail = 0;
|
||
|
|
const check = (label, ok, detail) => {
|
||
|
|
console.log((ok ? ' ok ' : ' FAIL ') + label + (detail ? ' ' + detail : ''));
|
||
|
|
ok ? pass++ : fail++;
|
||
|
|
};
|
||
|
|
console.log('\ncaption arrival, measured from the moment the engine sent it:');
|
||
|
|
for (const l of [room, remote]) {
|
||
|
|
const c = l.got.find((g) => g.t === 'caption');
|
||
|
|
const dt = c ? (c.at - t0) / 1000 : null;
|
||
|
|
console.log(' ' + l.label + ' ' + (c ? dt.toFixed(2) + 's "' + c.text + '"' : 'NEVER ARRIVED'));
|
||
|
|
}
|
||
|
|
const rc = room.got.find((g) => g.t === 'caption'), xc = remote.got.find((g) => g.t === 'caption');
|
||
|
|
check('the room gets it immediately', rc && (rc.at - t0) < 700, rc ? ((rc.at-t0)/1000).toFixed(2) + 's' : 'missing');
|
||
|
|
check('the internet waits the delay', xc && (xc.at - t0) >= DELAY * 1000 - 300, xc ? ((xc.at-t0)/1000).toFixed(2) + 's' : 'missing');
|
||
|
|
check('both got the same words', rc && xc && rc.text === xc.text);
|
||
|
|
check('hello tells the room 0', hello(room).delaySec === 0);
|
||
|
|
check('hello tells the internet ' + DELAY, hello(remote).delaySec === DELAY);
|
||
|
|
|
||
|
|
console.log('\nlate joiner backlog (scrollback in the hello frame):');
|
||
|
|
function joiner(headers) {
|
||
|
|
const ws = new WebSocket(B.replace('http', 'ws') + '/ws?role=listener&lang=eng_Latn', { headers });
|
||
|
|
return new Promise((res) => {
|
||
|
|
ws.on('message', (raw) => { const m = JSON.parse(raw); if (m.t === 'hello') { ws.close(); res(m); } });
|
||
|
|
});
|
||
|
|
}
|
||
|
|
const n = (h) => (h.scrollback || []).length;
|
||
|
|
const jr = await joiner({});
|
||
|
|
const jx = await joiner({ 'x-forwarded-for': '203.0.113.8' });
|
||
|
|
console.log(' a sentence spoken ' + DELAY + '+s ago:');
|
||
|
|
console.log(' room backlog ' + n(jr) + ' · remote backlog ' + n(jx));
|
||
|
|
check('the room sees it', n(jr) >= 1);
|
||
|
|
check('the internet sees it too — it is past their clock', n(jx) >= 1);
|
||
|
|
|
||
|
|
/* Now the case that matters: something said JUST NOW must not reach a remote
|
||
|
|
joiner's backlog, or the delay is defeated by refreshing the page. */
|
||
|
|
const before = n(jx);
|
||
|
|
eng.send(JSON.stringify({ t: 'final', text: 'a secret just spoken', cid: 'c2', tr: {} }));
|
||
|
|
await sleep(400);
|
||
|
|
const jr2 = await joiner({});
|
||
|
|
const jx2 = await joiner({ 'x-forwarded-for': '203.0.113.9' });
|
||
|
|
const txt = (h) => (h.scrollback || []).map((r) => r.text).join(' | ');
|
||
|
|
console.log(' a sentence spoken 0.4s ago:');
|
||
|
|
console.log(' room backlog: ' + txt(jr2));
|
||
|
|
console.log(' remote backlog: ' + txt(jx2));
|
||
|
|
check('the room sees the new sentence', txt(jr2).includes('a secret just spoken'));
|
||
|
|
check('the internet does NOT — refreshing cannot skip the delay',
|
||
|
|
!txt(jx2).includes('a secret just spoken'));
|
||
|
|
check('server sized the audio window for the delay', logs.join('').includes('audio window now'));
|
||
|
|
|
||
|
|
console.log('\n--- server log ---');
|
||
|
|
console.log(logs.join('').split('\n').filter((l) => /delay|window|START/.test(l)).map((l) => ' ' + l).join('\n'));
|
||
|
|
console.log('\n' + pass + ' passed, ' + fail + ' failed');
|
||
|
|
srv.kill(); fs.rmSync(DIR, { recursive: true, force: true });
|
||
|
|
process.exit(fail ? 1 : 0);
|