UNPKG

@nestjsvn/swagger-sse

Version:

OpenAPI documentation and interactive Swagger UI for NestJS Server-Sent Events endpoints

269 lines (268 loc) 13.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const setup_1 = require("../e2e/setup"); const test_controllers_1 = require("../e2e/test-controllers"); class PerformanceMonitor { constructor() { this.startTime = 0; this.eventCount = 0; this.firstEventReceived = false; this.firstEventTime = 0; } start() { this.startTime = Date.now(); this.eventCount = 0; this.firstEventReceived = false; this.firstEventTime = 0; } recordEvent() { this.eventCount++; if (!this.firstEventReceived) { this.firstEventReceived = true; this.firstEventTime = Date.now() - this.startTime; } } getMetrics(duration) { return { firstEventTime: this.firstEventTime, eventsPerSecond: this.eventCount / (duration / 1000), }; } } describe('Performance Benchmarks', () => { let context; let monitor; beforeAll(async () => { context = await setup_1.E2ETestSetup.createContext('performance-benchmarks', { controllers: [test_controllers_1.TestEventsController], }); monitor = new PerformanceMonitor(); }); afterAll(async () => { await setup_1.E2ETestSetup.cleanup('performance-benchmarks'); }); describe('Connection Performance', () => { it('should establish SSE connection within acceptable time', async () => { await context.page.goto(`${context.baseUrl}/api-docs`); await context.page.waitForSelector('.swagger-ui'); await context.page.click('[data-path="/test-events/basic"] .opblock-summary'); const startTime = Date.now(); await setup_1.SSETestHelpers.connectToSSE(context.page); const connectionTime = Date.now() - startTime; console.log(`Connection established in ${connectionTime}ms`); // Connection should be established within 1 second expect(connectionTime).toBeLessThan(1000); }); it('should handle multiple concurrent connections', async () => { const connectionPromises = []; const startTime = Date.now(); // Create multiple browser contexts for concurrent connections for (let i = 0; i < 5; i++) { const testContext = await setup_1.E2ETestSetup.createContext(`concurrent-${i}`, { controllers: [test_controllers_1.TestEventsController], }); const promise = (async () => { await testContext.page.goto(`${testContext.baseUrl}/api-docs`); await testContext.page.waitForSelector('.swagger-ui'); await testContext.page.click('[data-path="/test-events/basic"] .opblock-summary'); await setup_1.SSETestHelpers.connectToSSE(testContext.page); return testContext; })(); connectionPromises.push(promise); } const contexts = await Promise.all(connectionPromises); const totalTime = Date.now() - startTime; console.log(`${contexts.length} concurrent connections established in ${totalTime}ms`); // All connections should be established within 5 seconds expect(totalTime).toBeLessThan(5000); // Cleanup concurrent contexts for (let i = 0; i < contexts.length; i++) { await setup_1.E2ETestSetup.cleanup(`concurrent-${i}`); } }); }); describe('Event Processing Performance', () => { it('should handle high-frequency events efficiently', async () => { await context.page.goto(`${context.baseUrl}/api-docs`); await context.page.waitForSelector('.swagger-ui'); await context.page.click('[data-path="/test-events/high-frequency"] .opblock-summary'); monitor.start(); // Set up event monitoring await context.page.evaluate(() => { let eventCount = 0; // eslint-disable-next-line @typescript-eslint/unbound-method const originalAddEventListener = EventSource.prototype.addEventListener; EventSource.prototype.addEventListener = function (type, listener, options) { if (type === 'performance-metric') { const wrappedListener = (event) => { eventCount++; window.eventCount = eventCount; if (listener) listener(event); }; return originalAddEventListener.call(this, type, wrappedListener, options); } return originalAddEventListener.call(this, type, listener, options); }; }); await setup_1.SSETestHelpers.connectToSSE(context.page); // Wait for high-frequency events (50 events at 100ms intervals = 5 seconds) await context.page.waitForTimeout(6000); const eventCount = await context.page.evaluate(() => window.eventCount || 0); const _metrics = monitor.getMetrics(6000); console.log(`Processed ${eventCount} events in 6 seconds`); console.log(`Events per second: ${eventCount / 6}`); // Should process at least 40 events (allowing for some timing variance) expect(eventCount).toBeGreaterThan(40); // Should handle at least 8 events per second expect(eventCount / 6).toBeGreaterThan(8); }); it('should maintain performance with large event payloads', async () => { await context.page.goto(`${context.baseUrl}/api-docs`); await context.page.waitForSelector('.swagger-ui'); await context.page.click('[data-path="/test-events/basic"] .opblock-summary'); // Monitor memory usage const initialMemory = await context.page.evaluate(() => { return performance.memory ? performance.memory.usedJSHeapSize : 0; }); await setup_1.SSETestHelpers.connectToSSE(context.page); await setup_1.SSETestHelpers.waitForSSEEvents(context.page, 5, 15000); const finalMemory = await context.page.evaluate(() => { return performance.memory ? performance.memory.usedJSHeapSize : 0; }); const memoryIncrease = finalMemory - initialMemory; console.log(`Memory increase: ${memoryIncrease} bytes`); // Memory increase should be reasonable (less than 10MB) expect(memoryIncrease).toBeLessThan(10 * 1024 * 1024); }); }); describe('UI Rendering Performance', () => { it('should render events without blocking UI', async () => { await context.page.goto(`${context.baseUrl}/api-docs`); await context.page.waitForSelector('.swagger-ui'); await context.page.click('[data-path="/test-events/high-frequency"] .opblock-summary'); await setup_1.SSETestHelpers.connectToSSE(context.page); // Measure UI responsiveness during high-frequency events const _startTime = Date.now(); // Perform UI interactions while events are streaming const uiResponsiveness = await context.page.evaluate(async () => { const button = document.querySelector('[data-testid="sse-disconnect-btn"]'); if (!button) return false; const clickStart = performance.now(); button.click(); // Wait for UI to respond await new Promise(resolve => setTimeout(resolve, 100)); const clickEnd = performance.now(); return clickEnd - clickStart < 200; // Should respond within 200ms }); expect(uiResponsiveness).toBe(true); }); it('should handle large event logs efficiently', async () => { await context.page.goto(`${context.baseUrl}/api-docs`); await context.page.waitForSelector('.swagger-ui'); await context.page.click('[data-path="/test-events/high-frequency"] .opblock-summary'); await setup_1.SSETestHelpers.connectToSSE(context.page); // Wait for many events to accumulate await context.page.waitForTimeout(8000); // Measure scrolling performance in event log const scrollPerformance = await context.page.evaluate(() => { const eventLog = document.querySelector('[data-testid="sse-event-log"]'); if (!eventLog) return false; const scrollStart = performance.now(); eventLog.scrollTop = eventLog.scrollHeight; const scrollEnd = performance.now(); return scrollEnd - scrollStart < 50; // Scrolling should be smooth }); expect(scrollPerformance).toBe(true); }); }); describe('Memory Management', () => { it('should not leak memory during long-running connections', async () => { await context.page.goto(`${context.baseUrl}/api-docs`); await context.page.waitForSelector('.swagger-ui'); await context.page.click('[data-path="/test-events/long-running"] .opblock-summary'); // Take initial memory snapshot const initialMemory = await context.page.evaluate(() => { if (performance.memory) { return { used: performance.memory.usedJSHeapSize, total: performance.memory.totalJSHeapSize, }; } return { used: 0, total: 0 }; }); await setup_1.SSETestHelpers.connectToSSE(context.page); // Let connection run for extended period await context.page.waitForTimeout(15000); // Force garbage collection if available await context.page.evaluate(() => { if (window.gc) { window.gc(); } }); const finalMemory = await context.page.evaluate(() => { if (performance.memory) { return { used: performance.memory.usedJSHeapSize, total: performance.memory.totalJSHeapSize, }; } return { used: 0, total: 0 }; }); const memoryGrowth = finalMemory.used - initialMemory.used; console.log(`Memory growth over 15 seconds: ${memoryGrowth} bytes`); // Memory growth should be minimal (less than 5MB) expect(memoryGrowth).toBeLessThan(5 * 1024 * 1024); }); it('should clean up resources on disconnect', async () => { await context.page.goto(`${context.baseUrl}/api-docs`); await context.page.waitForSelector('.swagger-ui'); await context.page.click('[data-path="/test-events/basic"] .opblock-summary'); await setup_1.SSETestHelpers.connectToSSE(context.page); await context.page.waitForTimeout(2000); // Take memory snapshot before disconnect const beforeDisconnect = await context.page.evaluate(() => { return performance.memory ? performance.memory.usedJSHeapSize : 0; }); await setup_1.SSETestHelpers.disconnectFromSSE(context.page); // Force garbage collection await context.page.evaluate(() => { if (window.gc) { window.gc(); } }); await context.page.waitForTimeout(1000); const afterDisconnect = await context.page.evaluate(() => { return performance.memory ? performance.memory.usedJSHeapSize : 0; }); const memoryFreed = beforeDisconnect - afterDisconnect; console.log(`Memory freed on disconnect: ${memoryFreed} bytes`); // Should free some memory (or at least not increase significantly) expect(Number(afterDisconnect)).toBeLessThanOrEqual(Number(beforeDisconnect) + 1024 * 1024); // Allow 1MB variance }); }); describe('Network Performance', () => { it('should handle network interruptions gracefully', async () => { await context.page.goto(`${context.baseUrl}/api-docs`); await context.page.waitForSelector('.swagger-ui'); await context.page.click('[data-path="/test-events/basic"] .opblock-summary'); await setup_1.SSETestHelpers.connectToSSE(context.page); await context.page.waitForTimeout(2000); // Simulate network interruption by going offline await context.page.context().setOffline(true); await context.page.waitForTimeout(2000); // Check error handling const status = await context.page.getAttribute('[data-testid="sse-status"]', 'data-status'); expect(status).toBe('error'); // Restore network await context.page.context().setOffline(false); // Should be able to reconnect await context.page.click('[data-testid="sse-connect-btn"]'); await setup_1.SSETestHelpers.waitForSSEConnection(context.page); const reconnectedStatus = await context.page.getAttribute('[data-testid="sse-status"]', 'data-status'); expect(reconnectedStatus).toBe('connected'); }); }); });