forked from cardosofelipe/fast-next-template
refactor(connection): improve retry and cleanup behavior in project events
- Refined retry delay logic for clarity and correctness in `getNextRetryDelay`. - Added `connectRef` to ensure latest `connect` function is called in retries. - Separated cleanup and connection management effects to prevent premature disconnections. - Enhanced inline comments for maintainability.
This commit is contained in:
@@ -130,6 +130,8 @@ export function useProjectEvents(
|
||||
const isManualDisconnectRef = useRef(false);
|
||||
const mountedRef = useRef(true);
|
||||
const pingHandlerRef = useRef<(() => void) | null>(null);
|
||||
// Ref to hold latest connect function to avoid stale closure in scheduleReconnect
|
||||
const connectRef = useRef<(() => void) | null>(null);
|
||||
|
||||
/**
|
||||
* Update connection state and notify callback
|
||||
@@ -206,11 +208,13 @@ export function useProjectEvents(
|
||||
|
||||
/**
|
||||
* Calculate next retry delay with exponential backoff
|
||||
* Returns current delay, then increases for next call
|
||||
*/
|
||||
const getNextRetryDelay = useCallback(() => {
|
||||
const nextDelay = currentRetryDelayRef.current * BACKOFF_MULTIPLIER;
|
||||
currentRetryDelayRef.current = Math.min(nextDelay, maxRetryDelay);
|
||||
return currentRetryDelayRef.current;
|
||||
const delay = currentRetryDelayRef.current;
|
||||
// Increase for next call (capped at max)
|
||||
currentRetryDelayRef.current = Math.min(delay * BACKOFF_MULTIPLIER, maxRetryDelay);
|
||||
return delay;
|
||||
}, [maxRetryDelay]);
|
||||
|
||||
/**
|
||||
@@ -234,7 +238,8 @@ export function useProjectEvents(
|
||||
retryTimeoutRef.current = setTimeout(() => {
|
||||
if (!mountedRef.current || isManualDisconnectRef.current) return;
|
||||
setRetryCount((prev) => prev + 1);
|
||||
connect();
|
||||
// Use ref to always call latest connect function (avoids stale closure)
|
||||
connectRef.current?.();
|
||||
}, delay);
|
||||
}, [retryCount, maxRetryAttempts, getNextRetryDelay, updateConnectionState]);
|
||||
|
||||
@@ -344,6 +349,9 @@ export function useProjectEvents(
|
||||
initialRetryDelay,
|
||||
]);
|
||||
|
||||
// Keep ref updated with latest connect function for scheduleReconnect
|
||||
connectRef.current = connect;
|
||||
|
||||
/**
|
||||
* Manually disconnect from SSE
|
||||
*/
|
||||
@@ -371,11 +379,22 @@ export function useProjectEvents(
|
||||
clearProjectEvents(projectId);
|
||||
}, [clearProjectEvents, projectId]);
|
||||
|
||||
// Consolidated connection management effect
|
||||
// Handles both initial mount and auth state changes to prevent race conditions
|
||||
// Effect 1: Unmount cleanup only
|
||||
// Separated from connection management to prevent cleanup running on every state change
|
||||
// This fixes: when connectionState changes, the old effect cleanup would close EventSource
|
||||
useEffect(() => {
|
||||
mountedRef.current = true;
|
||||
|
||||
return () => {
|
||||
mountedRef.current = false;
|
||||
cleanup();
|
||||
};
|
||||
}, [cleanup]);
|
||||
|
||||
// Effect 2: Connection management
|
||||
// No cleanup here - only Effect 1 handles cleanup on unmount
|
||||
// This prevents: cleanup closing EventSource immediately after connect() creates it
|
||||
useEffect(() => {
|
||||
// Connect when authenticated with a project and not manually disconnected
|
||||
if (autoConnect && isAuthenticated && accessToken && projectId) {
|
||||
if (connectionState === 'disconnected' && !isManualDisconnectRef.current) {
|
||||
@@ -385,21 +404,7 @@ export function useProjectEvents(
|
||||
// Disconnect when auth is lost
|
||||
disconnect();
|
||||
}
|
||||
|
||||
return () => {
|
||||
mountedRef.current = false;
|
||||
cleanup();
|
||||
};
|
||||
}, [
|
||||
autoConnect,
|
||||
isAuthenticated,
|
||||
accessToken,
|
||||
projectId,
|
||||
connectionState,
|
||||
connect,
|
||||
disconnect,
|
||||
cleanup,
|
||||
]);
|
||||
}, [autoConnect, isAuthenticated, accessToken, projectId, connectionState, connect, disconnect]);
|
||||
|
||||
return {
|
||||
events,
|
||||
|
||||
@@ -62,8 +62,8 @@ function isValidToken(token: string): boolean {
|
||||
* @returns Unix timestamp
|
||||
*/
|
||||
function calculateExpiry(expiresIn?: number): number {
|
||||
// Default to 15 minutes if not provided or invalid
|
||||
let seconds = expiresIn || 900;
|
||||
// Use nullish coalescing - 0 is invalid but should trigger warning, not silent fallback
|
||||
let seconds = expiresIn ?? 900;
|
||||
|
||||
// Validate positive number and prevent overflow
|
||||
if (seconds <= 0 || seconds > 31536000) {
|
||||
|
||||
Reference in New Issue
Block a user