/**
 * runPool —— 连续调度的 worker pool(替代「每 N 个一批 await Promise.all」的栅栏写法)。
 *
 * 为什么不用批次栅栏:每批都要等本批**最慢**那个,快的 worker 白等。
 * 2026-07-26 在测试服实测 recompute-persona(12 万患者样本):
 *   单患者 p50 139ms / p90 230ms / p99 386ms / max 75.7s,均值 165ms
 *   concurrency=8 理论上限 8÷0.165 = 2,909 人/分,实际只有 ~1,600 → **效率 55%**
 * 缺口正好是 E[8 次抽样最大值](≈p90)与均值之比。极端例子:那个 75.7 秒的患者
 * 把同批 7 个 worker 一起冻了 75 秒。
 *
 * 本实现:N 个 worker 各自从共享游标取下一个,谁空谁取 —— 慢任务只拖住自己那条线。
 *
 * 语义与 `for (i+=N) await Promise.all(slice)` 保持一致的地方:
 *   · 全部处理完才 resolve;
 *   · 并发上限严格 ≤ concurrency;
 *   · **不做错误吞咽** —— fn 抛出会让整个 runPool reject(调用方自己 try/catch 每项,
 *     跟原来 `.map(runOne)` 里 runOne 自带 try/catch 的用法一致)。
 */
export async function runPool<T>(
  items: readonly T[],
  concurrency: number,
  fn: (item: T, index: number) => Promise<void>,
): Promise<void> {
  const n = Math.max(1, Math.min(Math.floor(concurrency) || 1, items.length));
  if (items.length === 0) return;
  let cursor = 0;
  await Promise.all(
    Array.from({ length: n }, async () => {
      for (;;) {
        const idx = cursor++;
        if (idx >= items.length) return;
        await fn(items[idx]!, idx);
      }
    }),
  );
}
