From e6d937fd9b16bb2ff32b702235315542ade38653 Mon Sep 17 00:00:00 2001 From: Anthony G <1@anthony.sh> Date: Mon, 10 Aug 2026 14:46:30 +0200 Subject: [PATCH] feat: add worker main poll loop --- worker/__tests__/index.test.ts | 51 ++++++++++++++++++++++++++++++++++ worker/index.ts | 41 +++++++++++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 worker/__tests__/index.test.ts create mode 100644 worker/index.ts diff --git a/worker/__tests__/index.test.ts b/worker/__tests__/index.test.ts new file mode 100644 index 0000000..e2dde55 --- /dev/null +++ b/worker/__tests__/index.test.ts @@ -0,0 +1,51 @@ +import { prisma } from '@/lib/prisma' +import * as processor from '../processor' + +jest.mock('@/lib/prisma', () => ({ + prisma: { + download: { + findMany: jest.fn(), + updateMany: jest.fn(), + }, + }, +})) +jest.mock('../processor') + +const mockFindMany = prisma.download.findMany as jest.Mock +const mockUpdateMany = prisma.download.updateMany as jest.Mock +const mockProcess = processor.processDownload as jest.Mock + +describe('pollOnce', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + it('processes all PENDING downloads', async () => { + mockFindMany.mockResolvedValue([{ id: 'dl-1' }, { id: 'dl-2' }]) + mockUpdateMany.mockResolvedValue({ count: 2 }) + mockProcess.mockResolvedValue(undefined) + + const { pollOnce } = await import('../index') + await pollOnce() + + expect(mockUpdateMany).toHaveBeenCalledWith( + expect.objectContaining({ + where: expect.objectContaining({ status: 'PENDING' }), + data: expect.objectContaining({ status: 'PROCESSING' }), + }) + ) + expect(mockProcess).toHaveBeenCalledTimes(2) + expect(mockProcess).toHaveBeenCalledWith('dl-1') + expect(mockProcess).toHaveBeenCalledWith('dl-2') + }) + + it('does nothing when queue is empty', async () => { + mockFindMany.mockResolvedValue([]) + + const { pollOnce } = await import('../index') + await pollOnce() + + expect(mockUpdateMany).not.toHaveBeenCalled() + expect(mockProcess).not.toHaveBeenCalled() + }) +}) diff --git a/worker/index.ts b/worker/index.ts new file mode 100644 index 0000000..2730ea9 --- /dev/null +++ b/worker/index.ts @@ -0,0 +1,41 @@ +import { prisma } from '@/lib/prisma' +import { config } from '../config/app.config' +import { processDownload } from './processor' + +export async function pollOnce(): Promise { + const pending = await prisma.download.findMany({ + where: { status: 'PENDING' }, + orderBy: { submittedAt: 'asc' }, + take: config.WORKER_CONCURRENCY, + select: { id: true }, + }) + + if (pending.length === 0) return + + const ids = pending.map((d) => d.id) + + await prisma.download.updateMany({ + where: { id: { in: ids }, status: 'PENDING' }, + data: { status: 'PROCESSING', startedAt: new Date() }, + }) + + await Promise.all(ids.map((id) => processDownload(id))) +} + +export async function runWorker(): Promise { + console.log('[worker] started') + while (true) { + try { + await pollOnce() + } catch (err) { + console.error('[worker] poll error:', err) + } + await new Promise((resolve) => + setTimeout(resolve, config.WORKER_POLL_INTERVAL_MS) + ) + } +} + +if (require.main === module) { + runWorker().catch(console.error) +}