interface BatchContext { completedBatches: number; remainingBatches: number; } type BatchConverterSync = (batch: I[], context: BatchContext) => O[]; type BatchHandlerSync = (batch: I[], context: BatchContext) => void; type BatchConverterAsync = (batch: I[], context: BatchContext) => Promise; type BatchHandlerAsync = (batch: I[], context: BatchContext) => Promise; type BatchConverter = BatchConverterSync | BatchConverterAsync; type BatchHandler = BatchHandlerSync | BatchHandlerAsync; type FixedBatcher = { batchSize: number } | { batchCount: number, mode?: Mode }; interface ExecutorResult { updated: A; makeNextBatch: boolean; } interface PredicateBatcher { executor: (element: I, accumulator: A) => ExecutorResult; initial: A; persistAccumulator?: boolean; } interface PredicateBatcherAsyncInterface { executor: (element: I, accumulator: A) => Promise>; initial: A; persistAccumulator?: boolean; } type PredicateBatcherAsync = PredicateBatcher | PredicateBatcherAsyncInterface; type Batcher = FixedBatcher | PredicateBatcher; type BatcherAsync = Batcher | PredicateBatcherAsync; enum TimeUnit { Milliseconds, Seconds, Minutes } interface Interval { magnitude: number; unit: TimeUnit; } enum Mode { Balanced, Even } const convert = (interval: Interval) => { const { magnitude, unit } = interval; switch (unit) { default: case TimeUnit.Milliseconds: return magnitude; case TimeUnit.Seconds: return magnitude * 1000; case TimeUnit.Minutes: return magnitude * 1000 * 60; } }; interface Array { fixedBatch(batcher: FixedBatcher): T[][]; predicateBatch(batcher: PredicateBatcher): T[][]; predicateBatchAsync(batcher: PredicateBatcherAsync): Promise; batch(batcher: Batcher): T[][]; batchAsync(batcher: BatcherAsync): Promise; batchedForEach(batcher: Batcher, handler: BatchHandlerSync): void; batchedMap(batcher: Batcher, handler: BatchConverterSync): O[]; batchedForEachAsync(batcher: Batcher, handler: BatchHandler): Promise; batchedMapAsync(batcher: Batcher, handler: BatchConverter): Promise; batchedForEachInterval(batcher: Batcher, handler: BatchHandler, interval: Interval): Promise; batchedMapInterval(batcher: Batcher, handler: BatchConverter, interval: Interval): Promise; lastElement(): T; } module.exports.AssignArrayExtensions = function () { Array.prototype.fixedBatch = module.exports.fixedBatch; Array.prototype.predicateBatch = module.exports.predicateBatch; Array.prototype.predicateBatchAsync = module.exports.predicateBatchAsync; Array.prototype.batch = module.exports.batch; Array.prototype.batchAsync = module.exports.batchAsync; Array.prototype.batchedForEach = module.exports.batchedForEach; Array.prototype.batchedMap = module.exports.batchedMap; Array.prototype.batchedForEachAsync = module.exports.batchedForEachAsync; Array.prototype.batchedMapAsync = module.exports.batchedMapAsync; Array.prototype.batchedForEachInterval = module.exports.batchedForEachInterval; Array.prototype.batchedMapInterval = module.exports.batchedMapInterval; Array.prototype.lastElement = module.exports.lastElement; }; module.exports.fixedBatch = function (batcher: FixedBatcher): T[][] { const batches: T[][] = []; const length = this.length; let i = 0; if ("batchSize" in batcher) { const { batchSize } = batcher; while (i < this.length) { const cap = Math.min(i + batchSize, length); batches.push(this.slice(i, i = cap)); } } else if ("batchCount" in batcher) { let { batchCount, mode } = batcher; const resolved = mode || Mode.Balanced; if (batchCount < 1) { throw new Error("Batch count must be a positive integer!"); } if (batchCount === 1) { return [this]; } if (batchCount >= this.length) { return this.map((element: T) => [element]); } let length = this.length; let size: number; if (length % batchCount === 0) { size = Math.floor(length / batchCount); while (i < length) { batches.push(this.slice(i, i += size)); } } else if (resolved === Mode.Balanced) { while (i < length) { size = Math.ceil((length - i) / batchCount--); batches.push(this.slice(i, i += size)); } } else { batchCount--; size = Math.floor(length / batchCount); if (length % size === 0) { size--; } while (i < size * batchCount) { batches.push(this.slice(i, i += size)); } batches.push(this.slice(size * batchCount)); } } return batches; }; module.exports.predicateBatch = function (batcher: PredicateBatcher): T[][] { const batches: T[][] = []; let batch: T[] = []; const { executor, initial, persistAccumulator } = batcher; let accumulator = initial; for (let element of this) { const { updated, makeNextBatch } = executor(element, accumulator); accumulator = updated; if (!makeNextBatch) { batch.push(element); } else { batches.push(batch); batch = [element]; if (!persistAccumulator) { accumulator = initial; } } } batches.push(batch); return batches; }; module.exports.predicateBatchAsync = async function (batcher: PredicateBatcherAsync): Promise { const batches: T[][] = []; let batch: T[] = []; const { executor, initial, persistAccumulator } = batcher; let accumulator: A = initial; for (let element of this) { const { updated, makeNextBatch } = await executor(element, accumulator); accumulator = updated; if (!makeNextBatch) { batch.push(element); } else { batches.push(batch); batch = [element]; if (!persistAccumulator) { accumulator = initial; } } } batches.push(batch); return batches; }; module.exports.batch = function (batcher: Batcher): T[][] { if ("executor" in batcher) { return this.predicateBatch(batcher); } else { return this.fixedBatch(batcher); } }; module.exports.batchAsync = async function (batcher: BatcherAsync): Promise { if ("executor" in batcher) { return this.predicateBatchAsync(batcher); } else { return this.fixedBatch(batcher); } }; module.exports.batchedForEach = function (batcher: Batcher, handler: BatchHandlerSync): void { if (this.length) { let completed = 0; const batches = this.batch(batcher); const quota = batches.length; for (let batch of batches) { const context: BatchContext = { completedBatches: completed, remainingBatches: quota - completed, }; handler(batch, context); completed++; } } }; module.exports.batchedMap = function (batcher: Batcher, handler: BatchConverterSync): O[] { if (!this.length) { return []; } let collector: O[] = []; let completed = 0; const batches = this.batch(batcher); const quota = batches.length; for (let batch of batches) { const context: BatchContext = { completedBatches: completed, remainingBatches: quota - completed, }; collector.push(...handler(batch, context)); completed++; } return collector; }; module.exports.batchedForEachAsync = async function (batcher: BatcherAsync, handler: BatchHandler): Promise { if (this.length) { let completed = 0; const batches = await this.batchAsync(batcher); const quota = batches.length; for (let batch of batches) { const context: BatchContext = { completedBatches: completed, remainingBatches: quota - completed, }; await handler(batch, context); completed++; } } }; module.exports.batchedMapAsync = async function (batcher: BatcherAsync, handler: BatchConverter): Promise { if (!this.length) { return []; } let collector: O[] = []; let completed = 0; const batches = await this.batchAsync(batcher); const quota = batches.length; for (let batch of batches) { const context: BatchContext = { completedBatches: completed, remainingBatches: quota - completed, }; collector.push(...(await handler(batch, context))); completed++; } return collector; }; module.exports.batchedForEachInterval = async function (batcher: BatcherAsync, handler: BatchHandler, interval: Interval): Promise { if (!this.length) { return; } const batches = await this.batchAsync(batcher); const quota = batches.length; return new Promise(async resolve => { const iterator = batches[Symbol.iterator](); let completed = 0; while (true) { const next = iterator.next(); await new Promise(resolve => { setTimeout(async () => { const batch = next.value; const context: BatchContext = { completedBatches: completed, remainingBatches: quota - completed, }; await handler(batch, context); resolve(); }, convert(interval)); }); if (++completed === quota) { break; } } resolve(); }); }; module.exports.batchedMapInterval = async function (batcher: BatcherAsync, handler: BatchConverter, interval: Interval): Promise { if (!this.length) { return []; } let collector: O[] = []; const batches = await this.batchAsync(batcher); const quota = batches.length; return new Promise(async resolve => { const iterator = batches[Symbol.iterator](); let completed = 0; while (true) { const next = iterator.next(); await new Promise(resolve => { setTimeout(async () => { const batch = next.value; const context: BatchContext = { completedBatches: completed, remainingBatches: quota - completed, }; collector.push(...(await handler(batch, context))); resolve(); }, convert(interval)); }); if (++completed === quota) { resolve(collector); break; } } }); }; module.exports.lastElement = function () { if (!this.length) { return undefined; } const last: T = this[this.length - 1]; return last; };