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:
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* jobRunner — the single source of truth for enqueueing async jobs.
|
||||
*
|
||||
* Uses InProcessQueue (no Redis) instead of BullMQ.
|
||||
* When a job finishes the worker emits a `job:update` Socket.IO event
|
||||
* to the originating client so the frontend can react in real time.
|
||||
*/
|
||||
import { InProcessQueue } from "./inProcessQueue";
|
||||
import { io } from "../socket";
|
||||
import { runEligibilityProcessor } from "./processors/eligibilityProcessor";
|
||||
import { runClaimStatusProcessor } from "./processors/claimStatusProcessor";
|
||||
import { runClaimSubmitProcessor } from "./processors/claimSubmitProcessor";
|
||||
import { runOcrProcessor } from "./processors/ocrProcessor";
|
||||
import type { SeleniumJobData, OcrJobData } from "./queues";
|
||||
|
||||
// ── Queue instances ──────────────────────────────────────────────────────────
|
||||
// Selenium: 1 browser at a time (mirrors Python semaphore)
|
||||
const seleniumQ = new InProcessQueue<SeleniumJobData>(1);
|
||||
// OCR: allow 2 concurrent (mirrors Python MAX_CONCURRENCY=2)
|
||||
const ocrQ = new InProcessQueue<OcrJobData>(2);
|
||||
|
||||
// ── WebSocket helper ─────────────────────────────────────────────────────────
|
||||
function emitJobUpdate(
|
||||
socketId: string | undefined,
|
||||
jobId: string,
|
||||
jobType: string,
|
||||
status: "active" | "completed" | "failed",
|
||||
extra: Record<string, any> = {}
|
||||
) {
|
||||
const payload = { jobId, jobType, status, ...extra };
|
||||
if (socketId && io) {
|
||||
io.to(socketId).emit("job:update", payload);
|
||||
} else if (io) {
|
||||
io.emit("job:update", payload);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Selenium enqueue ─────────────────────────────────────────────────────────
|
||||
export function enqueueSeleniumJob(data: SeleniumJobData): string {
|
||||
const { jobType, socketId } = data;
|
||||
|
||||
const jobId = seleniumQ.add(data, async (job) => {
|
||||
emitJobUpdate(socketId, job.id, jobType, "active", {
|
||||
message: "Selenium browser starting…",
|
||||
});
|
||||
|
||||
if (jobType === "eligibility-check") {
|
||||
return runEligibilityProcessor({
|
||||
enrichedPayload: data.enrichedPayload,
|
||||
userId: data.userId,
|
||||
insuranceId: data.insuranceId!,
|
||||
formFirstName: data.formFirstName,
|
||||
formLastName: data.formLastName,
|
||||
formDob: data.formDob,
|
||||
});
|
||||
}
|
||||
if (jobType === "claim-status-check") {
|
||||
return runClaimStatusProcessor({
|
||||
enrichedPayload: data.enrichedPayload,
|
||||
insuranceId: data.insuranceId!,
|
||||
});
|
||||
}
|
||||
if (jobType === "claim-submit" || jobType === "claim-pre-auth") {
|
||||
return runClaimSubmitProcessor({
|
||||
enrichedPayload: data.enrichedPayload,
|
||||
files: data.files ?? [],
|
||||
claimId: data.claimId,
|
||||
variant: jobType === "claim-pre-auth" ? "claim-pre-auth" : "claimsubmit",
|
||||
});
|
||||
}
|
||||
throw new Error(`Unknown selenium jobType: ${jobType}`);
|
||||
});
|
||||
|
||||
// Attach completion/failure callbacks after the job is in the queue.
|
||||
// We poll the job object once per tick until it settles.
|
||||
(async () => {
|
||||
while (true) {
|
||||
await new Promise((r) => setTimeout(r, 500));
|
||||
const job = seleniumQ.getJob(jobId);
|
||||
if (!job) break;
|
||||
if (job.status === "completed") {
|
||||
emitJobUpdate(socketId, jobId, jobType, "completed", {
|
||||
result: job.result,
|
||||
});
|
||||
console.log(`[seleniumQ] job ${jobId} (${jobType}) completed`);
|
||||
break;
|
||||
}
|
||||
if (job.status === "failed") {
|
||||
emitJobUpdate(socketId, jobId, jobType, "failed", {
|
||||
error: job.error,
|
||||
});
|
||||
console.error(`[seleniumQ] job ${jobId} (${jobType}) failed:`, job.error);
|
||||
break;
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
return jobId;
|
||||
}
|
||||
|
||||
// ── OCR enqueue ──────────────────────────────────────────────────────────────
|
||||
export function enqueueOcrJob(data: OcrJobData): string {
|
||||
const { socketId } = data;
|
||||
|
||||
const jobId = ocrQ.add(data, async () => {
|
||||
emitJobUpdate(socketId, jobId, "ocr", "active", {
|
||||
message: "OCR processing started…",
|
||||
});
|
||||
return runOcrProcessor({ files: data.files });
|
||||
});
|
||||
|
||||
(async () => {
|
||||
while (true) {
|
||||
await new Promise((r) => setTimeout(r, 500));
|
||||
const job = ocrQ.getJob(jobId);
|
||||
if (!job) break;
|
||||
if (job.status === "completed") {
|
||||
emitJobUpdate(socketId, jobId, "ocr", "completed", {
|
||||
result: { rows: job.result },
|
||||
});
|
||||
console.log(`[ocrQ] job ${jobId} completed`);
|
||||
break;
|
||||
}
|
||||
if (job.status === "failed") {
|
||||
emitJobUpdate(socketId, jobId, "ocr", "failed", { error: job.error });
|
||||
console.error(`[ocrQ] job ${jobId} failed:`, job.error);
|
||||
break;
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
return jobId;
|
||||
}
|
||||
Reference in New Issue
Block a user