All files / client/src/hooks use-backend-health.ts

100% Statements 61/61
87.5% Branches 35/40
100% Functions 12/12
100% Lines 58/58

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152            37x 37x 37x     37x     37x   37x 37x     37x 2x 2x 2x         37x 19x   19x 29x 29x 29x 29x         22x 20x 18x 18x     8x 8x 8x     28x       19x 19x   19x 19x 19x         37x 23x 2x         21x 1x                 37x 31x 6x 6x         25x   2x 2x               37x 31x 31x     31x   31x   2x       37x   3x 2x               2x   1x         37x 5x 5x               37x                  
import { useState, useEffect, useRef, useCallback } from "react";
import { useToast } from "@/hooks/use-toast";
import { useWebSocket } from "@/hooks/use-websocket";
import type { QueryClient } from "@tanstack/react-query";
 
export function useBackendHealth(queryClient: QueryClient) {
  const [backendReachable, setBackendReachable] = useState(true);
  const [backendPingError, setBackendPingError] = useState<string | null>(null);
  const [showErrorGlitch, setShowErrorGlitch] = useState(false);
 
  // Ref to track if backend was ever unreachable (for recovery toast)
  const wasBackendUnreachableRef = useRef(false);
 
  // Ref to track previous backend reachable state for detecting transitions
  const prevBackendReachableRef = useRef(true);
 
  const { toast } = useToast();
  const { isConnected, connectionError, hasEverConnected } = useWebSocket();
 
  // Trigger visual glitch effect on compilation error
  const triggerErrorGlitch = useCallback((duration = 600) => {
    try {
      setShowErrorGlitch(true);
      globalThis.setTimeout(() => setShowErrorGlitch(false), duration);
    } catch {}
  }, []);
 
  // Lightweight backend ping every second
  useEffect(() => {
    let cancelled = false;
 
    const ping = async () => {
      const controller = new AbortController();
      const timeout = setTimeout(() => controller.abort(), 800);
      try {
        const res = await fetch("/api/health", {
          method: "GET",
          cache: "no-store",
          signal: controller.signal,
        });
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        if (!cancelled) {
          setBackendReachable(true);
          setBackendPingError(null);
        }
      } catch (err) {
        Eif (!cancelled) {
          setBackendReachable(false);
          setBackendPingError((err as Error)?.message || "Health check failed");
        }
      } finally {
        clearTimeout(timeout);
      }
    };
 
    const interval = setInterval(ping, 1000);
    ping();
 
    return () => {
      cancelled = true;
      clearInterval(interval);
    };
  }, []);
 
  // WebSocket reachability notifications
  useEffect(() => {
    if (connectionError) {
      toast({
        title: "Backend unreachable",
        description: connectionError,
        variant: "destructive",
      });
    } else if (!isConnected && hasEverConnected) {
      toast({
        title: "Connection lost",
        description: "Trying to re-establish backend connection...",
        variant: "destructive",
      });
    }
  }, [connectionError, isConnected, hasEverConnected, toast]);
 
  // Show toast when HTTP backend becomes unreachable or recovers
  useEffect(() => {
    if (!backendReachable) {
      wasBackendUnreachableRef.current = true;
      toast({
        title: "Backend unreachable",
        description: backendPingError || "Could not reach API server.",
        variant: "destructive",
      });
    } else if (backendReachable && wasBackendUnreachableRef.current) {
      // Backend recovered after being unreachable
      wasBackendUnreachableRef.current = false;
      toast({
        title: "Backend reachable again",
        description: "Connection restored.",
      });
    }
  }, [backendReachable, backendPingError, toast]);
 
  // Refetch sketches when backend becomes reachable again (false -> true transition)
  useEffect(() => {
    const wasUnreachable = !prevBackendReachableRef.current;
    const isNowReachable = backendReachable;
 
    // Update the ref for next check
    prevBackendReachableRef.current = backendReachable;
 
    if (wasUnreachable && isNowReachable) {
      // Backend just transitioned from unreachable to reachable
      queryClient.refetchQueries({ queryKey: ["/api/sketches"] });
    }
  }, [backendReachable, queryClient]);
 
  const ensureBackendConnected = useCallback(
    (actionLabel: string) => {
      if (!backendReachable || !isConnected) {
        toast({
          title: "Backend unreachable",
          description:
            backendPingError ||
            connectionError ||
            `${actionLabel} failed because the backend is not reachable. Please check the server or retry in a moment.`,
          variant: "destructive",
        });
        return false;
      }
      return true;
    },
    [backendReachable, isConnected, backendPingError, connectionError, toast],
  );
 
  const isBackendUnreachableError = useCallback((error: unknown) => {
    const message = (error as Error | undefined)?.message || "";
    return (
      message.includes("Failed to fetch") ||
      message.includes("NetworkError") ||
      message.includes("ERR_CONNECTION") ||
      message.includes("Network request failed")
    );
  }, []);
 
  return {
    backendReachable,
    backendPingError,
    showErrorGlitch,
    ensureBackendConnected,
    isBackendUnreachableError,
    triggerErrorGlitch,
  };
}