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            36x 36x 36x     36x     36x   36x 36x     36x 2x 2x 2x         36x 19x   19x 27x 27x 27x 27x         18x 16x 14x 14x     10x 10x 10x     26x       19x 19x   19x 19x 19x         36x 24x 2x         22x 1x                 36x 32x 6x 6x         26x   2x 2x               36x 32x 32x     32x   32x   2x       36x   3x 2x               2x   1x         36x 5x 5x               36x                  
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);
      window.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,
  };
}