/** * Like Promise.all but returns [result, error] tuples (never throws). */ export async function allSettledTuples(promises) { const results = await Promise.allSettled(promises); return results.map(r => r.status === 'fulfilled' ? [r.value, null] : [null, r.reason] ); } /** * Retry an async function up to `times` times with exponential back-off. */ export async function retry(fn, { times = 3, baseDelay = 200 } = {}) { let lastErr; for (let i = 0; i < times; i++) { try { return await fn(); } catch (e) { lastErr = e; if (i < times - 1) await sleep(baseDelay * 2 ** i); } } throw lastErr; } /** * Run `fn` on items in chunks of `concurrency`. */ export async function pMap(items, fn, { concurrency = 5 } = {}) { const results = []; for (let i = 0; i < items.length; i += concurrency) { const chunk = items.slice(i, i + concurrency); results.push(...await Promise.all(chunk.map(fn))); } return results; } const sleep = (ms) => new Promise(r => setTimeout(r, ms));