← back to Govarbitrage

src/worker/index.ts

67 lines

import { prisma } from "@/lib/db";
import { runResearch } from "@/pipeline/research";
import { ollamaReachable } from "@/lib/ai";

// Research worker. Two modes:
//   • REDIS_URL set   → BullMQ worker consuming a "research" queue.
//   • REDIS_URL unset → direct poll loop over PENDING listings (fine at this scale).

const QUEUE = "research";

async function processListing(listingId: string) {
  await prisma.listing.update({ where: { id: listingId }, data: { researchStatus: "QUEUED" } });
  try {
    const r = await runResearch(listingId, { useAI: true, writeIdentity: true });
    console.log(`✓ researched ${listingId} — net ${r.expectedNetProfit.toFixed(0)}, ROI ${(r.roi * 100).toFixed(0)}%`);
  } catch (e) {
    await prisma.listing.update({ where: { id: listingId }, data: { researchStatus: "FAILED" } });
    await prisma.listingEvent.create({
      data: { listingId, type: "RESEARCH_FAILED", message: (e as Error).message },
    });
    console.error(`✗ ${listingId}: ${(e as Error).message}`);
  }
}

async function pendingIds(): Promise<string[]> {
  const rows = await prisma.listing.findMany({
    where: { researchStatus: { in: ["PENDING", "QUEUED"] } },
    select: { id: true },
    take: 25,
  });
  return rows.map((r) => r.id);
}

async function main() {
  const local = await ollamaReachable();
  console.log(`GovArbitrage worker starting. Local AI (Ollama) reachable: ${local}`);

  if (process.env.REDIS_URL) {
    // BullMQ mode (optional dependency).
    const { Worker, Queue } = await import("bullmq");
    const connection = { url: process.env.REDIS_URL };
    const queue = new Queue(QUEUE, { connection });
    // Enqueue any currently-pending listings.
    for (const id of await pendingIds()) await queue.add("research", { listingId: id });
    const worker = new Worker(
      QUEUE,
      async (job) => processListing(job.data.listingId as string),
      { connection, concurrency: 2 },
    );
    worker.on("completed", (job) => console.log(`job ${job.id} done`));
    console.log("BullMQ worker online.");
  } else {
    // Direct poll loop.
    console.log("No REDIS_URL — running direct poll loop (Ctrl-C to stop).");
    for (;;) {
      const ids = await pendingIds();
      for (const id of ids) await processListing(id);
      await new Promise((r) => setTimeout(r, 5000));
    }
  }
}

main().catch((e) => {
  console.error(e);
  process.exit(1);
});