вроде все работает
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
import { mapWithLimit } from './parallel';
|
||||
|
||||
describe('mapWithLimit', () => {
|
||||
it('возвращает результаты в порядке входного массива, а не в порядке завершения', async () => {
|
||||
// Первый элемент заведомо медленнее второго — если бы порядок задавало
|
||||
// завершение, они бы поменялись местами.
|
||||
const settled = await mapWithLimit(
|
||||
[30, 0, 10],
|
||||
delay => new Promise<number>(resolve => setTimeout(() => resolve(delay), delay)),
|
||||
{ limit: 3 }
|
||||
);
|
||||
|
||||
expect(settled.map(s => (s.ok ? s.value : 'err'))).toEqual([30, 0, 10]);
|
||||
});
|
||||
|
||||
it('отказ одного элемента не прерывает остальные', async () => {
|
||||
const settled = await mapWithLimit([1, 2, 3], n =>
|
||||
n === 2 ? Promise.reject(new Error('bang')) : Promise.resolve(n)
|
||||
);
|
||||
|
||||
expect(settled[0]).toEqual({ ok: true, value: 1 });
|
||||
expect(settled[1].ok).toBe(false);
|
||||
expect(settled[2]).toEqual({ ok: true, value: 3 });
|
||||
});
|
||||
|
||||
it('не превышает предел одновременных запросов', async () => {
|
||||
let running = 0;
|
||||
let peak = 0;
|
||||
|
||||
await mapWithLimit(
|
||||
[1, 2, 3, 4, 5, 6, 7],
|
||||
() => {
|
||||
running += 1;
|
||||
peak = Math.max(peak, running);
|
||||
return new Promise<void>(resolve =>
|
||||
setTimeout(() => {
|
||||
running -= 1;
|
||||
resolve();
|
||||
}, 5)
|
||||
);
|
||||
},
|
||||
{ limit: 2 }
|
||||
);
|
||||
|
||||
expect(peak).toBe(2);
|
||||
});
|
||||
|
||||
it('onSettled вызывается по разу на каждый элемент, включая упавшие', async () => {
|
||||
let done = 0;
|
||||
|
||||
await mapWithLimit([1, 2, 3], n => (n === 2 ? Promise.reject(new Error('bang')) : Promise.resolve(n)), {
|
||||
onSettled: () => {
|
||||
done += 1;
|
||||
}
|
||||
});
|
||||
|
||||
expect(done).toBe(3);
|
||||
});
|
||||
|
||||
it('пустой список — пустой результат, fn не вызывается', async () => {
|
||||
const fn = jest.fn();
|
||||
const settled = await mapWithLimit([], fn);
|
||||
|
||||
expect(settled).toEqual([]);
|
||||
expect(fn).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Параллельный запуск с ограничением одновременных запросов и результатом
|
||||
* «по каждому элементу отдельно».
|
||||
*
|
||||
* Зачем свой велосипед вместо Promise.allSettled: его нет в целевом lib этого
|
||||
* проекта (tsconfig.json — es5/es2015.*, без es2020.promise). Плюс здесь нужен
|
||||
* не только «не падать целиком», но и ПОТОЛОК одновременных запросов: без него
|
||||
* пакетная замена на сотню строк уходит в SharePoint сотней параллельных
|
||||
* запросов и упирается в его троттлинг (429), после чего часть строк не
|
||||
* записывается — то есть «быстрее» превращается в «половина не сохранилась».
|
||||
*/
|
||||
|
||||
export type SettledResult<R> = { ok: true; value: R } | { ok: false; error: unknown };
|
||||
|
||||
/** Сколько запросов держим в воздухе по умолчанию — компромисс между скоростью и троттлингом SharePoint. */
|
||||
export const DEFAULT_CONCURRENCY = 5;
|
||||
|
||||
export interface IMapWithLimitOptions {
|
||||
limit?: number;
|
||||
/** Вызывается после завершения КАЖДОГО элемента — для полосы прогресса. */
|
||||
onSettled?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Пропускает items через fn, держа не больше limit запросов одновременно.
|
||||
* Результат — в том же порядке, что items; отказ одного элемента не прерывает
|
||||
* остальные (см. SettledResult).
|
||||
*/
|
||||
export async function mapWithLimit<T, R>(
|
||||
items: T[],
|
||||
fn: (item: T, index: number) => Promise<R>,
|
||||
options: IMapWithLimitOptions = {}
|
||||
): Promise<Array<SettledResult<R>>> {
|
||||
const limit = Math.max(1, options.limit ?? DEFAULT_CONCURRENCY);
|
||||
const results: Array<SettledResult<R>> = new Array(items.length);
|
||||
let next = 0;
|
||||
|
||||
// Ровно limit «рабочих», каждый в цикле забирает следующий свободный индекс.
|
||||
// Так предел соблюдается точно, без пауз между пачками (в отличие от разбиения
|
||||
// на чанки, где приходится ждать самый медленный элемент каждой пачки).
|
||||
const worker = async (): Promise<void> => {
|
||||
for (;;) {
|
||||
const index = next;
|
||||
next += 1;
|
||||
if (index >= items.length) return;
|
||||
|
||||
try {
|
||||
results[index] = { ok: true, value: await fn(items[index], index) };
|
||||
} catch (error) {
|
||||
results[index] = { ok: false, error };
|
||||
}
|
||||
if (options.onSettled) options.onSettled();
|
||||
}
|
||||
};
|
||||
|
||||
await Promise.all(new Array(Math.min(limit, items.length)).fill(0).map(worker));
|
||||
|
||||
return results;
|
||||
}
|
||||
Reference in New Issue
Block a user