93 lines
2.4 KiB
TypeScript
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;
|
|
}
|