aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests/harness/offer_retry_harness.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/tests/harness/offer_retry_harness.mjs')
-rw-r--r--packages/meshbay-hub/tests/harness/offer_retry_harness.mjs84
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 }));