feat: add BullMQ queue infrastructure and frontend job status hook
- apps/Backend/src/queue/: connection, queues, workers, processors - apps/Frontend/src/hooks/use-job-status.ts: WebSocket job progress hook - apps/Frontend/src/lib/socket.ts: shared Socket.IO singleton Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
78
apps/Frontend/src/hooks/use-job-status.ts
Normal file
78
apps/Frontend/src/hooks/use-job-status.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* useJobStatus — tracks a BullMQ job via WebSocket `job:update` events.
|
||||
*
|
||||
* Usage:
|
||||
* const { status, result, error } = useJobStatus(jobId);
|
||||
*
|
||||
* The hook listens for `job:update` events emitted by the backend workers.
|
||||
* When the jobId changes, the previous listener is removed and a fresh one
|
||||
* is registered for the new job.
|
||||
*/
|
||||
import { useEffect, useState } from "react";
|
||||
import { socket } from "@/lib/socket";
|
||||
|
||||
export type JobStatus = "queued" | "active" | "completed" | "failed" | null;
|
||||
|
||||
export interface JobUpdatePayload {
|
||||
jobId: string;
|
||||
jobType: string;
|
||||
status: JobStatus;
|
||||
message?: string;
|
||||
result?: any;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface UseJobStatusReturn {
|
||||
status: JobStatus;
|
||||
message: string;
|
||||
result: any;
|
||||
error: string | null;
|
||||
socketId: string | null;
|
||||
}
|
||||
|
||||
export function useJobStatus(jobId: string | null): UseJobStatusReturn {
|
||||
const [status, setStatus] = useState<JobStatus>(jobId ? "queued" : null);
|
||||
const [message, setMessage] = useState("");
|
||||
const [result, setResult] = useState<any>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [socketId, setSocketId] = useState<string | null>(
|
||||
socket.id ?? null
|
||||
);
|
||||
|
||||
// Keep socketId in sync with the socket connection
|
||||
useEffect(() => {
|
||||
const onConnect = () => setSocketId(socket.id ?? null);
|
||||
socket.on("connect", onConnect);
|
||||
if (socket.connected) setSocketId(socket.id ?? null);
|
||||
return () => { socket.off("connect", onConnect); };
|
||||
}, []);
|
||||
|
||||
// Reset state when the jobId changes
|
||||
useEffect(() => {
|
||||
if (!jobId) {
|
||||
setStatus(null);
|
||||
setMessage("");
|
||||
setResult(null);
|
||||
setError(null);
|
||||
return;
|
||||
}
|
||||
|
||||
setStatus("queued");
|
||||
setMessage("");
|
||||
setResult(null);
|
||||
setError(null);
|
||||
|
||||
const handler = (payload: JobUpdatePayload) => {
|
||||
if (payload.jobId !== jobId) return;
|
||||
setStatus(payload.status);
|
||||
if (payload.message) setMessage(payload.message);
|
||||
if (payload.result !== undefined) setResult(payload.result);
|
||||
if (payload.error) setError(payload.error);
|
||||
};
|
||||
|
||||
socket.on("job:update", handler);
|
||||
return () => { socket.off("job:update", handler); };
|
||||
}, [jobId]);
|
||||
|
||||
return { status, message, result, error, socketId };
|
||||
}
|
||||
24
apps/Frontend/src/lib/socket.ts
Normal file
24
apps/Frontend/src/lib/socket.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Shared Socket.IO client singleton.
|
||||
*
|
||||
* Import `socket` anywhere in the frontend to use the shared connection.
|
||||
* The socket connects lazily — the first import triggers the connection.
|
||||
*/
|
||||
import { io, Socket } from "socket.io-client";
|
||||
|
||||
const SOCKET_URL =
|
||||
import.meta.env.VITE_API_BASE_URL_BACKEND ||
|
||||
(typeof window !== "undefined" ? window.location.origin : "");
|
||||
|
||||
export const socket: Socket = io(SOCKET_URL, {
|
||||
withCredentials: true,
|
||||
autoConnect: true,
|
||||
});
|
||||
|
||||
socket.on("connect", () => {
|
||||
console.log("[socket] connected:", socket.id);
|
||||
});
|
||||
|
||||
socket.on("disconnect", () => {
|
||||
console.log("[socket] disconnected");
|
||||
});
|
||||
Reference in New Issue
Block a user