This commit is contained in:
yohlo
2025-08-20 22:35:40 -05:00
commit f51c278cd3
169 changed files with 8173 additions and 0 deletions

View File

@@ -0,0 +1,153 @@
import { useEffect, useRef } from "react";
import { useQueryClient } from "@tanstack/react-query";
import { Logger } from "@/lib/logger";
import { useAuth } from "@/contexts/auth-context";
/*
import { authClient } from "~/lib/auth-client";
import { getIdeaFn } from "~/routes/-fn/getIdeaFn";
import { useSession } from "~/lib/sessionContext";
*/
// Global state for new ideas notification
let newIdeasAvailable = false;
let newIdeasCallbacks: (() => void)[] = [];
const logger = new Logger('ServerEvents');
// Event handler types for better type safety
type SSEEvent = {
type: string;
[key: string]: any;
};
type EventHandler = (event: SSEEvent, queryClient: ReturnType<typeof useQueryClient>, currentSessionId?: string) => void;
// Event handlers map - add new handlers here for easy extension
const eventHandlers: Record<string, EventHandler> = {
"connected": () => {
logger.info("ServerEvents | New Connection");
},
"ping": () => {
// Keep-alive ping, no action needed
},
"test": (event, queryClient) => {
},
// Add new event handlers here:
// "idea-updated": (event, queryClient) => {
// queryClient.invalidateQueries({ queryKey: ["ideas"] });
// },
// "vote-changed": (event, queryClient) => {
// queryClient.invalidateQueries({ queryKey: ["idea", event.ideaId] });
// },
};
// Functions to manage new ideas notification state
export function getNewIdeasAvailable(): boolean {
return newIdeasAvailable;
}
export function clearNewIdeasAvailable(): void {
newIdeasAvailable = false;
}
export function subscribeToNewIdeas(callback: () => void): () => void {
newIdeasCallbacks.push(callback);
// Return unsubscribe function
return () => {
const index = newIdeasCallbacks.indexOf(callback);
if (index > -1) {
newIdeasCallbacks.splice(index, 1);
}
};
}
export function useServerEvents() {
const queryClient = useQueryClient();
const { user } = useAuth();
const retryCountRef = useRef(0);
const shouldConnectRef = useRef(true);
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
useEffect(() => {
if (!user?.id) return;
shouldConnectRef.current = true;
retryCountRef.current = 0;
const connectEventSource = () => {
if (!shouldConnectRef.current) return;
const eventSource = new EventSource(`/api/events/$`);
eventSource.onopen = () => {
// Reset retry count on successful connection
retryCountRef.current = 0;
};
eventSource.onmessage = (event) => {
try {
const data: SSEEvent = JSON.parse(event.data);
logger.info("Event received", data);
// Use the event handler pattern for extensible event processing
const handler = eventHandlers[data.type];
if (handler) {
handler(data, queryClient, user?.id);
} else {
logger.warn(`Unhandled SSE event type: ${data.type}`);
}
} catch (error) {
logger.error("Error parsing SSE message", error);
}
};
eventSource.onerror = (error) => {
logger.error("SSE connection error", error);
eventSource.close();
// Only retry if we should still be connecting and haven't exceeded max retries
if (shouldConnectRef.current && retryCountRef.current < 5) {
retryCountRef.current += 1;
const delay = Math.min(
1000 * Math.pow(2, retryCountRef.current - 1),
30000
); // Cap at 30 seconds
logger.info(
`SSE reconnection attempt ${retryCountRef.current}/5 in ${delay}ms`
);
timeoutRef.current = setTimeout(() => {
if (shouldConnectRef.current) {
connectEventSource();
}
}, delay);
} else if (retryCountRef.current >= 5) {
logger.error("SSE max reconnection attempts reached");
}
};
return eventSource;
};
const eventSource = connectEventSource();
return () => {
logger.info("Closing SSE connection");
shouldConnectRef.current = false;
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
if (eventSource) {
eventSource.close();
}
};
}, [user?.id, queryClient]);
}