summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests/harness/offer_retry_harness.mjs
blob: 3a4b568796bc621abb1fd1927162d4e860851c91 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
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 }));