Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 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 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 | 7x 1x 1x 3x 4x 4x 4x 3x 3x 1x 1x 3x 3x 21x 21x 14x 23x 23x 21x 21x 6x 6x 6x 3x 3x 3x 9x 11x 11x 6x 6x 6x 2x 6x | /**
* Shared HTTP helpers for the Zeroheight clients: a sliding-window rate
* limiter, a small concurrency pool, and retry with backoff.
*/
/** @param {number} ms */
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
/**
* Allow at most `limit` calls per `windowMs`. `acquire()` resolves once a
* slot is free.
* @param {{ limit: number, windowMs: number, now?: () => number, wait?: (ms: number) => Promise<void> }} opts
*/
export function createRateLimiter({ limit, windowMs, now = Date.now, wait = sleep }) {
/** @type {number[]} */
const stamps = [];
let queue = Promise.resolve();
async function take() {
for (;;) {
const t = now();
while (stamps.length && t - stamps[0] >= windowMs) stamps.shift();
if (stamps.length < limit) {
stamps.push(t);
return;
}
await wait(windowMs - (t - stamps[0]) + 1);
}
}
return {
/** @returns {Promise<void>} */
acquire() {
queue = queue.then(take);
return queue;
},
};
}
/**
* Map over `items` with at most `concurrency` calls in flight. Results keep
* input order.
* @template T, R
* @param {T[]} items
* @param {number} concurrency
* @param {(item: T, index: number) => Promise<R>} fn
* @returns {Promise<R[]>}
*/
export async function mapPool(items, concurrency, fn) {
/** @type {R[]} */
const results = new Array(items.length);
let next = 0;
async function worker() {
while (next < items.length) {
const i = next++;
results[i] = await fn(items[i], i);
}
}
await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, worker));
return results;
}
export class HttpError extends Error {
/**
* @param {string} message
* @param {number} status
* @param {number | null} retryAfterMs
*/
constructor(message, status, retryAfterMs = null) {
super(message);
this.status = status;
this.retryAfterMs = retryAfterMs;
}
}
/**
* Read `retry-after` or `ratelimit-reset` (both in seconds) from a response.
* @param {Response} res
* @returns {number | null}
*/
export function retryAfterMs(res) {
const raw = res.headers.get('retry-after') ?? res.headers.get('ratelimit-reset');
const secs = raw == null ? NaN : Number(raw);
return Number.isFinite(secs) ? secs * 1000 : null;
}
/**
* Retry `fn` on 429, 5xx, and network errors with exponential backoff.
* Other errors (4xx, tool errors) are thrown immediately.
* @template T
* @param {() => Promise<T>} fn
* @param {{ retries?: number, baseMs?: number, wait?: (ms: number) => Promise<void> }} [opts]
* @returns {Promise<T>}
*/
export async function withRetry(fn, { retries = 3, baseMs = 1000, wait = sleep } = {}) {
for (let attempt = 0; ; attempt++) {
try {
return await fn();
} catch (err) {
const status = err instanceof HttpError ? err.status : null;
const retryable = status == null ? err instanceof TypeError : status === 429 || status >= 500;
if (!retryable || attempt >= retries) throw err;
const hinted = err instanceof HttpError ? err.retryAfterMs : null;
await wait(hinted ?? baseMs * 2 ** attempt);
}
}
}
|