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 }));
|