Skip to content

Make report generation asynchronous using a job queue#663

Open
ngoiyaeric wants to merge 3 commits into
mainfrom
feat/async-report-generation-14383399858639898697
Open

Make report generation asynchronous using a job queue#663
ngoiyaeric wants to merge 3 commits into
mainfrom
feat/async-report-generation-14383399858639898697

Conversation

@ngoiyaeric

@ngoiyaeric ngoiyaeric commented Jun 8, 2026

Copy link
Copy Markdown
Collaborator

Implemented a database-backed job queue to make report generation asynchronous. This change adds a 'jobs' table, a background worker script, and updates the frontend to poll for job completion, ensuring that users can continue to interact with the system while reports are being generated.


PR created automatically by Jules for task 14383399858639898697 started by @ngoiyaeric

Summary by CodeRabbit

  • New Features
    • Report generation now processes asynchronously as background jobs instead of synchronously.
    • Report download functionality now monitors job completion status before generating the PDF.

- 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>
@google-labs-jules

Copy link
Copy Markdown
Contributor

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@qodo-code-review

Copy link
Copy Markdown
Contributor

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@vercel

vercel Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
qcx Error Error Jun 9, 2026 4:25am

@coderabbitai

coderabbitai Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@ngoiyaeric, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 43 minutes and 4 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 28d25cba-49f8-4dd3-9a98-4a13bbf6ebe9

📥 Commits

Reviewing files that changed from the base of the PR and between 536fdc7 and 5e20729.

📒 Files selected for processing (1)
  • app/api/queue/route.ts

Walkthrough

This PR introduces asynchronous job-queue processing for report generation. The changes move report generation from a synchronous request-response model to a background job pattern: users submit a job, receive an immediate response with a job ID, then poll for completion.

Changes

Asynchronous Report Generation via Job Queue

Layer / File(s) Summary
Jobs table schema and relations
lib/db/schema.ts
Adds a jobs table with UUID id, user ownership (cascade delete), job type and status (default pending), JSON payload/result fields, optional error, and timestamps; extends user relations to include jobs as a one-to-many relationship.
Job data access and status query layer
lib/actions/jobs.ts, lib/actions/jobs-client.ts
Implements enqueueJob, getJob, updateJob, and getJobStatus as server-side data-access functions with error handling; exports a server action wrapper for client-side status polling.
Queue callback handler and job processing
app/api/queue/route.ts
Vercel Queue callback handler fetches jobs, executes generateReportContext, updates job status to completed with result, or failed with error message, and logs operations.
Generate report context action – job enqueueing
app/actions.tsx
Changes generate_report_context action to enqueue an async background job instead of executing synchronously; returns { jobId, status: 'processing' } immediately and triggers queue processing via Vercel Queue send.
Download button polling and result handling
components/download-report-button.tsx
Updates button to submit action for job enqueueing, poll getJobStatus(jobId) with bounded retry (≈60s), extract title and summary from completed job result, and continue with existing PDF generation.
Queue dependency
package.json
Adds @vercel/queue package.

Sequence Diagram

sequenceDiagram
  participant UI as DownloadReportButton
  participant Actions as app/actions.tsx
  participant Queue as Vercel Queue
  participant Handler as app/api/queue/route.ts
  participant DB as Jobs Table
  UI->>Actions: submit generate_report_context
  Actions->>DB: enqueueJob with messages
  DB-->>Actions: jobId, pending status
  Actions->>Queue: send('report-generation')
  Actions-->>UI: return { jobId, status: 'processing' }
  UI->>UI: poll getJobStatus(jobId)
  Queue->>Handler: invoke callback
  Handler->>DB: getJob(jobId)
  Handler->>Handler: generateReportContext(messages)
  Handler->>DB: updateJob to completed with result
  UI->>DB: getJobStatus returns completed
  UI->>UI: extract title/summary from result
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

  • QueueLab/QCX#660: Directly addresses the same feature—migrating report generation to an asynchronous job queue with enqueueing, status tracking, and background worker processing.

Possibly related PRs

  • QueueLab/QCX#638: Modifies components/download-report-button.tsx in the same report-generation flow; this PR implements job-based polling whereas the related PR may address initial template/PDF rendering.
  • QueueLab/QCX#659: Modifies app/actions.tsx and components/download-report-button.tsx for generate_report_context; this PR enqueues async jobs while the related PR may call the function synchronously.

Suggested labels

Review effort 3/5

Poem

🐰 A queue of jobs hops into place,
No more waiting face-to-face,
Enqueue, poll, result appears—
Background magic calms the fears!
~The CodeRabbit

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main objective: making report generation asynchronous using a job queue system.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/async-report-generation-14383399858639898697

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@ngoiyaeric

Copy link
Copy Markdown
Collaborator Author

@coderabbitai use vercel queue not database https://vercel.com/docs/queues

@coderabbitai

coderabbitai Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find all queue/job related files in the PR
fd -e ts -e js -e sql -e py . | xargs grep -l -i "job\|queue\|worker" 2>/dev/null | head -40

echo "---"
# Look for migration/schema files
fd -e sql . | head -20

echo "---"
# Look for worker scripts
fd "worker" . | head -20
fd "queue" . | head -20

echo "---"
# Show package.json to see dependencies
cat package.json 2>/dev/null || fd "package.json" . -d 1 | xargs cat

Length of output: 4483


🌐 Web query:

Vercel Queues API documentation how to create queue publish message worker 2025

Length of output: 18691


🏁 Script executed:

#!/bin/bash
cat lib/actions/jobs.ts
echo "==="
cat lib/actions/jobs-client.ts
echo "==="
cat bin/worker.ts
echo "==="
# Check if there's a vercel.json
cat vercel.json 2>/dev/null || echo "No vercel.json found"
echo "==="
# Check for any API routes that trigger report generation
fd "route" app/api --type f | xargs grep -l -i "report\|job\|queue" 2>/dev/null | head -10
echo "==="
# Find the report generation trigger point
rg -n "createJob\|enqueueJob\|generateReport\|jobs" app/ --type ts -l | head -10

Length of output: 3781


🏁 Script executed:

#!/bin/bash
# Find where enqueueJob / getJobStatus are called from
rg -rn "enqueueJob\|getJobStatus\|jobs-client\|jobs'" app/ --type ts 2>/dev/null | head -40

echo "==="
# Find generateReportContext usage
rg -rn "generateReportContext" --type ts 2>/dev/null | head -20

echo "==="
# Look at lib/db/schema.ts for the jobs table definition
rg -n "jobs" lib/db/schema.ts | head -30

echo "==="
# Check drizzle migrations for the jobs table
cat drizzle/migrations/0000_sweet_metal_master.sql | grep -A 20 -i "job"

Length of output: 1185


🏁 Script executed:

#!/bin/bash
# Search more broadly for enqueueJob/getJobStatus usage
rg -rn "enqueueJob\|getJobStatus\|jobId\|job_id\|pollJob\|jobs-client" . --type ts --type tsx 2>/dev/null | grep -v "node_modules\|\.next" | head -40

echo "==="
# Read app/actions.tsx relevant parts
grep -n "enqueue\|job\|queue\|report" app/actions.tsx -i | head -40

echo "==="
# Find any polling logic in components
rg -rn "jobId\|pollJob\|setJobId\|getJobStatus" app/ 2>/dev/null | grep -v "node_modules" | head -30

echo "==="
# Check if there are any API routes for jobs/queue
fd "route.ts" app/api | xargs grep -l -i "job\|queue\|report" 2>/dev/null

echo "==="
# jobs table schema
sed -n '100,140p' lib/db/schema.ts

Length of output: 3147


🏁 Script executed:

sed -n '1,80p' app/actions.tsx

Length of output: 3651


🏁 Script executed:

# Confirm the exact lines to remove in schema.ts
grep -n "jobs\|jobsRelations" lib/db/schema.ts

Length of output: 302



@ngoiyaeric Here are all the concrete changes to migrate from the DB worker to Vercel Queue.


1. Install the SDK

bun add `@vercel/queue`

2. vercel.json (create new file)

{
  "functions": {
    "app/api/queues/generate-report/route.ts": {
      "experimentalTriggers": [
        {
          "type": "queue/v2beta",
          "topic": "report-generation"
        }
      ]
    }
  }
}

3. app/api/queues/generate-report/route.ts (create new file)

import { handleCallback } from '`@vercel/queue`'
import { generateReportContext } from '`@/lib/actions/chat`'
import { Redis } from '`@upstash/redis`'

const redis = Redis.fromEnv()
const JOB_TTL_SECONDS = 3600 // 1 hour

export const POST = handleCallback(async (payload, metadata) => {
  const { messages } = payload as { messages: any[] }
  const key = `report-job:${metadata.messageId}`

  try {
    const result = await generateReportContext(messages)
    await redis.set(key, JSON.stringify({ status: 'completed', result }), {
      ex: JOB_TTL_SECONDS,
    })
  } catch (error) {
    const errMsg = error instanceof Error ? error.message : String(error)
    await redis.set(key, JSON.stringify({ status: 'failed', error: errMsg }), {
      ex: JOB_TTL_SECONDS,
    })
    throw error // rethrow so Vercel Queue retries the message
  }
})

4. app/actions.tsx (update producer)

- import { enqueueJob } from '`@/lib/actions/jobs`'
+ import { send } from '`@vercel/queue`'
  try {
    const messages = JSON.parse(messagesString) as AIMessage[];
-   const userId = await getCurrentUserIdOnServer() || 'anonymous';
-
-   // Enqueue job for asynchronous processing
-   const jobId = await enqueueJob(userId, 'generate_report_context', { messages });
-
-   return { jobId, status: 'processing' };
+   // Publish to Vercel Queue; messageId is used by the client to poll status
+   const { messageId } = await send('report-generation', { messages });
+   return { jobId: messageId, status: 'processing' };
  } catch (e) {

5. lib/actions/jobs-client.ts (replace entirely)

'use server'

import { Redis } from '`@upstash/redis`'

const redis = Redis.fromEnv()

export async function getJobStatus(jobId: string) {
  // jobId is the Vercel Queue messageId written by the consumer
  const raw = await redis.get<string>(`report-job:${jobId}`)
  if (!raw) return { status: 'processing' as const }
  const data = typeof raw === 'string' ? JSON.parse(raw) : raw
  return data as {
    status: 'completed' | 'failed'
    result?: any
    error?: string
  }
}

6. lib/db/schema.ts (remove jobs table)

- 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),
    ...
-   jobs: many(jobs),
  }));

- export const jobsRelations = relations(jobs, ({ one }) => ({
-   user: one(users, {
-     fields: [jobs.userId],
-     references: [users.id],
-   }),
- }));

Add a Drizzle migration to drop the table:

-- drizzle/migrations/0002_drop_jobs_table.sql
DROP TABLE IF EXISTS "jobs";

7. Delete these files

  • bin/worker.ts — replaced by the Vercel Queue push consumer
  • lib/actions/jobs.ts — DB job management is no longer needed

How it works end-to-end

sequenceDiagram
    participant Client
    participant actions.tsx (Producer)
    participant Vercel Queue
    participant Consumer Route
    participant Upstash Redis

    Client->>actions.tsx (Producer): submit generate_report_context
    actions.tsx (Producer)->>Vercel Queue: send('report-generation', {messages})
    Vercel Queue-->>actions.tsx (Producer): { messageId }
    actions.tsx (Producer)-->>Client: { jobId: messageId, status: 'processing' }
    Vercel Queue->>Consumer Route: POST handleCallback(payload, metadata)
    Consumer Route->>Consumer Route: generateReportContext(messages)
    Consumer Route->>Upstash Redis: SET report-job:{messageId} result TTL=1h
    Client->>getJobStatus(jobId): poll
    getJobStatus(jobId)->>Upstash Redis: GET report-job:{messageId}
    Upstash Redis-->>Client: { status: 'completed', result }
Loading

Note: Make sure UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN are set in your Vercel project environment variables. After deploying, run vercel env pull locally to get them in .env.local for development.


✏️ Learnings added
Learnt from: ngoiyaeric
URL: https://github.com/QueueLab/QCX/pull/663

Timestamp: 2026-06-09T03:56:21.436Z
Learning: In the QueueLab/QCX repository, the user wants async report generation to use Vercel Queue (`vercel/queue`) instead of a database-backed job queue. The producer should use `send()` from `vercel/queue`, results should be stored in Upstash Redis (already a dependency) keyed by `messageId`, and the consumer should be a push-mode route configured via `vercel.json` with `experimentalTriggers`. The DB `jobs` table and `bin/worker.ts` polling worker should be removed.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@google-labs-jules

Copy link
Copy Markdown
Contributor

@coderabbitai use vercel queue not database https://vercel.com/docs/queues

I have updated the implementation to use Vercel Queues instead of a custom database-backed worker. The submit action now enqueues the job in the database and triggers processing via send('report-generation', { jobId }) from @vercel/queue. I've also implemented the consumer in app/api/queue/route.ts using handleCallback.

- 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>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/actions.tsx (1)

56-82: ⚠️ Potential issue | 🔴 Critical

Fix critical issues in Vercel Queue enqueue flow in app/actions.tsx (lines ~56-82)

  • Auth bypass (line ~63): getCurrentUserIdOnServer() || 'anonymous' allows unauthenticated callers to enqueue jobs. Require authentication and fail early when no user id is available.
  • Queue wiring (lines ~70): The repo contains no vercel.json/queues configuration (jq shows “No vercel.json or queues config”), so send('report-generation', { jobId }) can’t reliably reach a consumer. Ensure the send() queue/topic name matches the configured Vercel Queue trigger (or add the missing config).
  • Broken async contract (lines ~69-75): If send() throws, the code still returns { jobId, status: 'processing' }, which can leave the client polling forever even though the enqueue trigger failed. Fail the action or persist a failed job state.
  • Return-shape mismatch (lines ~77-80): Success returns { jobId, status: 'processing' } while the error path returns { title, summary }. Use a consistent/discriminated response shape so the client can safely rely on jobId.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/actions.tsx` around lines 56 - 82, Require an authenticated user and fail
early by replacing the permissive fallback in getCurrentUserIdOnServer() usage:
if getCurrentUserIdOnServer() returns falsy, throw or return a consistent error
response instead of using 'anonymous'; ensure you locate this check near the
enqueueJob call. Verify the Vercel Queue topic name passed to
send('report-generation', { jobId }) matches your actual queues configuration
(or add the missing vercel.json/queues config) so the trigger can be reached;
update the string if the configured topic is different. Change the send() error
handling so the action does not return a processing success when send() fails:
on queue-send failure, mark the enqueued job as failed (or update job status via
enqueueJob/DB) and return a consistent discriminated response shape (e.g., {
status: 'processing'|'failed', jobId?: string, error?: string }) so callers can
reliably inspect jobId and status; adjust the catch block around enqueueJob/send
and the final return to follow that unified shape. Ensure these changes
reference getCurrentUserIdOnServer, enqueueJob, send, and jobId in
app/actions.tsx.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/api/queue/route.ts`:
- Around line 7-10: Replace early returns with thrown errors so Vercel Queue can
retry: where the code checks for missing jobId (the if (!jobId) block) and where
it checks for a missing job record (the "job not found" branch), change the
return to throw a new Error including a clear message and any relevant
identifiers (e.g., jobId) and still log details via console.error before
throwing; ensure both checks throw so Vercel Queue can apply retry/DLQ logic
instead of treating the message as successfully handled.
- Around line 1-38: The POST handler wrapped by handleCallback should (1)
re-throw the caught error after calling updateJob so Vercel Queue can retry —
modify the catch in the POST handler to await updateJob(... status: 'failed'
...) and then throw the error instead of returning; (2) validate runtime payload
shape before processing by checking that job.payload exists and that messages is
an Array (e.g., const messages = job.payload?.messages; if
(!Array.isArray(messages)) handle as non-retriable error or throw); (3) remove
redundant updatedAt: new Date() from the updateJob(...) calls (updateJob sets
updatedAt internally) in both the 'completed' and 'failed' updates; and (4)
optionally wrap generateReportContext(messages) with a cancellable
timeout/AbortSignal to guard against long-running executions — locate these
changes in the POST handler in app/api/queue/route.ts referencing functions
handleCallback, getJob, updateJob, and generateReportContext.

In `@components/download-report-button.tsx`:
- Around line 61-88: The polling logic in DownloadReportButton incorrectly
destructures the submit response, polls inefficiently, assumes jobResult shape,
lacks unmount cleanup, and uses a magic attempts constant; fix by first
assigning the submit result to a variable and type-guarding that it contains
jobId before destructuring (replace direct `const { jobId } = await (actions as
any).submit(...)` with a response check for 'jobId'), replace the fixed 1s tight
loop using getJobStatus(jobId) with an exponential-backoff retry strategy (e.g.,
baseDelay doubling on each attempt and a total timeout constant instead of
maxAttempts) to reduce server load, validate the final jobResult object shape
before casting (ensure it has required title and summary fields and throw if
invalid), add component unmount cleanup (use an AbortSignal or a mounted flag in
useEffect to stop further polling and cancel pending timers/requests), and
replace the magic `maxAttempts` number with a named timeout/duration constant
computed from baseDelay and maxRetries.

In `@lib/actions/jobs-client.ts`:
- Around line 1-7: Replace the trivial wrapper getJobStatus in
lib/actions/jobs-client.ts: remove the import/alias of getJobStatusServer and
either delete this file and update callers to call the real getJobStatus in
./jobs, or implement the required Redis-backed getJobStatus(jobId) that reads
the Upstash key report-job:{jobId}, parses the stored value and returns the
normalized state ("processing"|"completed"|"failed"); if using Vercel Queue +
Redis, implement the same Redis lookup inside getJobStatus and map stored
payloads to those states. Ensure you reference and update any callers that
currently import getJobStatus from lib/actions/jobs-client.ts to use the new
location/implementation.

In `@lib/actions/jobs.ts`:
- Line 6: Remove the unused import revalidatePath from the module; update
lib/actions/jobs.ts by deleting the "import { revalidatePath } from
'next/cache'" statement (ensure no other code relies on revalidatePath and that
imports in that file like any other named imports remain correctly formatted).
- Around line 8-32: Unify error handling across job CRUD functions by making all
functions propagate errors instead of swallowing them; specifically, update
getJob (and any other read helpers like updateJob) to either remove the
try/catch or rethrow after logging so it behaves like enqueueJob (which
currently throws). Locate the getJob function and change its error path to call
console.error('Error getting job:', error) and then throw error (or remove the
catch entirely), ensuring callers receive exceptions consistently rather than
sometimes getting null.
- Around line 45-46: Remove the redundant function-level "'use server'"
directive inside getJobStatus: open the getJobStatus function (export async
function getJobStatus) and delete the "'use server'" string so the module-level
directive only remains; no behavioral changes otherwise.
- Around line 34-43: The updateJob function currently types updates as
Partial<typeof jobs.$inferInsert> which allows callers to pass non-updatable
fields (e.g., id) or updatedAt which you then overwrite; create a dedicated
update shape (e.g., type JobUpdate = Partial<Pick<typeof jobs.$inferInsert,
'status' | 'result' | 'error' | 'payload'>> or use Omit<typeof
jobs.$inferInsert, 'id' | 'createdAt' | 'updatedAt'> wrapped in Partial) and
change the updateJob signature to updateJob(jobId: string, updates: JobUpdate),
keep the server-side updatedAt: new Date() assignment, and optionally assert or
throw if updates contains forbidden keys before calling db.update so callers
cannot override id/updatedAt.

In `@lib/db/schema.ts`:
- Around line 126-127: Remove the ORM relations that reference the non-existent
jobs table: delete the "jobs: many(jobs)" entry inside the users schema/object
and any corresponding inverse relation entries in the jobs-related schema (e.g.,
properties using many(jobs) or a user/jobs relation) as noted around the same
block (lines 129–134). Also remove any imports, type references, or variables
named "jobs" used solely for those relations and adjust related types or
validators in the same schema file (e.g., the users model/schema) so there are
no lingering references to the jobs table.
- Around line 105-115: The PR left a persistent Postgres "jobs" table (export
const jobs) without corresponding Drizzle migrations and without real Upstash
Redis integration; add a new migration SQL that creates/drops the jobs table
matching the schema in lib/db/schema.ts (including id, user_id FK,
payload/result/error JSONB, created_at/updated_at timestamps) and add a CHECK or
ENUM constraint for status (allowed values like
'pending','running','completed','failed'); update lib/actions/jobs.ts and
app/api/queue/route.ts to either (a) move result storage to Upstash Redis
(integrate `@upstash/redis` client and store result keys there, reading/writing
from Redis) or (b) keep results in Postgres but ensure migrations reflect that
decision, and ensure updateJob() correctly sets updatedAt (or rely on DB
trigger/default) so updatedAt changes on state updates.

---

Outside diff comments:
In `@app/actions.tsx`:
- Around line 56-82: Require an authenticated user and fail early by replacing
the permissive fallback in getCurrentUserIdOnServer() usage: if
getCurrentUserIdOnServer() returns falsy, throw or return a consistent error
response instead of using 'anonymous'; ensure you locate this check near the
enqueueJob call. Verify the Vercel Queue topic name passed to
send('report-generation', { jobId }) matches your actual queues configuration
(or add the missing vercel.json/queues config) so the trigger can be reached;
update the string if the configured topic is different. Change the send() error
handling so the action does not return a processing success when send() fails:
on queue-send failure, mark the enqueued job as failed (or update job status via
enqueueJob/DB) and return a consistent discriminated response shape (e.g., {
status: 'processing'|'failed', jobId?: string, error?: string }) so callers can
reliably inspect jobId and status; adjust the catch block around enqueueJob/send
and the final return to follow that unified shape. Ensure these changes
reference getCurrentUserIdOnServer, enqueueJob, send, and jobId in
app/actions.tsx.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 6b9699d1-28f5-456a-8682-2421ae21e249

📥 Commits

Reviewing files that changed from the base of the PR and between abdede5 and 536fdc7.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (8)
  • .env
  • app/actions.tsx
  • app/api/queue/route.ts
  • components/download-report-button.tsx
  • lib/actions/jobs-client.ts
  • lib/actions/jobs.ts
  • lib/db/schema.ts
  • package.json
💤 Files with no reviewable changes (1)
  • .env
📜 Review details
🔇 Additional comments (4)
package.json (1)

55-55: LGTM!

app/actions.tsx (1)

18-20: LGTM!

components/download-report-button.tsx (1)

14-14: LGTM!

lib/actions/jobs.ts (1)

1-54: Critical: Don’t delete lib/actions/jobs.ts yet—confirm the Redis-backed replacement and whether the module is still used.
lib/actions/jobs.ts is still DB-backed (Drizzle jobs insert/select/update) and getJobStatus() returns data from the jobs table. The migration plan requires switching to Redis-backed job status (reading report-job:{messageId} via Upstash Redis), but no @upstash/redis / report-job usage was located in the current search. Add/wire a Redis-backed getJobStatus(jobId) and remove lib/actions/jobs.ts only once nothing imports it.

Comment thread app/api/queue/route.ts
Comment on lines +1 to +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(),
});
}
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify route structure and Redis integration

# Check for the expected route path
fd -t f 'route.ts' app/api/queues/

# Search for Upstash Redis setup and usage
rg -n 'Redis|UPSTASH' --type ts --type env -C2

# Check vercel.json for queue configuration
cat vercel.json 2>/dev/null || echo "vercel.json not found"

Repository: QueueLab/QCX

Length of output: 227


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Locate the queue callback route implementation(s)
echo "== handleCallback routes =="
rg -n "handleCallback" -S --glob "**/*.ts" --glob "**/*.tsx" . || true

# 2) Inspect app/api tree relevant to queue/generate-report
echo
echo "== List api route directories (shallow) =="
ls -la app/api 2>/dev/null || true
find app/api -maxdepth 3 -type f -name "route.ts" 2>/dev/null | sed 's|^\./||' | sort || true

# 3) Read the provided route file if it exists
echo
echo "== app/api/queue/route.ts (first 120 lines) =="
if [ -f "app/api/queue/route.ts" ]; then
  sed -n '1,120p' app/api/queue/route.ts
else
  echo "MISSING: app/api/queue/route.ts"
fi

# 4) Inspect jobs helpers for updatedAt behavior
echo
echo "== updateJob/getJob implementations =="
rg -n "function updateJob|export (async )?function updateJob|updateJob\\(" app/lib/actions/jobs.ts app/lib/actions/jobs.* app/lib/actions -S 2>/dev/null || true
for f in $(fd -t f "jobs.ts" app/lib/actions 2>/dev/null || true); do
  echo
  echo "---- $f (first 200 lines) ----"
  sed -n '1,200p' "$f"
done

# 5) Search for Upstash Redis usage and env vars
echo
echo "== Upstash/Redis usage =="
rg -n "UPSTASH|Upstash|upstash|Redis" -S --glob "**/*.ts" --glob "**/*.js" --glob "**/*.tsx" --glob "**/*.env*" --glob "vercel.json" . || true

# 6) Check queue-related vercel config (if present)
echo
echo "== vercel.json (contents if exists) =="
if [ -f "vercel.json" ]; then
  cat vercel.json
else
  echo "MISSING: vercel.json"
fi

# 7) Check other queue/route paths possibly matching the PR summary expectation
echo
echo "== Look for generate-report routes =="
find app/api -type f -name "route.ts" 2>/dev/null | xargs -r -n 1 basename | head -n 200 || true
rg -n "generate-report|generateReport|report" app/api -S --glob "**/*.ts" --glob "**/*.tsx" || true

Repository: QueueLab/QCX

Length of output: 2420


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate jobs helper files
echo "== Locate jobs helper(s) =="
fd -t f -i "*jobs*.ts" app/lib || true
fd -t f -i "jobs.ts" app/lib || true
fd -t f -i "*job*.ts" app/lib/actions || true

# Read likely jobs files
for f in $(fd -t f -i "*jobs*.ts" app/lib 2>/dev/null); do
  echo
  echo "---- $f (first 200 lines) ----"
  sed -n '1,200p' "$f"
done

# Search for upstash/redis clients (beyond env mentions)
echo
echo "== Redis/Upstash client code usage =="
rg -n "(upstash|UPSTASH|`@upstash`\/redis|Redis|createClient|redis\.)" -S --glob "**/*.ts" --glob "**/*.tsx" app lib src . || true

# Ensure we understand queue callback retry behavior usage in codebase
echo
echo "== Other `@vercel/queue` usage patterns =="
rg -n "`@vercel/queue`|handleCallback" -S --glob "**/*.ts" . || true

Repository: QueueLab/QCX

Length of output: 791


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Find updateJob/getJob definitions =="
rg -n "updateJob\\b|getJob\\b" -S --glob "**/*.ts" --glob "**/*.tsx" . || true

echo
echo "== Find generateReportContext definition =="
rg -n "generateReportContext\\b" -S --glob "**/*.ts" --glob "**/*.tsx" . || true

echo
echo "== List files named jobs.ts / actions/jobs =="
fd -t f -i "jobs.ts" . || true
fd -t f -i "jobs" . || true

echo
echo "== Inspect any found jobs-related file(s) (first 200 lines) =="
# Take a limited set of likely matches to avoid huge output
for f in $(rg -l "export (async )?function updateJob|export (async )?function getJob|updateJob\\(|getJob\\(" -S --glob "**/*.ts" . | head -n 20); do
  echo
  echo "---- $f (first 200 lines) ----"
  sed -n '1,200p' "$f"
done

echo
echo "== Redis/Upstash code usage (broader search) =="
rg -n "(upstash|UPSTASH|`@upstash/redis`|upstash_redis|redis)" -S --glob "**/*.ts" --glob "**/*.tsx" . || true

echo
echo "== Find vercel/queue config docs/examples usage =="
rg -n "handleCallback|`@vercel/queue`" -S --glob "**/*.ts" --glob "**/*.tsx" . || true

echo
echo "== Locate any queue payload fields or TTL write logic =="
rg -n "(TTL|ttl|expire|expiration|setex|hset|write.*redis|redis.*set)" -S --glob "**/*.ts" --glob "**/*.tsx" . || true

Repository: QueueLab/QCX

Length of output: 4958


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== generateReportContext implementation =="
sed -n '1,200p' lib/actions/chat.ts
echo
echo "== generateReportContext (tail if longer) =="
sed -n '200,400p' lib/actions/chat.ts 2>/dev/null || true

Repository: QueueLab/QCX

Length of output: 7912


🌐 Web query:

@vercel/queue handleCallback retry behavior throw vs return

💡 Result:

In @vercel/queue, the behavior of handleCallback regarding exceptions and return values is as follows: Throwing an Error: When your handler function throws an error, the message is not acknowledged and is automatically scheduled for redelivery [1][2]. This triggers the retry process, where the message will be delivered again after a delay—either the default interval or a custom delay if one is defined in the retry configuration [2][3]. Returning: Messages are automatically acknowledged (marked as successfully processed) when your handler function completes execution without throwing an error [1][4]. Custom Retry Configuration: You can control this behavior using the retry option within the handleCallback configuration object [1][2]. This option accepts a callback that receives the error and metadata [2][3]. Based on the return value of this retry callback, you can customize the outcome: - { afterSeconds: number }: Reschedules the message for redelivery after the specified number of seconds [1][3]. - { acknowledge: true }: Acknowledges the message immediately, effectively stopping any further retries [1][2]. - undefined: Allows the error to propagate normally, causing the system to use the default retry behavior (redelivery after the default interval) [2][3]. It is important to ensure your handler is idempotent, as retries will cause your code to execute multiple times if a previous attempt failed [5][6].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Search for enqueueJob/send usage =="
rg -n "enqueueJob\\b|`@vercel/queue`\\b|send\\b\\(" -S --glob "**/*.ts" --glob "**/*.tsx" . || true

echo
echo "== Inspect app/actions.tsx around Vercel Queue send =="
if [ -f "app/actions.tsx" ]; then
  nl -ba app/actions.tsx | sed -n '1,120p'
  nl -ba app/actions.tsx | sed -n '120,240p'
else
  echo "MISSING: app/actions.tsx"
fi

echo
echo "== Search for generate-report / queues route strings =="
rg -n "(generate-report|generateReport|/api/queues|api/queues|queues/)" -S --glob "**/*.ts" --glob "**/*.tsx" . || true

echo
echo "== Inspect jobs table schema =="
rg -n "export const jobs\\b|jobs\\b.*schema|updatedAt|result|error" -S --glob "**/*.ts" --glob "**/*.tsx" lib/app src . || true
# Print jobs schema file if we can locate it
for f in $(rg -l "export const jobs\\b" . || true); do
  echo
  echo "---- $f (first 200 lines) ----"
  sed -n '1,200p' "$f"
done

Repository: QueueLab/QCX

Length of output: 699


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Inspect app/actions.tsx around Vercel Queue send =="
if [ -f "app/actions.tsx" ]; then
  sed -n '1,140p' app/actions.tsx
else
  echo "MISSING: app/actions.tsx"
fi

echo
echo "== Inspect app/actions.tsx near enqueueJob/send call =="
if [ -f "app/actions.tsx" ]; then
  rg -n "enqueueJob|send\\(" app/actions.tsx
fi

echo
echo "== Search for generate-report / queues route strings =="
rg -n "(generate-report|generateReport|/api/queues|api/queues|queues/)" -S --glob "**/*.ts" --glob "**/*.tsx" . || true

echo
echo "== Locate jobs table schema (drizzle) =="
rg -n "jobs\\s*=\\s*|export const jobs\\b|updatedAt\\b|result\\b|error\\b" -S --glob "**/*.ts" --glob "**/*.tsx" lib . || true

echo
echo "== Print any file containing 'export const jobs' =="
for f in $(rg -l "export const jobs\\b" lib . 2>/dev/null || true); do
  echo
  echo "---- $f (first 220 lines) ----"
  sed -n '1,220p' "$f"
done

Repository: QueueLab/QCX

Length of output: 50368


Critical: Vercel Queue worker swallows failures, so jobs won’t retry

  • In app/api/queue/route.ts, the catch block updates the job to failed but does not re-throw; handleCallback will treat the handler as successful and acknowledge the message, preventing Vercel Queue retries. Re-throw the error after updateJob(...) (and only return for non-retriable cases like missing jobId/unknown job).
  • const { messages } = job.payload as { messages: any[] } has no runtime validation; add guards that job.payload exists and messages is an array before calling generateReportContext.
  • Remove redundant updatedAt: new Date() from updateJob(...) calls—lib/actions/jobs.ts already sets updatedAt internally.
  • Optional: add a timeout/abort around generateReportContext(messages) to avoid long-running/hung queue executions.

Also, the earlier “wrong route path” and “Upstash Redis storage” assertions don’t match this codebase: the producer uses send('report-generation', { jobId }) and the consumer is app/api/queue/route.ts, and job results are persisted to the Postgres jobs table via lib/actions/jobs.ts (no Upstash/Redis client usage found).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/api/queue/route.ts` around lines 1 - 38, The POST handler wrapped by
handleCallback should (1) re-throw the caught error after calling updateJob so
Vercel Queue can retry — modify the catch in the POST handler to await
updateJob(... status: 'failed' ...) and then throw the error instead of
returning; (2) validate runtime payload shape before processing by checking that
job.payload exists and that messages is an Array (e.g., const messages =
job.payload?.messages; if (!Array.isArray(messages)) handle as non-retriable
error or throw); (3) remove redundant updatedAt: new Date() from the
updateJob(...) calls (updateJob sets updatedAt internally) in both the
'completed' and 'failed' updates; and (4) optionally wrap
generateReportContext(messages) with a cancellable timeout/AbortSignal to guard
against long-running executions — locate these changes in the POST handler in
app/api/queue/route.ts referencing functions handleCallback, getJob, updateJob,
and generateReportContext.

Comment thread app/api/queue/route.ts
Comment on lines +7 to +10
if (!jobId) {
console.error('No jobId in queue payload');
return;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Early returns prevent Vercel Queue from retrying failures.

Lines 9 and 16 return early on missing jobId or job not found. Per Vercel Queue documentation, the callback should throw an error to trigger retries. Returning without throwing signals success, and the message is removed from the queue.

If jobId is missing or the job is not found, these are likely transient or poison-message issues. For poison messages, you should throw to let Vercel Queue apply retry/DLQ logic. For transient DB failures (job not found due to replication lag), throwing enables retry.

🔁 Recommended fix: throw instead of return
   const { jobId } = payload;
   if (!jobId) {
     console.error('No jobId in queue payload');
-    return;
+    throw new Error('No jobId in queue payload');
   }

   // ...
   const job = await getJob(jobId);
   if (!job) {
     console.error(`Job ${jobId} not found`);
-    return;
+    throw new Error(`Job ${jobId} not found`);
   }

Also applies to: 14-17

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/api/queue/route.ts` around lines 7 - 10, Replace early returns with
thrown errors so Vercel Queue can retry: where the code checks for missing jobId
(the if (!jobId) block) and where it checks for a missing job record (the "job
not found" branch), change the return to throw a new Error including a clear
message and any relevant identifiers (e.g., jobId) and still log details via
console.error before throwing; ensure both checks throw so Vercel Queue can
apply retry/DLQ logic instead of treating the message as successfully handled.

Comment on lines +61 to +88
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 };

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

Polling implementation has multiple critical issues.

  1. Line 61: Unsafe destructuring assumes response shape.
    The submit action can return different shapes: { jobId, status } on success or { title, summary } on error (see app/actions.tsx lines 77 vs 80). Destructuring { jobId } without validating the response will produce undefined on the error path, causing the check on line 63 to throw a misleading error.

    Add a type guard:

    const response = await (actions as any).submit(formData);
    if (!('jobId' in response)) {
      throw new Error('Failed to start report generation');
    }
    const { jobId } = response;
  2. Lines 72-82: Polling server actions in a tight loop is inefficient and blocks the client.

    • Each getJobStatus(jobId) call is a server action that makes a network round-trip. Calling it 60 times at 1-second intervals is expensive and creates unnecessary load.
    • The await new Promise(resolve => setTimeout(resolve, 1000)) on line 81 blocks the client's render thread during each 1s sleep (though React 19 handles this better with concurrent rendering, it's still suboptimal).
    • Consider using exponential backoff (e.g., 500ms → 1s → 2s → 4s) to reduce server load and improve UX.
    • Consider moving polling to a non-blocking pattern (e.g., React Query or SWR with refetch intervals).
  3. Line 88: Unsafe type cast without validation.
    jobResult as { title: string, summary: string } assumes the shape without checking. If the result shape is different (e.g., missing summary), the code will silently use undefined.

    Add validation:

    if (!jobResult || typeof jobResult !== 'object' || !('title' in jobResult)) {
      throw new Error('Invalid job result format');
    }
    const { title, summary } = jobResult as { title: string; summary: string };
  4. No cleanup on component unmount.
    If the user navigates away during polling, the loop continues running in the background until timeout. Add cleanup with useEffect and an abort signal or flag to stop polling when the component unmounts.

  5. Magic number 60 without explanation.
    Line 70: const maxAttempts = 60; // 60 seconds assumes 1-second intervals, but if intervals change, the comment becomes misleading. Use a named constant or calculate from timeout duration.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/download-report-button.tsx` around lines 61 - 88, The polling
logic in DownloadReportButton incorrectly destructures the submit response,
polls inefficiently, assumes jobResult shape, lacks unmount cleanup, and uses a
magic attempts constant; fix by first assigning the submit result to a variable
and type-guarding that it contains jobId before destructuring (replace direct
`const { jobId } = await (actions as any).submit(...)` with a response check for
'jobId'), replace the fixed 1s tight loop using getJobStatus(jobId) with an
exponential-backoff retry strategy (e.g., baseDelay doubling on each attempt and
a total timeout constant instead of maxAttempts) to reduce server load, validate
the final jobResult object shape before casting (ensure it has required title
and summary fields and throw if invalid), add component unmount cleanup (use an
AbortSignal or a mounted flag in useEffect to stop further polling and cancel
pending timers/requests), and replace the magic `maxAttempts` number with a
named timeout/duration constant computed from baseDelay and maxRetries.

Comment on lines +1 to +7
'use server'

import { getJobStatus as getJobStatusServer } from './jobs'

export async function getJobStatus(jobId: string) {
return await getJobStatusServer(jobId)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

Critical: This file should have been removed or replaced.

Per the PR summary and bot migration plan:

"Replace lib/actions/jobs-client.ts with a Redis-backed getJobStatus(jobId) that reads report-job:{messageId} from Upstash Redis and returns processing/completed/failed states."

The current implementation is an unnecessary wrapper that imports getJobStatus from ./jobs, aliases it, and re-exports it without adding any value. If the DB-backed approach is kept, this indirection should be removed; if migrating to Vercel Queue + Redis, this should be replaced with a Redis-based implementation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/actions/jobs-client.ts` around lines 1 - 7, Replace the trivial wrapper
getJobStatus in lib/actions/jobs-client.ts: remove the import/alias of
getJobStatusServer and either delete this file and update callers to call the
real getJobStatus in ./jobs, or implement the required Redis-backed
getJobStatus(jobId) that reads the Upstash key report-job:{jobId}, parses the
stored value and returns the normalized state
("processing"|"completed"|"failed"); if using Vercel Queue + Redis, implement
the same Redis lookup inside getJobStatus and map stored payloads to those
states. Ensure you reference and update any callers that currently import
getJobStatus from lib/actions/jobs-client.ts to use the new
location/implementation.

Comment thread lib/actions/jobs.ts
import { db } from '@/lib/db'
import { jobs } from '@/lib/db/schema'
import { eq } from 'drizzle-orm'
import { revalidatePath } from 'next/cache'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Remove unused import.

revalidatePath is imported but never used in this file.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/actions/jobs.ts` at line 6, Remove the unused import revalidatePath from
the module; update lib/actions/jobs.ts by deleting the "import { revalidatePath
} from 'next/cache'" statement (ensure no other code relies on revalidatePath
and that imports in that file like any other named imports remain correctly
formatted).

Comment thread lib/actions/jobs.ts
Comment on lines +8 to +32
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
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Inconsistent error handling across CRUD functions.

enqueueJob (line 20) and updateJob (line 41) rethrow errors, while getJob (line 30) catches and returns null. This inconsistency forces callers to handle two different failure modes (exceptions vs null checks) for similar operations.

Consider a unified strategy: either propagate all errors or return Result<T, Error> types consistently.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/actions/jobs.ts` around lines 8 - 32, Unify error handling across job
CRUD functions by making all functions propagate errors instead of swallowing
them; specifically, update getJob (and any other read helpers like updateJob) to
either remove the try/catch or rethrow after logging so it behaves like
enqueueJob (which currently throws). Locate the getJob function and change its
error path to call console.error('Error getting job:', error) and then throw
error (or remove the catch entirely), ensuring callers receive exceptions
consistently rather than sometimes getting null.

Comment thread lib/actions/jobs.ts
Comment on lines +34 to +43
export async function updateJob(jobId: string, updates: Partial<typeof jobs.$inferInsert>) {
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
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Type mismatch: updatedAt set manually but included in inferred insert type.

Line 37 manually sets updatedAt: new Date(), but the updates parameter is typed as Partial<typeof jobs.$inferInsert>. The $inferInsert type includes updatedAt (which has a default), so callers could pass it in updates and it would be overwritten. Additionally, if updates contains fields not valid for updates (e.g., id), the type doesn't prevent it.

Prefer a dedicated update shape type or explicitly omit non-updatable fields:

type JobUpdate = Partial<Pick<typeof jobs.$inferInsert, 'status' | 'result' | 'error' | 'payload'>>

export async function updateJob(jobId: string, updates: JobUpdate) {
  // ...
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/actions/jobs.ts` around lines 34 - 43, The updateJob function currently
types updates as Partial<typeof jobs.$inferInsert> which allows callers to pass
non-updatable fields (e.g., id) or updatedAt which you then overwrite; create a
dedicated update shape (e.g., type JobUpdate = Partial<Pick<typeof
jobs.$inferInsert, 'status' | 'result' | 'error' | 'payload'>> or use
Omit<typeof jobs.$inferInsert, 'id' | 'createdAt' | 'updatedAt'> wrapped in
Partial) and change the updateJob signature to updateJob(jobId: string, updates:
JobUpdate), keep the server-side updatedAt: new Date() assignment, and
optionally assert or throw if updates contains forbidden keys before calling
db.update so callers cannot override id/updatedAt.

Comment thread lib/actions/jobs.ts
Comment on lines +45 to +46
export async function getJobStatus(jobId: string) {
'use server'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Remove redundant 'use server' directive.

Line 1 already declares 'use server' for the entire module, making this function-level directive redundant.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/actions/jobs.ts` around lines 45 - 46, Remove the redundant
function-level "'use server'" directive inside getJobStatus: open the
getJobStatus function (export async function getJobStatus) and delete the "'use
server'" string so the module-level directive only remains; no behavioral
changes otherwise.

Comment thread lib/db/schema.ts
Comment on lines +105 to +115
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(),
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check for Upstash Redis usage and migration to drop jobs table

# Search for Upstash Redis imports/usage
rg -n '`@upstash/redis`' --type ts -C2

# Search for migration files that drop jobs table
fd -e sql -e ts 'migrat' --exec rg -l 'DROP TABLE.*jobs'

# Verify if jobs table is referenced in queue handler
rg -n "db.*jobs|from.*jobs|jobs\\..*" app/api/queue/route.ts app/api/queues/ -A2

Repository: QueueLab/QCX

Length of output: 323


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Upstash Redis usage
echo "=== Upstash Redis imports/usage ==="
rg -n "`@upstash/redis`" --type ts --type js -S . || true

# 2) Where migrations live, and whether any drop jobs table exists
echo
echo "=== Migration files mentioning jobs table drops ==="
# Try common migration dirs/patterns without assuming structure
for dir in ./.; do
  fd -t f -e sql -e ts -e js -e mjs -e cjs -e kys -e drizzle -e knex . \
    --hidden --exclude-dir node_modules --exclude-dir dist --exclude-dir build \
    | rg -n "migrat|migration|drizzle|knex|sequelize|schema" -S -g'*.{sql,ts,js,mjs,cjs}' \
    | head -n 2000 >/tmp/migration_candidates.txt || true

  if [ -s /tmp/migration_candidates.txt ]; then
    xargs -r rg -n --no-heading -S "DROP TABLE.*jobs|drop table.*jobs|DROP\\s+TABLE\\s+jobs|alter table.*jobs" \
      < /tmp/migration_candidates.txt || true
  else
    echo "No migration candidate files found by heuristic."
  fi
done

# 3) References to the jobs table/schema
echo
echo "=== References to jobs schema/table ==="
rg -n "lib/db/schema|schema\\.ts|\\bjobs\\b" --type ts --type js -S lib app || true

# 4) Check queue handlers for job persistence
echo
echo "=== Queue handler references to jobs/db ==="
rg -n "handleCallback|queue|jobs|updateJob|getJob|jobs\\." --type ts -S app lib || true

# 5) Show the exact schema.ts around the jobs definition for context
echo
echo "=== lib/db/schema.ts around jobs definition ==="
if [ -f "lib/db/schema.ts" ]; then
  nl -ba lib/db/schema.ts | sed -n '80,140p'
else
  echo "lib/db/schema.ts not found"
fi

Repository: QueueLab/QCX

Length of output: 4933


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Search: `@upstash/redis` and upstash usage ==="
rg -n "`@upstash/redis`|`@upstash`|upstash" --type ts --type js --hidden -S . || true
echo
rg -n "UPSTASH|UPSTASH_REDIS|REDIS_URL|REDIS" --hidden -S . || true

echo
echo "=== Search: queue + DB jobs persistence ==="
rg -n "handleCallback|`@vercel/queue`|enqueueJob|updateJob|getJob" --type ts --type js -S app lib || true

echo
echo "=== Migration directories/files discovery ==="
# List likely migration-related files/dirs without assuming structure
rg -n "drizzle|migration" --hidden -S . --glob '!**/node_modules/**' --glob '!**/dist/**' || true

# Find candidate SQL/TS migration files by name (heuristic)
echo
echo "=== Candidate migration files (by filename) ==="
find . -maxdepth 6 -type f \
  \( -iname "*migration*" -o -iname "*migrations*" -o -iname "*drizzle*" -o -iname "*schema*" -o -iname "*ops*" \) \
  \( -iname "*.sql" -o -iname "*.ts" -o -iname "*.js" -o -iname "*.mjs" \) 2>/dev/null | head -n 200 || true

echo
echo "=== Search migrations for jobs table drop ==="
# Search SQL/TS/JS for drop/alter statements on jobs (heuristic)
rg -n "DROP\\s+TABLE\\s+jobs|drop\\s+table\\s+jobs|ALTER\\s+TABLE\\s+jobs|alter\\s+table\\s+jobs" --hidden -S . \
  --glob '!**/node_modules/**' --glob '!**/dist/**' --glob '!**/build/**' --glob '!**/.next/**' || true

echo
echo "=== Show lib/db/schema.ts jobs section (lines ~100-140) ==="
if [ -f lib/db/schema.ts ]; then
  sed -n '95,145p' lib/db/schema.ts
else
  echo "lib/db/schema.ts not found"
fi

Repository: QueueLab/QCX

Length of output: 11180


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Directory listing: drizzle/* ==="
ls -la drizzle 2>/dev/null || true
ls -la drizzle/migrations 2>/dev/null || true
find drizzle -maxdepth 2 -type f 2>/dev/null | head -n 200 || true

echo
echo "=== Search drizzle/migrations for jobs table changes ==="
rg -n "DROP\\s+TABLE\\s+jobs|drop\\s+table\\s+jobs|ALTER\\s+TABLE\\s+jobs|alter\\s+table\\s+jobs|jobs\\b" -S drizzle/migrations 2>/dev/null || true

echo
echo "=== Inspect lib/actions/jobs.ts ==="
if [ -f lib/actions/jobs.ts ]; then
  sed -n '1,120p' lib/actions/jobs.ts
else
  echo "lib/actions/jobs.ts not found"
fi

echo
echo "=== Inspect app/api/queue/route.ts ==="
if [ -f app/api/queue/route.ts ]; then
  sed -n '1,120p' app/api/queue/route.ts
else
  echo "app/api/queue/route.ts not found"
fi

echo
echo "=== Status values used in queue handler code ==="
rg -n "updateJob\\(.*status\\s*:\\s*'|status\\s*:\\s*'|status:\\s*\"|set\\(\"status\"|processing|pending|completed|failed|error" -S app lib

echo
echo "=== Queries filtering by jobs.userId or jobs.status ==="
rg -n "from\\(jobs\\)|\\.select\\(\\).*jobs|where\\(eq\\(jobs\\.(userId|status)" -S lib app

Repository: QueueLab/QCX

Length of output: 25508


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Show drizzle/migrations/*.sql files headers and any jobs-related statements ==="
for f in drizzle/migrations/*.sql; do
  echo
  echo "--- $f ---"
  # Print lines around any mention of jobs
  rg -n "\\bjobs\\b" "$f" -n || true
  # Print full file if small-ish, else only relevant ranges
  lines=$(wc -l < "$f")
  echo "Lines: $lines"
  if [ "$lines" -le 200 ]; then
    cat "$f"
  else
    # best-effort: show sections near jobs keywords
    rg -n -C3 "\\bjobs\\b" "$f" || true
  fi
done

echo
echo "=== Search for DROP/ALTER jobs in the whole migrations folder ==="
rg -n "DROP\\s+TABLE\\s+jobs|drop\\s+table\\s+jobs|ALTER\\s+TABLE\\s+jobs|alter\\s+table\\s+jobs" drizzle/migrations || true

echo
echo "=== Check drizzle.config.ts and migrate.ts migrationsFolder setting ==="
sed -n '1,120p' drizzle.config.ts
sed -n '1,120p' lib/db/migrate.ts

Repository: QueueLab/QCX

Length of output: 5253


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Search for indexes/constraints on jobs in lib/db/schema.ts ==="
rg -n "index\\(|unique\\(|check\\(|constraint\\(|enum\\(" lib/db/schema.ts -S || true

echo
echo "=== Show jobs table block again with nearby lines (for context) ==="
sed -n '90,140p' lib/db/schema.ts

Repository: QueueLab/QCX

Length of output: 2259


Critical: Queue worker still persists into Postgres jobs, but Drizzle migrations don’t create (or remove) that table, and there’s no real Upstash Redis usage.

  • app/api/queue/route.ts + lib/actions/jobs.ts update jobs.status/result/error, and lib/db/schema.ts defines the jobs table.
  • drizzle/migrations/*.sql only create chats/messages/users and calendar_notes—there’s no migration creating (or dropping) jobs, so this will break on a fresh DB.
  • No @upstash/redis integration exists in code (only README/env mentions), so the “Upstash Redis for result storage” PR objective isn’t reflected here.
  • status is unconstrained text (add enum/CHECK). updatedAt is explicitly set in updateJob(), so it does update during job state changes.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/db/schema.ts` around lines 105 - 115, The PR left a persistent Postgres
"jobs" table (export const jobs) without corresponding Drizzle migrations and
without real Upstash Redis integration; add a new migration SQL that
creates/drops the jobs table matching the schema in lib/db/schema.ts (including
id, user_id FK, payload/result/error JSONB, created_at/updated_at timestamps)
and add a CHECK or ENUM constraint for status (allowed values like
'pending','running','completed','failed'); update lib/actions/jobs.ts and
app/api/queue/route.ts to either (a) move result storage to Upstash Redis
(integrate `@upstash/redis` client and store result keys there, reading/writing
from Redis) or (b) keep results in Postgres but ensure migrations reflect that
decision, and ensure updateJob() correctly sets updatedAt (or rely on DB
trigger/default) so updatedAt changes on state updates.

Comment thread lib/db/schema.ts
Comment on lines +126 to +127
jobs: many(jobs),
}));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Remove relations for jobs table.

Since the jobs table should not exist (see previous comment), these ORM relations linking users ↔ jobs must also be removed.

Also applies to: 129-134

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/db/schema.ts` around lines 126 - 127, Remove the ORM relations that
reference the non-existent jobs table: delete the "jobs: many(jobs)" entry
inside the users schema/object and any corresponding inverse relation entries in
the jobs-related schema (e.g., properties using many(jobs) or a user/jobs
relation) as noted around the same block (lines 129–134). Also remove any
imports, type references, or variables named "jobs" used solely for those
relations and adjust related types or validators in the same schema file (e.g.,
the users model/schema) so there are no lingering references to the jobs table.

- 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants