diff --git a/package.json b/package.json index 3c4a7af..e89f7da 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "0xalice-tgram-bot", - "version": "0.2.17", + "version": "0.2.18", "description": "Batched Telegram notification bot for 0xAlice", "type": "module", "main": "./dist/cjs/index.js", diff --git a/src/__tests__/batcher.test.ts b/src/__tests__/batcher.test.ts index 22bcdfd..11ada81 100644 --- a/src/__tests__/batcher.test.ts +++ b/src/__tests__/batcher.test.ts @@ -7,6 +7,7 @@ describe('MessageBatcher', () => { let batcher: MessageBatcher; beforeEach(() => { + jest.useFakeTimers(); processedMessages = []; mockProcessor = { name: 'mock', @@ -22,6 +23,7 @@ describe('MessageBatcher', () => { } jest.clearAllMocks(); jest.clearAllTimers(); + jest.useRealTimers(); }); it('should process messages with concurrent processors', async () => { @@ -170,9 +172,9 @@ describe('MessageBatcher', () => { batcher.info('message 1'); batcher.info('message 2'); - // Wait for processing - await new Promise(process.nextTick); - await batcher.flush(); + // Run timers and wait for processing + jest.advanceTimersByTime(1000); + await Promise.resolve(); expect(processBatchSpy).toHaveBeenCalledWith( expect.arrayContaining([ @@ -184,7 +186,7 @@ describe('MessageBatcher', () => { }), ]) ); - }, 10000); + }); it('should handle sync processor errors', async () => { const errorProcessor = { @@ -264,7 +266,7 @@ describe('MessageBatcher', () => { const slowProcessor = { name: 'processor1', processBatch: jest.fn().mockImplementation(async () => { - await new Promise(resolve => setTimeout(resolve, 50)); + await Promise.resolve(); }), }; const fastProcessor = { diff --git a/src/batcher.ts b/src/batcher.ts index f577706..67f2f60 100644 --- a/src/batcher.ts +++ b/src/batcher.ts @@ -5,12 +5,8 @@ import { type MessageProcessor, type MessageBatcher, } from './types'; - // Export for testing let globalBatcher: MessageBatcher | null = null; -const resetGlobalBatcher = () => { - globalBatcher = null; -}; export function createMessageBatcher( processors: MessageProcessor[], @@ -30,6 +26,9 @@ export function createMessageBatcher( const maxWaitMs = config.maxWaitMs ?? 60_000; // 1 minute function startProcessing(): void { + if (processInterval) { + clearInterval(processInterval); + } processInterval = setInterval(async () => { for (const chatId of queues.keys()) { await processBatch(chatId); @@ -47,8 +46,10 @@ export function createMessageBatcher( } function removeAllExtraProcessors(): void { + for (const processor of extraProcessors) { + removeExtraProcessor(processor); + } extraProcessors = []; - // processorNames.clear(); } function removeExtraProcessor(processor: MessageProcessor): void { @@ -161,10 +162,8 @@ export function createMessageBatcher( queues.set(chatId, []); const allProcessors = [...processors, ...extraProcessors]; - await concurrentExhaust( - allProcessors, - concurrentProcessors, - (processor) => exhaustProcessor(processor, batch) + await concurrentExhaust(allProcessors, concurrentProcessors, (processor) => + exhaustProcessor(processor, batch) ); } @@ -218,6 +217,7 @@ export function createMessageBatcher( globalBatcher = null; } + // Initialize processing startProcessing(); globalBatcher = { @@ -233,6 +233,8 @@ export function createMessageBatcher( timers, addExtraProcessor, removeExtraProcessor, + removeAllExtraProcessors, }; + return globalBatcher; } diff --git a/src/types.ts b/src/types.ts index 6747ef2..114e959 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,5 +1,12 @@ export type NotificationLevel = 'info' | 'warning' | 'error'; +// Predefined severity levels with option for custom strings +export type SeverityLevel = + | 'low' + | 'medium' + | 'high' + | (string & NonNullable); + export type Message = { chatId: string; text: string; @@ -39,6 +46,7 @@ export interface MessageBatcher { timers: Map; addExtraProcessor(processor: MessageProcessor): void; removeExtraProcessor(processor: MessageProcessor): void; + removeAllExtraProcessors(): void; } export type ProcessorOptions = { diff --git a/src/utils/errorClassifier.ts b/src/utils/errorClassifier.ts index 46dfe3a..3a395d5 100644 --- a/src/utils/errorClassifier.ts +++ b/src/utils/errorClassifier.ts @@ -1,3 +1,5 @@ +import { SeverityLevel } from "../types"; + // Define the named object interface export type ErrorPatternConfig = { name: string; @@ -7,7 +9,7 @@ export type ErrorPatternConfig = { | Promise | ((message: string) => Promise); category: string; - severity: 'low' | 'medium' | 'high'; + severity: SeverityLevel; aggregation?: { windowMs: number; countThreshold: number; @@ -23,7 +25,7 @@ type ErrorPattern = readonly [ | ((message: string) => Promise) ), string, // category - 'low' | 'medium' | 'high', // severity + SeverityLevel, // severity [number, number]? // [windowMs, countThreshold] for aggregation ]; @@ -68,7 +70,7 @@ const DEFAULT_ERROR_PATTERNS: ErrorPatternConfig[] = [ // Store custom patterns let customPatterns: ErrorPattern[] = []; -export function addErrorPatterns(patterns: ErrorPatternConfig[]): void { +export function addErrorPatterns(patterns: readonly ErrorPatternConfig[]): void { customPatterns = customPatterns.concat(patterns.map(configToPattern)); } @@ -95,7 +97,7 @@ const errorTracker = new Map< type ClassifiedError = readonly [ string, // originalMessage string, // category - 'low' | 'medium' | 'high', // severity + SeverityLevel, // severity string[], // details (key-value pairs flattened) boolean, // isAggregated number?, // occurrences