mirror of
https://github.com/RayLabsHQ/gitea-mirror.git
synced 2025-12-11 05:56:46 +03:00
feat: enhance SSE connection with reconnect logic and error handling
- Updated `useSSE` hook to include max reconnect attempts and exponential backoff for reconnections. - Improved error handling for SSE messages and connection errors. - Added connection status reset on successful connection. fix: improve SQLite database connection handling - Simplified database initialization and connection logic. - Ensured the database file is created if it doesn't exist. fix: enhance Redis client connection with retry strategy - Implemented exponential backoff for Redis connection retries. - Added event handlers for connection success and error handling. feat: improve SSE API endpoint with robust Redis connection management - Added connection retry logic for Redis in the SSE API. - Implemented heartbeat messages to keep the connection alive. - Enhanced error handling for Redis subscription and connection attempts.
This commit is contained in:
@@ -11,49 +11,130 @@ export const GET: APIRoute = async ({ request }) => {
|
||||
|
||||
const channel = `mirror-status:${userId}`;
|
||||
let isClosed = false;
|
||||
let connectionAttempts = 0;
|
||||
const MAX_ATTEMPTS = 5;
|
||||
const RETRY_DELAY = 1000; // 1 second
|
||||
|
||||
const stream = new ReadableStream({
|
||||
start(controller) {
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
const handleMessage = (ch: string, message: string) => {
|
||||
if (isClosed || ch !== channel) return;
|
||||
// Function to send a message to the client
|
||||
const sendMessage = (message: string) => {
|
||||
if (isClosed) return;
|
||||
try {
|
||||
controller.enqueue(encoder.encode(`data: ${message}\n\n`));
|
||||
controller.enqueue(encoder.encode(message));
|
||||
} catch (err) {
|
||||
console.error("Stream enqueue error:", err);
|
||||
}
|
||||
};
|
||||
|
||||
redisSubscriber.subscribe(channel, (err) => {
|
||||
if (err) {
|
||||
isClosed = true;
|
||||
controller.error(err);
|
||||
// Function to handle Redis connection and subscription
|
||||
const connectToRedis = () => {
|
||||
if (isClosed) return;
|
||||
|
||||
try {
|
||||
// Set up message handler for Bun's Redis client
|
||||
redisSubscriber.onmessage = (message, channelName) => {
|
||||
if (isClosed || channelName !== channel) return;
|
||||
sendMessage(`data: ${message}\n\n`);
|
||||
};
|
||||
|
||||
// Send initial connection message
|
||||
sendMessage(": connecting to Redis...\n\n");
|
||||
|
||||
// Use a try-catch block specifically for the subscribe operation
|
||||
let subscribed = false;
|
||||
try {
|
||||
// Bun's Redis client expects a string for the channel
|
||||
// We need to wrap this in a try-catch because it can throw if Redis is down
|
||||
subscribed = redisSubscriber.subscribe(channel);
|
||||
|
||||
if (subscribed) {
|
||||
// If we get here, subscription was successful
|
||||
sendMessage(": connected\n\n");
|
||||
|
||||
// Reset connection attempts on successful connection
|
||||
connectionAttempts = 0;
|
||||
|
||||
// Send a heartbeat every 30 seconds to keep the connection alive
|
||||
const heartbeatInterval = setInterval(() => {
|
||||
if (!isClosed) {
|
||||
sendMessage(": heartbeat\n\n");
|
||||
} else {
|
||||
clearInterval(heartbeatInterval);
|
||||
}
|
||||
}, 30000);
|
||||
} else {
|
||||
throw new Error("Failed to subscribe to Redis channel");
|
||||
}
|
||||
|
||||
} catch (subscribeErr) {
|
||||
// Handle subscription error
|
||||
console.error("Redis subscribe error:", subscribeErr);
|
||||
|
||||
// Retry connection if we haven't exceeded max attempts
|
||||
if (connectionAttempts < MAX_ATTEMPTS) {
|
||||
connectionAttempts++;
|
||||
const nextRetryDelay = RETRY_DELAY * Math.pow(2, connectionAttempts - 1);
|
||||
console.log(`Retrying Redis connection (attempt ${connectionAttempts}/${MAX_ATTEMPTS}) in ${nextRetryDelay}ms...`);
|
||||
|
||||
// Send retry message to client
|
||||
sendMessage(`: retrying connection (${connectionAttempts}/${MAX_ATTEMPTS}) in ${nextRetryDelay}ms...\n\n`);
|
||||
|
||||
// Wait before retrying
|
||||
setTimeout(connectToRedis, nextRetryDelay);
|
||||
} else {
|
||||
// Max retries exceeded, send error but keep the connection open
|
||||
console.error("Max Redis connection attempts exceeded");
|
||||
sendMessage(`data: {"error": "Redis connection failed after ${MAX_ATTEMPTS} attempts"}\n\n`);
|
||||
|
||||
// Set up a longer retry after max attempts
|
||||
setTimeout(() => {
|
||||
connectionAttempts = 0; // Reset counter for a fresh start
|
||||
sendMessage(": attempting to reconnect after cooling period...\n\n");
|
||||
connectToRedis();
|
||||
}, 30000); // Try again after 30 seconds
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// This catches any other errors outside the subscribe operation
|
||||
console.error("Redis connection error:", err);
|
||||
sendMessage(`data: {"error": "Redis connection error"}\n\n`);
|
||||
|
||||
// Still attempt to retry
|
||||
if (connectionAttempts < MAX_ATTEMPTS) {
|
||||
connectionAttempts++;
|
||||
setTimeout(connectToRedis, RETRY_DELAY * Math.pow(2, connectionAttempts - 1));
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
redisSubscriber.on("message", handleMessage);
|
||||
|
||||
try {
|
||||
controller.enqueue(encoder.encode(": connected\n\n"));
|
||||
} catch (err) {
|
||||
console.error("Initial enqueue error:", err);
|
||||
}
|
||||
// Start the initial connection
|
||||
connectToRedis();
|
||||
|
||||
// Handle client disconnection
|
||||
request.signal?.addEventListener("abort", () => {
|
||||
if (!isClosed) {
|
||||
isClosed = true;
|
||||
redisSubscriber.off("message", handleMessage);
|
||||
redisSubscriber.unsubscribe(channel);
|
||||
try {
|
||||
redisSubscriber.unsubscribe(channel);
|
||||
} catch (err) {
|
||||
console.error("Error unsubscribing from Redis:", err);
|
||||
}
|
||||
controller.close();
|
||||
}
|
||||
});
|
||||
},
|
||||
cancel() {
|
||||
// extra safety in case cancel is triggered
|
||||
// Extra safety in case cancel is triggered
|
||||
if (!isClosed) {
|
||||
isClosed = true;
|
||||
redisSubscriber.unsubscribe(channel);
|
||||
try {
|
||||
redisSubscriber.unsubscribe(channel);
|
||||
} catch (err) {
|
||||
console.error("Error unsubscribing from Redis:", err);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user