@nestjsvn/swagger-sse
Version:
OpenAPI documentation and interactive Swagger UI for NestJS Server-Sent Events endpoints
1,018 lines (870 loc) • 32.9 kB
JavaScript
/**
* Enhanced Swagger UI SSE Plugin - Client-side JavaScript
*
* This file provides:
* - EventSource connection management with performance optimizations
* - Real-time event display with virtual scrolling
* - Connection status indicators with theme support
* - Event filtering and formatting
* - Integration with vanilla JS components
*/
(function () {
'use strict';
// Global state for SSE connections and components
window.sseConnections = new Map();
window.sseComponents = new Map();
// Performance monitoring
const performanceMonitor = {
eventCount: 0,
renderTime: 0,
lastRenderTime: Date.now(),
averageRenderTime: 0,
};
/**
* Initialize SSE plugin when Swagger UI loads
*/
function initSsePlugin() {
console.log('Enhanced SSE Plugin initialized');
// Initialize theme manager first
initializeThemeManager();
// Wait for Swagger UI to be ready
if (typeof window.ui !== 'undefined') {
console.log('Swagger UI detected, setting up enhanced SSE plugin');
setupSsePlugin();
} else {
// Retry after a short delay
setTimeout(initSsePlugin, 500);
}
}
/**
* Initialize theme manager
*/
function initializeThemeManager() {
if (window.ThemeManager && !window._sseThemeManager) {
window._sseThemeManager = new window.ThemeManager({
defaultTheme: 'light',
storageKey: 'swagger-sse-theme',
autoDetectSystem: true,
});
console.log('SSE Plugin: Theme manager initialized');
}
}
/**
* Setup SSE plugin integration with Swagger UI
*/
function setupSsePlugin() {
// Override the default execute function for SSE endpoints
if (window.ui?.getSystem) {
const system = window.ui.getSystem();
console.log('Enhanced SSE Plugin: Swagger UI system available');
// Monitor for SSE endpoint executions
monitorSseExecutions();
// Setup performance monitoring
setupPerformanceMonitoring();
// Setup accessibility features
setupAccessibilityFeatures();
}
}
/**
* Setup performance monitoring
*/
function setupPerformanceMonitoring() {
// Monitor render performance
const observer = new PerformanceObserver(list => {
for (const entry of list.getEntries()) {
if (entry.name.includes('sse-render')) {
performanceMonitor.renderTime += entry.duration;
performanceMonitor.averageRenderTime =
performanceMonitor.renderTime / ++performanceMonitor.eventCount;
}
}
});
if (typeof PerformanceObserver !== 'undefined') {
observer.observe({ entryTypes: ['measure'] });
}
}
/**
* Setup accessibility features
*/
function setupAccessibilityFeatures() {
// Add keyboard navigation support
document.addEventListener('keydown', handleKeyboardNavigation);
// Add focus management
document.addEventListener('focusin', handleFocusManagement);
// Add screen reader announcements
setupScreenReaderSupport();
}
/**
* Handle keyboard navigation
*/
function handleKeyboardNavigation(event) {
if (event.target.closest('.sse-interface')) {
switch (event.key) {
case 'Escape':
// Close any open modals or reset focus
const activeElement = document.activeElement;
if (activeElement && activeElement.blur) {
activeElement.blur();
}
break;
case 'Tab':
// Ensure proper tab order
document.body.classList.add('sse-keyboard-navigation');
break;
}
}
}
/**
* Handle focus management
*/
function handleFocusManagement(event) {
// Remove keyboard navigation class on mouse interaction
document.addEventListener(
'mousedown',
() => {
document.body.classList.remove('sse-keyboard-navigation');
},
{ once: true },
);
}
/**
* Setup screen reader support
*/
function setupScreenReaderSupport() {
// Create live region for announcements
if (!document.getElementById('sse-live-region')) {
const liveRegion = document.createElement('div');
liveRegion.id = 'sse-live-region';
liveRegion.setAttribute('aria-live', 'polite');
liveRegion.setAttribute('aria-atomic', 'true');
liveRegion.style.position = 'absolute';
liveRegion.style.left = '-10000px';
liveRegion.style.width = '1px';
liveRegion.style.height = '1px';
liveRegion.style.overflow = 'hidden';
document.body.appendChild(liveRegion);
}
}
/**
* Announce to screen readers
*/
function announceToScreenReader(message) {
const liveRegion = document.getElementById('sse-live-region');
if (liveRegion) {
liveRegion.textContent = message;
setTimeout(() => {
liveRegion.textContent = '';
}, 1000);
}
}
/**
* Extract event types from OpenAPI schema
*/
function extractEventTypesFromSchema(url) {
try {
if (!window.ui || !window.ui.getSystem) {
return [];
}
const system = window.ui.getSystem();
const state = system.getState();
if (!state) {
return [];
}
// Get the spec from Swagger UI state
let spec = null;
if (state.get) {
spec = state.get('spec');
} else if (state.spec) {
spec = state.spec;
}
if (!spec) {
return [];
}
// Access the parsed JSON spec using Immutable.js Map methods
const specJson = spec.get('json');
if (!specJson) {
return [];
}
// Convert Immutable.js structure to regular JavaScript object if needed
let paths = null;
if (specJson.get) {
paths = specJson.get('paths');
} else if (specJson.paths) {
paths = specJson.paths;
}
if (!paths) {
// Try to get paths from resolvedSubtrees as fallback
const resolvedSubtrees = spec.get('resolvedSubtrees');
if (resolvedSubtrees && resolvedSubtrees.get) {
paths = resolvedSubtrees.get('paths');
} else {
return [];
}
}
const eventTypes = new Set();
// Extract path from URL
const urlPath = new URL(url).pathname;
// Handle both Immutable.js Map and regular JavaScript object
const pathKeys = paths.keySeq ? paths.keySeq().toArray() : Object.keys(paths);
// Look for matching path in OpenAPI spec
pathKeys.forEach(pathKey => {
if (pathKey === urlPath || urlPath.includes(pathKey)) {
const pathItem = paths.get ? paths.get(pathKey) : paths[pathKey];
const methodKeys = pathItem.keySeq ? pathItem.keySeq().toArray() : Object.keys(pathItem);
methodKeys.forEach(method => {
const operation = pathItem.get ? pathItem.get(method) : pathItem[method];
// Look for SSE-specific extensions or schemas
const responses = operation.get ? operation.get('responses') : operation.responses;
if (responses) {
const response200 = responses.get ? responses.get('200') : responses['200'];
if (response200) {
const content = response200.get ? response200.get('content') : response200.content;
if (content) {
const eventStreamContent = content.get
? content.get('text/event-stream')
: content['text/event-stream'];
if (eventStreamContent) {
const schema = eventStreamContent.get
? eventStreamContent.get('schema')
: eventStreamContent.schema;
// Extract event types from oneOf discriminator pattern
const oneOf = schema.get ? schema.get('oneOf') : schema.oneOf;
if (oneOf) {
const oneOfArray = oneOf.toArray ? oneOf.toArray() : oneOf;
oneOfArray.forEach(eventSchema => {
const properties = eventSchema.get
? eventSchema.get('properties')
: eventSchema.properties;
if (properties) {
const typeProperty = properties.get
? properties.get('type')
: properties.type;
if (typeProperty) {
const typeValue =
(typeProperty.get ? typeProperty.get('const') : typeProperty.const) ||
(typeProperty.get ? typeProperty.get('enum') : typeProperty.enum);
const finalValue = Array.isArray(typeValue) ? typeValue[0] : typeValue;
if (finalValue) {
eventTypes.add(finalValue);
}
}
}
});
}
// Check for x-sse-events extension
const xSseEvents = schema.get
? schema.get('x-sse-events')
: schema['x-sse-events'];
if (xSseEvents) {
const eventsArray = xSseEvents.toArray ? xSseEvents.toArray() : xSseEvents;
eventsArray.forEach(eventType => {
eventTypes.add(eventType);
});
}
}
}
}
}
});
}
});
const result = Array.from(eventTypes);
console.log('Enhanced SSE Plugin: Extracted event types from schema:', result);
return result;
} catch (error) {
console.warn('Enhanced SSE Plugin: Error extracting event types from schema:', error);
return [];
}
}
/**
* Monitor for SSE endpoint executions
*/
function monitorSseExecutions() {
console.log('Enhanced SSE Plugin: Setting up execution monitoring');
// Store original fetch to intercept requests if needed
const originalFetch = window.fetch;
window.fetch = function (url) {
// Check if this is an SSE endpoint request
if (typeof url === 'string' && isSseEndpoint(url)) {
console.log('Enhanced SSE Plugin: Intercepting fetch request to SSE endpoint:', url);
// Return a rejected promise to prevent the request
return Promise.reject(new Error('SSE endpoint request intercepted by plugin'));
}
return originalFetch.apply(this, arguments);
};
// Listen for execute button clicks on SSE endpoints
document.addEventListener(
'click',
function (event) {
const executeBtn = event.target.closest('.execute-wrapper .btn.execute');
if (!executeBtn) {
return;
}
console.log('Enhanced SSE Plugin: Execute button clicked:', executeBtn);
// Check if this is an SSE endpoint
const operationContainer = executeBtn.closest('.opblock');
if (!operationContainer) {
return;
}
const pathElement = operationContainer.querySelector('.opblock-summary-path');
if (!pathElement) {
return;
}
const path = pathElement.textContent.trim();
console.log('Enhanced SSE Plugin: Endpoint path:', path);
// Check if this path corresponds to an SSE endpoint
if (isSseEndpoint(path)) {
console.log('Enhanced SSE Plugin: Confirmed SSE endpoint, intercepting execution');
// Prevent the default Swagger UI execution
event.preventDefault();
event.stopPropagation();
// Handle SSE execution with custom UI
handleSseExecution(operationContainer, path);
return false;
}
},
true,
);
}
/**
* Check if a path is an SSE endpoint by examining OpenAPI schema
*/
function isSseEndpoint(path) {
try {
// Get the current state from Swagger UI
const state = window.ui?.getState ? window.ui.getState() : null;
if (!state) {
return false;
}
// Get the spec from Swagger UI state
let spec = null;
if (state.get) {
spec = state.get('spec');
} else if (state.spec) {
spec = state.spec;
}
if (!spec) {
return false;
}
// Access the parsed JSON spec using Immutable.js Map methods
const specJson = spec.get('json');
if (!specJson) {
return false;
}
// Convert Immutable.js structure to regular JavaScript object if needed
let paths = null;
if (specJson.get) {
paths = specJson.get('paths');
} else if (specJson.paths) {
paths = specJson.paths;
}
if (!paths) {
// Try to get paths from resolvedSubtrees as fallback
const resolvedSubtrees = spec.get('resolvedSubtrees');
if (resolvedSubtrees && resolvedSubtrees.get) {
paths = resolvedSubtrees.get('paths');
} else {
return false;
}
}
// Parse the URL to get the path
const urlPath = path.startsWith('http') ? new URL(path).pathname : path;
// Handle both Immutable.js Map and regular JavaScript object
const pathKeys = paths.keySeq ? paths.keySeq().toArray() : Object.keys(paths);
// Look for matching path in OpenAPI spec
for (const pathKey of pathKeys) {
if (pathKey === urlPath || urlPath.includes(pathKey)) {
const pathItem = paths.get ? paths.get(pathKey) : paths[pathKey];
if (pathItem) {
const methodKeys = pathItem.keySeq
? pathItem.keySeq().toArray()
: Object.keys(pathItem);
// Check each HTTP method for SSE indicators
for (const method of methodKeys) {
const operation = pathItem.get ? pathItem.get(method) : pathItem[method];
// Check for x-sse-endpoint extension (primary indicator)
const xSseEndpoint = operation.get
? operation.get('x-sse-endpoint')
: operation['x-sse-endpoint'];
if (xSseEndpoint) {
return true;
}
// Check for text/event-stream content-type in responses
const responses = operation.get ? operation.get('responses') : operation.responses;
if (responses) {
const response200 = responses.get ? responses.get('200') : responses['200'];
if (response200) {
const content = response200.get
? response200.get('content')
: response200.content;
if (content) {
const eventStreamContent = content.get
? content.get('text/event-stream')
: content['text/event-stream'];
if (eventStreamContent) {
return true;
}
}
}
}
}
}
}
}
return false;
} catch (error) {
console.warn('Enhanced SSE Plugin: Error checking SSE endpoint:', error);
return false;
}
}
/**
* Extract parameters from Swagger UI form
*/
function extractParametersFromForm(container) {
const parameters = {};
// Find all parameter inputs
const parameterInputs = container.querySelectorAll(
'.parameters-container input, .parameters-container select, .parameters-container textarea',
);
parameterInputs.forEach(input => {
const paramName = input.getAttribute('data-param-name') || input.name;
if (paramName && input.value) {
parameters[paramName] = input.value;
}
});
return parameters;
}
/**
* Build URL with parameters
*/
function buildUrlWithParameters(baseUrl, parameters) {
if (!parameters || Object.keys(parameters).length === 0) {
return baseUrl;
}
const url = new URL(baseUrl);
Object.entries(parameters).forEach(([key, value]) => {
url.searchParams.append(key, value);
});
return url.toString();
}
/**
* Handle SSE endpoint execution with enhanced components
*/
function handleSseExecution(container, path) {
console.log('Enhanced SSE Plugin: Handling SSE execution for:', path);
// Extract parameters from the form
const parameters = extractParametersFromForm(container);
// Build the full URL with parameters
const baseUrl = window.location.origin;
const fullUrl = buildUrlWithParameters(baseUrl + path, parameters);
// Create enhanced SSE interface
createEnhancedSseInterface(container, fullUrl, parameters);
}
/**
* Create enhanced SSE interface with vanilla JS components
*/
function createEnhancedSseInterface(container, url, parameters = {}) {
console.log('Enhanced SSE Plugin: Creating enhanced SSE interface for:', url);
// Find the responses wrapper
const responsesWrapper = container.querySelector('.responses-wrapper');
if (!responsesWrapper) {
console.error('Enhanced SSE Plugin: Could not find responses wrapper');
return;
}
// Remove any existing SSE interface
const existingInterface = responsesWrapper.querySelector('.sse-interface');
if (existingInterface) {
// Cleanup existing components
cleanupSseComponents(existingInterface);
existingInterface.remove();
}
// Create the enhanced SSE interface HTML
const sseInterface = document.createElement('div');
sseInterface.className = 'sse-interface';
sseInterface.setAttribute('role', 'region');
sseInterface.setAttribute('aria-label', 'Server-Sent Events Interface');
// Build parameters display
const parametersHtml =
Object.keys(parameters).length > 0
? `
<div class="sse-parameters" style="margin-bottom: 1rem; padding: 1rem; background-color: #f8f9fa; border-radius: 4px; border: 1px solid #e9ecef;">
<h4 style="margin: 0 0 0.5rem 0; font-size: 1rem; color: #495057;">Parameters</h4>
<div class="sse-parameters-list">
${Object.entries(parameters)
.map(
([key, value]) => `
<div class="sse-parameter-item" style="margin-bottom: 0.25rem;">
<span class="sse-parameter-name" style="font-weight: bold; color: #007bff;">${key}:</span>
<span class="sse-parameter-value" style="margin-left: 0.5rem; color: #28a745;">${value}</span>
</div>
`,
)
.join('')}
</div>
</div>
`
: '';
sseInterface.innerHTML = `
<div class="sse-interface-header">
<h3>Server-Sent Events</h3>
<div class="sse-theme-controls">
<div id="sse-theme-selector"></div>
</div>
</div>
${parametersHtml}
<div class="sse-connection-container" id="sse-connection-${Date.now()}">
<!-- Connection status component will be inserted here -->
</div>
<div class="sse-filter-container" id="sse-filter-${Date.now()}">
<!-- Event filter component will be inserted here -->
</div>
<div class="sse-events-container" id="sse-events-${Date.now()}">
<!-- Event list component will be inserted here -->
</div>
<div class="sse-performance-stats" style="display: none;">
<small>
Events: <span id="sse-event-count">0</span> |
Avg Render: <span id="sse-avg-render">0ms</span>
</small>
</div>
`;
// Insert the interface
responsesWrapper.appendChild(sseInterface);
// Initialize enhanced components
initializeEnhancedComponents(sseInterface, url, parameters);
// Setup theme selector
setupThemeSelector(sseInterface);
// Hide loading elements
hideLoadingElements(container);
// Announce interface creation
announceToScreenReader('Server-Sent Events interface created');
console.log('Enhanced SSE Plugin: Enhanced interface creation completed');
}
/**
* Initialize enhanced vanilla JS components
*/
function initializeEnhancedComponents(interfaceElement, url, parameters = {}) {
createBasicSseInterface(interfaceElement, url, parameters);
}
/**
* Setup theme selector
*/
function setupThemeSelector(interfaceElement) {
const themeContainer = interfaceElement.querySelector('#sse-theme-selector');
if (window._sseThemeManager && themeContainer) {
window._sseThemeManager.createThemeToggle(themeContainer, {
showLabel: false,
className: 'sse-theme-toggle-btn',
});
console.log('Enhanced SSE Plugin: Theme selector initialized');
}
}
/**
* Update performance statistics
*/
function updatePerformanceStats() {
performanceMonitor.eventCount++;
const eventCountElement = document.getElementById('sse-event-count');
const avgRenderElement = document.getElementById('sse-avg-render');
if (eventCountElement) {
eventCountElement.textContent = performanceMonitor.eventCount;
}
if (avgRenderElement) {
avgRenderElement.textContent = `${performanceMonitor.averageRenderTime.toFixed(1)}ms`;
}
// Show performance stats if event count is high
if (performanceMonitor.eventCount > 50) {
const statsElement = document.querySelector('.sse-performance-stats');
if (statsElement) {
statsElement.style.display = 'block';
}
}
}
/**
* Create basic SSE interface as fallback
*/
function createBasicSseInterface(interfaceElement, url, parameters = {}) {
console.log('Enhanced SSE Plugin: Creating basic fallback interface');
// Build parameters display
const parametersHtml =
Object.keys(parameters).length > 0
? `
<div class="sse-parameters" style="margin-bottom: 1rem; padding: 1rem; background-color: #f8f9fa; border-radius: 4px; border: 1px solid #e9ecef;">
<h4 style="margin: 0 0 0.5rem 0; font-size: 1rem; color: #495057;">Parameters</h4>
<div class="sse-parameters-list">
${Object.entries(parameters)
.map(
([key, value]) => `
<div class="sse-parameter-item" style="margin-bottom: 0.25rem;">
<span class="sse-parameter-name" style="font-weight: bold; color: #007bff;">${key}:</span>
<span class="sse-parameter-value" style="margin-left: 0.5rem; color: #28a745;">${value}</span>
</div>
`,
)
.join('')}
</div>
</div>
`
: '';
interfaceElement.innerHTML = `
${parametersHtml}
<div class="sse-controls">
<button class="sse-btn sse-btn-primary sse-connect-btn" data-testid="sse-connect-btn">
Connect
</button>
<button class="sse-btn sse-btn-danger sse-disconnect-btn" data-testid="sse-disconnect-btn" disabled>
Disconnect
</button>
<span class="sse-status disconnected" data-testid="sse-status" data-status="disconnected">
DISCONNECTED
</span>
</div>
<div class="sse-event-log" data-testid="sse-event-log">
<h4>Event Log</h4>
<div class="sse-events"></div>
<div class="sse-log-controls">
<button class="sse-btn sse-btn-secondary sse-btn-sm sse-clear-btn" data-testid="sse-clear-btn">
Clear Log
</button>
<button class="sse-btn sse-btn-secondary sse-btn-sm sse-export-btn">
Export JSON
</button>
</div>
</div>
`;
// Setup basic event listeners
setupBasicSseInterfaceListeners(interfaceElement, url);
}
/**
* Setup event listeners for basic SSE interface
*/
function setupBasicSseInterfaceListeners(interfaceElement, url) {
const connectBtn = interfaceElement.querySelector('.sse-connect-btn');
const disconnectBtn = interfaceElement.querySelector('.sse-disconnect-btn');
const clearBtn = interfaceElement.querySelector('.sse-clear-btn');
const exportBtn = interfaceElement.querySelector('.sse-export-btn');
const statusElement = interfaceElement.querySelector('.sse-status');
const eventsContainer = interfaceElement.querySelector('.sse-events');
if (!eventsContainer) {
console.error('Enhanced SSE Plugin: Events container not found in interface!');
return;
}
let eventSource = null;
let events = [];
// Connect button
connectBtn.addEventListener('click', () => {
console.log('Enhanced SSE Plugin: Basic connect button clicked');
try {
if (typeof EventSource === 'undefined') {
console.error('Enhanced SSE Plugin: EventSource not supported');
updateStatus('error');
announceToScreenReader('EventSource not supported in this browser');
return;
}
if (eventSource) {
eventSource.close();
eventSource = null;
}
// Extract event types from the OpenAPI schema if available
const eventTypes = extractEventTypesFromSchema(url);
console.log('Enhanced SSE Plugin: Extracted event types from schema:', eventTypes);
// Create simple EventSource connection
eventSource = new EventSource(url);
// Store connection for cleanup
window.sseConnections.set(url, eventSource);
// Standard EventSource event handlers
eventSource.onopen = event => {
console.log('Enhanced SSE Plugin: Connection opened', event);
updateStatus('connected');
connectBtn.disabled = true;
disconnectBtn.disabled = false;
announceToScreenReader('Connected to server');
};
eventSource.onerror = error => {
console.error('Enhanced SSE Plugin: Connection error:', error);
updateStatus('error');
connectBtn.disabled = false;
disconnectBtn.disabled = true;
announceToScreenReader('Connection error occurred');
};
eventSource.onmessage = event => {
console.log('Enhanced SSE Plugin: Default message received:', event);
// Try to extract event type from message data as fallback
try {
const data = JSON.parse(event.data);
if (data.type && typeof data.type === 'string') {
const eventType = data.type;
console.log('Enhanced SSE Plugin: Detected event type from message data:', eventType);
// Register listener for this event type if not already registered
if (!eventTypes.includes(eventType)) {
eventTypes.push(eventType);
console.log(
'Enhanced SSE Plugin: Auto-registering handler for discovered event type:',
eventType,
);
eventSource.addEventListener(eventType, typedEvent => {
console.log(
`Enhanced SSE Plugin: Typed event handler for ${eventType}:`,
typedEvent,
);
addEvent(eventType, typedEvent.data, typedEvent);
});
}
addEvent(eventType, event.data, event);
return;
}
} catch (e) {
// Not JSON or no type field, treat as generic message
}
addEvent('message', event.data, event);
};
// Register listeners for schema-defined event types
if (eventTypes && eventTypes.length > 0) {
eventTypes.forEach(eventType => {
console.log(`Enhanced SSE Plugin: Registering listener for event type: ${eventType}`);
eventSource.addEventListener(eventType, event => {
console.log(`Enhanced SSE Plugin: Received ${eventType} event:`, event);
addEvent(eventType, event.data, event);
});
});
}
console.log(
'Enhanced SSE Plugin: Simple EventSource initialized with schema-based event listeners',
);
updateStatus('connecting');
announceToScreenReader('Connecting to server');
} catch (error) {
console.error('Enhanced SSE Plugin: Basic connection exception:', error);
updateStatus('error');
connectBtn.disabled = false;
disconnectBtn.disabled = true;
announceToScreenReader('Failed to connect to server');
}
});
// Disconnect button
disconnectBtn.addEventListener('click', () => {
console.log('Enhanced SSE Plugin: Disconnecting');
if (eventSource) {
eventSource.close();
window.sseConnections.delete(url);
eventSource = null;
}
updateStatus('disconnected');
connectBtn.disabled = false;
disconnectBtn.disabled = true;
announceToScreenReader('Disconnected from server');
});
// Clear button
clearBtn.addEventListener('click', () => {
events = [];
eventsContainer.innerHTML = '';
announceToScreenReader('Event log cleared');
});
// Export button
exportBtn.addEventListener('click', () => {
const data = JSON.stringify(events, null, 2);
const blob = new Blob([data], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `sse-events-${new Date().toISOString()}.json`;
a.click();
URL.revokeObjectURL(url);
announceToScreenReader('Events exported to JSON file');
});
// Helper functions
function updateStatus(status) {
statusElement.className = `sse-status ${status}`;
statusElement.textContent = status.toUpperCase();
statusElement.setAttribute('data-status', status);
}
function addEvent(type, data, originalEvent) {
const timestamp = new Date();
const event = {
type,
data: tryParseJson(data),
timestamp: timestamp.toISOString(),
id: originalEvent.lastEventId || null,
};
events.push(event);
// Add to UI with performance optimization
const eventElement = document.createElement('div');
eventElement.className = 'sse-event-item';
eventElement.setAttribute('data-testid', 'sse-event');
eventElement.setAttribute('data-event-type', type);
eventElement.innerHTML = `
<div class="sse-event-header">
<span class="sse-event-type">${type}</span>
<span class="sse-event-timestamp">${timestamp.toLocaleTimeString()}</span>
</div>
<div class="sse-event-data">${JSON.stringify(event.data, null, 2)}</div>
`;
eventsContainer.appendChild(eventElement);
// Performance optimization: limit events
if (events.length > 100) {
events = events.slice(-100);
const eventElements = eventsContainer.children;
if (eventElements.length > 100) {
eventsContainer.removeChild(eventElements[0]);
}
}
// Smooth scroll to bottom
eventsContainer.scrollTo({
top: eventsContainer.scrollHeight,
behavior: 'smooth',
});
// Update performance stats
updatePerformanceStats();
}
function tryParseJson(data) {
try {
return JSON.parse(data);
} catch {
return data;
}
}
}
/**
* Cleanup SSE components
*/
function cleanupSseComponents(interfaceElement) {
// Close any active EventSource connections
const eventSource = interfaceElement.querySelector('.sse-connect-btn')?._eventSource;
if (eventSource) {
eventSource.close();
}
console.log('Enhanced SSE Plugin: Components cleaned up');
}
/**
* Hide loading elements
*/
function hideLoadingElements(container) {
const loadingElement = container.querySelector('.loading-container');
if (loadingElement) {
loadingElement.style.display = 'none';
}
const executeBtn = container.querySelector('.execute-wrapper .btn.execute');
if (executeBtn) {
executeBtn.classList.remove('loading');
executeBtn.disabled = false;
executeBtn.style.opacity = '1';
executeBtn.style.pointerEvents = 'auto';
}
}
// Cleanup on page unload
window.addEventListener('beforeunload', () => {
// Close all active EventSource connections
window.sseConnections.forEach(connection => {
if (connection && connection.close) {
connection.close();
}
});
window.sseConnections.clear();
});
// Initialize when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initSsePlugin);
} else {
initSsePlugin();
}
})();