// Run the SHIPPED `postOffer` against a fake hub and a fake clock. // // `postOffer` and the constants that govern it are lifted out of transport.js // as text. What is modelled is the hub — a list of the statuses it answers in // turn, each with an optional Retry-After — and the clock. // // Usage: node offer_retry_harness.mjs import { readFileSync } from 'fs'; import { delimiter } from 'path'; // transport.js and the parts split out of it, joined. const src = process.argv[2].split(delimiter).map((p) => readFileSync(p, 'utf8')).join('\n'); const cfg = JSON.parse(process.argv[3] || '{}'); const { // What the hub answers, one entry per POST: a status, or [status, retryAfter]. answers = [200], // After this many ms the caller closes its transport. null: never. closeAt = null, runUntil = 600000, } = cfg; let now = 0; let nextId = 1; let timers = []; globalThis.setTimeout = (fn, ms) => { const t = { at: now + (ms || 0), fn, id: nextId++ }; timers.push(t); return t.id; }; globalThis.clearTimeout = (id) => { timers = timers.filter((t) => t.id !== id); }; const flush = async () => { for (let i = 0; i < 50; i++) await Promise.resolve(); }; async function run(until) { await flush(); while (timers.length) { const due = timers.reduce((a, b) => (b.at < a.at ? b : a)); if (due.at > until) break; timers = timers.filter((t) => t !== due); now = due.at; due.fn(); await flush(); } now = until; } // Jitter at its midpoint, so the delays asserted are the nominal ones. Math.random = () => 0.5; const posts = []; const call = async () => { const a = answers[Math.min(posts.length, answers.length - 1)]; const [status, retryAfter] = Array.isArray(a) ? a : [a, null]; posts.push(now); return { ok: status >= 200 && status < 300, status, headers: new Headers(retryAfter === null ? {} : { 'Retry-After': String(retryAfter) }), json: async () => ({ detail: `status ${status}` }), }; }; const lift = (signature, end) => { const start = src.indexOf(signature); if (start < 0) throw new Error(`${signature} is gone from transport.js`); return src.slice(start, src.indexOf(end, start) + end.length); }; const make = new Function( `${lift('const OFFER_RETRY_STATUSES', ';\n')} ${lift('const OFFER_RETRY_DELAYS_MS', ';\n')} ${lift('const OFFER_RETRY_AFTER_MAX_MS', ';\n')} ${lift('async function postOffer(', '\n}\n')} return postOffer;`, ); const postOffer = make(); let closed = false; if (closeAt !== null) setTimeout(() => { closed = true; }, closeAt); const retries = []; let outcome = null; postOffer(call, 'https://hub.example/v1/nodes/n/webrtc/offer', {}, { isClosed: () => closed, onRetry: (status, delay) => retries.push({ status, delay }), }).then(() => { outcome = { result: 'answered', at: now }; }) .catch((e) => { outcome = { result: 'failed', at: now, status: e.status ?? null }; }); await run(runUntil); console.log(JSON.stringify({ ...(outcome || { result: 'pending' }), posts, retries }));