From cc8fc3f14e23a4af6f195a61480c450c8000dc68 Mon Sep 17 00:00:00 2001 From: cj-vana Date: Fri, 10 Oct 2025 15:00:40 -0400 Subject: [PATCH] Revert "fix: add automatic WebSocket reconnection for Run of Show viewer" This reverts commit 977c101d973e8a649b3a05d24df7aa95771e265f. --- apps/web/src/hooks/useRealtimeSubscription.ts | 249 ------------------ apps/web/src/pages/SharedShowModePage.tsx | 123 ++++----- 2 files changed, 48 insertions(+), 324 deletions(-) delete mode 100644 apps/web/src/hooks/useRealtimeSubscription.ts diff --git a/apps/web/src/hooks/useRealtimeSubscription.ts b/apps/web/src/hooks/useRealtimeSubscription.ts deleted file mode 100644 index bfa832e..0000000 --- a/apps/web/src/hooks/useRealtimeSubscription.ts +++ /dev/null @@ -1,249 +0,0 @@ -/** - * React hook for Supabase real-time subscriptions with automatic reconnection. - * Handles network failures, timeouts, and connection errors with exponential backoff. - */ - -import { useEffect, useState, useCallback, useRef } from "react"; -import { supabase } from "@/lib/supabase"; -import type { RealtimeChannel } from "@supabase/supabase-js"; - -export type ConnectionStatus = - | "connecting" - | "connected" - | "reconnecting" - | "disconnected" - | "error"; - -interface UseRealtimeSubscriptionOptions { - /** Table name to subscribe to */ - table: string; - /** Event type (INSERT, UPDATE, DELETE, or *) */ - event: "INSERT" | "UPDATE" | "DELETE" | "*"; - /** Filter expression (e.g., "id=eq.123") */ - filter?: string; - /** Callback when update is received */ - onUpdate: (payload: any) => void; - /** Whether subscription is enabled */ - enabled?: boolean; - /** Maximum retry attempts before giving up (default: 10) */ - maxRetries?: number; - /** Initial retry delay in ms (default: 1000) */ - initialRetryDelay?: number; - /** Maximum retry delay in ms (default: 30000) */ - maxRetryDelay?: number; -} - -interface UseRealtimeSubscriptionResult { - /** Current connection status */ - status: ConnectionStatus; - /** Current retry attempt (0 if connected) */ - retryCount: number; - /** Error message if status is 'error' */ - error: string | null; - /** Manually trigger reconnection */ - reconnect: () => void; -} - -/** - * Hook for Supabase real-time subscriptions with automatic reconnection. - * - * Features: - * - Exponential backoff retry strategy - * - Automatic reconnection on network failures - * - Connection health monitoring - * - Proper cleanup on unmount - * - * @example - * ```tsx - * const { status, retryCount } = useRealtimeSubscription({ - * table: 'run_of_shows', - * event: 'UPDATE', - * filter: `id=eq.${showId}`, - * onUpdate: (payload) => { - * setShowData(payload.new); - * }, - * }); - * ``` - */ -export const useRealtimeSubscription = ({ - table, - event, - filter, - onUpdate, - enabled = true, - maxRetries = 10, - initialRetryDelay = 1000, - maxRetryDelay = 30000, -}: UseRealtimeSubscriptionOptions): UseRealtimeSubscriptionResult => { - const [status, setStatus] = useState("disconnected"); - const [retryCount, setRetryCount] = useState(0); - const [error, setError] = useState(null); - - const channelRef = useRef(null); - const retryTimeoutRef = useRef(null); - const mountedRef = useRef(true); - - /** - * Calculate retry delay with exponential backoff. - */ - const getRetryDelay = useCallback( - (attempt: number): number => { - const delay = Math.min(initialRetryDelay * Math.pow(2, attempt), maxRetryDelay); - // Add jitter to prevent thundering herd - const jitter = Math.random() * 0.3 * delay; - return delay + jitter; - }, - [initialRetryDelay, maxRetryDelay], - ); - - /** - * Cleanup existing channel. - */ - const cleanup = useCallback(async () => { - if (channelRef.current) { - try { - await supabase.removeChannel(channelRef.current); - console.log(`[Realtime] Removed channel for ${table}`); - } catch (err) { - console.error("[Realtime] Error removing channel:", err); - } - channelRef.current = null; - } - - if (retryTimeoutRef.current) { - clearTimeout(retryTimeoutRef.current); - retryTimeoutRef.current = null; - } - }, [table]); - - /** - * Attempt to connect/reconnect to realtime channel. - */ - const connect = useCallback( - async (attempt: number = 0) => { - if (!mountedRef.current) return; - - // Give up after max retries - if (attempt >= maxRetries) { - setStatus("error"); - setError(`Failed to connect after ${maxRetries} attempts. Please refresh the page.`); - console.error(`[Realtime] Max retries (${maxRetries}) exceeded for ${table}`); - return; - } - - // Cleanup any existing channel first - await cleanup(); - - // Update status - if (attempt === 0) { - setStatus("connecting"); - setRetryCount(0); - setError(null); - } else { - setStatus("reconnecting"); - setRetryCount(attempt); - } - - console.log( - `[Realtime] Connecting to ${table} (attempt ${attempt + 1}/${maxRetries})${filter ? ` with filter: ${filter}` : ""}`, - ); - - try { - // Create channel with unique name - const channelName = `public:${table}${filter ? `:${filter}` : ""}:${Date.now()}`; - const channel = supabase.channel(channelName); - - // Setup postgres_changes listener - channel.on( - "postgres_changes", - { - event, - schema: "public", - table, - ...(filter && { filter }), - }, - (payload) => { - if (mountedRef.current) { - console.log(`[Realtime] Received ${event} event:`, payload); - onUpdate(payload); - } - }, - ); - - // Subscribe with status callback - channel.subscribe((subscriptionStatus, err) => { - if (!mountedRef.current) return; - - console.log(`[Realtime] Subscription status: ${subscriptionStatus}`, err || ""); - - if (subscriptionStatus === "SUBSCRIBED") { - setStatus("connected"); - setRetryCount(0); - setError(null); - console.log(`[Realtime] Successfully subscribed to ${table}`); - } else if (subscriptionStatus === "CHANNEL_ERROR") { - console.error(`[Realtime] Channel error for ${table}:`, err); - // Schedule reconnection - const delay = getRetryDelay(attempt); - console.log(`[Realtime] Retrying in ${Math.round(delay / 1000)}s...`); - retryTimeoutRef.current = setTimeout(() => { - connect(attempt + 1); - }, delay); - } else if (subscriptionStatus === "TIMED_OUT") { - console.warn(`[Realtime] Subscription timed out for ${table}`); - // Schedule reconnection - const delay = getRetryDelay(attempt); - console.log(`[Realtime] Retrying in ${Math.round(delay / 1000)}s...`); - retryTimeoutRef.current = setTimeout(() => { - connect(attempt + 1); - }, delay); - } - }); - - channelRef.current = channel; - } catch (err) { - console.error(`[Realtime] Error setting up channel for ${table}:`, err); - // Schedule reconnection - const delay = getRetryDelay(attempt); - console.log(`[Realtime] Retrying in ${Math.round(delay / 1000)}s...`); - retryTimeoutRef.current = setTimeout(() => { - connect(attempt + 1); - }, delay); - } - }, - [table, event, filter, onUpdate, maxRetries, getRetryDelay, cleanup], - ); - - /** - * Manually trigger reconnection. - */ - const reconnect = useCallback(() => { - console.log(`[Realtime] Manual reconnect triggered for ${table}`); - connect(0); - }, [connect, table]); - - /** - * Setup subscription on mount and when dependencies change. - */ - useEffect(() => { - if (!enabled) { - setStatus("disconnected"); - return; - } - - mountedRef.current = true; - connect(0); - - return () => { - mountedRef.current = false; - cleanup(); - }; - }, [enabled, connect, cleanup]); - - return { - status, - retryCount, - error, - reconnect, - }; -}; diff --git a/apps/web/src/pages/SharedShowModePage.tsx b/apps/web/src/pages/SharedShowModePage.tsx index 6a61b59..3b81c0b 100644 --- a/apps/web/src/pages/SharedShowModePage.tsx +++ b/apps/web/src/pages/SharedShowModePage.tsx @@ -4,9 +4,8 @@ import { supabase } from "../lib/supabase"; import { getSharedResource, SharedRunOfShowData } from "../lib/shareUtils"; import Header from "../components/Header"; import Footer from "../components/Footer"; -import { Loader, Clock, AlertTriangle, WifiOff, RefreshCw } from "lucide-react"; +import { Loader, Clock, AlertTriangle } from "lucide-react"; import { RunOfShowItem, CustomColumnDefinition } from "./RunOfShowEditor"; // Assuming these types are exported -import { useRealtimeSubscription } from "../hooks/useRealtimeSubscription"; // Utility functions (can be moved to a shared util file later) const parseDurationToSeconds = (durationStr?: string): number | null => { @@ -87,30 +86,47 @@ const SharedShowModePage: React.FC = () => { fetchSharedData(); }, [shareCode]); - // Real-time subscription with automatic reconnection - const { - status: realtimeStatus, - retryCount, - reconnect, - } = useRealtimeSubscription({ - table: "run_of_shows", - event: "UPDATE", - filter: sharedData?.id ? `id=eq.${sharedData.id}` : undefined, - enabled: !!sharedData?.id, - onUpdate: (payload) => { - console.log("Real-time update received:", payload); - const updatedRunOfShow = payload.new as SharedRunOfShowData; - setSharedData((prevData) => { - if (!prevData) return null; - return { - ...prevData, - live_show_data: updatedRunOfShow.live_show_data, - items: updatedRunOfShow.items || prevData.items, - last_edited: updatedRunOfShow.last_edited || prevData.last_edited, - }; + useEffect(() => { + if (!sharedData || !sharedData.id) return; + + const channel = supabase + .channel(`public:run_of_shows:id=eq.${sharedData.id}`) + .on( + "postgres_changes", + { + event: "UPDATE", + schema: "public", + table: "run_of_shows", + filter: `id=eq.${sharedData.id}`, + }, + (payload) => { + console.log("Real-time update received:", payload); + const updatedRunOfShow = payload.new as SharedRunOfShowData; + setSharedData((prevData) => { + if (!prevData) return null; + return { + ...prevData, + live_show_data: updatedRunOfShow.live_show_data, + items: updatedRunOfShow.items || prevData.items, + last_edited: updatedRunOfShow.last_edited || prevData.last_edited, + }; + }); + }, + ) + .subscribe((status, err) => { + if (status === "SUBSCRIBED") { + console.log("Subscribed to real-time updates for Run of Show ID:", sharedData.id); + } + if (status === "CHANNEL_ERROR" || status === "TIMED_OUT") { + console.error("Real-time subscription error:", status, err); + setError("Connection lost. Please refresh to see live updates."); + } }); - }, - }); + + return () => { + supabase.removeChannel(channel); + }; + }, [sharedData]); // Auto-scroll to current item useEffect(() => { @@ -258,57 +274,14 @@ const SharedShowModePage: React.FC = () => { -
-
-
- - Current Cue -
-
- - Next Cue -
+
+
+ + Current Cue
- - {/* Connection Status Indicator */} -
- {realtimeStatus === "connected" && ( -
- - Live -
- )} - {realtimeStatus === "connecting" && ( -
- - Connecting... -
- )} - {realtimeStatus === "reconnecting" && ( -
- - Reconnecting ({retryCount})... -
- )} - {realtimeStatus === "disconnected" && ( -
- - Offline -
- )} - {realtimeStatus === "error" && ( -
- - Connection failed - -
- )} +
+ + Next Cue