Make report generation asynchronous using a job queue#663
Conversation
- 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>
|
👋 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 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 reviews are paused for this user.Troubleshooting steps vary by plan Learn more → On a Teams plan? Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center? |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Review limit reached
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 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 configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (1)
WalkthroughThis 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. ChangesAsynchronous Report Generation via Job Queue
Sequence DiagramsequenceDiagram
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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related issues
Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
|
|
|
@coderabbitai use vercel queue not database https://vercel.com/docs/queues |
|
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 catLength of output: 4483 🌐 Web query:
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 -10Length 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.tsLength of output: 3147 🏁 Script executed: sed -n '1,80p' app/actions.tsxLength of output: 3651 🏁 Script executed: # Confirm the exact lines to remove in schema.ts
grep -n "jobs\|jobsRelations" lib/db/schema.tsLength of output: 302
1. Install the SDKbun add `@vercel/queue`2.
|
I have updated the implementation to use Vercel Queues instead of a custom database-backed worker. The |
- 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>
There was a problem hiding this comment.
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 | 🔴 CriticalFix 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/queuesconfiguration (jq shows “No vercel.json or queues config”), sosend('report-generation', { jobId })can’t reliably reach a consumer. Ensure thesend()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 afailedjob 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 onjobId.🤖 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
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (8)
.envapp/actions.tsxapp/api/queue/route.tscomponents/download-report-button.tsxlib/actions/jobs-client.tslib/actions/jobs.tslib/db/schema.tspackage.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 deletelib/actions/jobs.tsyet—confirm the Redis-backed replacement and whether the module is still used.
lib/actions/jobs.tsis still DB-backed (Drizzlejobsinsert/select/update) andgetJobStatus()returns data from thejobstable. The migration plan requires switching to Redis-backed job status (readingreport-job:{messageId}via Upstash Redis), but no@upstash/redis/report-jobusage was located in the current search. Add/wire a Redis-backedgetJobStatus(jobId)and removelib/actions/jobs.tsonly once nothing imports it.
| 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(), | ||
| }); | ||
| } | ||
| }); |
There was a problem hiding this comment.
🧩 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" || trueRepository: 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" . || trueRepository: 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" . || trueRepository: 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 || trueRepository: 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:
- 1: https://vercel.com/docs/queues/sdk
- 2: https://registry.npmjs.org/@vercel/queue
- 3: https://npmx.dev/package/@vercel/queue/v/%5E0.1.6
- 4: https://aming.ourdisc.net/docs/queues/sdk
- 5: https://vercel.com/docs/queues/quickstart.md
- 6: https://vercel.com/docs/queues/quickstart
🏁 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"
doneRepository: 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"
doneRepository: QueueLab/QCX
Length of output: 50368
Critical: Vercel Queue worker swallows failures, so jobs won’t retry
- In
app/api/queue/route.ts, thecatchblock updates the job tofailedbut does not re-throw;handleCallbackwill treat the handler as successful and acknowledge the message, preventing Vercel Queue retries. Re-throw the error afterupdateJob(...)(and onlyreturnfor non-retriable cases like missingjobId/unknownjob). const { messages } = job.payload as { messages: any[] }has no runtime validation; add guards thatjob.payloadexists andmessagesis an array before callinggenerateReportContext.- Remove redundant
updatedAt: new Date()fromupdateJob(...)calls—lib/actions/jobs.tsalready setsupdatedAtinternally. - 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.
| if (!jobId) { | ||
| console.error('No jobId in queue payload'); | ||
| return; | ||
| } |
There was a problem hiding this comment.
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.
| 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 }; |
There was a problem hiding this comment.
Polling implementation has multiple critical issues.
-
Line 61: Unsafe destructuring assumes response shape.
Thesubmitaction 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 produceundefinedon 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;
-
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).
- Each
-
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., missingsummary), the code will silently useundefined.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 };
-
No cleanup on component unmount.
If the user navigates away during polling, the loop continues running in the background until timeout. Add cleanup withuseEffectand an abort signal or flag to stop polling when the component unmounts. -
Magic number 60 without explanation.
Line 70:const maxAttempts = 60; // 60 secondsassumes 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.
| 'use server' | ||
|
|
||
| import { getJobStatus as getJobStatusServer } from './jobs' | ||
|
|
||
| export async function getJobStatus(jobId: string) { | ||
| return await getJobStatusServer(jobId) | ||
| } |
There was a problem hiding this comment.
Critical: This file should have been removed or replaced.
Per the PR summary and bot migration plan:
"Replace
lib/actions/jobs-client.tswith a Redis-backedgetJobStatus(jobId)that readsreport-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.
| import { db } from '@/lib/db' | ||
| import { jobs } from '@/lib/db/schema' | ||
| import { eq } from 'drizzle-orm' | ||
| import { revalidatePath } from 'next/cache' |
There was a problem hiding this comment.
🧹 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).
| 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 | ||
| } | ||
| } |
There was a problem hiding this comment.
🧹 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.
| 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 | ||
| } | ||
| } |
There was a problem hiding this comment.
🧹 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.
| export async function getJobStatus(jobId: string) { | ||
| 'use server' |
There was a problem hiding this comment.
🧹 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.
| 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(), | ||
| }); |
There was a problem hiding this comment.
🧩 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/ -A2Repository: 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"
fiRepository: 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"
fiRepository: 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 appRepository: 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.tsRepository: 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.tsRepository: 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.tsupdatejobs.status/result/error, andlib/db/schema.tsdefines thejobstable.drizzle/migrations/*.sqlonly createchats/messages/usersandcalendar_notes—there’s no migration creating (or dropping)jobs, so this will break on a fresh DB.- No
@upstash/redisintegration exists in code (only README/env mentions), so the “Upstash Redis for result storage” PR objective isn’t reflected here. statusis unconstrained text (add enum/CHECK).updatedAtis explicitly set inupdateJob(), 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.
| jobs: many(jobs), | ||
| })); |
There was a problem hiding this comment.
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>
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