diff options
Diffstat (limited to 'packages/meshbay-hub/tests/harness/offer_retry_harness.mjs')
| -rw-r--r-- | packages/meshbay-hub/tests/harness/offer_retry_harness.mjs | 84 |
1 files changed, 84 insertions, 0 deletions
diff --git a/packages/meshbay-hub/tests/harness/offer_retry_harness.mjs b/packages/meshbay-hub/tests/harness/offer_retry_harness.mjs new file mode 100644 index 0000000..3a4b568 --- /dev/null +++ b/packages/meshbay-hub/tests/harness/offer_retry_harness.mjs @@ -0,0 +1,84 @@ +// 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 <path to transport.js> <json config> +import { readFileSync } from 'fs'; + +const src = readFileSync(process.argv[2], 'utf8'); +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 })); |