From bb1db2ed7cd8a79b78e4058334460d757e2d4053 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2026 14:06:38 +0000 Subject: [PATCH 1/3] feat: make report generation asynchronous using a job queue - Added a `jobs` table to the database schema for tracking background tasks. - Implemented `lib/actions/jobs.ts` for enqueuing and monitoring jobs. - Created a background worker script in `bin/worker.ts` to process enqueued tasks. - Modified the report processing server action in `app/actions.tsx` to enqueue report context generation as a background job. - Updated the `DownloadReportButton` component to poll for job completion before generating the PDF. - Added a client-side server action wrapper in `lib/actions/jobs-client.ts` for status checking. Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com> --- .env | 1 - app/actions.tsx | 11 ++++- bin/worker.ts | 58 +++++++++++++++++++++++++++ components/download-report-button.tsx | 30 +++++++++++++- lib/actions/jobs-client.ts | 7 ++++ lib/actions/jobs.ts | 54 +++++++++++++++++++++++++ lib/db/schema.ts | 20 +++++++++ 7 files changed, 177 insertions(+), 4 deletions(-) delete mode 100644 .env create mode 100644 bin/worker.ts create mode 100644 lib/actions/jobs-client.ts create mode 100644 lib/actions/jobs.ts diff --git a/.env b/.env deleted file mode 100644 index b454ca74..00000000 --- a/.env +++ /dev/null @@ -1 +0,0 @@ -DATABASE_URL="postgresql://user:password@host:port/db" diff --git a/app/actions.tsx b/app/actions.tsx index 8b693603..783c21ee 100644 --- a/app/actions.tsx +++ b/app/actions.tsx @@ -15,6 +15,8 @@ import { FollowupPanel } from '@/components/followup-panel' import { inquire, researcher, taskManager, querySuggestor, resolutionSearch, type DrawnFeature } from '@/lib/agents' import { writer } from '@/lib/agents/writer' import { saveChat, getSystemPrompt, generateReportContext } from '@/lib/actions/chat' +import { enqueueJob } from '@/lib/actions/jobs' +import { getCurrentUserIdOnServer } from '@/lib/auth/get-current-user' import { Chat, AIMessage } from '@/lib/types' import { UserMessage } from '@/components/user-message' import { BotMessage } from '@/components/message' @@ -57,9 +59,14 @@ async function submit(formData?: FormData, skip?: boolean) { } try { const messages = JSON.parse(messagesString) as AIMessage[]; - return await generateReportContext(messages); + const userId = await getCurrentUserIdOnServer() || 'anonymous'; + + // Enqueue job for asynchronous processing + const jobId = await enqueueJob(userId, 'generate_report_context', { messages }); + + return { jobId, status: 'processing' }; } catch (e) { - console.error('Failed to parse messages for report context:', e); + console.error('Failed to enqueue report context job:', e); return { title: 'QCX Intelligence Analysis', summary: 'Automated executive summary is currently unavailable.' }; } } diff --git a/bin/worker.ts b/bin/worker.ts new file mode 100644 index 00000000..5e1a81dd --- /dev/null +++ b/bin/worker.ts @@ -0,0 +1,58 @@ +import { db } from '../lib/db' +import { jobs } from '../lib/db/schema' +import { eq, and } from 'drizzle-orm' +import { generateReportContext } from '../lib/actions/chat' +import { updateJob } from '../lib/actions/jobs' + +async function processJobs() { + console.log('Worker started, polling for jobs...') + + while (true) { + try { + const pendingJobs = await db.select() + .from(jobs) + .where(eq(jobs.status, 'pending')) + .limit(10) + + for (const job of pendingJobs) { + console.log(`Processing job ${job.id} (${job.type})...`) + + // Mark as processing + await updateJob(job.id, { status: 'processing' }) + + try { + let result: any = null + + if (job.type === 'generate_report_context') { + const { messages } = job.payload as { messages: any[] } + result = await generateReportContext(messages) + } else { + throw new Error(`Unknown job type: ${job.type}`) + } + + // Mark as completed + await updateJob(job.id, { + status: 'completed', + result, + updatedAt: new Date() + }) + console.log(`Job ${job.id} completed successfully.`) + } catch (error) { + console.error(`Job ${job.id} failed:`, error) + await updateJob(job.id, { + status: 'failed', + error: error instanceof Error ? error.message : String(error), + updatedAt: new Date() + }) + } + } + } catch (error) { + console.error('Error in worker loop:', error) + } + + // Wait for 2 seconds before next poll + await new Promise(resolve => setTimeout(resolve, 2000)) + } +} + +processJobs().catch(console.error) diff --git a/components/download-report-button.tsx b/components/download-report-button.tsx index 6aaf658c..9949232c 100644 --- a/components/download-report-button.tsx +++ b/components/download-report-button.tsx @@ -11,6 +11,7 @@ import { AI } from '@/app/actions' import { useActions } from 'ai/rsc' import { toast } from 'sonner' import { ReportTemplate } from './report-template' +import { getJobStatus } from '@/lib/actions/jobs-client' import { createPortal } from 'react-dom' export const DownloadReportButton = () => { @@ -57,7 +58,34 @@ export const DownloadReportButton = () => { formData.append('action', 'generate_report_context'); formData.append('messages', JSON.stringify(aiState.messages)); - const { title, summary } = await (actions as any).submit(formData); + const { jobId } = await (actions as any).submit(formData); + + if (!jobId) { + throw new Error('Failed to start report generation job'); + } + + // Polling for job completion + let jobResult = null; + let attempts = 0; + const maxAttempts = 60; // 60 seconds + + while (attempts < maxAttempts) { + const { status, result, error } = await getJobStatus(jobId); + if (status === 'completed') { + jobResult = result; + break; + } else if (status === 'failed') { + throw new Error(error || 'Job failed'); + } + attempts++; + await new Promise(resolve => setTimeout(resolve, 1000)); + } + + if (!jobResult) { + throw new Error('Report generation timed out'); + } + + const { title, summary } = jobResult as { title: string, summary: string }; const finalTitle = title || 'QCX Intelligence Analysis' setReportTitle(finalTitle) diff --git a/lib/actions/jobs-client.ts b/lib/actions/jobs-client.ts new file mode 100644 index 00000000..a3679bf5 --- /dev/null +++ b/lib/actions/jobs-client.ts @@ -0,0 +1,7 @@ +'use server' + +import { getJobStatus as getJobStatusServer } from './jobs' + +export async function getJobStatus(jobId: string) { + return await getJobStatusServer(jobId) +} diff --git a/lib/actions/jobs.ts b/lib/actions/jobs.ts new file mode 100644 index 00000000..10caa9ec --- /dev/null +++ b/lib/actions/jobs.ts @@ -0,0 +1,54 @@ +'use server' + +import { db } from '@/lib/db' +import { jobs } from '@/lib/db/schema' +import { eq } from 'drizzle-orm' +import { revalidatePath } from 'next/cache' + +export async function enqueueJob(userId: string, type: string, payload: any) { + try { + const result = await db.insert(jobs).values({ + userId, + type, + payload, + status: 'pending', + }).returning({ id: jobs.id }) + + return result[0].id + } catch (error) { + console.error('Error enqueuing job:', error) + throw error + } +} + +export async function getJob(jobId: string) { + try { + const result = await db.select().from(jobs).where(eq(jobs.id, jobId)).limit(1) + return result[0] || null + } catch (error) { + console.error('Error getting job:', error) + return null + } +} + +export async function updateJob(jobId: string, updates: Partial) { + try { + await db.update(jobs) + .set({ ...updates, updatedAt: new Date() }) + .where(eq(jobs.id, jobId)) + } catch (error) { + console.error('Error updating job:', error) + throw error + } +} + +export async function getJobStatus(jobId: string) { + 'use server' + const job = await getJob(jobId) + if (!job) return { status: 'not_found' } + return { + status: job.status, + result: job.result, + error: job.error + } +} diff --git a/lib/db/schema.ts b/lib/db/schema.ts index e8e6b943..2e1e87bb 100644 --- a/lib/db/schema.ts +++ b/lib/db/schema.ts @@ -102,6 +102,18 @@ export const calendarNotes = pgTable('calendar_notes', { updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), }); +export const jobs = pgTable('jobs', { + id: uuid('id').primaryKey().defaultRandom(), + userId: uuid('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }), + type: text('type').notNull(), + status: text('status').notNull().default('pending'), + payload: jsonb('payload'), + result: jsonb('result'), + error: text('error'), + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), + updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), +}); + // Relations export const usersRelations = relations(users, ({ many }) => ({ chats: many(chats), @@ -111,6 +123,14 @@ export const usersRelations = relations(users, ({ many }) => ({ systemPrompts: many(systemPrompts), locations: many(locations), visualizations: many(visualizations), + jobs: many(jobs), +})); + +export const jobsRelations = relations(jobs, ({ one }) => ({ + user: one(users, { + fields: [jobs.userId], + references: [users.id], + }), })); export const chatsRelations = relations(chats, ({ one, many }) => ({ From 536fdc7c8ce8c95aea03ec91e5d4616988984eb6 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 9 Jun 2026 04:07:27 +0000 Subject: [PATCH 2/3] feat: switch report generation to Vercel Queues - Replaced the custom background worker with Vercel Queues for processing. - Updated `app/actions.tsx` to use `@vercel/queue` for job triggering. - Implemented the queue consumer as an API route in `app/api/queue/route.ts`. - Removed the manual `bin/worker.ts` script. Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com> --- app/actions.tsx | 10 ++++++++ app/api/queue/route.ts | 38 +++++++++++++++++++++++++++ bin/worker.ts | 58 ------------------------------------------ bun.lock | 39 ++++++++++++++++++++++++++++ package.json | 1 + 5 files changed, 88 insertions(+), 58 deletions(-) create mode 100644 app/api/queue/route.ts delete mode 100644 bin/worker.ts diff --git a/app/actions.tsx b/app/actions.tsx index 783c21ee..a95373f6 100644 --- a/app/actions.tsx +++ b/app/actions.tsx @@ -17,6 +17,7 @@ import { writer } from '@/lib/agents/writer' import { saveChat, getSystemPrompt, generateReportContext } from '@/lib/actions/chat' import { enqueueJob } from '@/lib/actions/jobs' import { getCurrentUserIdOnServer } from '@/lib/auth/get-current-user' +import { send } from '@vercel/queue' import { Chat, AIMessage } from '@/lib/types' import { UserMessage } from '@/components/user-message' import { BotMessage } from '@/components/message' @@ -64,6 +65,15 @@ async function submit(formData?: FormData, skip?: boolean) { // Enqueue job for asynchronous processing const jobId = await enqueueJob(userId, 'generate_report_context', { messages }); + // Send to Vercel Queue + try { + await send('report-generation', { jobId }); + } catch (queueError) { + console.error('Failed to send to Vercel Queue:', queueError); + // We continue because the worker might still poll if it's running, + // but ideally Vercel Queue handles the trigger. + } + return { jobId, status: 'processing' }; } catch (e) { console.error('Failed to enqueue report context job:', e); diff --git a/app/api/queue/route.ts b/app/api/queue/route.ts new file mode 100644 index 00000000..be958634 --- /dev/null +++ b/app/api/queue/route.ts @@ -0,0 +1,38 @@ +import { handleCallback } from '@vercel/queue'; +import { generateReportContext } from '@/lib/actions/chat'; +import { updateJob, getJob } from '@/lib/actions/jobs'; + +export const POST = handleCallback(async (payload: any) => { + const { jobId } = payload; + if (!jobId) { + console.error('No jobId in queue payload'); + return; + } + + try { + const job = await getJob(jobId); + if (!job) { + console.error(`Job ${jobId} not found`); + return; + } + + await updateJob(jobId, { status: 'processing' }); + + const { messages } = job.payload as { messages: any[] }; + const result = await generateReportContext(messages); + + await updateJob(jobId, { + status: 'completed', + result, + updatedAt: new Date(), + }); + console.log(`Job ${jobId} completed via Vercel Queue`); + } catch (error) { + console.error(`Job ${jobId} failed in Vercel Queue:`, error); + await updateJob(jobId, { + status: 'failed', + error: error instanceof Error ? error.message : String(error), + updatedAt: new Date(), + }); + } +}); diff --git a/bin/worker.ts b/bin/worker.ts deleted file mode 100644 index 5e1a81dd..00000000 --- a/bin/worker.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { db } from '../lib/db' -import { jobs } from '../lib/db/schema' -import { eq, and } from 'drizzle-orm' -import { generateReportContext } from '../lib/actions/chat' -import { updateJob } from '../lib/actions/jobs' - -async function processJobs() { - console.log('Worker started, polling for jobs...') - - while (true) { - try { - const pendingJobs = await db.select() - .from(jobs) - .where(eq(jobs.status, 'pending')) - .limit(10) - - for (const job of pendingJobs) { - console.log(`Processing job ${job.id} (${job.type})...`) - - // Mark as processing - await updateJob(job.id, { status: 'processing' }) - - try { - let result: any = null - - if (job.type === 'generate_report_context') { - const { messages } = job.payload as { messages: any[] } - result = await generateReportContext(messages) - } else { - throw new Error(`Unknown job type: ${job.type}`) - } - - // Mark as completed - await updateJob(job.id, { - status: 'completed', - result, - updatedAt: new Date() - }) - console.log(`Job ${job.id} completed successfully.`) - } catch (error) { - console.error(`Job ${job.id} failed:`, error) - await updateJob(job.id, { - status: 'failed', - error: error instanceof Error ? error.message : String(error), - updatedAt: new Date() - }) - } - } - } catch (error) { - console.error('Error in worker loop:', error) - } - - // Wait for 2 seconds before next poll - await new Promise(resolve => setTimeout(resolve, 2000)) - } -} - -processJobs().catch(console.error) diff --git a/bun.lock b/bun.lock index 5e0fc295..d8f985e0 100644 --- a/bun.lock +++ b/bun.lock @@ -40,6 +40,7 @@ "@types/pg": "^8.15.4", "@upstash/redis": "^1.35.0", "@vercel/analytics": "^1.5.0", + "@vercel/queue": "^0.3.0", "@vercel/speed-insights": "^1.2.0", "@vis.gl/react-google-maps": "^1.7.1", "ai": "^4.3.19", @@ -1057,6 +1058,14 @@ "@vercel/analytics": ["@vercel/analytics@1.6.1", "", { "peerDependencies": { "@remix-run/react": "^2", "@sveltejs/kit": "^1 || ^2", "next": ">= 13", "react": "^18 || ^19 || ^19.0.0-rc", "svelte": ">= 4", "vue": "^3", "vue-router": "^4" }, "optionalPeers": ["@remix-run/react", "@sveltejs/kit", "next", "react", "svelte", "vue", "vue-router"] }, "sha512-oH9He/bEM+6oKlv3chWuOOcp8Y6fo6/PSro8hEkgCW3pu9/OiCXiUpRUogDh3Fs3LH2sosDrx8CxeOLBEE+afg=="], + "@vercel/cli-config": ["@vercel/cli-config@0.2.0", "", { "dependencies": { "xdg-app-paths": "5", "zod": "4.1.11" } }, "sha512-fJRRRB7734BDuXZ89yBEaA2ncYhH7bWX30mk04W80J6VAfQc+4iB8lyzAdaGpFV3/vNlkt9VZt+/uoQoWX6UsQ=="], + + "@vercel/cli-exec": ["@vercel/cli-exec@0.1.1", "", { "dependencies": { "execa": "5.1.1" } }, "sha512-LMRMEai3Z+BODyxGcU9+KiWrS/UElNiOLKiNRfGNt2Vu3NTEmXgFeXG9wBfocAnTe5yJCX/DY6k3k7S/LkPp/g=="], + + "@vercel/oidc": ["@vercel/oidc@3.6.1", "", { "dependencies": { "@vercel/cli-config": "0.2.0", "@vercel/cli-exec": "0.1.1", "jose": "^5.9.6" } }, "sha512-8ipTFoiX3WBRrvXLjSrmgAiwtMDQk3EgSxe8N7v2rXBz39NBIIyoGXeVbJRoBcP8WEuVnvjvIQsggbGU7ZKrMw=="], + + "@vercel/queue": ["@vercel/queue@0.3.0", "", { "dependencies": { "@vercel/oidc": "^3.0.5", "minimatch": "^10.2.4", "mixpart": "0.0.6", "picocolors": "^1.1.1" } }, "sha512-TGZlA2GGZtUVR592d5ORAqZXqWqUs7JD8MjazXMv07AxpsbLtUAbCxXOqRj/weoX7j6LrfZfD6cGfd7Llk24/Q=="], + "@vercel/speed-insights": ["@vercel/speed-insights@1.3.1", "", { "peerDependencies": { "@sveltejs/kit": "^1 || ^2", "next": ">= 13", "react": "^18 || ^19 || ^19.0.0-rc", "svelte": ">= 4", "vue": "^3", "vue-router": "^4" }, "optionalPeers": ["@sveltejs/kit", "next", "react", "svelte", "vue", "vue-router"] }, "sha512-PbEr7FrMkUrGYvlcLHGkXdCkxnylCWePx7lPxxq36DNdfo9mcUjLOmqOyPDHAOgnfqgGGdmE3XI9L/4+5fr+vQ=="], "@vis.gl/react-google-maps": ["@vis.gl/react-google-maps@1.7.1", "", { "dependencies": { "@types/google.maps": "^3.54.10", "fast-deep-equal": "^3.1.3" }, "peerDependencies": { "react": ">=16.8.0 || ^19.0 || ^19.0.0-rc", "react-dom": ">=16.8.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-F/GJzJyri7Jqf+bkLNxoi2RcH2hCIo1I3//PyiILqQzdzglMoqZVO1DLXlHPifNdebk1/zib6dMJA3i73nwmuQ=="], @@ -1437,6 +1446,8 @@ "exa-js": ["exa-js@1.10.2", "", { "dependencies": { "cross-fetch": "~4.1.0", "dotenv": "~16.4.7", "openai": "^5.0.1", "zod": "^3.22.0", "zod-to-json-schema": "^3.20.0" } }, "sha512-nObBipoXKL5uL6Dc8Q3c9GA4xEfunQMdGGqtcCkQSNilzR6AExyVkBxTdpWqC20jKa3/dvXu6otXQLaRyZNqGw=="], + "execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="], + "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], "express-rate-limit": ["express-rate-limit@7.5.1", "", { "peerDependencies": { "express": ">= 4.11" } }, "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw=="], @@ -1535,6 +1546,8 @@ "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + "get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="], + "get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="], "get-tsconfig": ["get-tsconfig@4.13.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="], @@ -1613,6 +1626,8 @@ "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + "human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="], + "humanize-ms": ["humanize-ms@1.2.1", "", { "dependencies": { "ms": "^2.0.0" } }, "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ=="], "iceberg-js": ["iceberg-js@0.8.1", "", {}, "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA=="], @@ -1867,6 +1882,8 @@ "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], + "merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="], + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], "mgrs": ["mgrs@1.0.0", "", {}, "sha512-awNbTOqCxK1DBGjalK3xqWIstBZgN6fxsMSiXLs9/spqWkF2pAhb2rrYCFSsr1/tT7PhcDGjZndG8SWYn0byYA=="], @@ -1945,6 +1962,8 @@ "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], + "mixpart": ["mixpart@0.0.6", "", {}, "sha512-CRdXtgfQH2jARmtNmPR0Q7jL20fiESbaYk1b0KvLD0jCdUuemepREtsbd8nbiY6BHV9OGGddAZITNXklupUPUQ=="], + "moo-server": ["moo-server@1.3.0", "", {}, "sha512-9A8/eor2DXwpv1+a4pZAAydqLFVrWoKoO1fzdzqLUhYVXAO1Kgd1FR2gFZi7YdHzF0s4W8cDNwCfKJQrvLqxDw=="], "motion-dom": ["motion-dom@12.24.11", "", { "dependencies": { "motion-utils": "^12.24.10" } }, "sha512-DlWOmsXMJrV8lzZyd+LKjG2CXULUs++bkq8GZ2Sr0R0RRhs30K2wtY+LKiTjhmJU3W61HK+rB0GLz6XmPvTA1A=="], @@ -1977,6 +1996,8 @@ "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], + "npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="], + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], "object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="], @@ -2011,6 +2032,8 @@ "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], + "os-paths": ["os-paths@4.4.0", "", {}, "sha512-wrAwOeXp1RRMFfQY8Sy7VaGVmPocaLwSFOYCGKSyo8qmJ+/yaafCl5BCA1IQZWqFSRBrKDYFeR9d/VyQzfH/jg=="], + "own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="], "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], @@ -2347,6 +2370,8 @@ "strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="], + "strip-final-newline": ["strip-final-newline@2.0.0", "", {}, "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="], + "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], "strnum": ["strnum@1.1.2", "", {}, "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA=="], @@ -2563,6 +2588,10 @@ "wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="], + "xdg-app-paths": ["xdg-app-paths@5.5.1", "", { "dependencies": { "os-paths": "^4.0.1", "xdg-portable": "^7.2.0" } }, "sha512-hI3flOB4PLZIy5prbtTpirobtPE2ZtZ52szO+2mM9Efp6ErM398La+C1lIpNWDfNoQk+6Lsi6nMcCwVB7pxeMQ=="], + + "xdg-portable": ["xdg-portable@7.3.0", "", { "dependencies": { "os-paths": "^4.0.1" } }, "sha512-sqMMuL1rc0FmMBOzCpd0yuy9trqF2yTTVe+E9ogwCSWQCdDEtQUwrZPT6AxqtsFGRNxycgncbP/xmOOSPw5ZUw=="], + "xml-utils": ["xml-utils@1.10.2", "", {}, "sha512-RqM+2o1RYs6T8+3DzDSoTRAUfrvaejbVHcp3+thnAtDKo8LskR+HomLajEy5UjTz24rpka7AxVBRR3g2wTUkJA=="], "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="], @@ -2681,6 +2710,12 @@ "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], + "@vercel/cli-config/zod": ["zod@4.1.11", "", {}, "sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg=="], + + "@vercel/oidc/jose": ["jose@5.10.0", "", {}, "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg=="], + + "@vercel/queue/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], + "ai/@ai-sdk/provider": ["@ai-sdk/provider@1.1.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg=="], "ai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@2.2.8", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "nanoid": "^3.3.8", "secure-json-parse": "^2.7.0" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA=="], @@ -2887,6 +2922,8 @@ "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + "@vercel/queue/minimatch/brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="], + "ajv-formats/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], "cli-table3/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], @@ -2929,6 +2966,8 @@ "@types/request/form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + "@vercel/queue/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + "open-codex/openai/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], } } diff --git a/package.json b/package.json index 84291c6e..cc6de7fd 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "@types/pg": "^8.15.4", "@upstash/redis": "^1.35.0", "@vercel/analytics": "^1.5.0", + "@vercel/queue": "^0.3.0", "@vercel/speed-insights": "^1.2.0", "@vis.gl/react-google-maps": "^1.7.1", "ai": "^4.3.19", From 5e2072904726c6f6a887953c175800075be22d53 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 9 Jun 2026 04:24:22 +0000 Subject: [PATCH 3/3] fix: satisfy Next.js route type requirements for Vercel Queue handler - Wrapped `handleCallback` in a standard `POST` export in `app/api/queue/route.ts`. - This ensures the build process recognizes it as a valid API route. Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com> --- app/api/queue/route.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/api/queue/route.ts b/app/api/queue/route.ts index be958634..d942964e 100644 --- a/app/api/queue/route.ts +++ b/app/api/queue/route.ts @@ -1,8 +1,9 @@ import { handleCallback } from '@vercel/queue'; import { generateReportContext } from '@/lib/actions/chat'; import { updateJob, getJob } from '@/lib/actions/jobs'; +import { NextRequest } from 'next/server'; -export const POST = handleCallback(async (payload: any) => { +const queueHandler = handleCallback(async (payload: any) => { const { jobId } = payload; if (!jobId) { console.error('No jobId in queue payload'); @@ -36,3 +37,7 @@ export const POST = handleCallback(async (payload: any) => { }); } }); + +export async function POST(request: NextRequest) { + return queueHandler(request); +}