Files
ulweb/web/lib/use-job-stream.ts
2026-08-20 22:00:38 -05:00

93 lines
2.4 KiB
TypeScript

import { useEffect, useRef, useState } from "react";
import type { LiveStatus, MapPoint } from "../components/map/types";
export type JobStreamState = "connecting" | "live" | "offline" | "error";
// Subscribes to live points + live position status for a job over the
// backend WebSocket. Reconnects with capped exponential backoff; resubscribes
// on reconnect.
export function useJobStream(
jobId: string | null,
onPoints: (points: MapPoint[]) => void,
onStatus?: (status: LiveStatus) => void,
) {
const [connectionState, setConnectionState] =
useState<JobStreamState>("connecting");
const pointsRef = useRef(onPoints);
pointsRef.current = onPoints;
const statusRef = useRef(onStatus);
statusRef.current = onStatus;
useEffect(() => {
if (!jobId) {
setConnectionState("offline");
return;
}
let socket: WebSocket | null = null;
let closed = false;
let attempt = 0;
let timer: ReturnType<typeof setTimeout> | null = null;
const connect = () => {
let socketErrored = false;
setConnectionState(attempt === 0 ? "connecting" : "offline");
const proto = window.location.protocol === "https:" ? "wss" : "ws";
socket = new WebSocket(`${proto}://${window.location.host}/api/ws`);
socket.onopen = () => {
attempt = 0;
setConnectionState("live");
socket?.send(
JSON.stringify({ type: "subscribe", channel: `job:${jobId}` }),
);
};
socket.onmessage = (event) => {
try {
const msg = JSON.parse(event.data);
if (msg.jobId !== jobId) {
return;
}
if (msg.type === "points") {
pointsRef.current(msg.points);
} else if (msg.type === "status") {
statusRef.current?.(msg as LiveStatus);
}
} catch {
// ignore malformed frames
}
};
socket.onerror = () => {
socketErrored = true;
setConnectionState("error");
};
socket.onclose = () => {
if (closed) {
return;
}
if (!socketErrored) {
setConnectionState("offline");
}
attempt += 1;
const delay = Math.min(1000 * 2 ** attempt, 15000);
timer = setTimeout(connect, delay);
};
};
connect();
return () => {
closed = true;
if (timer) {
clearTimeout(timer);
}
socket?.close();
};
}, [jobId]);
return connectionState;
}