2019-09-26 19:56:31 +00:00
|
|
|
import PQueue from 'p-queue';
|
|
|
|
|
2020-09-14 21:56:35 +00:00
|
|
|
declare global {
|
|
|
|
interface Window {
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
batchers: Array<BatcherType<any>>;
|
|
|
|
waitForAllBatchers: () => Promise<unknown>;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-09-26 19:56:31 +00:00
|
|
|
window.batchers = [];
|
|
|
|
|
|
|
|
window.waitForAllBatchers = async () => {
|
2019-10-25 18:48:28 +00:00
|
|
|
await Promise.all(window.batchers.map(item => item.flushAndWait()));
|
2019-09-26 19:56:31 +00:00
|
|
|
};
|
|
|
|
|
2020-04-13 17:37:29 +00:00
|
|
|
export type BatcherOptionsType<ItemType> = {
|
2019-09-26 19:56:31 +00:00
|
|
|
wait: number;
|
|
|
|
maxSize: number;
|
|
|
|
processBatch: (items: Array<ItemType>) => Promise<void>;
|
|
|
|
};
|
|
|
|
|
2020-04-13 17:37:29 +00:00
|
|
|
export type BatcherType<ItemType> = {
|
2019-09-26 19:56:31 +00:00
|
|
|
add: (item: ItemType) => void;
|
|
|
|
anyPending: () => boolean;
|
|
|
|
onIdle: () => Promise<void>;
|
2019-10-25 18:48:28 +00:00
|
|
|
flushAndWait: () => Promise<void>;
|
2019-09-26 19:56:31 +00:00
|
|
|
unregister: () => void;
|
|
|
|
};
|
|
|
|
|
|
|
|
async function sleep(ms: number): Promise<void> {
|
|
|
|
// tslint:disable-next-line:no-string-based-set-timeout
|
|
|
|
await new Promise(resolve => setTimeout(resolve, ms));
|
|
|
|
}
|
|
|
|
|
|
|
|
export function createBatcher<ItemType>(
|
|
|
|
options: BatcherOptionsType<ItemType>
|
|
|
|
): BatcherType<ItemType> {
|
|
|
|
let batcher: BatcherType<ItemType>;
|
2020-09-14 21:56:35 +00:00
|
|
|
let timeout: NodeJS.Timeout | null;
|
2019-09-26 19:56:31 +00:00
|
|
|
let items: Array<ItemType> = [];
|
|
|
|
const queue = new PQueue({ concurrency: 1 });
|
|
|
|
|
|
|
|
function _kickBatchOff() {
|
|
|
|
const itemsRef = items;
|
|
|
|
items = [];
|
|
|
|
// tslint:disable-next-line:no-floating-promises
|
|
|
|
queue.add(async () => {
|
|
|
|
await options.processBatch(itemsRef);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function add(item: ItemType) {
|
|
|
|
items.push(item);
|
|
|
|
|
|
|
|
if (timeout) {
|
|
|
|
clearTimeout(timeout);
|
|
|
|
timeout = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (items.length >= options.maxSize) {
|
|
|
|
_kickBatchOff();
|
|
|
|
} else {
|
|
|
|
timeout = setTimeout(() => {
|
|
|
|
timeout = null;
|
|
|
|
_kickBatchOff();
|
|
|
|
}, options.wait);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function anyPending(): boolean {
|
|
|
|
return queue.size > 0 || queue.pending > 0 || items.length > 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
async function onIdle() {
|
|
|
|
while (anyPending()) {
|
|
|
|
if (queue.size > 0 || queue.pending > 0) {
|
2020-09-14 21:56:35 +00:00
|
|
|
// eslint-disable-next-line no-await-in-loop
|
2019-09-26 19:56:31 +00:00
|
|
|
await queue.onIdle();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (items.length > 0) {
|
2020-09-14 21:56:35 +00:00
|
|
|
// eslint-disable-next-line no-await-in-loop
|
2019-09-26 19:56:31 +00:00
|
|
|
await sleep(options.wait * 2);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function unregister() {
|
2020-09-14 21:56:35 +00:00
|
|
|
window.batchers = window.batchers.filter(item => item !== batcher);
|
2019-09-26 19:56:31 +00:00
|
|
|
}
|
|
|
|
|
2019-10-25 18:48:28 +00:00
|
|
|
async function flushAndWait() {
|
|
|
|
if (timeout) {
|
|
|
|
clearTimeout(timeout);
|
|
|
|
timeout = null;
|
|
|
|
}
|
|
|
|
if (items.length) {
|
|
|
|
_kickBatchOff();
|
|
|
|
}
|
|
|
|
|
|
|
|
return onIdle();
|
|
|
|
}
|
|
|
|
|
2019-09-26 19:56:31 +00:00
|
|
|
batcher = {
|
|
|
|
add,
|
|
|
|
anyPending,
|
|
|
|
onIdle,
|
2019-10-25 18:48:28 +00:00
|
|
|
flushAndWait,
|
2019-09-26 19:56:31 +00:00
|
|
|
unregister,
|
|
|
|
};
|
|
|
|
|
|
|
|
window.batchers.push(batcher);
|
|
|
|
|
|
|
|
return batcher;
|
|
|
|
}
|