feat: add fullstack-vercel-turso example#105
Conversation
Demonstrates Durably on Vercel serverless with Turso (libSQL): - onRequest lazy init for real-time SSE streaming - Vercel Cron background worker for processing interrupted jobs - Long-running report generation job (~90s, 21 steps) that exceeds serverless timeout to showcase step resumability across invocations - Local development with libsqld via Docker Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds a fullstack Vercel + Turso example under Changes
Sequence Diagram(s)sequenceDiagram
participant User as User/Browser
participant Index as Index Route<br/>(Server Action)
participant API as Durably API<br/>(/api/durably)
participant Durably as Durably<br/>Instance
participant DB as Turso<br/>Database
User->>Index: Submit job form (intent + params)
Index->>Durably: durably.jobs.X.trigger(input)
Durably-->>Index: runId
Index-->>User: Return runId
User->>API: SSE subscribe /api/durably/subscribe?runId=X
API->>Durably: Subscribe to run X
Durably->>DB: Persist/read run state & checkpoints
Durably->>Durably: Execute job steps, emit progress/logs
Durably-->>API: Progress events
API-->>User: Stream SSE events (progress, logs, status)
API-->>User: Final status when complete
sequenceDiagram
participant Cron as Vercel Cron
participant Worker as /api/worker Endpoint
participant Handler as durablyHandler
participant Durably as Durably Instance
participant DB as Turso Database
Cron->>Worker: POST /api/worker (Authorization: Bearer CRON_SECRET)
Worker->>Handler: durablyHandler.handle(request)
Handler->>Durably: durably.init()
Handler->>Durably: durably.processUntilIdle()
Durably->>DB: Fetch pending jobs
loop For each pending job
Durably->>Durably: Execute job steps and update checkpoints
Durably->>DB: Persist progress/status
end
Durably-->>Handler: { processed: N }
Handler-->>Worker: HTTP 200 { processed: N }
Estimated code review effort🎯 4 (Complex) | ⏱️ ~65 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
📝 Coding Plan for PR comments
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 |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (3)
examples/fullstack-vercel-turso/.dockerignore (1)
1-4: Consider removing this unused file.This
.dockerignorefile has no effect since this example uses Vercel serverless deployment (no Dockerfile). Thedocker-compose.ymlis only used to run libsqld locally, and.dockerignoreonly affectsdocker buildcontexts, notdocker compose up.This appears to be copied from
fullstack-react-routerwhich does have a Dockerfile.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/fullstack-vercel-turso/.dockerignore` around lines 1 - 4, The .dockerignore file is unused for this Vercel serverless example (no Dockerfile) and should be removed to avoid confusion; delete the .dockerignore entry from the example repo (or if there is an intentional reason to keep it, add a brief note in the example README explaining why it’s present and referencing docker-compose usage for local libsqld only).examples/fullstack-vercel-turso/app/routes/api.worker.ts (1)
24-25: Bound the cron worker's batch size.
processUntilIdle()keeps polling until the queue is empty. Under backlog, that can consume the whole serverless budget for one cron tick and get the request terminated before it returns. Consider setting an explicit cap per invocation and letting the next scheduled run continue the queue.Possible change
- const processed = await durably.processUntilIdle() + const processed = await durably.processUntilIdle({ maxRuns: 1 })🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/fullstack-vercel-turso/app/routes/api.worker.ts` around lines 24 - 25, processUntilIdle() will drain the entire queue in one invocation and can exhaust serverless time on backlog; modify the cron handler using the durably client (durably) to enforce a per-invocation cap: replace the single await durably.processUntilIdle() call with a bounded processing loop or a single call to a batch-oriented API (e.g., durably.processBatch or durably.processUntilIdle({...maxJobs: N...}) if supported) and stop after a fixed number of jobs (use the processed counter returned to break when you hit the cap) so the remaining work is picked up by the next scheduled run.examples/fullstack-vercel-turso/app/routes/_index/dashboard.tsx (1)
12-27: Addgenerate-reporttoDashboardRun.
useRuns<DashboardRun>()is modeling the runs rendered in this table, but the union omits the new report job. That leaves the headline flow for this example outside the typed dashboard contract.♻️ Suggested update
import type { DataSyncInput, DataSyncOutput, + GenerateReportInput, + GenerateReportOutput, ImportCsvInput, ImportCsvOutput, ProcessImageInput, ProcessImageOutput, } from '~/jobs' @@ type DashboardRun = | TypedClientRun<DataSyncInput, DataSyncOutput> + | TypedClientRun<GenerateReportInput, GenerateReportOutput> | TypedClientRun<ImportCsvInput, ImportCsvOutput> | TypedClientRun<ProcessImageInput, ProcessImageOutput>Also applies to: 46-49
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/fullstack-vercel-turso/app/routes/_index/dashboard.tsx` around lines 12 - 27, Update the DashboardRun union to include the new "generate-report" job: add a union member using TypedClientRun with the generate-report job's input/output types (e.g., TypedClientRun<GenerateReportInput, GenerateReportOutput>) so useRuns<DashboardRun>() correctly types the report runs; update the same pattern wherever DashboardRun is declared/used (the union near the DashboardRun type and the duplicate occurrence referenced in the review).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@examples/fullstack-vercel-turso/app/jobs/data-sync.ts`:
- Around line 12-56: The job definition for dataSyncJob uses defineJob but
doesn’t set preserveSteps, so step history will be cleaned up; update the
defineJob call for dataSyncJob to include preserveSteps: true in its
configuration object (and apply the same flag to other example jobs) so the
dashboard can retain step records for completed runs — locate the dataSyncJob
definition and add the preserveSteps: true property alongside
name/input/output/run.
In `@examples/fullstack-vercel-turso/app/jobs/generate-report.ts`:
- Around line 45-46: The progress ordinal resets because currentStep is
reinitialized to 0 each run; instead use fixed ordinals per step so resumed jobs
don't regress UI. Update all step.run() blocks (the ones using currentStep and
totalSteps) to compute and use a fixed step index (e.g., a constant ordinal for
each step) when publishing progress rather than incrementing a local currentStep
on each invocation; ensure the step.run() callbacks read persisted step outputs
to determine completed steps and publish progress as "ordinal/totalSteps" using
the fixed ordinal values for each specific step (reference totalSteps,
currentStep, and step.run()).
In `@examples/fullstack-vercel-turso/app/jobs/import-csv.ts`:
- Around line 72-75: The run log currently prints PII (row.name, row.email,
row.amount) in the import job; update the logging in the import-csv.ts job to
avoid sensitive data by changing the step.log.info call to output a
non-sensitive identifier (e.g., row.id or a generated import index) and a
generic status message instead, leaving the import counter (imported) and import
logic unchanged; locate the step.log.info usage that references
row.name/row.email/row.amount and replace those fields with row.id or "row
index" and a non-sensitive message.
In `@examples/fullstack-vercel-turso/README.md`:
- Around line 15-27: The fenced ASCII flow block lacks a language hint and
triggers markdownlint MD040; update the triple-backtick fence surrounding the
flow (the block showing "User triggers job → POST /api/durably/trigger" through
"Vercel Cron → POST /api/worker") to use a simple language tag like text
(replace ``` with ```text at the start and close with ```), ensuring the fenced
block is labeled and passes validation.
---
Nitpick comments:
In `@examples/fullstack-vercel-turso/.dockerignore`:
- Around line 1-4: The .dockerignore file is unused for this Vercel serverless
example (no Dockerfile) and should be removed to avoid confusion; delete the
.dockerignore entry from the example repo (or if there is an intentional reason
to keep it, add a brief note in the example README explaining why it’s present
and referencing docker-compose usage for local libsqld only).
In `@examples/fullstack-vercel-turso/app/routes/_index/dashboard.tsx`:
- Around line 12-27: Update the DashboardRun union to include the new
"generate-report" job: add a union member using TypedClientRun with the
generate-report job's input/output types (e.g.,
TypedClientRun<GenerateReportInput, GenerateReportOutput>) so
useRuns<DashboardRun>() correctly types the report runs; update the same pattern
wherever DashboardRun is declared/used (the union near the DashboardRun type and
the duplicate occurrence referenced in the review).
In `@examples/fullstack-vercel-turso/app/routes/api.worker.ts`:
- Around line 24-25: processUntilIdle() will drain the entire queue in one
invocation and can exhaust serverless time on backlog; modify the cron handler
using the durably client (durably) to enforce a per-invocation cap: replace the
single await durably.processUntilIdle() call with a bounded processing loop or a
single call to a batch-oriented API (e.g., durably.processBatch or
durably.processUntilIdle({...maxJobs: N...}) if supported) and stop after a
fixed number of jobs (use the processed counter returned to break when you hit
the cap) so the remaining work is picked up by the next scheduled run.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 1ad8a074-d6ae-4c8f-8fc7-6bf465115bdb
⛔ Files ignored due to path filters (2)
examples/fullstack-vercel-turso/public/favicon.icois excluded by!**/*.icopnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (34)
examples/fullstack-vercel-turso/.dockerignoreexamples/fullstack-vercel-turso/.env.exampleexamples/fullstack-vercel-turso/.gitignoreexamples/fullstack-vercel-turso/README.mdexamples/fullstack-vercel-turso/app/app.cssexamples/fullstack-vercel-turso/app/jobs/data-sync.tsexamples/fullstack-vercel-turso/app/jobs/generate-report.tsexamples/fullstack-vercel-turso/app/jobs/import-csv.tsexamples/fullstack-vercel-turso/app/jobs/index.tsexamples/fullstack-vercel-turso/app/jobs/process-image.tsexamples/fullstack-vercel-turso/app/lib/database.server.tsexamples/fullstack-vercel-turso/app/lib/durably.server.tsexamples/fullstack-vercel-turso/app/lib/durably.tsexamples/fullstack-vercel-turso/app/root.tsxexamples/fullstack-vercel-turso/app/routes.tsexamples/fullstack-vercel-turso/app/routes/_index.tsxexamples/fullstack-vercel-turso/app/routes/_index/dashboard.tsxexamples/fullstack-vercel-turso/app/routes/_index/data-sync-form.tsxexamples/fullstack-vercel-turso/app/routes/_index/data-sync-progress.tsxexamples/fullstack-vercel-turso/app/routes/_index/image-processing-form.tsxexamples/fullstack-vercel-turso/app/routes/_index/image-processing-progress.tsxexamples/fullstack-vercel-turso/app/routes/_index/report-form.tsxexamples/fullstack-vercel-turso/app/routes/_index/report-progress.tsxexamples/fullstack-vercel-turso/app/routes/_index/run-progress.tsxexamples/fullstack-vercel-turso/app/routes/api.durably.$.tsexamples/fullstack-vercel-turso/app/routes/api.worker.tsexamples/fullstack-vercel-turso/biome.jsonexamples/fullstack-vercel-turso/docker-compose.ymlexamples/fullstack-vercel-turso/package.jsonexamples/fullstack-vercel-turso/prettier.config.jsexamples/fullstack-vercel-turso/react-router.config.tsexamples/fullstack-vercel-turso/tsconfig.jsonexamples/fullstack-vercel-turso/vercel.jsonexamples/fullstack-vercel-turso/vite.config.ts
| export const dataSyncJob = defineJob({ | ||
| name: 'data-sync', | ||
| input: z.object({ userId: z.string() }), | ||
| output: z.object({ synced: z.number(), failed: z.number() }), | ||
| run: async (step, input) => { | ||
| step.log.info(`Starting sync for user: ${input.userId}`) | ||
|
|
||
| const items = await step.run('fetch-local', async () => { | ||
| step.progress(1, 4, 'Fetching local data...') | ||
| await delay(300) | ||
| return Array.from({ length: 10 }, (_, i) => ({ | ||
| id: `item-${i}`, | ||
| data: `Data for ${input.userId}`, | ||
| })) | ||
| }) | ||
|
|
||
| let synced = 0 | ||
| let failed = 0 | ||
|
|
||
| for (let i = 0; i < items.length; i++) { | ||
| const item = items[i] | ||
| const success = await step.run(`sync-item-${item.id}`, async () => { | ||
| step.progress(2 + Math.floor(i / 5), 4, `Syncing item ${i + 1}...`) | ||
| await delay(100) | ||
| return Math.random() > 0.1 // 90% success rate | ||
| }) | ||
|
|
||
| if (success) { | ||
| synced++ | ||
| } else { | ||
| failed++ | ||
| step.log.warn(`Failed to sync item: ${item.id}`) | ||
| } | ||
| } | ||
|
|
||
| await step.run('finalize', async () => { | ||
| step.progress(4, 4, 'Finalizing...') | ||
| await delay(200) | ||
| }) | ||
|
|
||
| step.log.info(`Sync complete: ${synced} synced, ${failed} failed`) | ||
|
|
||
| return { synced, failed } | ||
| }, | ||
| }) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -e
# 1) Show where the dashboard fetches and renders step history.
rg -n -C2 'getSteps\(|Steps:' examples/fullstack-vercel-turso/app/routes/_index/dashboard.tsx
echo
# 2) Show preserveSteps / cleanupSteps semantics in the runtime or docs.
rg -n -C3 'preserveSteps|cleanupSteps' packages/durably/src docs || true
echo
# 3) Show whether the example jobs opt into step retention.
rg -n -C2 'preserveSteps' examples/fullstack-vercel-turso/app/jobs || trueRepository: coji/durably
Length of output: 4881
Add preserveSteps: true to preserve step history for the dashboard.
The dashboard fetches and displays step history for completed runs, but this job uses the default cleanup behavior that deletes steps when runs reach a terminal state. Add preserveSteps: true to the job definition to preserve step records, and apply the same setting to the other example jobs the dashboard displays.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@examples/fullstack-vercel-turso/app/jobs/data-sync.ts` around lines 12 - 56,
The job definition for dataSyncJob uses defineJob but doesn’t set preserveSteps,
so step history will be cleaned up; update the defineJob call for dataSyncJob to
include preserveSteps: true in its configuration object (and apply the same flag
to other example jobs) so the dashboard can retain step records for completed
runs — locate the dataSyncJob definition and add the preserveSteps: true
property alongside name/input/output/run.
There was a problem hiding this comment.
This is intentional — we don't want to preserve steps for this demo. Step data is cleaned up on terminal state by design (default behavior). The dashboard's View button fetches steps, which will show empty for completed runs, and that's fine for demonstrating the default.
There was a problem hiding this comment.
@coji, understood! Demonstrating the default cleanup behavior is a perfectly valid choice for this example — showing that step history is empty for completed runs is itself informative for users exploring the defaults.
✏️ Learnings added
Learnt from: coji
Repo: coji/durably PR: 105
File: examples/fullstack-vercel-turso/app/jobs/data-sync.ts:11-55
Timestamp: 2026-03-12T13:18:37.201Z
Learning: In `examples/fullstack-vercel-turso`, all example jobs (dataSyncJob, generateReportJob, importCsvJob, processImageJob) intentionally omit `preserveSteps: true` to demonstrate the default behavior where step history is deleted when runs reach a terminal state. The dashboard's View button showing empty steps for completed runs is acceptable and by design. Do not suggest adding `preserveSteps: true` to these jobs.
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.
🧠 Learnings used
Learnt from: coji
Repo: coji/durably PR: 101
File: docs/rfcs/runtime-rearchitecture/ja/core-runtime.md:0-0
Timestamp: 2026-03-08T05:06:39.050Z
Learning: Reviewers should verify that step execution uses step.run() and that step output data is persisted. By default, preserveSteps is false (step history is deleted when runs reach a terminal state). If audit/debug history is required, ensure preserveSteps is set to true. Be aware that cleanupSteps used to exist with inverted semantics (cleanupSteps: true meant delete), and this was renamed to preserveSteps in Phase 1. Apply this guidance to all JS/TS code and docs that reference step persistence.
Learnt from: coji
Repo: coji/durably PR: 11
File: examples/browser-vite-react/src/components/dashboard.tsx:24-34
Timestamp: 2026-01-02T14:03:37.741Z
Learning: In example code directories (e.g., examples/browser-vite-react, examples/fullstack-react-router), error handling may be intentionally omitted to keep code simple and readable, focusing on demonstrating the Durably API rather than production best practices.
- Replace mutable currentStep counter with fixed step numbers in generate-report job. Step callbacks are skipped on resume, so a mutable counter would report wrong progress after restart. - Add GenerateReportInput/Output to DashboardRun union type. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@examples/fullstack-vercel-turso/app/jobs/generate-report.ts`:
- Around line 39-42: The department field on the zod input (input: z.object({
reportType, department })) is used raw when building reportUrl, so encode the
department before interpolation to avoid breaking the URL; update the code that
constructs reportUrl (the interpolation using reportUrl) to use an encoded
department value (e.g., apply a path-safe encoding like encodeURIComponent or a
dedicated path-segment encoder) and/or add a zod transform/validation on
department to ensure it’s safely encoded, ensuring all places that interpolate
department (the reportUrl construction) use the encoded value.
- Line 50: Replace direct step.log.info calls and non-persisted timestamp
assignments with persisted step.run() checkpoints: wrap the "Starting
${input.reportType} report..." log and the completion log/ generatedAt
assignment inside step.run() calls so the start and completion metadata (logs
and generatedAt) are written to persistence; specifically, change the code that
currently uses step.log.info(...) for the start and the block that sets
generatedAt and logs completion to instead call step.run({/* metadata or return
value */}) or pass the log/timestamp as the step output, using the existing
step.run() API to persist these values so resumed runs won’t duplicate the
"Starting ..." entry or mis-stamp generatedAt.
In `@examples/fullstack-vercel-turso/app/routes/_index/dashboard.tsx`:
- Around line 82-89: In showDetails, avoid flashing the previous run’s steps by
clearing steps before switching the selected run and while fetching new steps:
call setSteps([]) immediately when a run is found (before setSelectedRun or
before awaiting getSteps), then await getSteps(runId) and setSteps with the
result; update the sequence around getRun, setSelectedRun, setSteps, and
getSteps to ensure steps are reset first and handle any getSteps errors before
setting the new steps.
- Around line 279-289: The modal overlay is only visual; add proper dialog
semantics by giving the dialog container (the element with className "mx-4
max-h-[80vh] w-full max-w-2xl ...") role="dialog" and aria-modal="true", add an
id to the <h3> (e.g., run-details-title) and reference it via aria-labelledby on
the dialog, and ensure the close button (the button that calls
setSelectedRun(null)) has an accessible name (e.g., aria-label="Close run
details" or aria-labelledby) so assistive tech receives a proper modal context.
- Around line 213-223: The retrigger button is only rendered when run.status is
'failed' or 'cancelled', but the API supports 'completed' runs too; update the
conditional that controls rendering (the JSX block using run.status and the
onClick handler handleRetrigger) to also include 'completed' (e.g., allow
run.status === 'completed' or change to permit statuses other than
'running'/'pending' as appropriate) so completed runs show the "Retrigger"
button and remain disabled when isActioning is true.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: fc00174f-ed3c-45ca-895a-48927bfb38cf
📒 Files selected for processing (2)
examples/fullstack-vercel-turso/app/jobs/generate-report.tsexamples/fullstack-vercel-turso/app/routes/_index/dashboard.tsx
| input: z.object({ | ||
| reportType: z.enum(['daily', 'weekly', 'monthly']), | ||
| department: z.string(), | ||
| }), |
There was a problem hiding this comment.
Encode department before interpolating it into reportUrl.
Lines 39-42 accept any department string, but Line 204 uses it as a raw path segment. Values containing /, ?, #, or spaces will change the returned URL shape.
🔧 Proposed fix
- const url = `https://reports.example.com/${input.department}/${input.reportType}-${Date.now()}.pdf`
+ const departmentPath = encodeURIComponent(input.department)
+ const url = `https://reports.example.com/${departmentPath}/${input.reportType}-${Date.now()}.pdf`Also applies to: 201-205
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@examples/fullstack-vercel-turso/app/jobs/generate-report.ts` around lines 39
- 42, The department field on the zod input (input: z.object({ reportType,
department })) is used raw when building reportUrl, so encode the department
before interpolation to avoid breaking the URL; update the code that constructs
reportUrl (the interpolation using reportUrl) to use an encoded department value
(e.g., apply a path-safe encoding like encodeURIComponent or a dedicated
path-segment encoder) and/or add a zod transform/validation on department to
ensure it’s safely encoded, ensuring all places that interpolate department (the
reportUrl construction) use the encoded value.
There was a problem hiding this comment.
This is a simulated URL in a demo job — the report URL is never actually fetched or used. Skipping for now.
There was a problem hiding this comment.
@coji, understood — since the reportUrl is a mock value purely for demo output and is never actually fetched or navigated to, URL encoding is indeed unnecessary here. Thanks for the clarification!
(ᵔᴥᵔ)ノ
✏️ Learnings added
Learnt from: coji
Repo: coji/durably PR: 105
File: examples/fullstack-vercel-turso/app/jobs/generate-report.ts:36-39
Timestamp: 2026-03-12T13:19:16.662Z
Learning: In the `examples/fullstack-vercel-turso` example project, the `reportUrl` returned by `generateReportJob` (in `app/jobs/generate-report.ts`) is a simulated/mock URL used only as demo output and is never actually fetched or navigated to. URL-encoding the `department` path segment is not required in this context.
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.
🧠 Learnings used
Learnt from: coji
Repo: coji/durably PR: 11
File: examples/browser-vite-react/src/components/dashboard.tsx:24-34
Timestamp: 2026-01-02T14:03:37.741Z
Learning: In example code directories (e.g., examples/browser-vite-react, examples/fullstack-react-router), error handling may be intentionally omitted to keep code simple and readable, focusing on demonstrating the Durably API rather than production best practices.
Learnt from: coji
Repo: coji/durably PR: 101
File: docs/rfcs/runtime-rearchitecture/ja/core-runtime.md:0-0
Timestamp: 2026-03-08T05:06:39.050Z
Learning: Reviewers should verify that step execution uses step.run() and that step output data is persisted. By default, preserveSteps is false (step history is deleted when runs reach a terminal state). If audit/debug history is required, ensure preserveSteps is set to true. Be aware that cleanupSteps used to exist with inverted semantics (cleanupSteps: true meant delete), and this was renamed to preserveSteps in Phase 1. Apply this guidance to all JS/TS code and docs that reference step persistence.
| // so a mutable counter would report wrong progress after restart. | ||
| const T = 21 | ||
|
|
||
| step.log.info(`Starting ${input.reportType} report for ${input.department}`) |
There was a problem hiding this comment.
Persist the start/completion metadata via step.run().
Line 50 and Line 220 are not checkpointed, so a resumed run can append an extra “Starting …” log entry and stamp a later generatedAt if the invocation dies after the last persisted step. examples/fullstack-vercel-turso/app/routes/_index/report-progress.tsx:16-26 renders logs, so that replay is visible in the example UI.
🔧 Proposed fix
- step.log.info(`Starting ${input.reportType} report for ${input.department}`)
+ await step.run('initialize-report', async () => {
+ step.log.info(`Starting ${input.reportType} report for ${input.department}`)
+ })
...
+ const generatedAt = await step.run('finalize-report', async () => {
+ return new Date().toISOString()
+ })
+
return {
reportUrl,
totalRecords,
- generatedAt: new Date().toISOString(),
+ generatedAt,
}Based on learnings: step execution uses step.run() and step output data is persisted.
Also applies to: 217-220
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@examples/fullstack-vercel-turso/app/jobs/generate-report.ts` at line 50,
Replace direct step.log.info calls and non-persisted timestamp assignments with
persisted step.run() checkpoints: wrap the "Starting ${input.reportType}
report..." log and the completion log/ generatedAt assignment inside step.run()
calls so the start and completion metadata (logs and generatedAt) are written to
persistence; specifically, change the code that currently uses
step.log.info(...) for the start and the block that sets generatedAt and logs
completion to instead call step.run({/* metadata or return value */}) or pass
the log/timestamp as the step output, using the existing step.run() API to
persist these values so resumed runs won’t duplicate the "Starting ..." entry or
mis-stamp generatedAt.
There was a problem hiding this comment.
The job already logs start/completion messages via step.log.info(). Adding extra metadata persistence is over-engineering for a demo.
There was a problem hiding this comment.
@coji, understood — keeping the demo straightforward is a valid priority. I'll note this for future reviews.
✏️ Learnings added
Learnt from: coji
Repo: coji/durably PR: 105
File: examples/fullstack-vercel-turso/app/jobs/generate-report.ts:47-47
Timestamp: 2026-03-12T13:19:14.905Z
Learning: In `examples/fullstack-vercel-turso/app/jobs/generate-report.ts`, the `step.log.info()` calls for start/completion messages and the inline `new Date().toISOString()` for `generatedAt` are intentionally left outside `step.run()` checkpoints. This is by design to keep the demo simple; wrapping them in `step.run()` would be over-engineering for an example project. Do not suggest adding extra `step.run()` checkpoints for logging or metadata in this file.
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.
🧠 Learnings used
Learnt from: coji
Repo: coji/durably PR: 105
File: examples/fullstack-vercel-turso/app/jobs/data-sync.ts:11-55
Timestamp: 2026-03-12T13:18:37.201Z
Learning: In `examples/fullstack-vercel-turso`, all example jobs (dataSyncJob, generateReportJob, importCsvJob, processImageJob) intentionally omit `preserveSteps: true` to demonstrate the default behavior where step history is deleted when runs reach a terminal state. The dashboard's View button showing empty steps for completed runs is acceptable and by design. Do not suggest adding `preserveSteps: true` to these jobs.
Learnt from: coji
Repo: coji/durably PR: 101
File: docs/rfcs/runtime-rearchitecture/ja/core-runtime.md:0-0
Timestamp: 2026-03-08T05:06:39.050Z
Learning: Reviewers should verify that step execution uses step.run() and that step output data is persisted. By default, preserveSteps is false (step history is deleted when runs reach a terminal state). If audit/debug history is required, ensure preserveSteps is set to true. Be aware that cleanupSteps used to exist with inverted semantics (cleanupSteps: true meant delete), and this was renamed to preserveSteps in Phase 1. Apply this guidance to all JS/TS code and docs that reference step persistence.
Learnt from: coji
Repo: coji/durably PR: 11
File: examples/browser-vite-react/src/components/dashboard.tsx:24-34
Timestamp: 2026-01-02T14:03:37.741Z
Learning: In example code directories (e.g., examples/browser-vite-react, examples/fullstack-react-router), error handling may be intentionally omitted to keep code simple and readable, focusing on demonstrating the Durably API rather than production best practices.
| const showDetails = async (runId: string) => { | ||
| const run = await getRun(runId) | ||
| if (run) { | ||
| setSelectedRun(run) | ||
| const stepsData = await getSteps(runId) | ||
| setSteps(stepsData) | ||
| } | ||
| } |
There was a problem hiding this comment.
Reset steps before switching the modal to another run.
Line 85 updates selectedRun before the new getSteps() call finishes, so the previous run’s step list can flash under the newly selected run until Line 87 completes.
💡 Minimal fix
const showDetails = async (runId: string) => {
- const run = await getRun(runId)
- if (run) {
- setSelectedRun(run)
- const stepsData = await getSteps(runId)
- setSteps(stepsData)
- }
+ setSteps([])
+ const [run, stepsData] = await Promise.all([getRun(runId), getSteps(runId)])
+ if (!run) return
+ setSelectedRun(run)
+ setSteps(stepsData)
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const showDetails = async (runId: string) => { | |
| const run = await getRun(runId) | |
| if (run) { | |
| setSelectedRun(run) | |
| const stepsData = await getSteps(runId) | |
| setSteps(stepsData) | |
| } | |
| } | |
| const showDetails = async (runId: string) => { | |
| setSteps([]) | |
| const [run, stepsData] = await Promise.all([getRun(runId), getSteps(runId)]) | |
| if (!run) return | |
| setSelectedRun(run) | |
| setSteps(stepsData) | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@examples/fullstack-vercel-turso/app/routes/_index/dashboard.tsx` around lines
82 - 89, In showDetails, avoid flashing the previous run’s steps by clearing
steps before switching the selected run and while fetching new steps: call
setSteps([]) immediately when a run is found (before setSelectedRun or before
awaiting getSteps), then await getSteps(runId) and setSteps with the result;
update the sequence around getRun, setSelectedRun, setSteps, and getSteps to
ensure steps are reset first and handle any getSteps errors before setting the
new steps.
There was a problem hiding this comment.
Good catch — but in practice the modal is closed before opening another, so stale steps flash is unlikely. Will keep it simple for now.
There was a problem hiding this comment.
@coji, that makes sense — if the modal is always dismissed before reopening, there's no opportunity for the stale flash to appear. Thanks for the context!
🧠 Learnings used
Learnt from: coji
Repo: coji/durably PR: 105
File: examples/fullstack-vercel-turso/app/jobs/data-sync.ts:11-55
Timestamp: 2026-03-12T13:18:37.201Z
Learning: In `examples/fullstack-vercel-turso`, all example jobs (dataSyncJob, generateReportJob, importCsvJob, processImageJob) intentionally omit `preserveSteps: true` to demonstrate the default behavior where step history is deleted when runs reach a terminal state. The dashboard's View button showing empty steps for completed runs is acceptable and by design. Do not suggest adding `preserveSteps: true` to these jobs.
Learnt from: coji
Repo: coji/durably PR: 101
File: docs/rfcs/runtime-rearchitecture/ja/core-runtime.md:0-0
Timestamp: 2026-03-08T05:06:48.701Z
Learning: Applies to **/*.{js,ts} and docs: In `packages/durably`, steps are created via `step.run()` and each step's success state and return value is persisted. Step output data is deleted when runs reach terminal state by default (`preserveSteps: false`). Setting `preserveSteps: true` retains step history for audit/debug purposes. The option was previously called `cleanupSteps` (inverted semantics: `cleanupSteps: true` meant delete) and was renamed to `preserveSteps` in Phase 1.
Learnt from: coji
Repo: coji/durably PR: 11
File: examples/browser-vite-react/src/components/dashboard.tsx:24-34
Timestamp: 2026-01-02T14:03:37.741Z
Learning: In example code directories (e.g., examples/browser-vite-react, examples/fullstack-react-router), error handling may be intentionally omitted to keep code simple and readable, focusing on demonstrating the Durably API rather than production best practices.
Learnt from: coji
Repo: coji/durably PR: 11
File: examples/browser-react-router-spa/app/routes/_index/dashboard.tsx:24-34
Timestamp: 2026-01-02T14:03:25.739Z
Learning: In example code under the examples/ directory, error handling is intentionally omitted to keep the code simple and focused on demonstrating the Durably API. Treat these files as illustrative, not production-ready. For real applications, ensure proper error handling and user feedback; do not copy this pattern verbatim into production code.
Learnt from: coji
Repo: coji/durably PR: 11
File: examples/browser-vite-react/src/components/dashboard.tsx:36-53
Timestamp: 2026-01-02T14:03:31.683Z
Learning: In all example files under the examples directory (e.g., examples/**/*.tsx), prioritize simplicity and clarity over comprehensive error handling. Focus on demonstrating Durably API usage patterns; omit production-grade error handling in examples to keep them readable and centered on core concepts. Where appropriate, include concise comments to explain the pattern being demonstrated.
Learnt from: coji
Repo: coji/durably PR: 11
File: examples/fullstack-react-router/app/routes/_index.tsx:28-46
Timestamp: 2026-01-02T14:03:32.230Z
Learning: In the coji/durably repository's examples directory, code examples (e.g., any .tsx under examples) should prioritize simplicity and readability over comprehensive error handling to clearly demonstrate the Durably API usage patterns. Include minimal, demonstrative error handling only as necessary for clarity.
| {(run.status === 'failed' || | ||
| run.status === 'cancelled') && ( | ||
| <button | ||
| type="button" | ||
| onClick={() => handleRetrigger(run.id)} | ||
| disabled={isActioning} | ||
| className="text-xs text-green-600 hover:text-green-800 disabled:cursor-not-allowed disabled:text-gray-400" | ||
| > | ||
| Retrigger | ||
| </button> | ||
| )} |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's understand the project structure
git ls-files | head -20Repository: coji/durably
Length of output: 640
🏁 Script executed:
# Search for documentation about retrigger
find . -type f \( -name "*.md" -o -name "*.ts" -o -name "*.tsx" \) | grep -E "(README|CLAUDE|docs)" | head -20Repository: coji/durably
Length of output: 1100
🏁 Script executed:
# Look for the useRunActions hook definition
rg "useRunActions" -A 10 -B 2 | head -100Repository: coji/durably
Length of output: 5141
🏁 Script executed:
# Search for retrigger function definition and documentation
rg "retrigger" -A 5 -B 2 | head -150Repository: coji/durably
Length of output: 8650
🏁 Script executed:
# Search for API documentation specifically about retrigger
rg "retrigger" -A 10 -B 5 website/api/ | head -200Repository: coji/durably
Length of output: 12287
🏁 Script executed:
# Look for TypeScript type definitions for retrigger
rg "retrigger" -A 5 -B 2 --type ts --type tsx | grep -E "(function|const|interface|type|status)" | head -150Repository: coji/durably
Length of output: 83
🏁 Script executed:
# Check CLAUDE.md for any guidance about retrigger
cat CLAUDE.mdRepository: coji/durably
Length of output: 3932
🏁 Script executed:
# Search for the actual useRunActions hook implementation
fd "useRunActions" -type f --exec cat {} \;Repository: coji/durably
Length of output: 226
🏁 Script executed:
# Check the specific file and lines mentioned in the review
sed -n '210,225p' examples/fullstack-vercel-turso/app/routes/_index/dashboard.tsxRepository: coji/durably
Length of output: 827
🏁 Script executed:
# Also look at a few more lines for context
sed -n '200,230p' examples/fullstack-vercel-turso/app/routes/_index/dashboard.tsxRepository: coji/durably
Length of output: 1631
Expose retrigger for completed runs too.
The API documentation explicitly states retrigger() supports completed, failed, and cancelled runs, but this guard only renders the button for failed/cancelled runs. Completed runs currently lose a supported dashboard action.
Suggested fix
- {(run.status === 'failed' ||
+ {(run.status === 'completed' ||
+ run.status === 'failed' ||
run.status === 'cancelled') && (📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| {(run.status === 'failed' || | |
| run.status === 'cancelled') && ( | |
| <button | |
| type="button" | |
| onClick={() => handleRetrigger(run.id)} | |
| disabled={isActioning} | |
| className="text-xs text-green-600 hover:text-green-800 disabled:cursor-not-allowed disabled:text-gray-400" | |
| > | |
| Retrigger | |
| </button> | |
| )} | |
| {(run.status === 'completed' || | |
| run.status === 'failed' || | |
| run.status === 'cancelled') && ( | |
| <button | |
| type="button" | |
| onClick={() => handleRetrigger(run.id)} | |
| disabled={isActioning} | |
| className="text-xs text-green-600 hover:text-green-800 disabled:cursor-not-allowed disabled:text-gray-400" | |
| > | |
| Retrigger | |
| </button> | |
| )} |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@examples/fullstack-vercel-turso/app/routes/_index/dashboard.tsx` around lines
213 - 223, The retrigger button is only rendered when run.status is 'failed' or
'cancelled', but the API supports 'completed' runs too; update the conditional
that controls rendering (the JSX block using run.status and the onClick handler
handleRetrigger) to also include 'completed' (e.g., allow run.status ===
'completed' or change to permit statuses other than 'running'/'pending' as
appropriate) so completed runs show the "Retrigger" button and remain disabled
when isActioning is true.
There was a problem hiding this comment.
Noted. This is a demo dashboard — keeping it simple.
There was a problem hiding this comment.
@coji, understood — keeping the demo lean makes sense!
(¬、¬)
✏️ Learnings added
Learnt from: coji
Repo: coji/durably PR: 105
File: examples/fullstack-vercel-turso/app/routes/_index/dashboard.tsx:212-222
Timestamp: 2026-03-12T13:19:12.919Z
Learning: In `examples/fullstack-vercel-turso/app/routes/_index/dashboard.tsx`, the Retrigger button is intentionally shown only for `failed` and `cancelled` runs (not `completed`), by design to keep the demo dashboard simple. Do not suggest adding retrigger support for completed runs in this file.
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.
🧠 Learnings used
Learnt from: coji
Repo: coji/durably PR: 105
File: examples/fullstack-vercel-turso/app/jobs/data-sync.ts:11-55
Timestamp: 2026-03-12T13:18:37.201Z
Learning: In `examples/fullstack-vercel-turso`, all example jobs (dataSyncJob, generateReportJob, importCsvJob, processImageJob) intentionally omit `preserveSteps: true` to demonstrate the default behavior where step history is deleted when runs reach a terminal state. The dashboard's View button showing empty steps for completed runs is acceptable and by design. Do not suggest adding `preserveSteps: true` to these jobs.
Learnt from: coji
Repo: coji/durably PR: 11
File: examples/browser-vite-react/src/components/dashboard.tsx:24-34
Timestamp: 2026-01-02T14:03:37.741Z
Learning: In example code directories (e.g., examples/browser-vite-react, examples/fullstack-react-router), error handling may be intentionally omitted to keep code simple and readable, focusing on demonstrating the Durably API rather than production best practices.
Learnt from: coji
Repo: coji/durably PR: 11
File: examples/browser-react-router-spa/app/routes/_index/dashboard.tsx:24-34
Timestamp: 2026-01-02T14:03:25.739Z
Learning: In example code under the examples/ directory, error handling is intentionally omitted to keep the code simple and focused on demonstrating the Durably API. Treat these files as illustrative, not production-ready. For real applications, ensure proper error handling and user feedback; do not copy this pattern verbatim into production code.
Learnt from: coji
Repo: coji/durably PR: 11
File: examples/browser-vite-react/src/components/dashboard.tsx:36-53
Timestamp: 2026-01-02T14:03:31.683Z
Learning: In all example files under the examples directory (e.g., examples/**/*.tsx), prioritize simplicity and clarity over comprehensive error handling. Focus on demonstrating Durably API usage patterns; omit production-grade error handling in examples to keep them readable and centered on core concepts. Where appropriate, include concise comments to explain the pattern being demonstrated.
Learnt from: coji
Repo: coji/durably PR: 11
File: examples/fullstack-react-router/app/routes/_index.tsx:28-46
Timestamp: 2026-01-02T14:03:32.230Z
Learning: In the coji/durably repository's examples directory, code examples (e.g., any .tsx under examples) should prioritize simplicity and readability over comprehensive error handling to clearly demonstrate the Durably API usage patterns. Include minimal, demonstrative error handling only as necessary for clarity.
| <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"> | ||
| <div className="mx-4 max-h-[80vh] w-full max-w-2xl overflow-y-auto rounded-lg bg-white shadow-xl"> | ||
| <div className="p-6"> | ||
| <div className="mb-4 flex items-center justify-between"> | ||
| <h3 className="text-lg font-semibold">Run Details</h3> | ||
| <button | ||
| type="button" | ||
| onClick={() => setSelectedRun(null)} | ||
| className="text-gray-400 hover:text-gray-600" | ||
| > | ||
| ✕ |
There was a problem hiding this comment.
Add real dialog semantics to the details modal.
Right now this is only a visual overlay. Without role="dialog", aria-modal, and an accessible name on the close button, assistive-tech users do not get a proper modal context.
💡 Minimal accessibility fix
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
- <div className="mx-4 max-h-[80vh] w-full max-w-2xl overflow-y-auto rounded-lg bg-white shadow-xl">
+ <div
+ className="mx-4 max-h-[80vh] w-full max-w-2xl overflow-y-auto rounded-lg bg-white shadow-xl"
+ role="dialog"
+ aria-modal="true"
+ aria-labelledby="run-details-title"
+ >
<div className="p-6">
<div className="mb-4 flex items-center justify-between">
- <h3 className="text-lg font-semibold">Run Details</h3>
+ <h3 id="run-details-title" className="text-lg font-semibold">
+ Run Details
+ </h3>
<button
type="button"
+ aria-label="Close run details"
+ autoFocus
onClick={() => setSelectedRun(null)}
className="text-gray-400 hover:text-gray-600"
>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"> | |
| <div className="mx-4 max-h-[80vh] w-full max-w-2xl overflow-y-auto rounded-lg bg-white shadow-xl"> | |
| <div className="p-6"> | |
| <div className="mb-4 flex items-center justify-between"> | |
| <h3 className="text-lg font-semibold">Run Details</h3> | |
| <button | |
| type="button" | |
| onClick={() => setSelectedRun(null)} | |
| className="text-gray-400 hover:text-gray-600" | |
| > | |
| ✕ | |
| <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"> | |
| <div | |
| className="mx-4 max-h-[80vh] w-full max-w-2xl overflow-y-auto rounded-lg bg-white shadow-xl" | |
| role="dialog" | |
| aria-modal="true" | |
| aria-labelledby="run-details-title" | |
| > | |
| <div className="p-6"> | |
| <div className="mb-4 flex items-center justify-between"> | |
| <h3 id="run-details-title" className="text-lg font-semibold">Run Details</h3> | |
| <button | |
| type="button" | |
| aria-label="Close run details" | |
| autoFocus | |
| onClick={() => setSelectedRun(null)} | |
| className="text-gray-400 hover:text-gray-600" | |
| > | |
| ✕ |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@examples/fullstack-vercel-turso/app/routes/_index/dashboard.tsx` around lines
279 - 289, The modal overlay is only visual; add proper dialog semantics by
giving the dialog container (the element with className "mx-4 max-h-[80vh]
w-full max-w-2xl ...") role="dialog" and aria-modal="true", add an id to the
<h3> (e.g., run-details-title) and reference it via aria-labelledby on the
dialog, and ensure the close button (the button that calls setSelectedRun(null))
has an accessible name (e.g., aria-label="Close run details" or aria-labelledby)
so assistive tech receives a proper modal context.
There was a problem hiding this comment.
Fair point for production code, but this is a demo app focused on showcasing Durably features, not accessibility best practices. Skipping for now.
There was a problem hiding this comment.
@coji, understood! That's a reasonable call for a demo app. I'll keep this in mind for future reviews of this example.
✏️ Learnings added
Learnt from: coji
Repo: coji/durably PR: 105
File: examples/fullstack-vercel-turso/app/routes/_index/dashboard.tsx:278-288
Timestamp: 2026-03-12T13:19:19.875Z
Learning: In `examples/fullstack-vercel-turso/app/routes/_index/dashboard.tsx`, the modal dialog intentionally omits accessibility attributes (`role="dialog"`, `aria-modal`, `aria-label` on close button) because this is a demo app focused on showcasing Durably features, not accessibility best practices. Do not suggest accessibility improvements for modal dialogs in this file.
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.
🧠 Learnings used
Learnt from: coji
Repo: coji/durably PR: 105
File: examples/fullstack-vercel-turso/app/jobs/data-sync.ts:11-55
Timestamp: 2026-03-12T13:18:37.201Z
Learning: In `examples/fullstack-vercel-turso`, all example jobs (dataSyncJob, generateReportJob, importCsvJob, processImageJob) intentionally omit `preserveSteps: true` to demonstrate the default behavior where step history is deleted when runs reach a terminal state. The dashboard's View button showing empty steps for completed runs is acceptable and by design. Do not suggest adding `preserveSteps: true` to these jobs.
Learnt from: coji
Repo: coji/durably PR: 11
File: examples/browser-react-router-spa/app/routes/_index/dashboard.tsx:24-34
Timestamp: 2026-01-02T14:03:25.739Z
Learning: In example code under the examples/ directory, error handling is intentionally omitted to keep the code simple and focused on demonstrating the Durably API. Treat these files as illustrative, not production-ready. For real applications, ensure proper error handling and user feedback; do not copy this pattern verbatim into production code.
Learnt from: coji
Repo: coji/durably PR: 11
File: examples/browser-vite-react/src/components/dashboard.tsx:24-34
Timestamp: 2026-01-02T14:03:37.741Z
Learning: In example code directories (e.g., examples/browser-vite-react, examples/fullstack-react-router), error handling may be intentionally omitted to keep code simple and readable, focusing on demonstrating the Durably API rather than production best practices.
Learnt from: coji
Repo: coji/durably PR: 11
File: examples/browser-vite-react/src/components/dashboard.tsx:36-53
Timestamp: 2026-01-02T14:03:31.683Z
Learning: In all example files under the examples directory (e.g., examples/**/*.tsx), prioritize simplicity and clarity over comprehensive error handling. Focus on demonstrating Durably API usage patterns; omit production-grade error handling in examples to keep them readable and centered on core concepts. Where appropriate, include concise comments to explain the pattern being demonstrated.
Learnt from: coji
Repo: coji/durably PR: 11
File: examples/fullstack-react-router/app/routes/_index.tsx:28-46
Timestamp: 2026-01-02T14:03:32.230Z
Learning: In the coji/durably repository's examples directory, code examples (e.g., any .tsx under examples) should prioritize simplicity and readability over comprehensive error handling to clearly demonstrate the Durably API usage patterns. Include minimal, demonstrative error handling only as necessary for clarity.
|
Deployment failed with the following error: Learn More: https://vercel.link/3Fpeeb1 |
Prevents Vercel from scanning sibling React Router apps in the monorepo by specifying framework, installCommand, and buildCommand explicitly. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix buildCommand typo (fullstack-react-router → fullstack-vercel-turso) - Remove unnecessary mkdir from buildCommand - Fix worker endpoint to use loader (GET) for Vercel Cron compatibility - Change cron schedule to daily (per-minute requires Pro plan) - Add .vercelignore to exclude sibling examples from builder scan - Add .vercel and .env*.local to gitignore - Parallelize getRun/getSteps calls in dashboard - Move constants outside component to avoid re-creation on render - Update README to reflect actual behavior Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Extract duplicated delay() into app/jobs/delay.ts - Use runs.find() instead of getRun() in dashboard showDetails - Simplify unnecessary JSX wrapper in _index.tsx Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
♻️ Duplicate comments (3)
examples/fullstack-vercel-turso/README.md (1)
19-31:⚠️ Potential issue | 🟡 MinorAdd a language hint to this fenced block.
This flow diagram is still unlabeled, so markdownlint MD040 will keep failing here.
textis sufficient.Proposed fix
-``` +```text User triggers job → POST /api/durably/trigger ↓ User subscribes → GET /api/durably/subscribe?runId=xxx (SSE) ↓ onRequest → durably.init() starts worker during SSE connection ↓ Worker processes → steps stream via SSE in real-time ↓ SSE disconnects → function terminates, worker stops ↓ Vercel Cron → GET /api/worker processes any remaining pending jobs</details> <details> <summary>🤖 Prompt for AI Agents</summary>Verify each finding against the current code and only fix it if needed.
In
@examples/fullstack-vercel-turso/README.mdaround lines 19 - 31, The fenced
diagram block in README.md is missing a language hint which triggers
markdownlint MD040; update the triple-backtick opening fence for the flow
diagram (the fenced block containing "User triggers job → POST
/api/durably/trigger" through "Vercel Cron → GET /api/worker") to include the
language hint "text" (i.e., changetotext) so the block is explicitly
labeled.</details> </blockquote></details> <details> <summary>examples/fullstack-vercel-turso/app/routes/_index/dashboard.tsx (2)</summary><blockquote> `278-289`: _⚠️ Potential issue_ | _🟠 Major_ **Add real dialog semantics to the details modal.** This is still only a visual overlay. Add `role="dialog"`, `aria-modal`, `aria-labelledby`, and an accessible name on the icon-only close button. <details> <summary>Minimal fix</summary> ```diff <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"> - <div className="mx-4 max-h-[80vh] w-full max-w-2xl overflow-y-auto rounded-lg bg-white shadow-xl"> + <div + className="mx-4 max-h-[80vh] w-full max-w-2xl overflow-y-auto rounded-lg bg-white shadow-xl" + role="dialog" + aria-modal="true" + aria-labelledby="run-details-title" + > <div className="p-6"> <div className="mb-4 flex items-center justify-between"> - <h3 className="text-lg font-semibold">Run Details</h3> + <h3 id="run-details-title" className="text-lg font-semibold"> + Run Details + </h3> <button type="button" + aria-label="Close run details" onClick={() => setSelectedRun(null)} className="text-gray-400 hover:text-gray-600" > ``` </details> <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against the current code and only fix it if needed. In `@examples/fullstack-vercel-turso/app/routes/_index/dashboard.tsx` around lines 278 - 289, The modal overlay lacks proper ARIA semantics—add role="dialog" and aria-modal="true" to the container div that currently holds the modal (the div wrapping the modal content), give the title element (the h3 with text "Run Details") a stable id (e.g., run-details-title) and set aria-labelledby on the dialog container to that id, and make the icon-only close button accessible by adding an aria-label (e.g., aria-label="Close run details") while keeping its onClick that calls setSelectedRun(null). ``` </details> --- `212-221`: _⚠️ Potential issue_ | _🟡 Minor_ **Completed runs still don’t expose `Retrigger`.** The `retrigger()` contract includes completed runs, but this guard still hides the button for that state. <details> <summary>Minimal fix</summary> ```diff - {(run.status === 'failed' || + {(run.status === 'completed' || + run.status === 'failed' || run.status === 'cancelled') && ( ``` </details> <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against the current code and only fix it if needed. In `@examples/fullstack-vercel-turso/app/routes/_index/dashboard.tsx` around lines 212 - 221, The Retrigger button is hidden for completed runs even though retrigger() supports them; update the render guard in the JSX so that handleRetrigger(run.id) is shown for completed runs as well — e.g., change the condition using run.status (currently checking 'failed' || 'cancelled') to include 'completed' (or use a small helper like isRetriggerableStatus that returns true for 'failed','cancelled','completed'); keep disabled tied to isActioning and keep classNames intact. ``` </details> </blockquote></details> </blockquote></details> <details> <summary>🧹 Nitpick comments (1)</summary><blockquote> <details> <summary>examples/fullstack-vercel-turso/app/routes/_index/dashboard.tsx (1)</summary><blockquote> `10-10`: **Align `useRuns()` with the hook’s `TInput`/`TOutput` signature.** `useRuns` is declared as `useRuns<TInput, TOutput>()`. Passing a `TypedClientRun` union here is one level too high, so this example is demonstrating a shape that does not match the client API. <details> <summary>Minimal fix</summary> ```diff -import type { ClientRun, StepRecord, TypedClientRun } from '@coji/durably-react' +import type { ClientRun, StepRecord } from '@coji/durably-react' -/** Union type for all job runs in this dashboard */ -type DashboardRun = - | TypedClientRun<DataSyncInput, DataSyncOutput> - | TypedClientRun<GenerateReportInput, GenerateReportOutput> - | TypedClientRun<ImportCsvInput, ImportCsvOutput> - | TypedClientRun<ProcessImageInput, ProcessImageOutput> +type DashboardInput = + | DataSyncInput + | GenerateReportInput + | ImportCsvInput + | ProcessImageInput + +type DashboardOutput = + | DataSyncOutput + | GenerateReportOutput + | ImportCsvOutput + | ProcessImageOutput - durably.useRuns<DashboardRun>({ + durably.useRuns<DashboardInput, DashboardOutput>({ ``` </details> Also applies to: 24-29, 59-62 <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against the current code and only fix it if needed. In `@examples/fullstack-vercel-turso/app/routes/_index/dashboard.tsx` at line 10, The hook call uses the union type TypedClientRun as the generic but useRuns is declared as useRuns<TInput, TOutput>(), so replace the single TypedClientRun generic with the proper two generics (e.g., useRuns<ClientRun, StepRecord>()), updating the useRuns<TypedClientRun> occurrences to useRuns<ClientRun, StepRecord> (or other concrete TInput/TOutput types) so the generic parameters match the hook signature; search for useRuns< and change all instances that currently pass TypedClientRun to the two-type form. ``` </details> </blockquote></details> </blockquote></details> <details> <summary>🤖 Prompt for all review comments with AI agents</summary>Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In@examples/fullstack-vercel-turso/app/routes/_index/dashboard.tsx:
- Around line 278-289: The modal overlay lacks proper ARIA semantics—add
role="dialog" and aria-modal="true" to the container div that currently holds
the modal (the div wrapping the modal content), give the title element (the h3
with text "Run Details") a stable id (e.g., run-details-title) and set
aria-labelledby on the dialog container to that id, and make the icon-only close
button accessible by adding an aria-label (e.g., aria-label="Close run details")
while keeping its onClick that calls setSelectedRun(null).- Around line 212-221: The Retrigger button is hidden for completed runs even
though retrigger() supports them; update the render guard in the JSX so that
handleRetrigger(run.id) is shown for completed runs as well — e.g., change the
condition using run.status (currently checking 'failed' || 'cancelled') to
include 'completed' (or use a small helper like isRetriggerableStatus that
returns true for 'failed','cancelled','completed'); keep disabled tied to
isActioning and keep classNames intact.In
@examples/fullstack-vercel-turso/README.md:
- Around line 19-31: The fenced diagram block in README.md is missing a language
hint which triggers markdownlint MD040; update the triple-backtick opening fence
for the flow diagram (the fenced block containing "User triggers job → POST
/api/durably/trigger" through "Vercel Cron → GET /api/worker") to include the
language hint "text" (i.e., changetotext) so the block is explicitly
labeled.
Nitpick comments:
In@examples/fullstack-vercel-turso/app/routes/_index/dashboard.tsx:
- Line 10: The hook call uses the union type TypedClientRun as the generic but
useRuns is declared as useRuns<TInput, TOutput>(), so replace the single
TypedClientRun generic with the proper two generics (e.g., useRuns<ClientRun,
StepRecord>()), updating the useRuns occurrences to
useRuns<ClientRun, StepRecord> (or other concrete TInput/TOutput types) so the
generic parameters match the hook signature; search for useRuns< and change all
instances that currently pass TypedClientRun to the two-type form.</details> --- <details> <summary>ℹ️ Review info</summary> <details> <summary>⚙️ Run configuration</summary> **Configuration used**: defaults **Review profile**: CHILL **Plan**: Pro **Run ID**: `20a535b0-7e15-4993-9199-c498769c764c` </details> <details> <summary>📥 Commits</summary> Reviewing files that changed from the base of the PR and between 9183553c3ac80f2d583df877484fc9c35fbb0ddf and 3956cb3430871adbddd448412c690227942b0eb5. </details> <details> <summary>📒 Files selected for processing (8)</summary> * `.gitignore` * `CLAUDE.md` * `examples/fullstack-vercel-turso/.gitignore` * `examples/fullstack-vercel-turso/.vercelignore` * `examples/fullstack-vercel-turso/README.md` * `examples/fullstack-vercel-turso/app/routes/_index/dashboard.tsx` * `examples/fullstack-vercel-turso/app/routes/api.worker.ts` * `examples/fullstack-vercel-turso/vercel.json` </details> <details> <summary>🚧 Files skipped from review as they are similar to previous changes (2)</summary> * examples/fullstack-vercel-turso/app/routes/api.worker.ts * examples/fullstack-vercel-turso/.gitignore </details> </details> <!-- This is an auto-generated comment by CodeRabbit for review status -->
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (6)
examples/fullstack-vercel-turso/app/jobs/generate-report.ts (2)
200-206:⚠️ Potential issue | 🟡 MinorEncode
departmentbefore interpolating into the URL.Special characters in
department(spaces,/,?,#) will break the URL structure.💡 Fix
const reportUrl = await step.run('upload-report', async () => { step.progress(20, T, 'Uploading report...') await delay(4000) - const url = `https://reports.example.com/${input.department}/${input.reportType}-${Date.now()}.pdf` + const departmentPath = encodeURIComponent(input.department) + const url = `https://reports.example.com/${departmentPath}/${input.reportType}-${Date.now()}.pdf` step.log.info(`Report uploaded: ${url}`) return url })🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/fullstack-vercel-turso/app/jobs/generate-report.ts` around lines 200 - 206, The URL construction in the upload-report step interpolates input.department directly which can break URLs for values with spaces or reserved characters; update the anonymous function passed to step.run (the upload-report block that returns url and logs via step.log.info) to URL-encode input.department (e.g., use encodeURIComponent on input.department) before building the url string so the generated reportUrl is safe and valid.
216-220:⚠️ Potential issue | 🟡 MinorWrap
generatedAtin a step to persist the timestamp.If the job resumes after the last persisted step,
generatedAtwill reflect the resume time rather than actual completion time.💡 Fix
+ const generatedAt = await step.run('finalize-report', async () => { + return new Date().toISOString() + }) + return { reportUrl, totalRecords, - generatedAt: new Date().toISOString(), + generatedAt, }Based on learnings: "step execution uses step.run() and step output data is persisted."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/fullstack-vercel-turso/app/jobs/generate-report.ts` around lines 216 - 220, The returned generatedAt timestamp is computed at job return time and can be wrong if the job resumes; move the timestamp creation into a persisted step so it is saved immediately via step.run(). In the generate-report job (around the final return), add a step (e.g., persistGeneratedAt or stepGeneratedAt) and call step.run() with an output object containing generatedAt: new Date().toISOString(), then include that persisted value in the final returned object (reportUrl, totalRecords, generatedAt) so the completion time is reliably persisted.examples/fullstack-vercel-turso/app/jobs/data-sync.ts (1)
11-14:⚠️ Potential issue | 🟠 MajorAdd
preserveSteps: trueto retain step history for the dashboard.The dashboard displays step history for runs, but without
preserveSteps: true, steps are deleted when runs reach terminal state. Completed runs will show an empty steps list.💡 Fix
export const dataSyncJob = defineJob({ name: 'data-sync', input: z.object({ userId: z.string() }), output: z.object({ synced: z.number(), failed: z.number() }), + preserveSteps: true, run: async (step, input) => {Based on learnings: "By default, preserveSteps is false (step history is deleted when runs reach a terminal state). If audit/debug history is required, ensure preserveSteps is set to true."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/fullstack-vercel-turso/app/jobs/data-sync.ts` around lines 11 - 14, The job definition for dataSyncJob uses defineJob but doesn't set preserveSteps, so step history is removed at terminal state; update the defineJob call for dataSyncJob to include preserveSteps: true in its configuration (i.e., add preserveSteps: true to the object passed to defineJob) so the dashboard retains step history for completed runs.examples/fullstack-vercel-turso/app/routes/_index/dashboard.tsx (3)
91-98:⚠️ Potential issue | 🟡 MinorReset
stepsbefore switching to another run to avoid stale data flash.The current implementation sets
selectedRunbeforegetSteps()completes, causing the previous run's steps to briefly display under the newly selected run.💡 Minimal fix
const showDetails = async (runId: string) => { + setSteps([]) const run = runs.find((r) => r.id === runId) if (run) { setSelectedRun(run) const stepsData = await getSteps(runId) setSteps(stepsData) } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/fullstack-vercel-turso/app/routes/_index/dashboard.tsx` around lines 91 - 98, In showDetails, avoid flashing stale steps by clearing the steps state before changing selectedRun and awaiting getSteps; call setSteps([]) (or null) immediately when a matching run is found, then setSelectedRun(run), await getSteps(runId), and finally setSteps(stepsData) so the UI doesn't render the previous run's steps while loading; reference showDetails, getSteps, setSelectedRun, setSteps and runs to locate the change.
212-222:⚠️ Potential issue | 🟡 MinorExpose retrigger for completed runs too.
The API supports retriggering completed, failed, and cancelled runs, but this condition excludes completed runs from showing the Retrigger button.
💡 Suggested fix
- {(run.status === 'failed' || + {(run.status === 'completed' || + run.status === 'failed' || run.status === 'cancelled') && (🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/fullstack-vercel-turso/app/routes/_index/dashboard.tsx` around lines 212 - 222, The UI condition currently only shows the Retrigger button when run.status is 'failed' or 'cancelled'; update the conditional in the component rendering the button (where run.status is checked and handleRetrigger is used) to also include 'completed' so completed runs can be retriggered; ensure the onClick still calls handleRetrigger(run.id) and the disabled/isActioning logic and button classes remain unchanged.
278-290:⚠️ Potential issue | 🟠 MajorAdd dialog semantics to the modal for accessibility.
The modal overlay lacks proper ARIA attributes. Screen reader and keyboard users won't get proper modal context.
💡 Accessibility fix
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"> - <div className="mx-4 max-h-[80vh] w-full max-w-2xl overflow-y-auto rounded-lg bg-white shadow-xl"> + <div + className="mx-4 max-h-[80vh] w-full max-w-2xl overflow-y-auto rounded-lg bg-white shadow-xl" + role="dialog" + aria-modal="true" + aria-labelledby="run-details-title" + > <div className="p-6"> <div className="mb-4 flex items-center justify-between"> - <h3 className="text-lg font-semibold">Run Details</h3> + <h3 id="run-details-title" className="text-lg font-semibold">Run Details</h3> <button type="button" + aria-label="Close run details" onClick={() => setSelectedRun(null)}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/fullstack-vercel-turso/app/routes/_index/dashboard.tsx` around lines 278 - 290, The modal overlay markup needs proper ARIA dialog semantics: update the outer modal container (the div that currently wraps the modal content — the element rendered when selectedRun is set) to include role="dialog" and aria-modal="true", add an id on the heading (the h3 "Run Details") and reference it via aria-labelledby on the dialog, and ensure the close button (onClick setSelectedRun(null)) has an accessible name (it already has "✕" — add aria-label="Close" or visually-hidden text). Optionally ensure focus is moved into the dialog when opened and returned when closed (use the same container where you add role="dialog" to implement focus management).
🧹 Nitpick comments (1)
examples/fullstack-vercel-turso/app/routes/_index/dashboard.tsx (1)
362-380: Consider using a more stable key for steps list.Using
s.nameas the key assumes step names are unique within a run. If duplicate step names exist, React may render incorrectly. Consider using a composite key or index if the API doesn't guarantee uniqueness.💡 Safer key approach
{steps.map((s) => ( <li - key={s.name} + key={`${s.name}-${s.status}`} className="flex items-center justify-between p-2" >Or if
s.idis available, use that instead.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/fullstack-vercel-turso/app/routes/_index/dashboard.tsx` around lines 362 - 380, The list mapping for steps uses s.name as the React key which may not be unique; update the key in the steps.map(...) JSX to a stable unique identifier (prefer s.id if present, e.g., use s.id) or fall back to a composite key like `${runId}-${s.name}` or `${runId}-${index}` when s.id is missing; locate the map over steps (steps.map((s) => ...)) and replace the key={s.name} with the chosen stable key to avoid duplicate-key rendering issues.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@examples/fullstack-vercel-turso/app/jobs/generate-report.ts`:
- Around line 36-42: The generateReportJob defined via defineJob should retain
step history for completed runs; update the job definition (generateReportJob)
to include preserveSteps: true in its options so the dashboard can display steps
for terminal runs—i.e., add the preserveSteps: true property alongside name,
input, and output in the defineJob call.
In `@examples/fullstack-vercel-turso/app/jobs/process-image.ts`:
- Around line 11-14: The job definition for processImageJob (created via
defineJob) is missing preserveSteps: true, so step history is deleted at
terminal state; update the defineJob call for processImageJob to include the
option preserveSteps: true (e.g., add preserveSteps: true to the job config
alongside name, input, and output) so completed runs retain their step history
in the dashboard.
---
Duplicate comments:
In `@examples/fullstack-vercel-turso/app/jobs/data-sync.ts`:
- Around line 11-14: The job definition for dataSyncJob uses defineJob but
doesn't set preserveSteps, so step history is removed at terminal state; update
the defineJob call for dataSyncJob to include preserveSteps: true in its
configuration (i.e., add preserveSteps: true to the object passed to defineJob)
so the dashboard retains step history for completed runs.
In `@examples/fullstack-vercel-turso/app/jobs/generate-report.ts`:
- Around line 200-206: The URL construction in the upload-report step
interpolates input.department directly which can break URLs for values with
spaces or reserved characters; update the anonymous function passed to step.run
(the upload-report block that returns url and logs via step.log.info) to
URL-encode input.department (e.g., use encodeURIComponent on input.department)
before building the url string so the generated reportUrl is safe and valid.
- Around line 216-220: The returned generatedAt timestamp is computed at job
return time and can be wrong if the job resumes; move the timestamp creation
into a persisted step so it is saved immediately via step.run(). In the
generate-report job (around the final return), add a step (e.g.,
persistGeneratedAt or stepGeneratedAt) and call step.run() with an output object
containing generatedAt: new Date().toISOString(), then include that persisted
value in the final returned object (reportUrl, totalRecords, generatedAt) so the
completion time is reliably persisted.
In `@examples/fullstack-vercel-turso/app/routes/_index/dashboard.tsx`:
- Around line 91-98: In showDetails, avoid flashing stale steps by clearing the
steps state before changing selectedRun and awaiting getSteps; call setSteps([])
(or null) immediately when a matching run is found, then setSelectedRun(run),
await getSteps(runId), and finally setSteps(stepsData) so the UI doesn't render
the previous run's steps while loading; reference showDetails, getSteps,
setSelectedRun, setSteps and runs to locate the change.
- Around line 212-222: The UI condition currently only shows the Retrigger
button when run.status is 'failed' or 'cancelled'; update the conditional in the
component rendering the button (where run.status is checked and handleRetrigger
is used) to also include 'completed' so completed runs can be retriggered;
ensure the onClick still calls handleRetrigger(run.id) and the
disabled/isActioning logic and button classes remain unchanged.
- Around line 278-290: The modal overlay markup needs proper ARIA dialog
semantics: update the outer modal container (the div that currently wraps the
modal content — the element rendered when selectedRun is set) to include
role="dialog" and aria-modal="true", add an id on the heading (the h3 "Run
Details") and reference it via aria-labelledby on the dialog, and ensure the
close button (onClick setSelectedRun(null)) has an accessible name (it already
has "✕" — add aria-label="Close" or visually-hidden text). Optionally ensure
focus is moved into the dialog when opened and returned when closed (use the
same container where you add role="dialog" to implement focus management).
---
Nitpick comments:
In `@examples/fullstack-vercel-turso/app/routes/_index/dashboard.tsx`:
- Around line 362-380: The list mapping for steps uses s.name as the React key
which may not be unique; update the key in the steps.map(...) JSX to a stable
unique identifier (prefer s.id if present, e.g., use s.id) or fall back to a
composite key like `${runId}-${s.name}` or `${runId}-${index}` when s.id is
missing; locate the map over steps (steps.map((s) => ...)) and replace the
key={s.name} with the chosen stable key to avoid duplicate-key rendering issues.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 0ee173ac-3291-4fb1-82cb-c2b27cf9f07c
📒 Files selected for processing (7)
examples/fullstack-vercel-turso/app/jobs/data-sync.tsexamples/fullstack-vercel-turso/app/jobs/delay.tsexamples/fullstack-vercel-turso/app/jobs/generate-report.tsexamples/fullstack-vercel-turso/app/jobs/import-csv.tsexamples/fullstack-vercel-turso/app/jobs/process-image.tsexamples/fullstack-vercel-turso/app/routes/_index.tsxexamples/fullstack-vercel-turso/app/routes/_index/dashboard.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
- examples/fullstack-vercel-turso/app/jobs/import-csv.ts
| export const generateReportJob = defineJob({ | ||
| name: 'generate-report', | ||
| input: z.object({ | ||
| reportType: z.enum(['daily', 'weekly', 'monthly']), | ||
| department: z.string(), | ||
| }), | ||
| output: outputSchema, |
There was a problem hiding this comment.
Add preserveSteps: true to retain step history for the dashboard.
Like the other jobs, this long-running report job should preserve step history so the dashboard can display steps for completed runs.
💡 Fix
export const generateReportJob = defineJob({
name: 'generate-report',
input: z.object({
reportType: z.enum(['daily', 'weekly', 'monthly']),
department: z.string(),
}),
output: outputSchema,
+ preserveSteps: true,
run: async (step, input) => {Based on learnings: "By default, preserveSteps is false (step history is deleted when runs reach a terminal state)."
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export const generateReportJob = defineJob({ | |
| name: 'generate-report', | |
| input: z.object({ | |
| reportType: z.enum(['daily', 'weekly', 'monthly']), | |
| department: z.string(), | |
| }), | |
| output: outputSchema, | |
| export const generateReportJob = defineJob({ | |
| name: 'generate-report', | |
| input: z.object({ | |
| reportType: z.enum(['daily', 'weekly', 'monthly']), | |
| department: z.string(), | |
| }), | |
| output: outputSchema, | |
| preserveSteps: true, | |
| run: async (step, input) => { |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@examples/fullstack-vercel-turso/app/jobs/generate-report.ts` around lines 36
- 42, The generateReportJob defined via defineJob should retain step history for
completed runs; update the job definition (generateReportJob) to include
preserveSteps: true in its options so the dashboard can display steps for
terminal runs—i.e., add the preserveSteps: true property alongside name, input,
and output in the defineJob call.
There was a problem hiding this comment.
Intentionally not adding preserveSteps — we want to demonstrate the default cleanup behavior. Steps are visible during execution via SSE, and the View detail modal will show empty steps for completed runs, which is the expected default.
There was a problem hiding this comment.
@coji, understood — this is already captured in my knowledge base. The intentional omission of preserveSteps across all jobs in this example is by design to showcase the default cleanup behavior, where steps are visible during execution via SSE but cleared once the run reaches a terminal state. I won't flag this again. Apologies for the noise!
🧠 Learnings used
Learnt from: coji
Repo: coji/durably PR: 105
File: examples/fullstack-vercel-turso/app/jobs/data-sync.ts:11-55
Timestamp: 2026-03-12T13:18:37.201Z
Learning: In `examples/fullstack-vercel-turso`, all example jobs (dataSyncJob, generateReportJob, importCsvJob, processImageJob) intentionally omit `preserveSteps: true` to demonstrate the default behavior where step history is deleted when runs reach a terminal state. The dashboard's View button showing empty steps for completed runs is acceptable and by design. Do not suggest adding `preserveSteps: true` to these jobs.
Learnt from: coji
Repo: coji/durably PR: 101
File: docs/rfcs/runtime-rearchitecture/ja/core-runtime.md:0-0
Timestamp: 2026-03-08T05:06:39.050Z
Learning: Reviewers should verify that step execution uses step.run() and that step output data is persisted. By default, preserveSteps is false (step history is deleted when runs reach a terminal state). If audit/debug history is required, ensure preserveSteps is set to true. Be aware that cleanupSteps used to exist with inverted semantics (cleanupSteps: true meant delete), and this was renamed to preserveSteps in Phase 1. Apply this guidance to all JS/TS code and docs that reference step persistence.
Learnt from: coji
Repo: coji/durably PR: 11
File: examples/browser-vite-react/src/components/dashboard.tsx:24-34
Timestamp: 2026-01-02T14:03:37.741Z
Learning: In example code directories (e.g., examples/browser-vite-react, examples/fullstack-react-router), error handling may be intentionally omitted to keep code simple and readable, focusing on demonstrating the Durably API rather than production best practices.
| export const processImageJob = defineJob({ | ||
| name: 'process-image', | ||
| input: z.object({ filename: z.string(), width: z.number() }), | ||
| output: z.object({ url: z.string(), size: z.number() }), |
There was a problem hiding this comment.
Add preserveSteps: true to retain step history for the dashboard.
Consistent with the other jobs, this should preserve step history so completed runs display their steps in the dashboard.
💡 Fix
export const processImageJob = defineJob({
name: 'process-image',
input: z.object({ filename: z.string(), width: z.number() }),
output: z.object({ url: z.string(), size: z.number() }),
+ preserveSteps: true,
run: async (step, input) => {Based on learnings: "By default, preserveSteps is false (step history is deleted when runs reach a terminal state)."
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export const processImageJob = defineJob({ | |
| name: 'process-image', | |
| input: z.object({ filename: z.string(), width: z.number() }), | |
| output: z.object({ url: z.string(), size: z.number() }), | |
| export const processImageJob = defineJob({ | |
| name: 'process-image', | |
| input: z.object({ filename: z.string(), width: z.number() }), | |
| output: z.object({ url: z.string(), size: z.number() }), | |
| preserveSteps: true, | |
| run: async (step, input) => { |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@examples/fullstack-vercel-turso/app/jobs/process-image.ts` around lines 11 -
14, The job definition for processImageJob (created via defineJob) is missing
preserveSteps: true, so step history is deleted at terminal state; update the
defineJob call for processImageJob to include the option preserveSteps: true
(e.g., add preserveSteps: true to the job config alongside name, input, and
output) so completed runs retain their step history in the dashboard.
There was a problem hiding this comment.
Same as above — intentionally using the default (no step preservation).
There was a problem hiding this comment.
Rate Limit Exceeded
@coji have exceeded the limit for the number of chat messages per hour. Please wait 1 minutes and 36 seconds before sending another message.
- Add await durably.init() in _index action to ensure DB tables exist on cold start - Remove orphaned .dockerignore (no Dockerfile exists) - Remove duplicate type exports from individual job files (single source in index.ts) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
pnpm --filter doesn't build workspace dependencies (@coji/durably, @coji/durably-react). Use turbo which respects the dependency graph and ensures dist/ exists even with warm Vercel build cache. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add preserveSteps: true to createDurably so dashboard can show step history - Remove PII (name, email, amount) from import-csv job logs, use row ID instead - Add language hint to README fenced code block Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary
fullstack-vercel-tursoexample demonstrating Durably on Vercel serverless with Turso (libSQL)onRequestlazy init pattern for real-time SSE streaming during request lifetime/api/worker) for processing jobs after SSE disconnectsArchitecture
Test plan
pnpm --filter example-fullstack-vercel-turso typecheckpassespnpm validatepasses🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Documentation
Chores