UNPKG

keyed-batched-items-accumulator

Version:

A lightweight utility for Node.js projects that accumulates items into fixed-size batches per key, preserving insertion order within each key. Streams items directly into their respective batches at runtime, eliminating the overhead of post-processing 1D

162 lines (141 loc) 5.75 kB
/** * Copyright 2025 Ori Cohen https://github.com/ori88c * https://github.com/ori88c/keyed-batched-items-accumulator * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { KeyedBatchedAccumulator } from './keyed-batched-items-accumulator'; interface IMockEvent { id: number; } function validateBatches( extractedBatches: IMockEvent[][], expectedOrderedEvents: IMockEvent[], ): void { let expectedEventIndex = 0; for (const batch of extractedBatches) { for (const event of batch) { expect(event).toBe(expectedOrderedEvents[expectedEventIndex]); // Ensures reference equality. ++expectedEventIndex; } } expect(expectedEventIndex).toBe(expectedOrderedEvents.length); } describe('BatchedAccumulator tests', () => { describe('Happy path tests', () => { /** * Verifies that the accumulator maintains correct internal state when events are * randomly distributed across multiple keys, simulating realistic usage patterns. */ test('should maintain correct state and key tracking under randomized key-to-item associations', () => { // Arrange. const batchSize = 96; const keyedAccumulator = new KeyedBatchedAccumulator<IMockEvent>(batchSize); const totalNumberOfItems = batchSize * 17 + 49; const events: readonly IMockEvent[] = new Array<number>(totalNumberOfItems) .fill(0) .map((_, index): IMockEvent => ({ id: index })); const keys: readonly string[] = [ 'threat-detections', 'auth-events', 'network-logs', 'dns-queries', ]; const sampleRandomKey = (): string => keys[Math.floor(Math.random() * keys.length)]; const keyToEvents = new Map<string, IMockEvent[]>(); for (const key of keys) { keyToEvents.set(key, []); } // Act & Intermediate Assert: // Distribute events across keys at random and verify state consistency after each insertion. let pushedEventsCounter = 0; for (const event of events) { const chosenKey = sampleRandomKey(); const respectiveEvents = keyToEvents.get(chosenKey); respectiveEvents.push(event); keyedAccumulator.push(event, chosenKey); expect(keyedAccumulator.totalAccumulatedItemsCount).toBe(++pushedEventsCounter); expect(keyedAccumulator.isEmpty).toBe(false); expect(keyedAccumulator.isActiveKey(chosenKey)).toBe(true); expect(keyedAccumulator.getAccumulatedItemsCount(chosenKey)).toBe(respectiveEvents.length); } // Assert. expect(keyedAccumulator.activeKeysCount).toBe(keys.length); const activeKeys = keyedAccumulator.activeKeys; for (const key of keys) { expect(activeKeys.includes(key)).toBe(true); } const extractedBatches = keyedAccumulator.extractAccumulatedBatches(); expect(extractedBatches.size).toBe(keys.length); // After extraction, the accumulator should be reset with no retained items or keys. expect(keyedAccumulator.isEmpty).toBe(true); expect(keyedAccumulator.totalAccumulatedItemsCount).toBe(0); expect(keyedAccumulator.activeKeysCount).toBe(0); expect(keyedAccumulator.activeKeys).toEqual([]); for (const key of keys) { expect(keyedAccumulator.isActiveKey(key)).toBe(false); expect(keyedAccumulator.getAccumulatedItemsCount(key)).toBe(0); } for (const [key, batches] of extractedBatches) { validateBatches(batches, keyToEvents.get(key)); } }); test('should return an empty map when no items are accumulated', () => { const batchSize = 5; const accumulator = new KeyedBatchedAccumulator<IMockEvent>(batchSize); const numberOfExtractionAttempts = 15; let previousExtraction: Map<string, IMockEvent[][]>; for (let attempt = 0; attempt < numberOfExtractionAttempts; ++attempt) { const keyToBatches = accumulator.extractAccumulatedBatches(); // Reference inequality is expected, even when both are empty. expect(keyToBatches).not.toBe(previousExtraction); expect(keyToBatches.size).toBe(0); previousExtraction = keyToBatches; } }); }); describe('Negative path tests', () => { test('should throw an error when batch size is a non-natural number', () => { const invalidBatchSizes = [ -4.3, -2, 0, 0.001, 543.9938, 'natural number' as unknown as number, undefined as number, null as number, true as unknown as number, ]; for (const batchSize of invalidBatchSizes) { expect(() => new KeyedBatchedAccumulator<string>(batchSize)).toThrow(); } }); test('should throw an error if key is not a non-empty string', () => { const batchSize = 16; const accumulator = new KeyedBatchedAccumulator<number>(batchSize); const invalidKeys = [ -4.3 as unknown as string, 0 as unknown as string, '', undefined as string, null as string, true as unknown as string, {} as string, ]; for (const key of invalidKeys) { expect(() => accumulator.push(75, key)).toThrow(); } }); }); });