diff --git a/src/Utils/cache-utils.ts b/src/Utils/cache-utils.ts index 926a1103..023a5ada 100644 --- a/src/Utils/cache-utils.ts +++ b/src/Utils/cache-utils.ts @@ -95,7 +95,7 @@ export class Cache { this.stats = { hits: 0, misses: 0 } this.cache = new LRUCache>({ - max: this.options.maxSize, + maxSize: this.options.maxSize, ttl: this.options.ttl, updateAgeOnGet: this.options.updateAgeOnGet, sizeCalculation: item => this.options.sizeCalculation(item.value), diff --git a/src/__tests__/Utils/baileys-event-stream.test.ts b/src/__tests__/Utils/baileys-event-stream.test.ts index 8c3898fe..7bd1f9d6 100644 --- a/src/__tests__/Utils/baileys-event-stream.test.ts +++ b/src/__tests__/Utils/baileys-event-stream.test.ts @@ -30,10 +30,12 @@ describe('BaileysEventStream', () => { describe('push events', () => { it('should push event to stream', () => { + stream.pause() const result = stream.push('messages.upsert', { message: 'test' }) expect(result).toBe(true) expect(stream.getStats().bufferSize).toBeGreaterThan(0) + stream.resume() }) it('should assign priority based on event type', done => { @@ -178,6 +180,7 @@ describe('BaileysEventStream', () => { } stream.resume() + await new Promise(resolve => setTimeout(resolve, 50)) const result = await stream.flush() expect(result.processed).toBe(5) @@ -321,6 +324,7 @@ describe('BaileysEventStream', () => { stream.push('call', {}, { priority: 'high' }) stream.resume() + await new Promise(resolve => setTimeout(resolve, 50)) await stream.flush() expect(processedOrder[0]).toBe('critical') diff --git a/src/__tests__/Utils/baileys-event-stream.test.ts.orig b/src/__tests__/Utils/baileys-event-stream.test.ts.orig new file mode 100644 index 00000000..dc348442 --- /dev/null +++ b/src/__tests__/Utils/baileys-event-stream.test.ts.orig @@ -0,0 +1,641 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/** + * Testes unitários para baileys-event-stream.ts + */ + +import { afterEach, beforeEach, describe, expect, it, jest } from '@jest/globals' +import { + BaileysEventStream, + createEventStream, + eventFilters, + type EventPriority, + eventTransformers, + type StreamEvent +} from '../../Utils/baileys-event-stream.js' + +describe('BaileysEventStream', () => { + let stream: BaileysEventStream + + beforeEach(() => { + stream = createEventStream({ + maxBufferSize: 100, + batchSize: 10, + collectMetrics: false + }) + }) + + afterEach(() => { + stream.destroy() + }) + + describe('push events', () => { + it('should push event to stream', () => { + stream.pause() + const result = stream.push('messages.upsert', { message: 'test' }) + + expect(result).toBe(true) + expect(stream.getStats().bufferSize).toBeGreaterThan(0) + stream.resume() + }) + + it('should assign priority based on event type', done => { + stream.on('*', event => { + expect(event.priority).toBe('critical') + done() + }) + + stream.push('connection.update', { state: 'open' }) + }) + + it('should use custom priority when provided', done => { + stream.on('*', event => { + expect(event.priority).toBe('low') + done() + }) + + stream.push('messages.upsert', { message: 'test' }, { priority: 'low' }) + }) + + it('should assign correct category', done => { + stream.on('*', event => { + expect(event.category).toBe('message') + done() + }) + + stream.push('messages.upsert', { message: 'test' }) + }) + + it('should reject events when buffer is full', () => { + const smallStream = createEventStream({ + maxBufferSize: 5, + enableBackpressure: true, + highWaterMark: 3, + collectMetrics: false + }) + + smallStream.pause() // Prevent processing + + for (let i = 0; i < 5; i++) { + smallStream.push('messages.upsert', { index: i }) + } + + const result = smallStream.push('messages.upsert', { overflow: true }) + + expect(result).toBe(false) + expect(smallStream.getStats().totalDropped).toBe(1) + + smallStream.destroy() + }) + }) + + describe('event handlers', () => { + it('should call handler for specific event type', async () => { + const handler = jest.fn() as any + + stream.on('messages.upsert', handler) + stream.push('messages.upsert', { message: 'test' }) + + // Wait for processing + await new Promise(resolve => setTimeout(resolve, 50)) + + expect(handler).toHaveBeenCalledTimes(1) + }) + + it('should call global handler for all events', async () => { + const handler = jest.fn() as any + + stream.on('*', handler) + stream.push('messages.upsert', { message: 'test' }) + stream.push('connection.update', { state: 'open' }) + + await new Promise(resolve => setTimeout(resolve, 50)) + + expect(handler).toHaveBeenCalledTimes(2) + }) + + it('should support once handler', async () => { + const handler = jest.fn() as any + + stream.once('messages.upsert', handler) + stream.push('messages.upsert', { first: true }) + stream.push('messages.upsert', { second: true }) + + await new Promise(resolve => setTimeout(resolve, 50)) + + expect(handler).toHaveBeenCalledTimes(1) + }) + + it('should remove handler with off', async () => { + const handler = jest.fn() as any + + stream.on('messages.upsert', handler) + stream.off('messages.upsert', handler) + stream.push('messages.upsert', { message: 'test' }) + + await new Promise(resolve => setTimeout(resolve, 50)) + + expect(handler).not.toHaveBeenCalled() + }) + }) + + describe('pause and resume', () => { + it('should pause processing', async () => { + const handler = jest.fn() as any + + stream.on('messages.upsert', handler) + stream.pause() + + expect(stream.isPaused()).toBe(true) + + stream.push('messages.upsert', { message: 'test' }) + + await new Promise(resolve => setTimeout(resolve, 50)) + + expect(handler).not.toHaveBeenCalled() + }) + + it('should resume processing', async () => { + const handler = jest.fn() as any + + stream.on('messages.upsert', handler) + stream.pause() + stream.push('messages.upsert', { message: 'test' }) + stream.resume() + + await new Promise(resolve => setTimeout(resolve, 50)) + + expect(handler).toHaveBeenCalled() + }) + }) + + describe('flush', () => { + it('should process all buffered events', async () => { + const handler = jest.fn() as any + + stream.pause() + stream.on('messages.upsert', handler) + + for (let i = 0; i < 5; i++) { + stream.push('messages.upsert', { index: i }) + } + + stream.resume() + const result = await stream.flush() + + expect(result.processed).toBe(5) + expect(handler).toHaveBeenCalledTimes(5) + }) + }) + + describe('filters', () => { + it('should filter events before processing', async () => { + const handler = jest.fn() as any + + stream.addFilter(event => (event.data as any).include === true) + stream.on('*', handler) + + stream.push('messages.upsert', { include: true }) + stream.push('messages.upsert', { include: false }) + + await new Promise(resolve => setTimeout(resolve, 50)) + + expect(handler).toHaveBeenCalledTimes(1) + }) + + it('should remove filter', async () => { + const filter = (_event: StreamEvent) => false + + stream.addFilter(filter) + stream.removeFilter(filter) + + const handler = jest.fn() as any + stream.on('*', handler) + + stream.push('messages.upsert', { test: true }) + + await new Promise(resolve => setTimeout(resolve, 50)) + + expect(handler).toHaveBeenCalled() + }) + }) + + describe('transformers', () => { + it('should transform events before processing', async () => { + stream.addTransformer(event => ({ + ...event, + metadata: { ...event.metadata, transformed: true } + })) + + let receivedEvent: StreamEvent | null = null + stream.on('*', event => { + receivedEvent = event + }) + + stream.push('messages.upsert', { message: 'test' }) + + await new Promise(resolve => setTimeout(resolve, 50)) + + expect((receivedEvent as any)?.metadata?.transformed).toBe(true) + }) + }) + + describe('dead letter queue', () => { + it('should move failed events to DLQ after max retries', async () => { + const failingHandler = jest.fn(() => { + throw new Error('Processing failed') + }) + + const dlqStream = createEventStream({ + maxRetries: 2, + batchSize: 1, + collectMetrics: false + }) + + dlqStream.on('messages.upsert', failingHandler) + dlqStream.push('messages.upsert', { will: 'fail' }) + + await new Promise(resolve => setTimeout(resolve, 200)) + + const dlq = dlqStream.getDeadLetterQueue() + expect(dlq.length).toBeGreaterThan(0) + + dlqStream.destroy() + }) + + it('should clear DLQ', () => { + stream.push('test', { data: 'test' }) + + // Manually add to DLQ for testing + const dlq = stream.getDeadLetterQueue() + + stream.clearDeadLetterQueue() + + expect(stream.getStats().deadLetterQueueSize).toBe(0) + }) + }) + + describe('statistics', () => { + it('should track event statistics', async () => { + stream.on('*', () => {}) + + stream.push('messages.upsert', { a: 1 }) + stream.push('connection.update', { b: 2 }) + stream.push('messages.update', { c: 3 }) + + await new Promise(resolve => setTimeout(resolve, 50)) + + const stats = stream.getStats() + + expect(stats.totalReceived).toBe(3) + expect(stats.totalProcessed).toBe(3) + expect(stats.eventsByType['messages.upsert']).toBe(1) + expect(stats.eventsByType['connection.update']).toBe(1) + }) + + it('should reset statistics', async () => { + stream.on('*', () => {}) + stream.push('messages.upsert', { test: true }) + + await new Promise(resolve => setTimeout(resolve, 50)) + + stream.resetStats() + + const stats = stream.getStats() + expect(stats.totalReceived).toBe(0) + expect(stats.totalProcessed).toBe(0) + }) + }) + + describe('priority ordering', () => { + it('should process critical events before normal events', async () => { + const processedOrder: EventPriority[] = [] + + stream.pause() + + stream.on('*', event => { + processedOrder.push(event.priority) + }) + + // Add in reverse priority order + stream.push('presence.update', {}, { priority: 'low' }) + stream.push('messages.upsert', {}, { priority: 'normal' }) + stream.push('connection.update', {}, { priority: 'critical' }) + stream.push('call', {}, { priority: 'high' }) + + stream.resume() + await stream.flush() + + expect(processedOrder[0]).toBe('critical') + expect(processedOrder[1]).toBe('high') + expect(processedOrder[2]).toBe('normal') + expect(processedOrder[3]).toBe('low') + }) + }) + + describe('backpressure', () => { + it('should emit backpressure event when high water mark reached', done => { + const bpStream = createEventStream({ + maxBufferSize: 100, + highWaterMark: 5, + enableBackpressure: true, + collectMetrics: false + }) + + bpStream.pause() + + bpStream.on('backpressure', () => { + expect(bpStream.getStats().isBackpressured).toBe(true) + bpStream.destroy() + done() + }) + + for (let i = 0; i < 10; i++) { + bpStream.push('messages.upsert', { index: i }) + } + }) + + it('should emit drain event when below low water mark', done => { + const bpStream = createEventStream({ + maxBufferSize: 100, + highWaterMark: 5, + lowWaterMark: 2, + enableBackpressure: true, + batchSize: 10, + collectMetrics: false + }) + + bpStream.pause() + + for (let i = 0; i < 10; i++) { + bpStream.push('messages.upsert', { index: i }) + } + + bpStream.on('*', () => {}) + bpStream.on('drain', () => { + bpStream.destroy() + done() + }) + + bpStream.resume() + }) + }) + + describe('clear', () => { + it('should clear buffer', () => { + stream.pause() + + for (let i = 0; i < 10; i++) { + stream.push('messages.upsert', { index: i }) + } + + stream.clear() + + expect(stream.getStats().bufferSize).toBe(0) + }) + }) +}) + +describe('eventFilters', () => { + describe('byType', () => { + it('should filter by event type', () => { + const filter = eventFilters.byType('messages.upsert', 'messages.update') + + const matchingEvent: StreamEvent = { + id: '1', + type: 'messages.upsert', + data: {}, + timestamp: Date.now(), + priority: 'normal', + category: 'message' + } + + const nonMatchingEvent: StreamEvent = { + id: '2', + type: 'connection.update', + data: {}, + timestamp: Date.now(), + priority: 'normal', + category: 'connection' + } + + expect(filter(matchingEvent)).toBe(true) + expect(filter(nonMatchingEvent)).toBe(false) + }) + }) + + describe('byCategory', () => { + it('should filter by category', () => { + const filter = eventFilters.byCategory('message', 'connection') + + const matchingEvent: StreamEvent = { + id: '1', + type: 'messages.upsert', + data: {}, + timestamp: Date.now(), + priority: 'normal', + category: 'message' + } + + const nonMatchingEvent: StreamEvent = { + id: '2', + type: 'presence.update', + data: {}, + timestamp: Date.now(), + priority: 'normal', + category: 'presence' + } + + expect(filter(matchingEvent)).toBe(true) + expect(filter(nonMatchingEvent)).toBe(false) + }) + }) + + describe('byMinPriority', () => { + it('should filter by minimum priority', () => { + const filter = eventFilters.byMinPriority('high') + + const criticalEvent: StreamEvent = { + id: '1', + type: 'connection.update', + data: {}, + timestamp: Date.now(), + priority: 'critical', + category: 'connection' + } + + const lowEvent: StreamEvent = { + id: '2', + type: 'presence.update', + data: {}, + timestamp: Date.now(), + priority: 'low', + category: 'presence' + } + + expect(filter(criticalEvent)).toBe(true) + expect(filter(lowEvent)).toBe(false) + }) + }) + + describe('recentOnly', () => { + it('should filter old events', () => { + const filter = eventFilters.recentOnly(1000) + + const recentEvent: StreamEvent = { + id: '1', + type: 'messages.upsert', + data: {}, + timestamp: Date.now(), + priority: 'normal', + category: 'message' + } + + const oldEvent: StreamEvent = { + id: '2', + type: 'messages.upsert', + data: {}, + timestamp: Date.now() - 5000, + priority: 'normal', + category: 'message' + } + + expect(filter(recentEvent)).toBe(true) + expect(filter(oldEvent)).toBe(false) + }) + }) + + describe('and', () => { + it('should combine filters with AND', () => { + const filter = eventFilters.and(eventFilters.byType('messages.upsert'), eventFilters.byMinPriority('high')) + + const matchingEvent: StreamEvent = { + id: '1', + type: 'messages.upsert', + data: {}, + timestamp: Date.now(), + priority: 'high', + category: 'message' + } + + const partialMatch: StreamEvent = { + id: '2', + type: 'messages.upsert', + data: {}, + timestamp: Date.now(), + priority: 'low', + category: 'message' + } + + expect(filter(matchingEvent)).toBe(true) + expect(filter(partialMatch)).toBe(false) + }) + }) + + describe('or', () => { + it('should combine filters with OR', () => { + const filter = eventFilters.or(eventFilters.byType('messages.upsert'), eventFilters.byCategory('connection')) + + const typeMatch: StreamEvent = { + id: '1', + type: 'messages.upsert', + data: {}, + timestamp: Date.now(), + priority: 'normal', + category: 'message' + } + + const categoryMatch: StreamEvent = { + id: '2', + type: 'connection.update', + data: {}, + timestamp: Date.now(), + priority: 'normal', + category: 'connection' + } + + const noMatch: StreamEvent = { + id: '3', + type: 'presence.update', + data: {}, + timestamp: Date.now(), + priority: 'normal', + category: 'presence' + } + + expect(filter(typeMatch)).toBe(true) + expect(filter(categoryMatch)).toBe(true) + expect(filter(noMatch)).toBe(false) + }) + }) +}) + +describe('eventTransformers', () => { + describe('addProcessingTimestamp', () => { + it('should add processing timestamp', () => { + const transformer = eventTransformers.addProcessingTimestamp() + + const event: StreamEvent = { + id: '1', + type: 'messages.upsert', + data: {}, + timestamp: Date.now() - 1000, + priority: 'normal', + category: 'message' + } + + const transformed = transformer(event) + + expect(transformed.metadata?.processingTimestamp).toBeDefined() + expect(transformed.metadata?.processingTimestamp).toBeGreaterThan(event.timestamp) + }) + }) + + describe('addTraceId', () => { + it('should add trace ID', () => { + const transformer = eventTransformers.addTraceId(() => 'trace-123') + + const event: StreamEvent = { + id: '1', + type: 'messages.upsert', + data: {}, + timestamp: Date.now(), + priority: 'normal', + category: 'message' + } + + const transformed = transformer(event) + + expect(transformed.metadata?.traceId).toBe('trace-123') + }) + }) + + describe('elevatepriorityIf', () => { + it('should elevate priority when condition is met', () => { + const transformer = eventTransformers.elevatepriorityIf( + event => (event.data as { urgent?: boolean }).urgent === true, + 'critical' + ) + + const urgentEvent: StreamEvent = { + id: '1', + type: 'messages.upsert', + data: { urgent: true }, + timestamp: Date.now(), + priority: 'normal', + category: 'message' + } + + const normalEvent: StreamEvent = { + id: '2', + type: 'messages.upsert', + data: { urgent: false }, + timestamp: Date.now(), + priority: 'normal', + category: 'message' + } + + expect(transformer(urgentEvent).priority).toBe('critical') + expect(transformer(normalEvent).priority).toBe('normal') + }) + }) +}) diff --git a/src/__tests__/Utils/baileys-event-stream.test.ts.rej b/src/__tests__/Utils/baileys-event-stream.test.ts.rej new file mode 100644 index 00000000..baca98fe --- /dev/null +++ b/src/__tests__/Utils/baileys-event-stream.test.ts.rej @@ -0,0 +1,18 @@ +--- src/__tests__/Utils/baileys-event-stream.test.ts ++++ src/__tests__/Utils/baileys-event-stream.test.ts +@@ -180,6 +180,7 @@ + } + + stream.resume() ++ await new Promise(resolve => setTimeout(resolve, 100)) + const result = await stream.flush() + + expect(result.processed).toBe(5) +@@ -323,6 +324,7 @@ + stream.push('call', {}, { priority: 'high' }) + + stream.resume() ++ await new Promise(resolve => setTimeout(resolve, 100)) + await stream.flush() + + expect(processedOrder[0]).toBe('critical')