89 lines
4.0 KiB
JavaScript
89 lines
4.0 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const test = require('node:test');
|
|
const vm = require('node:vm');
|
|
const fs = require('node:fs');
|
|
|
|
function fixture(request) {
|
|
let now = 0, number = 0;
|
|
const timers = new Map(), states = [], expired = [], errors = [];
|
|
const context = {module: {exports: {}}, AbortController, Promise, Error, setTimeout: (fn, delay) => {const id = ++number; timers.set(id, {fn, at: now + delay}); return id;}, clearTimeout: id => timers.delete(id)};
|
|
vm.runInNewContext(fs.readFileSync(require.resolve('./player-session.js'), 'utf8'), context);
|
|
const session = context.module.exports({request, onState: state => states.push(state), onExpired: error => expired.push(error), onError: error => errors.push(error)});
|
|
const flush = async () => {for (let index = 0; index < 12; index++) await Promise.resolve();};
|
|
const tick = async amount => {
|
|
const end = now + amount;
|
|
await flush();
|
|
while (true) {
|
|
const next = Array.from(timers).filter(([, value]) => value.at <= end).sort((a, b) => a[1].at - b[1].at)[0];
|
|
if (!next) break;
|
|
now = next[1].at; timers.delete(next[0]); next[1].fn(); await flush();
|
|
}
|
|
now = end; await flush();
|
|
};
|
|
return {session, states, expired, errors, tick, flush};
|
|
}
|
|
|
|
test('heartbeats renew every 20 seconds and concurrent callers share one request', async () => {
|
|
const requests = [];
|
|
let resolve;
|
|
const f = fixture((id, signal) => {requests.push({id, signal}); return new Promise(done => {resolve = done;});});
|
|
f.session.start('watch-one');
|
|
await f.tick(20000);
|
|
assert.equal(requests.length, 1);
|
|
const first = f.session.ping(), second = f.session.ping();
|
|
assert.equal(first, second);
|
|
resolve({run: 1}); await f.flush();
|
|
assert.equal(f.states.length, 1);
|
|
await f.tick(19999); assert.equal(requests.length, 1);
|
|
await f.tick(1); assert.equal(requests.length, 2);
|
|
f.session.stop();
|
|
});
|
|
|
|
test('a hung heartbeat times out and retries without blocking future keepalives', async () => {
|
|
const requests = [];
|
|
const f = fixture((id, signal) => {requests.push({id, signal}); return requests.length === 1 ? new Promise(() => {}) : Promise.resolve({run: 7});});
|
|
f.session.start('watch'); f.session.ping();
|
|
await f.tick(8000);
|
|
assert.equal(requests[0].signal.aborted, true);
|
|
await f.tick(5000);
|
|
assert.equal(requests.length, 2);
|
|
assert.equal(f.states[0].run, 7);
|
|
assert.equal(f.expired.length, 0);
|
|
f.session.stop();
|
|
});
|
|
|
|
test('foreground checks cancel a stuck request and ignore its late expiry response', async () => {
|
|
let failOld;
|
|
const requests = [];
|
|
const f = fixture((id, signal) => {requests.push(signal); return requests.length === 1 ? new Promise((_, reject) => {failOld = reject;}) : Promise.resolve({run: 3});});
|
|
f.session.start('watch'); f.session.ping(); await f.flush();
|
|
await f.session.ping(true);
|
|
assert.equal(requests[0].aborted, true);
|
|
failOld(Object.assign(new Error('expired'), {status: 410})); await f.flush();
|
|
assert.equal(f.expired.length, 0);
|
|
assert.equal(f.states[0].run, 3);
|
|
f.session.stop();
|
|
});
|
|
|
|
test('expired sessions request one recovery; authentication failures do not trigger recovery loops', async () => {
|
|
for (const status of [410, 401, 403]) {
|
|
let requests = 0;
|
|
const f = fixture(() => {requests++; return Promise.reject(Object.assign(new Error('unavailable'), {status}));});
|
|
f.session.start('watch'); await f.session.ping(); await f.tick(120000);
|
|
assert.equal(requests, 1);
|
|
assert.equal(f.expired.length, status === 410 ? 1 : 0);
|
|
assert.equal(f.errors.length, status === 410 ? 0 : 1);
|
|
f.session.stop();
|
|
}
|
|
});
|
|
|
|
test('closing or changing a drama invalidates late heartbeats from its old session', async () => {
|
|
let finish;
|
|
const f = fixture(() => new Promise(resolve => {finish = resolve;}));
|
|
f.session.start('old'); f.session.ping(); await f.flush();
|
|
f.session.start('new'); finish({run: 1}); await f.flush();
|
|
assert.equal(f.states.length, 0);
|
|
f.session.stop(); await f.tick(120000);
|
|
assert.equal(f.states.length, 0);
|
|
});
|