mirror of
https://github.com/khoaliber/dockhand.git
synced 2026-03-08 05:39:06 +00:00
Initial commit
This commit is contained in:
221
lib/stores/events.ts
Normal file
221
lib/stores/events.ts
Normal file
@@ -0,0 +1,221 @@
|
||||
import { writable, get } from 'svelte/store';
|
||||
import { currentEnvironment, environments } from './environment';
|
||||
|
||||
export interface DockerEvent {
|
||||
type: 'container' | 'image' | 'volume' | 'network';
|
||||
action: string;
|
||||
actor: {
|
||||
id: string;
|
||||
name: string;
|
||||
attributes: Record<string, string>;
|
||||
};
|
||||
time: number;
|
||||
timeNano: string;
|
||||
}
|
||||
|
||||
export type EventCallback = (event: DockerEvent) => void;
|
||||
|
||||
// Connection state
|
||||
export const sseConnected = writable<boolean>(false);
|
||||
export const sseError = writable<string | null>(null);
|
||||
export const lastEvent = writable<DockerEvent | null>(null);
|
||||
|
||||
// Event listeners
|
||||
const listeners: Set<EventCallback> = new Set();
|
||||
|
||||
let eventSource: EventSource | null = null;
|
||||
let reconnectTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
let reconnectAttempts = 0;
|
||||
let wantsConnection = false; // Track intent to be connected (even for edge envs without eventSource)
|
||||
let isEdgeMode = false; // Track if current env is edge (no SSE needed)
|
||||
const MAX_RECONNECT_ATTEMPTS = 5;
|
||||
const RECONNECT_DELAY = 3000;
|
||||
|
||||
// Check if environment is edge type (events come via Hawser WebSocket, not SSE)
|
||||
function isEdgeEnvironment(envId: number | null | undefined): boolean {
|
||||
if (!envId) return false;
|
||||
const envList = get(environments);
|
||||
const env = envList.find(e => e.id === envId);
|
||||
return env?.connectionType === 'hawser-edge';
|
||||
}
|
||||
|
||||
// Subscribe to events
|
||||
export function onDockerEvent(callback: EventCallback): () => void {
|
||||
listeners.add(callback);
|
||||
return () => listeners.delete(callback);
|
||||
}
|
||||
|
||||
// Notify all listeners
|
||||
function notifyListeners(event: DockerEvent) {
|
||||
lastEvent.set(event);
|
||||
listeners.forEach(callback => {
|
||||
try {
|
||||
callback(event);
|
||||
} catch (e) {
|
||||
console.error('Event listener error:', e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Connect to SSE endpoint
|
||||
export function connectSSE(envId?: number | null) {
|
||||
// Close existing connection
|
||||
disconnectSSE();
|
||||
|
||||
// Mark that we want to be connected
|
||||
wantsConnection = true;
|
||||
reconnectAttempts = 0;
|
||||
|
||||
// Don't connect if no environment is selected
|
||||
if (!envId) {
|
||||
sseConnected.set(false);
|
||||
sseError.set(null);
|
||||
return;
|
||||
}
|
||||
|
||||
// Edge environments receive events via Hawser agent WebSocket, not SSE
|
||||
if (isEdgeEnvironment(envId)) {
|
||||
isEdgeMode = true;
|
||||
// For edge environments, we're "connected" but via a different mechanism
|
||||
sseConnected.set(true);
|
||||
sseError.set(null);
|
||||
return;
|
||||
}
|
||||
|
||||
isEdgeMode = false;
|
||||
const url = `/api/events?env=${envId}`;
|
||||
|
||||
try {
|
||||
eventSource = new EventSource(url);
|
||||
|
||||
eventSource.addEventListener('connected', (e) => {
|
||||
console.log('SSE connected:', JSON.parse(e.data));
|
||||
sseConnected.set(true);
|
||||
sseError.set(null);
|
||||
reconnectAttempts = 0;
|
||||
});
|
||||
|
||||
eventSource.addEventListener('docker', (e) => {
|
||||
try {
|
||||
const event: DockerEvent = JSON.parse(e.data);
|
||||
notifyListeners(event);
|
||||
} catch (err) {
|
||||
console.error('Failed to parse docker event:', err);
|
||||
}
|
||||
});
|
||||
|
||||
eventSource.addEventListener('heartbeat', () => {
|
||||
// Connection is alive
|
||||
});
|
||||
|
||||
// Handle SSE error events (both server-sent and connection errors)
|
||||
eventSource.addEventListener('error', (e: Event) => {
|
||||
// Check if this is a server-sent error message (MessageEvent with data)
|
||||
const messageEvent = e as MessageEvent;
|
||||
if (messageEvent.data) {
|
||||
try {
|
||||
const data = JSON.parse(messageEvent.data);
|
||||
// Check if this is the edge environment message (fallback if env list wasn't loaded)
|
||||
if (data.message?.includes('Edge environments')) {
|
||||
isEdgeMode = true;
|
||||
sseConnected.set(true);
|
||||
sseError.set(null);
|
||||
if (eventSource) {
|
||||
eventSource.close();
|
||||
eventSource = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
// Not JSON, fall through to generic error handling
|
||||
}
|
||||
}
|
||||
|
||||
// Skip reconnection if we're in edge mode
|
||||
if (isEdgeMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.error('SSE error:', e);
|
||||
sseConnected.set(false);
|
||||
|
||||
// Attempt reconnection
|
||||
if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
|
||||
reconnectAttempts++;
|
||||
sseError.set(`Connection lost. Reconnecting (${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})...`);
|
||||
reconnectTimeout = setTimeout(() => {
|
||||
const env = get(currentEnvironment);
|
||||
connectSSE(env?.id);
|
||||
}, RECONNECT_DELAY);
|
||||
} else {
|
||||
sseError.set('Connection failed. Refresh the page to retry.');
|
||||
}
|
||||
});
|
||||
|
||||
eventSource.onerror = () => {
|
||||
// Handled by error event listener
|
||||
};
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('Failed to create EventSource:', error);
|
||||
sseError.set(error.message || 'Failed to connect');
|
||||
sseConnected.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Disconnect from SSE
|
||||
export function disconnectSSE() {
|
||||
if (reconnectTimeout) {
|
||||
clearTimeout(reconnectTimeout);
|
||||
reconnectTimeout = null;
|
||||
}
|
||||
if (eventSource) {
|
||||
eventSource.close();
|
||||
eventSource = null;
|
||||
}
|
||||
// Don't reset wantsConnection here - it's reset by explicit calls
|
||||
sseConnected.set(false);
|
||||
isEdgeMode = false;
|
||||
}
|
||||
|
||||
// Subscribe to environment changes and reconnect
|
||||
let currentEnvId: number | null = null;
|
||||
currentEnvironment.subscribe((env) => {
|
||||
const newEnvId = env?.id ?? null;
|
||||
if (newEnvId !== currentEnvId) {
|
||||
currentEnvId = newEnvId;
|
||||
// If no environment, disconnect
|
||||
if (!newEnvId) {
|
||||
disconnectSSE();
|
||||
wantsConnection = false;
|
||||
} else if (wantsConnection) {
|
||||
// Reconnect with new environment if we want to be connected
|
||||
// (using wantsConnection because eventSource is null for edge envs)
|
||||
connectSSE(newEnvId);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Helper to check if action affects container list
|
||||
export function isContainerListChange(event: DockerEvent): boolean {
|
||||
if (event.type !== 'container') return false;
|
||||
return ['create', 'destroy', 'start', 'stop', 'pause', 'unpause', 'die', 'kill', 'rename'].includes(event.action);
|
||||
}
|
||||
|
||||
// Helper to check if action affects image list
|
||||
export function isImageListChange(event: DockerEvent): boolean {
|
||||
if (event.type !== 'image') return false;
|
||||
return ['pull', 'push', 'delete', 'tag', 'untag', 'import'].includes(event.action);
|
||||
}
|
||||
|
||||
// Helper to check if action affects volume list
|
||||
export function isVolumeListChange(event: DockerEvent): boolean {
|
||||
if (event.type !== 'volume') return false;
|
||||
return ['create', 'destroy'].includes(event.action);
|
||||
}
|
||||
|
||||
// Helper to check if action affects network list
|
||||
export function isNetworkListChange(event: DockerEvent): boolean {
|
||||
if (event.type !== 'network') return false;
|
||||
return ['create', 'destroy', 'connect', 'disconnect'].includes(event.action);
|
||||
}
|
||||
Reference in New Issue
Block a user