47 lines
1.5 KiB
TypeScript
47 lines
1.5 KiB
TypeScript
|
|
import { computeStaggerMs, humanPause } from "@/lib/threads-browser/human-behavior";
|
||
|
|
|
||
|
|
/** 有限平行執行,批次間加隨機間隔以降低被封風險 */
|
||
|
|
export async function runWithConcurrency<T, R>(
|
||
|
|
items: T[],
|
||
|
|
worker: (item: T, index: number) => Promise<R>,
|
||
|
|
options?: {
|
||
|
|
concurrency?: number;
|
||
|
|
staggerMs?: [number, number];
|
||
|
|
onProgress?: (done: number, total: number) => void;
|
||
|
|
shouldAbort?: () => boolean | Promise<boolean>;
|
||
|
|
}
|
||
|
|
): Promise<R[]> {
|
||
|
|
const concurrency = Math.max(1, options?.concurrency ?? 1);
|
||
|
|
const stagger = options?.staggerMs ?? [3500, 9000];
|
||
|
|
const results: R[] = new Array(items.length);
|
||
|
|
let nextIndex = 0;
|
||
|
|
let done = 0;
|
||
|
|
|
||
|
|
async function runWorker() {
|
||
|
|
while (true) {
|
||
|
|
if (await options?.shouldAbort?.()) {
|
||
|
|
throw new Error("__JOB_ABORT__");
|
||
|
|
}
|
||
|
|
const index = nextIndex++;
|
||
|
|
if (index >= items.length) return;
|
||
|
|
if (index > 0) {
|
||
|
|
await new Promise((resolve) => setTimeout(resolve, computeStaggerMs(stagger)));
|
||
|
|
if (Math.random() < 0.15 + Math.random() * 0.18) {
|
||
|
|
await humanPause(Math.random() < 0.55 ? "short" : "medium");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (await options?.shouldAbort?.()) {
|
||
|
|
throw new Error("__JOB_ABORT__");
|
||
|
|
}
|
||
|
|
results[index] = await worker(items[index], index);
|
||
|
|
done++;
|
||
|
|
options?.onProgress?.(done, items.length);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const workers = Array.from({ length: Math.min(concurrency, items.length) }, () =>
|
||
|
|
runWorker()
|
||
|
|
);
|
||
|
|
await Promise.all(workers);
|
||
|
|
return results;
|
||
|
|
}
|