Skip to content

v1 Implementation: Core durably package with documentation#3

Merged
coji merged 76 commits into
mainfrom
feature/v1-implementation
Dec 21, 2025
Merged

v1 Implementation: Core durably package with documentation#3
coji merged 76 commits into
mainfrom
feature/v1-implementation

Conversation

@coji
Copy link
Copy Markdown
Owner

@coji coji commented Dec 21, 2025

Summary

  • Complete v1 implementation of @coji/durably package
  • VitePress documentation website with English and Japanese localization
  • React, Browser, and Node.js examples
  • Full test coverage (Node.js, Browser, React)

Key Features

  • Step-oriented resumable batch execution using SQLite
  • Works in both Node.js (better-sqlite3/libsql) and browsers (SQLite WASM with OPFS)
  • Type-safe job definitions with Zod schemas
  • Event system for monitoring and extensibility
  • Plugin architecture for optional features

Changes

  • Core package implementation (packages/durably)
  • Documentation website (website/)
  • Examples for Node.js, Browser, and React (examples/)
  • zod v4 compatibility (z.ZodType instead of deprecated z.ZodTypeAny)

Test plan

  • Node.js tests pass (pnpm test:node)
  • Browser tests pass (pnpm test:browser)
  • React tests pass (pnpm test:react)
  • Type check passes (pnpm typecheck)
  • Examples type check passes

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Introduces Durably — resumable, step-oriented workflows for Node & browser with SQLite persistence.
    • Job APIs: triggerAndWait, batchTrigger, streaming subscribe for run events, and richer job/step lifecycle controls.
    • New runnable examples and a React demo with Dashboard and browser demo UI.
  • Documentation

    • Comprehensive API reference, guides (getting started, Node/browser, deployment, events), and streaming/docs updates.
  • Chores

    • Monorepo setup, CI/CD and docs deploy workflows, browser test infra, and MIT license added.

✏️ Tip: You can customize this high-level summary in your review settings.

coji and others added 30 commits December 20, 2025 23:12
- Add pnpm-workspace.yaml for monorepo configuration
- Move source files to packages/durably/
- Configure Vitest for Node.js and browser testing
- Add test helpers for libSQL and SQLocal dialects
- Create example skeletons for Node.js and browser
- Update root package.json with workspace scripts

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Remove rootDir option that was conflicting with tests include pattern.
tsup handles rootDir automatically during build.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add database schema types (runs, steps, logs, schema_versions)
- Implement migration system with Kysely
- Add createDurably() factory function with migrate() method
- Migration is idempotent and tracks version in schema_versions table
- Add React test environment with browser mode for OPFS support
- Add StrictMode test placeholders for future implementation

Tests pass in all three environments:
- Node.js (libSQL)
- Browser (SQLocal/OPFS)
- React (browser mode with OPFS)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add Storage interface with KyselyStorage implementation
  - Run CRUD operations with idempotency key support
  - Step operations with completion tracking
  - Concurrency key filtering for job scheduling

- Add Event system with discriminated union types
  - Type-safe event emission and subscription
  - Auto-assigned sequence numbers and timestamps
  - Exception isolation between listeners

- Add defineJob() with type-safe schemas
  - Zod schema validation for input/output
  - JobHandle with trigger, getRun, getRuns methods
  - Job registry to prevent duplicate registration

- Remove duplicate React tests (keep only strict-mode.test.tsx)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add Worker with start/stop and polling loop
  - Processes pending runs sequentially
  - Graceful shutdown waits for current run
  - Emits run:start, run:complete, run:fail events

- Add JobContext for step execution
  - ctx.run() executes and persists step results
  - Completed steps are skipped on resume (replay)
  - Emits step:start, step:complete, step:fail events
  - ctx.progress() for progress reporting
  - ctx.log for structured logging

- Add concurrencyKey serialization
  - Runs with same key wait for each other
  - Different keys or null keys run independently

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…etry)

- Add heartbeat mechanism to update heartbeat_at during job execution
- Add stale run recovery to reset abandoned runs to pending status
- Add retry() API to reset failed runs for re-execution
- Add ctx.runId property to JobContext for run identification
- Fix undefined output handling in storage.updateRun()
- Fix worker stop() to properly wait for in-progress operations

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add durably.getRun(id) to fetch a run by ID
- Add durably.getRuns(filter) to list runs with optional filtering
- Progress API already working via ctx.progress()
- All tests passing for Node.js and browser environments

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Phase 6: Event system already implemented (run/step/log events)
- Phase 7.1: ctx.log (info/warn/error) with structured data support
- Phase 7.2: withLogPersistence plugin for database log storage
- Phase 8: use() plugin system for extending Durably

New features:
- DurablyPlugin interface for creating plugins
- durably.use(plugin) to register plugins
- withLogPersistence() plugin persists log events to logs table
- storage.createLog() and storage.getLogs() for log operations

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Implement 6 tests verifying Durably handles React StrictMode's double
mount/unmount behavior safely:
- Double mount/unmount handling
- Singleton pattern preventing duplicate initialization
- Concurrent migrate() calls safety
- stop() during migrate() safety
- Job execution after StrictMode double mount
- Event listener cleanup on unmount

Configure TypeScript and Vitest for React JSX support.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Exclude Vitest browser test failure screenshots from version control.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add batchTrigger() method to JobHandle that:
- Creates multiple runs in a single call
- Validates all inputs before creating any runs (atomic validation)
- Supports both simple inputs and inputs with trigger options
- Returns empty array for empty input

Add batchCreateRuns() to Storage interface for batch insert operations.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- format: check formatting with prettier
- format:fix: fix formatting with prettier

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Change lint script from 'biome check' to 'biome lint'
- Add lint:fix script with 'biome lint --write'
- Fix noNonNullAssertion errors in events.ts, worker.ts
- Fix noConfusingVoidType error in job.ts (void -> undefined)
- Disable noNonNullAssertion rule for test files in biome.json

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- examples/node: Turso/libSQL example with user sync job
- examples/browser: SQLocal (SQLite WASM + OPFS) example with progress tracking

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Remove sqlocal/vite plugin (not available)
- Fix ctx.progress() signature to use positional args

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Added development tooling to both node and browser examples:
- typecheck, lint, lint:fix, format, format:fix scripts
- tsconfig.json for TypeScript configuration
- devDependencies: biome, prettier, typescript

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- peerDependencies: zod >=4.0.0
- devDependencies and examples: zod ^4.2.1

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add React example with StrictMode-safe singleton pattern
- Add database stats display and refresh functionality
- Add reset database button (deletes OPFS file and reloads)
- Add Cross-Origin Isolation headers for OPFS persistence
- Add ifNotExists() to migrations for idempotent schema creation
- Export JobContext and JobHandle types from durably
- Add root scripts: validate, dev:react, typecheck, lint:fix, format:fix

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Redesign browser/react UI with modern card-based layout
- Add status indicator with color-coded states
- Add progress bar with gradient animation
- Add real-time stats grid display
- Update job to process-image with sequential steps (download, resize, upload)
- Add real-time event-based status updates
- Unify job definition across all examples

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add triggerAndWait() method to JobHandle that triggers a run and waits for completion via events
- Export TriggerAndWaitResult type from package
- Add tests for success, failure, and options handling
- Update node example to use triggerAndWait instead of manual polling
- Add local.db to .gitignore

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This ensures the job is registered before the worker starts,
fixing the "Unknown job" error on page reload during job execution.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Remove stats display and progress bar from browser example
- Add Reload Page button and resuming status detection
- Reduce browser example from 226 to 131 lines
- Reduce HTML from 244 to 65 lines
- Match React example's simple structure

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add triggerAndWait and batchTrigger to JobHandle interface
- Change ctx.setProgress to ctx.progress (positional args)
- Fix logs index column name (timestamp -> created_at)
- Mark $types as future implementation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Type-safe event subscription via $types is over-engineering.
triggerAndWait provides type safety where it matters.
Events are primarily for monitoring where unknown is fine.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
JSON.stringify never returns undefined, so ?? null is dead code.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Wrap idempotency key checks and inserts in a transaction to prevent
race conditions. Also switch test dialect from :memory: to temp files
to work around libsql transaction compatibility issues with in-memory DBs.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Track current step name in context, logs inside steps include stepName
- Logs outside steps have stepName: null
- Fix spec: logs table column timestamp -> created_at

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Dec 21, 2025

Warning

Rate limit exceeded

@coji has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 5 minutes and 50 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between badbe6d and ab53afb.

📒 Files selected for processing (4)
  • examples/browser/src/dashboard.ts (1 hunks)
  • examples/react/src/Dashboard.tsx (1 hunks)
  • packages/durably/src/job.ts (1 hunks)
  • packages/durably/tests/react/strict-mode.test.tsx (1 hunks)

Walkthrough

Adds a monorepo implementation of Durably: a typed job runtime with Kysely-backed storage, worker (polling/heartbeat/recovery), event system, plugins, migrations, extensive tests (node/browser/react), examples, documentation site, CI/docs workflows, formatting hooks, and packaging/tooling configs.

Changes

Cohort / File(s) Summary
Repo & Tooling
package.json, pnpm-workspace.yaml, .gitignore, biome.json, .claude/hooks/format-on-edit.ts, .claude/hooks/tsconfig.json
Monorepo manifest and workspace, pnpm settings, .gitignore additions, linter override for tests, and an editor hook that runs Prettier on edits.
CI / Docs Workflows
.github/workflows/ci.yml, .github/workflows/docs.yml
Adds CI for validate/test/browser and a docs build + GitHub Pages deploy workflow.
Core library
packages/durably/src/*.ts
New Durably implementation: createDurably, job registry/handles, JobContext, Kysely storage, worker (poll/polling/heartbeat/recovery), events emitter, migrations, schema types, errors, and plugins (log persistence).
Public API barrel
packages/durably/src/index.ts
Central exports re-exporting core functions, types, events, plugins, storage types, and errors.
Storage & Migrations
packages/durably/src/storage.ts, packages/durably/src/migrations.ts, packages/durably/src/schema.ts
Kysely-backed storage implementation, migration runner, and DB schema typings.
Worker & Execution
packages/durably/src/worker.ts, packages/durably/src/context.ts, packages/durably/src/job.ts, packages/durably/src/errors.ts
Worker loop and lifecycle, run execution, heartbeat/recovery, step execution context, job handle logic, and CancelledError.
Events system
packages/durably/src/events.ts
Typed event definitions and createEventEmitter implementation with sequence/timestamp semantics and onError handling.
Plugins
packages/durably/src/plugins/log-persistence.ts, packages/durably/src/plugins/index.ts
withLogPersistence plugin and plugin wiring (minor trailing-comma edit).
Tests — shared & platform-specific
packages/durably/tests/shared/*, packages/durably/tests/node/*, packages/durably/tests/browser/*, packages/durably/tests/react/*, packages/durably/tests/helpers/*
Large suite of reusable shared tests and platform-specific wiring for node, browser, and react (including React StrictMode tests).
Examples — Node / Browser / React
examples/node/*, examples/browser/*, examples/react/*
New example apps (Node, vanilla browser, React) with Vite configs, demos, dashboards, and package manifests.
Example tooling & configs
examples/*/tsconfig.json, examples/*/package.json, examples/*/vite.config.ts
TypeScript and Vite configs for examples, dev-server header middleware for COOP/COEP.
Docs & Website
website/.vitepress/*, website/api/*.md, website/guide/*.md, CLAUDE.md, README.md, docs/spec*.md
Bilingual VitePress site, API reference and guides, spec updates (ctx→context, new methods like triggerAndWait/subscribe/stream), license, and removal of implementation-plan.md.
Formatting / minor edits
packages/durably/src/plugins/index.ts, misc
Minor non-functional edits (trailing comma).
Removed placeholders
src/index.ts, docs/implementation-plan.md
Deletes old placeholder API surface and removes the implementation plan document.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Client
  participant Durably
  participant Storage
  participant Worker
  participant JobCtx as JobContext
  participant Events as EventEmitter

  Client->>Durably: defineJob(...) / trigger(...)
  Durably->>Storage: createRun(...)
  Durably->>Events: emit(run:created)
  Client->>Durably: start()
  Durably->>Worker: start poll loop
  Worker->>Storage: getNextPendingRun(excludeKeys)
  alt run found
    Worker->>Storage: transitionRunToRunning(runId)
    Worker->>Events: emit(run:start)
    Worker->>JobCtx: createJobContext(run)
    JobCtx->>Events: emit(step:start)
    JobCtx->>Storage: createStep / getCompletedStep
    JobCtx->>JobCtx: execute step fn
    JobCtx->>Storage: saveStepComplete / updateRunProgress/output
    JobCtx->>Events: emit(step:complete)
    par heartbeat
      Worker->>Storage: updateHeartbeat(runId)
    end
    alt success
      Worker->>Storage: finalizeRunCompleted(runId)
      Worker->>Events: emit(run:complete)
    else failure
      Worker->>Storage: markRunFailed(runId, error)
      Worker->>Events: emit(run:fail)
    end
  else no run
    Worker->>Worker: wait(pollInterval)
  end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Areas needing focused review:

  • packages/durably/src/durably.ts — lifecycle, plugin API, migrate/start/stop concurrency.
  • packages/durably/src/storage.ts — Kysely queries, idempotency, batch atomicity, JSON serialization/deserialization.
  • packages/durably/src/worker.ts — polling loop, heartbeat scheduling, stale recovery, cancellation and error classification (CancelledError).
  • packages/durably/src/job.ts and packages/durably/src/context.ts — input/output validation, triggerAndWait event-wait logic, step resume/skip correctness.
  • tests under packages/durably/tests/shared and react strict-mode — timing-sensitive tests and potential flakiness.

Possibly related PRs

Poem

🐰 I hopped from stub to full-grown tree,
Steps saved, events dancing merrily.
Tests line up, examples light the way,
Docs and CI help the carrots stay,
Durably blooms — a cozy burrow play!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 67.65% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'v1 Implementation: Core durably package with documentation' clearly and specifically describes the main changes—implementing v1 of the durably package along with documentation—which aligns with the substantial changeset across the core package, examples, documentation site, and tests.

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

❤️ Share

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

@vercel vercel Bot temporarily deployed to Preview – durably December 21, 2025 12:44 Inactive
Examples depend on built @coji/durably package for type declarations.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 9

🧹 Nitpick comments (19)
packages/durably/tests/react/strict-mode.test.tsx (2)

33-34: Consider using a more deterministic cleanup approach.

The arbitrary 100ms delay is a common workaround but can cause flaky tests. Consider awaiting specific promises or using vi.waitFor with a condition if flakiness is observed.


224-237: Consider using a loop-based polling pattern for clarity.

The recursive setTimeout pattern works but can be harder to follow. A loop with await would be more readable and easier to maintain.

🔎 Alternative polling pattern
-            // Wait for completion
-            const checkCompletion = async () => {
-              if (cleanedUp.current) return
-              try {
-                const updated = await job.getRun(run.id)
-                if (updated?.status === 'completed') {
-                  setResult((updated.output as { processed: string }).processed)
-                } else if (!cleanedUp.current) {
-                  setTimeout(checkCompletion, 50)
-                }
-              } catch {
-                // Ignore errors if cleaned up
-              }
-            }
-            checkCompletion()
+            // Wait for completion
+            const pollCompletion = async () => {
+              while (!cleanedUp.current) {
+                try {
+                  const updated = await job.getRun(run.id)
+                  if (updated?.status === 'completed') {
+                    setResult((updated.output as { processed: string }).processed)
+                    return
+                  }
+                  await new Promise((r) => setTimeout(r, 50))
+                } catch {
+                  if (cleanedUp.current) return
+                }
+              }
+            }
+            pollCompletion()
packages/durably/tsconfig.json (2)

17-17: Tests are being compiled and will be included in the npm package.

The "include": ["src/**/*", "tests/**/*"] in tsconfig.json means test files are compiled to dist/tests/. While package.json restricts the published files to the dist directory, this includes all contents of dist/, so compiled tests will be in the published npm package, increasing its size.

Recommended approach:

  • Use a separate tsconfig.test.json for type-checking tests without emitting to dist
  • Or exclude tests from include and handle test compilation separately

6-7: DOM types and JSX configuration are appropriate for this cross-environment package.

The "DOM" lib addition supports browser functionality (SQLite WASM with OPFS), which is valid for a cross-platform library. The "jsx": "react-jsx" configuration is necessary because TypeScript must compile the .tsx test file (tests/react/strict-mode.test.tsx). Since package.json correctly specifies "files": ["dist"], tests are not included in the published npm package—there's no bloat concern.

That said, if JSX is used only in tests, consider creating a separate tsconfig.test.json that extends the base config and adds JSX support specifically for test compilation. This would make the core configuration cleaner and signal that the library itself doesn't export React components—only the tests use JSX.

.claude/hooks/format-on-edit.ts (1)

29-33: Consider logging formatting errors for debugging.

The empty catch block silently swallows all errors. While this prevents the hook from failing, it makes debugging difficult when Prettier fails.

🔎 Proposed improvement
       try {
         execSync(`pnpm exec prettier --write "${filePath}"`, {
           stdio: 'inherit',
         })
-      } catch {}
+      } catch (error) {
+        // Log but don't fail - formatting errors shouldn't block the workflow
+        console.error(`Failed to format ${filePath}:`, error)
+      }
.github/workflows/ci.yml (1)

1-99: LGTM! Well-structured CI workflow with proper optimization.

The workflow is well-designed:

  • Concurrency cancellation prevents redundant runs
  • Frozen lockfile ensures reproducible builds
  • Separate jobs for validation and testing improve parallelization
  • Playwright browser caching reduces CI time and costs
  • Conditional installation logic for cached vs. uncached browsers

Minor suggestion: The comment at line 7 is in Japanese. Consider translating it to English for broader accessibility:

-# 同じブランチ/PRの古いワークフローをキャンセル
+# Cancel in-progress runs for the same branch/PR
 concurrency:
   group: ${{ github.workflow }}-${{ github.ref }}
   cancel-in-progress: true
website/.vitepress/theme/CopySourceButton.vue (1)

10-12: Hardcoded branch name may cause issues on feature branches.

The source URL points to main branch, but documentation previews on feature branches would fetch content from main instead of the current branch, potentially showing outdated or different content.

Consider making the branch configurable

You could expose the branch as a build-time config or use VitePress's site data to determine the appropriate branch dynamically.

examples/react/src/Dashboard.tsx (3)

11-82: Style duplication with styles.ts.

This component defines its own styles object while there's a shared styles.ts file in the same directory. Some properties like result appear in both. Consider consolidating to avoid drift between the two style definitions.

You could either extend the shared styles or move Dashboard-specific styles to the shared module if they're reusable.


104-111: Missing error handling in showDetails.

If durably.getRun or durably.storage.getSteps fails, the error is unhandled and may cause silent failures. Consider adding a try-catch with user feedback.

Proposed fix
 const showDetails = async (runId: string) => {
+  try {
     const run = await durably.getRun(runId)
     if (run) {
       setSelectedRun(run)
       const stepsData = await durably.storage.getSteps(runId)
       setSteps(stepsData.map((s) => ({ name: s.name, status: s.status })))
     }
+  } catch (error) {
+    console.error('Failed to load run details:', error)
+  }
 }

113-127: Missing error handling in action handlers.

handleRetry, handleCancel, and handleDelete don't handle potential errors from the durably API calls. Failed operations will silently fail while still triggering a refresh.

Proposed fix for handleRetry (apply similar pattern to others)
 const handleRetry = async (runId: string) => {
+  try {
     await durably.retry(runId)
-    refresh()
+    await refresh()
+  } catch (error) {
+    console.error('Failed to retry run:', error)
+  }
 }
examples/react/src/main.tsx (1)

5-9: Standard React 18+ bootstrap pattern.

The use of createRoot and StrictMode follows React 18+ best practices. While the type assertion as HTMLElement is a common pattern, a defensive null check could improve error messaging if the root element is missing. This is optional for an example application.

🔎 Optional: More defensive root element handling
+const rootElement = document.getElementById('root')
+if (!rootElement) {
+  throw new Error('Root element not found')
+}
+
-createRoot(document.getElementById('root') as HTMLElement).render(
+createRoot(rootElement).render(
   <StrictMode>
     <App />
   </StrictMode>,
 )
packages/durably/tests/shared/migrate.shared.ts (1)

14-60: Consider verifying table schemas, not just existence.

The tests correctly verify that the migration creates all required tables (durably_runs, durably_steps, durably_logs, durably_schema_versions). However, verifying the table structure (columns, types, constraints) would provide stronger guarantees against schema drift or migration bugs.

Example: Verify table columns
it('durably_runs table has required columns', async () => {
  durably = createDurably({ dialect: createDialect() })
  await durably.migrate()

  const result = await sql<{ name: string }>`
    SELECT name FROM pragma_table_info('durably_runs')
  `.execute(durably.db)

  const columns = result.rows.map(r => r.name)
  expect(columns).toContain('id')
  expect(columns).toContain('status')
  expect(columns).toContain('heartbeat_at')
  // ... other required columns
})
packages/durably/src/migrations.ts (2)

118-132: Consider narrowing the error catch.

The getCurrentVersion function catches all errors and assumes they indicate a missing table. While this works, it could potentially mask other database errors (connection issues, permission errors, etc.).

Optional: More specific error handling
 async function getCurrentVersion(db: Kysely<Database>): Promise<number> {
   try {
     const result = await db
       .selectFrom('durably_schema_versions')
       .select('version')
       .orderBy('version', 'desc')
       .limit(1)
       .executeTakeFirst()

     return result?.version ?? 0
-  } catch {
+  } catch (error) {
+    // Only return 0 if table doesn't exist; otherwise re-throw
+    const message = error instanceof Error ? error.message : String(error)
+    if (message.includes('no such table') || message.includes('does not exist')) {
+      return 0
+    }
+    throw error
-    // Table doesn't exist yet
-    return 0
   }
 }

137-153: Consider adding transaction support for migrations.

Currently, migrations run sequentially without wrapping them in a transaction. If a migration partially fails, the database could be left in an inconsistent state. Consider wrapping each migration in a transaction for atomicity.

Recommended: Transactional migrations
 export async function runMigrations(db: Kysely<Database>): Promise<void> {
   const currentVersion = await getCurrentVersion(db)

   for (const migration of migrations) {
     if (migration.version > currentVersion) {
-      await migration.up(db)
-
-      await db
-        .insertInto('durably_schema_versions')
-        .values({
-          version: migration.version,
-          applied_at: new Date().toISOString(),
-        })
-        .execute()
+      await db.transaction().execute(async (trx) => {
+        await migration.up(trx)
+
+        await trx
+          .insertInto('durably_schema_versions')
+          .values({
+            version: migration.version,
+            applied_at: new Date().toISOString(),
+          })
+          .execute()
+      })
     }
   }
 }

Note: Verify that both better-sqlite3 and libsql dialects properly support DDL statements within transactions.

examples/react/src/App.tsx (2)

101-108: Consider error handling for trigger operation.

The run function doesn't handle potential errors from processImage.trigger(). While errors will be caught by the event system (run:fail), users might expect immediate feedback if the trigger itself fails.

🔎 Proposed error handling
 const run = async () => {
   userTriggered.current = true
   setStatus('running')
   setStep(null)
   setResult(null)
-  await processImage.trigger({ filename: 'photo.jpg', width: 800 })
-  refreshDashboardRef.current?.()
+  try {
+    await processImage.trigger({ filename: 'photo.jpg', width: 800 })
+    refreshDashboardRef.current?.()
+  } catch (error) {
+    setStatus('error')
+    setResult(error instanceof Error ? error.message : String(error))
+  }
 }

186-195: Consider error handling for database reset.

The database reset operation might fail (e.g., file locked, permission issues), but errors aren't handled. While the page will reload regardless, showing a brief error message would improve UX.

🔎 Proposed error handling
 <button
   type="button"
   onClick={async () => {
-    await durably.stop()
-    await deleteDatabaseFile()
-    location.reload()
+    try {
+      await durably.stop()
+      await deleteDatabaseFile()
+      location.reload()
+    } catch (error) {
+      console.error('Failed to reset database:', error)
+      // Optionally show user-facing error
+      alert('Failed to reset database. Please refresh manually.')
+    }
   }}
   disabled={isProcessing}
 >
   Reset Database
 </button>
packages/durably/src/durably.ts (1)

210-229: Consider documenting that cancelled runs can also be retried.

The current implementation correctly allows both failed and cancelled runs to be retried (since they fall through to the updateRun call). The comment on line 224 says "Only failed runs can be retried", but this is slightly misleading as cancelled runs can also be retried.

Suggested documentation fix
-      // Only failed runs can be retried
+      // Only failed or cancelled runs can be retried
       await storage.updateRun(runId, {
         status: 'pending',
         error: null,
       })
packages/durably/src/worker.ts (1)

255-279: Consider wrapping poll errors to prevent silent worker death.

If recoverStaleRuns() or the storage operations in processNextRun() throw an unexpected error (e.g., database connection lost), the error will propagate unhandled and silently stop the polling loop. Consider catching and emitting a worker:error event to maintain observability while continuing the polling loop.

Suggested error handling
   async function poll(): Promise<void> {
     if (!running) {
       return
     }

     const doWork = async () => {
       // Recover stale runs before processing
       await recoverStaleRuns()
       await processNextRun()
     }

     try {
       currentRunPromise = doWork()
       await currentRunPromise
+    } catch (error) {
+      eventEmitter.emit({
+        type: 'worker:error',
+        error: error instanceof Error ? error.message : String(error),
+        context: 'poll',
+      })
     } finally {
       currentRunPromise = null
     }

     if (running) {
       pollingTimeout = setTimeout(() => poll(), config.pollingInterval)
     } else if (stopResolver) {
       stopResolver()
       stopResolver = null
     }
   }
packages/durably/src/storage.ts (1)

136-152: Consider adding error handling for JSON parsing.

The JSON.parse calls in rowToRun (and similarly in rowToStep, rowToLog) can throw if the database contains malformed JSON. While this shouldn't happen under normal operation, defensive error handling would improve robustness.

🔎 Proposed defensive parsing
 function rowToRun(row: Database['durably_runs']): Run {
+  const safeJsonParse = <T>(value: string | null, fallback: T): T => {
+    if (!value) return fallback
+    try {
+      return JSON.parse(value)
+    } catch {
+      return fallback
+    }
+  }
+
   return {
     id: row.id,
     jobName: row.job_name,
-    payload: JSON.parse(row.payload),
+    payload: safeJsonParse(row.payload, null),
     status: row.status,
     // ... rest of fields
   }
 }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 24fc197 and 9a69068.

⛔ Files ignored due to path filters (2)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • website/public/logo.svg is excluded by !**/*.svg
📒 Files selected for processing (107)
  • .claude/hooks/format-on-edit.ts (1 hunks)
  • .claude/hooks/tsconfig.json (1 hunks)
  • .github/workflows/ci.yml (1 hunks)
  • .github/workflows/docs.yml (1 hunks)
  • .gitignore (1 hunks)
  • CLAUDE.md (3 hunks)
  • LICENSE (1 hunks)
  • README.md (2 hunks)
  • biome.json (1 hunks)
  • docs/implementation-plan.md (0 hunks)
  • docs/spec-streaming.md (8 hunks)
  • docs/spec.md (20 hunks)
  • examples/browser/index.html (1 hunks)
  • examples/browser/package.json (1 hunks)
  • examples/browser/src/dashboard.ts (1 hunks)
  • examples/browser/src/main.ts (1 hunks)
  • examples/browser/tsconfig.json (1 hunks)
  • examples/browser/vite.config.ts (1 hunks)
  • examples/node/basic.ts (1 hunks)
  • examples/node/package.json (1 hunks)
  • examples/node/tsconfig.json (1 hunks)
  • examples/react/index.html (1 hunks)
  • examples/react/package.json (1 hunks)
  • examples/react/src/App.tsx (1 hunks)
  • examples/react/src/Dashboard.tsx (1 hunks)
  • examples/react/src/main.tsx (1 hunks)
  • examples/react/src/styles.ts (1 hunks)
  • examples/react/tsconfig.json (1 hunks)
  • examples/react/vercel.json (1 hunks)
  • examples/react/vite.config.ts (1 hunks)
  • package.json (1 hunks)
  • packages/durably/package.json (1 hunks)
  • packages/durably/src/context.ts (1 hunks)
  • packages/durably/src/durably.ts (1 hunks)
  • packages/durably/src/errors.ts (1 hunks)
  • packages/durably/src/events.ts (1 hunks)
  • packages/durably/src/index.ts (1 hunks)
  • packages/durably/src/job.ts (1 hunks)
  • packages/durably/src/migrations.ts (1 hunks)
  • packages/durably/src/plugins/index.ts (1 hunks)
  • packages/durably/src/plugins/log-persistence.ts (1 hunks)
  • packages/durably/src/schema.ts (1 hunks)
  • packages/durably/src/storage.ts (1 hunks)
  • packages/durably/src/worker.ts (1 hunks)
  • packages/durably/tests/browser/concurrency.test.ts (1 hunks)
  • packages/durably/tests/browser/durably.test.ts (1 hunks)
  • packages/durably/tests/browser/events.test.ts (1 hunks)
  • packages/durably/tests/browser/job.test.ts (1 hunks)
  • packages/durably/tests/browser/log.test.ts (1 hunks)
  • packages/durably/tests/browser/migrate.test.ts (1 hunks)
  • packages/durably/tests/browser/plugin.test.ts (1 hunks)
  • packages/durably/tests/browser/recovery.test.ts (1 hunks)
  • packages/durably/tests/browser/run-api.test.ts (1 hunks)
  • packages/durably/tests/browser/setup.test.ts (1 hunks)
  • packages/durably/tests/browser/step.test.ts (1 hunks)
  • packages/durably/tests/browser/storage.test.ts (1 hunks)
  • packages/durably/tests/browser/worker.test.ts (1 hunks)
  • packages/durably/tests/helpers/browser-dialect.ts (1 hunks)
  • packages/durably/tests/helpers/node-dialect.ts (1 hunks)
  • packages/durably/tests/node/concurrency.test.ts (1 hunks)
  • packages/durably/tests/node/durably.test.ts (1 hunks)
  • packages/durably/tests/node/events.test.ts (1 hunks)
  • packages/durably/tests/node/job.test.ts (1 hunks)
  • packages/durably/tests/node/log.test.ts (1 hunks)
  • packages/durably/tests/node/migrate.test.ts (1 hunks)
  • packages/durably/tests/node/plugin.test.ts (1 hunks)
  • packages/durably/tests/node/recovery.test.ts (1 hunks)
  • packages/durably/tests/node/run-api.test.ts (1 hunks)
  • packages/durably/tests/node/setup.test.ts (1 hunks)
  • packages/durably/tests/node/step.test.ts (1 hunks)
  • packages/durably/tests/node/storage.test.ts (1 hunks)
  • packages/durably/tests/node/worker.test.ts (1 hunks)
  • packages/durably/tests/react/strict-mode.test.tsx (1 hunks)
  • packages/durably/tests/shared/concurrency.shared.ts (1 hunks)
  • packages/durably/tests/shared/durably.shared.ts (1 hunks)
  • packages/durably/tests/shared/events.shared.ts (1 hunks)
  • packages/durably/tests/shared/job.shared.ts (1 hunks)
  • packages/durably/tests/shared/log.shared.ts (1 hunks)
  • packages/durably/tests/shared/migrate.shared.ts (1 hunks)
  • packages/durably/tests/shared/plugin.shared.ts (1 hunks)
  • packages/durably/tests/shared/recovery.shared.ts (1 hunks)
  • packages/durably/tests/shared/run-api.shared.ts (1 hunks)
  • packages/durably/tests/shared/setup.shared.ts (1 hunks)
  • packages/durably/tests/shared/step.shared.ts (1 hunks)
  • packages/durably/tests/shared/storage.shared.ts (1 hunks)
  • packages/durably/tests/shared/worker.shared.ts (1 hunks)
  • packages/durably/tsconfig.json (1 hunks)
  • packages/durably/vitest.browser.config.ts (1 hunks)
  • packages/durably/vitest.config.ts (1 hunks)
  • packages/durably/vitest.react.config.ts (1 hunks)
  • pnpm-workspace.yaml (1 hunks)
  • src/index.ts (0 hunks)
  • website/.vitepress/config.ts (1 hunks)
  • website/.vitepress/theme/CopySourceButton.vue (1 hunks)
  • website/.vitepress/theme/index.ts (1 hunks)
  • website/api/context.md (1 hunks)
  • website/api/create-durably.md (1 hunks)
  • website/api/define-job.md (1 hunks)
  • website/api/events.md (1 hunks)
  • website/api/index.md (1 hunks)
  • website/guide/browser.md (1 hunks)
  • website/guide/deployment.md (1 hunks)
  • website/guide/events.md (1 hunks)
  • website/guide/getting-started.md (1 hunks)
  • website/guide/index.md (1 hunks)
  • website/guide/jobs-and-steps.md (1 hunks)
  • website/guide/nodejs.md (1 hunks)
⛔ Files not processed due to max files limit (19)
  • website/guide/react.md
  • website/guide/resumability.md
  • website/index.md
  • website/ja/api/context.md
  • website/ja/api/create-durably.md
  • website/ja/api/define-job.md
  • website/ja/api/events.md
  • website/ja/api/index.md
  • website/ja/guide/browser.md
  • website/ja/guide/deployment.md
  • website/ja/guide/events.md
  • website/ja/guide/getting-started.md
  • website/ja/guide/index.md
  • website/ja/guide/jobs-and-steps.md
  • website/ja/guide/nodejs.md
  • website/ja/guide/react.md
  • website/ja/guide/resumability.md
  • website/ja/index.md
  • website/package.json
💤 Files with no reviewable changes (2)
  • src/index.ts
  • docs/implementation-plan.md
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{js,ts,jsx,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{js,ts,jsx,tsx}: Use durably.defineJob() to define jobs that receive a context object and payload
Use ctx.run() to create steps, where each step's success state and return value is persisted
Create run instances via trigger() API, which always persists runs as pending before execution
Use the event system (run:start, run:complete, run:fail, step:*, log:write) for extensibility
Use the plugin architecture (durably.use()) for optional features like log persistence

Files:

  • packages/durably/tests/browser/recovery.test.ts
  • packages/durably/tests/browser/concurrency.test.ts
  • packages/durably/vitest.config.ts
  • packages/durably/tests/helpers/browser-dialect.ts
  • packages/durably/tests/browser/worker.test.ts
  • packages/durably/tests/browser/step.test.ts
  • packages/durably/tests/node/plugin.test.ts
  • packages/durably/tests/node/job.test.ts
  • packages/durably/tests/shared/durably.shared.ts
  • packages/durably/src/errors.ts
  • examples/browser/src/main.ts
  • packages/durably/tests/browser/durably.test.ts
  • packages/durably/tests/browser/migrate.test.ts
  • packages/durably/tests/browser/log.test.ts
  • packages/durably/tests/browser/job.test.ts
  • packages/durably/tests/node/run-api.test.ts
  • packages/durably/tests/react/strict-mode.test.tsx
  • packages/durably/tests/browser/run-api.test.ts
  • examples/react/src/main.tsx
  • examples/react/vite.config.ts
  • packages/durably/tests/node/setup.test.ts
  • packages/durably/tests/shared/run-api.shared.ts
  • packages/durably/src/context.ts
  • packages/durably/src/plugins/log-persistence.ts
  • packages/durably/tests/shared/events.shared.ts
  • packages/durably/src/migrations.ts
  • examples/browser/src/dashboard.ts
  • examples/react/src/App.tsx
  • packages/durably/tests/shared/recovery.shared.ts
  • packages/durably/tests/shared/plugin.shared.ts
  • examples/react/src/Dashboard.tsx
  • packages/durably/src/schema.ts
  • packages/durably/src/worker.ts
  • packages/durably/tests/node/step.test.ts
  • packages/durably/src/index.ts
  • packages/durably/tests/node/worker.test.ts
  • packages/durably/src/events.ts
  • packages/durably/tests/browser/setup.test.ts
  • packages/durably/tests/node/durably.test.ts
  • packages/durably/src/durably.ts
  • packages/durably/vitest.browser.config.ts
  • examples/node/basic.ts
  • packages/durably/tests/shared/setup.shared.ts
  • packages/durably/tests/shared/migrate.shared.ts
  • packages/durably/tests/browser/plugin.test.ts
  • packages/durably/tests/browser/events.test.ts
  • packages/durably/src/storage.ts
  • examples/react/src/styles.ts
  • packages/durably/tests/node/log.test.ts
  • packages/durably/tests/node/recovery.test.ts
  • packages/durably/tests/shared/worker.shared.ts
  • packages/durably/tests/node/concurrency.test.ts
  • packages/durably/tests/helpers/node-dialect.ts
  • packages/durably/tests/shared/storage.shared.ts
  • packages/durably/src/job.ts
  • packages/durably/tests/node/events.test.ts
  • packages/durably/tests/shared/step.shared.ts
  • packages/durably/vitest.react.config.ts
  • packages/durably/tests/shared/concurrency.shared.ts
  • packages/durably/tests/shared/job.shared.ts
  • packages/durably/tests/browser/storage.test.ts
  • examples/browser/vite.config.ts
  • packages/durably/src/plugins/index.ts
  • packages/durably/tests/node/storage.test.ts
  • packages/durably/tests/shared/log.shared.ts
  • packages/durably/tests/node/migrate.test.ts
**/*browser*.{js,ts,jsx,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

For browser implementations, assume single tab usage (OPFS exclusivity) and handle background tab interruptions via heartbeat recovery

Files:

  • packages/durably/tests/helpers/browser-dialect.ts
  • packages/durably/vitest.browser.config.ts
🧠 Learnings (12)
📓 Common learnings
Learnt from: CR
Repo: coji/durably PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-20T14:01:28.808Z
Learning: Applies to **/*.{js,ts,jsx,tsx} : Use the plugin architecture (`durably.use()`) for optional features like log persistence
📚 Learning: 2025-12-20T14:01:28.808Z
Learnt from: CR
Repo: coji/durably PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-20T14:01:28.808Z
Learning: Applies to **/*browser*.{js,ts,jsx,tsx} : For browser implementations, assume single tab usage (OPFS exclusivity) and handle background tab interruptions via heartbeat recovery

Applied to files:

  • packages/durably/tests/browser/recovery.test.ts
  • packages/durably/tests/browser/concurrency.test.ts
  • website/guide/browser.md
  • examples/browser/index.html
  • docs/spec-streaming.md
  • CLAUDE.md
📚 Learning: 2025-12-20T14:01:28.808Z
Learnt from: CR
Repo: coji/durably PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-20T14:01:28.808Z
Learning: Applies to **/*.{js,ts,jsx,tsx} : Use the plugin architecture (`durably.use()`) for optional features like log persistence

Applied to files:

  • packages/durably/tsconfig.json
  • website/guide/events.md
  • packages/durably/tests/node/plugin.test.ts
  • website/guide/getting-started.md
  • website/api/events.md
  • packages/durably/src/plugins/log-persistence.ts
  • packages/durably/tests/shared/plugin.shared.ts
  • packages/durably/package.json
  • README.md
  • packages/durably/src/index.ts
  • packages/durably/src/durably.ts
  • package.json
  • examples/react/index.html
  • packages/durably/tests/browser/plugin.test.ts
  • packages/durably/vitest.react.config.ts
  • packages/durably/src/plugins/index.ts
  • CLAUDE.md
  • website/guide/index.md
📚 Learning: 2025-12-20T14:01:28.808Z
Learnt from: CR
Repo: coji/durably PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-20T14:01:28.808Z
Learning: Applies to **/*.{js,ts,jsx,tsx} : Use `durably.defineJob()` to define jobs that receive a context object and payload

Applied to files:

  • website/guide/nodejs.md
  • website/guide/events.md
  • packages/durably/tests/node/job.test.ts
  • website/guide/jobs-and-steps.md
  • website/guide/getting-started.md
  • examples/browser/src/main.ts
  • packages/durably/tests/browser/job.test.ts
  • packages/durably/tests/shared/run-api.shared.ts
  • website/api/events.md
  • packages/durably/src/context.ts
  • examples/react/src/App.tsx
  • packages/durably/package.json
  • packages/durably/src/worker.ts
  • README.md
  • website/api/define-job.md
  • packages/durably/src/index.ts
  • packages/durably/src/durably.ts
  • examples/node/basic.ts
  • website/guide/deployment.md
  • website/api/index.md
  • packages/durably/tests/shared/worker.shared.ts
  • docs/spec-streaming.md
  • docs/spec.md
  • website/api/context.md
  • packages/durably/src/job.ts
  • packages/durably/tests/shared/job.shared.ts
  • packages/durably/src/plugins/index.ts
  • CLAUDE.md
  • website/guide/index.md
📚 Learning: 2025-12-20T14:01:28.808Z
Learnt from: CR
Repo: coji/durably PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-20T14:01:28.808Z
Learning: Applies to **/*.{js,ts,jsx,tsx} : Use the event system (`run:start`, `run:complete`, `run:fail`, `step:*`, `log:write`) for extensibility

Applied to files:

  • website/guide/events.md
  • .claude/hooks/format-on-edit.ts
  • website/api/events.md
  • packages/durably/src/events.ts
  • docs/spec.md
  • packages/durably/tests/shared/step.shared.ts
  • packages/durably/tests/shared/log.shared.ts
📚 Learning: 2025-12-20T14:01:28.808Z
Learnt from: CR
Repo: coji/durably PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-20T14:01:28.808Z
Learning: Use the dialect injection pattern - pass Kysely dialect to `createDurably()` to abstract SQLite implementations

Applied to files:

  • packages/durably/tests/helpers/browser-dialect.ts
  • packages/durably/tests/shared/durably.shared.ts
  • packages/durably/tests/browser/durably.test.ts
  • website/api/create-durably.md
  • packages/durably/tests/node/durably.test.ts
  • packages/durably/tests/shared/setup.shared.ts
  • packages/durably/tests/shared/migrate.shared.ts
  • packages/durably/tests/helpers/node-dialect.ts
  • CLAUDE.md
📚 Learning: 2025-12-20T14:01:28.808Z
Learnt from: CR
Repo: coji/durably PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-20T14:01:28.808Z
Learning: Applies to **/*.{js,ts,jsx,tsx} : Use `ctx.run()` to create steps, where each step's success state and return value is persisted

Applied to files:

  • packages/durably/tests/browser/step.test.ts
  • .claude/hooks/format-on-edit.ts
  • website/guide/jobs-and-steps.md
  • packages/durably/tests/shared/run-api.shared.ts
  • packages/durably/src/context.ts
  • packages/durably/tests/node/step.test.ts
  • packages/durably/src/index.ts
  • docs/spec-streaming.md
  • packages/durably/tests/shared/storage.shared.ts
  • docs/spec.md
  • website/api/context.md
  • packages/durably/src/job.ts
  • packages/durably/tests/shared/step.shared.ts
  • CLAUDE.md
📚 Learning: 2025-12-20T14:01:28.808Z
Learnt from: CR
Repo: coji/durably PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-20T14:01:28.808Z
Learning: Applies to **/*.{sql,ts,js}@(migration|schema)* : Steps table must include fields: `status` (completed/failed), `output` (JSON), indexed by `run_id` and `index`

Applied to files:

  • website/guide/jobs-and-steps.md
  • packages/durably/src/migrations.ts
  • packages/durably/src/schema.ts
  • packages/durably/tests/shared/migrate.shared.ts
  • packages/durably/src/storage.ts
  • packages/durably/tests/shared/storage.shared.ts
  • docs/spec.md
  • packages/durably/tests/shared/step.shared.ts
  • CLAUDE.md
📚 Learning: 2025-12-20T14:01:28.808Z
Learnt from: CR
Repo: coji/durably PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-20T14:01:28.808Z
Learning: Browser implementations must require Secure Context (HTTPS/localhost) for OPFS access

Applied to files:

  • website/guide/browser.md
  • CLAUDE.md
📚 Learning: 2025-12-20T14:01:28.808Z
Learnt from: CR
Repo: coji/durably PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-20T14:01:28.808Z
Learning: Applies to **/*.{js,ts,jsx,tsx} : Create run instances via `trigger()` API, which always persists runs as `pending` before execution

Applied to files:

  • packages/durably/tests/shared/run-api.shared.ts
  • packages/durably/src/context.ts
  • docs/spec.md
  • packages/durably/src/job.ts
  • packages/durably/tests/shared/job.shared.ts
📚 Learning: 2025-12-20T14:01:28.808Z
Learnt from: CR
Repo: coji/durably PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-20T14:01:28.808Z
Learning: Applies to **/*.{sql,ts,js}@(migration|schema)* : Runs table must include fields: `status` (pending/running/completed/failed), `idempotency_key`, `concurrency_key`, `heartbeat_at`

Applied to files:

  • packages/durably/src/migrations.ts
  • packages/durably/src/schema.ts
  • packages/durably/tests/shared/migrate.shared.ts
  • docs/spec.md
  • CLAUDE.md
📚 Learning: 2025-12-20T14:01:28.808Z
Learnt from: CR
Repo: coji/durably PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-20T14:01:28.808Z
Learning: Applies to **/*.{sql,ts,js}@(migration|schema)* : Database schema must include four tables: `runs`, `steps`, `logs`, `schema_versions`

Applied to files:

  • packages/durably/src/migrations.ts
  • packages/durably/src/schema.ts
  • packages/durably/tests/shared/migrate.shared.ts
  • CLAUDE.md
🧬 Code graph analysis (41)
packages/durably/tests/browser/recovery.test.ts (2)
packages/durably/tests/shared/recovery.shared.ts (1)
  • createRecoveryTests (6-667)
packages/durably/tests/helpers/browser-dialect.ts (1)
  • createBrowserDialect (5-10)
packages/durably/tests/browser/concurrency.test.ts (2)
packages/durably/tests/shared/concurrency.shared.ts (1)
  • createConcurrencyTests (6-169)
packages/durably/tests/helpers/browser-dialect.ts (1)
  • createBrowserDialect (5-10)
packages/durably/tests/browser/worker.test.ts (2)
packages/durably/tests/shared/worker.shared.ts (1)
  • createWorkerTests (6-222)
packages/durably/tests/helpers/browser-dialect.ts (1)
  • createBrowserDialect (5-10)
packages/durably/tests/node/plugin.test.ts (2)
packages/durably/tests/shared/plugin.shared.ts (1)
  • createPluginTests (6-154)
packages/durably/tests/helpers/node-dialect.ts (1)
  • createNodeDialect (6-12)
packages/durably/tests/node/job.test.ts (2)
packages/durably/tests/shared/job.shared.ts (1)
  • createJobTests (6-230)
packages/durably/tests/helpers/node-dialect.ts (1)
  • createNodeDialect (6-12)
packages/durably/src/errors.ts (2)
packages/durably/src/index.ts (1)
  • CancelledError (43-43)
packages/durably/src/context.ts (1)
  • runId (19-21)
examples/browser/src/main.ts (1)
examples/browser/src/dashboard.ts (2)
  • refreshDashboard (25-77)
  • initDashboard (20-23)
packages/durably/tests/browser/durably.test.ts (2)
packages/durably/tests/shared/durably.shared.ts (1)
  • createDurablyTests (5-71)
packages/durably/tests/helpers/browser-dialect.ts (1)
  • createBrowserDialect (5-10)
packages/durably/tests/browser/migrate.test.ts (2)
packages/durably/tests/shared/migrate.shared.ts (1)
  • createMigrateTests (6-89)
packages/durably/tests/helpers/browser-dialect.ts (1)
  • createBrowserDialect (5-10)
packages/durably/tests/browser/job.test.ts (2)
packages/durably/tests/shared/job.shared.ts (1)
  • createJobTests (6-230)
packages/durably/tests/helpers/browser-dialect.ts (1)
  • createBrowserDialect (5-10)
packages/durably/tests/react/strict-mode.test.tsx (3)
packages/durably/src/durably.ts (2)
  • Durably (59-154)
  • createDurably (159-291)
packages/durably/tests/helpers/browser-dialect.ts (1)
  • createBrowserDialect (5-10)
packages/durably/src/context.ts (1)
  • run (23-112)
packages/durably/tests/browser/run-api.test.ts (2)
packages/durably/tests/shared/run-api.shared.ts (1)
  • createRunApiTests (6-486)
packages/durably/tests/helpers/browser-dialect.ts (1)
  • createBrowserDialect (5-10)
examples/react/src/main.tsx (1)
examples/react/src/App.tsx (1)
  • App (122-220)
packages/durably/tests/node/setup.test.ts (2)
packages/durably/tests/shared/setup.shared.ts (1)
  • createSetupTests (5-28)
packages/durably/tests/helpers/node-dialect.ts (1)
  • createNodeDialect (6-12)
packages/durably/tests/shared/run-api.shared.ts (3)
packages/durably/src/durably.ts (2)
  • Durably (59-154)
  • createDurably (159-291)
packages/durably/src/index.ts (2)
  • Durably (7-7)
  • createDurably (6-6)
packages/durably/src/context.ts (1)
  • run (23-112)
packages/durably/tests/shared/events.shared.ts (3)
packages/durably/src/durably.ts (2)
  • Durably (59-154)
  • createDurably (159-291)
packages/durably/src/index.ts (3)
  • Durably (7-7)
  • createDurably (6-6)
  • DurablyEvent (14-14)
packages/durably/src/events.ts (1)
  • DurablyEvent (103-111)
packages/durably/src/migrations.ts (2)
packages/durably/src/index.ts (1)
  • Database (32-32)
packages/durably/src/schema.ts (1)
  • Database (48-53)
examples/browser/src/dashboard.ts (1)
packages/durably/src/context.ts (2)
  • run (23-112)
  • runId (19-21)
examples/react/src/App.tsx (2)
packages/durably/src/index.ts (1)
  • createDurably (6-6)
examples/react/src/styles.ts (1)
  • styles (5-56)
packages/durably/tests/shared/plugin.shared.ts (4)
packages/durably/src/index.ts (4)
  • Durably (7-7)
  • createDurably (6-6)
  • DurablyPlugin (7-7)
  • withLogPersistence (10-10)
packages/durably/src/plugins/index.ts (1)
  • withLogPersistence (7-11)
packages/durably/src/plugins/log-persistence.ts (1)
  • withLogPersistence (6-21)
packages/durably/src/context.ts (1)
  • run (23-112)
examples/react/src/Dashboard.tsx (2)
examples/react/src/styles.ts (1)
  • styles (5-56)
packages/durably/src/context.ts (2)
  • runId (19-21)
  • run (23-112)
packages/durably/src/schema.ts (1)
packages/durably/src/index.ts (5)
  • RunsTable (34-34)
  • StepsTable (36-36)
  • LogsTable (33-33)
  • SchemaVersionsTable (35-35)
  • Database (32-32)
packages/durably/src/worker.ts (5)
packages/durably/src/storage.ts (1)
  • Storage (113-131)
packages/durably/src/events.ts (1)
  • EventEmitter (165-181)
packages/durably/src/job.ts (1)
  • JobRegistry (147-162)
packages/durably/src/context.ts (4)
  • run (23-112)
  • error (144-153)
  • runId (19-21)
  • createJobContext (9-156)
packages/durably/src/errors.ts (1)
  • CancelledError (6-11)
packages/durably/tests/node/step.test.ts (1)
packages/durably/tests/helpers/node-dialect.ts (1)
  • createNodeDialect (6-12)
packages/durably/tests/node/worker.test.ts (2)
packages/durably/tests/shared/worker.shared.ts (1)
  • createWorkerTests (6-222)
packages/durably/tests/helpers/node-dialect.ts (1)
  • createNodeDialect (6-12)
packages/durably/tests/browser/setup.test.ts (2)
packages/durably/tests/shared/setup.shared.ts (1)
  • createSetupTests (5-28)
packages/durably/tests/helpers/browser-dialect.ts (1)
  • createBrowserDialect (5-10)
packages/durably/tests/node/durably.test.ts (2)
packages/durably/tests/shared/durably.shared.ts (1)
  • createDurablyTests (5-71)
packages/durably/tests/helpers/node-dialect.ts (1)
  • createNodeDialect (6-12)
examples/node/basic.ts (1)
packages/durably/src/index.ts (1)
  • createDurably (6-6)
packages/durably/tests/shared/migrate.shared.ts (1)
packages/durably/src/durably.ts (2)
  • Durably (59-154)
  • createDurably (159-291)
packages/durably/tests/browser/plugin.test.ts (2)
packages/durably/tests/shared/plugin.shared.ts (1)
  • createPluginTests (6-154)
packages/durably/tests/helpers/browser-dialect.ts (1)
  • createBrowserDialect (5-10)
packages/durably/src/storage.ts (4)
packages/durably/src/index.ts (5)
  • Run (40-40)
  • RunFilter (40-40)
  • Step (40-40)
  • Log (40-40)
  • Database (32-32)
packages/durably/src/job.ts (1)
  • RunFilter (68-71)
packages/durably/src/context.ts (2)
  • runId (19-21)
  • run (23-112)
packages/durably/src/schema.ts (1)
  • Database (48-53)
packages/durably/tests/node/log.test.ts (2)
packages/durably/tests/shared/log.shared.ts (1)
  • createLogTests (6-178)
packages/durably/tests/helpers/node-dialect.ts (1)
  • createNodeDialect (6-12)
packages/durably/tests/node/recovery.test.ts (2)
packages/durably/tests/shared/recovery.shared.ts (1)
  • createRecoveryTests (6-667)
packages/durably/tests/helpers/node-dialect.ts (1)
  • createNodeDialect (6-12)
packages/durably/tests/node/concurrency.test.ts (2)
packages/durably/tests/shared/concurrency.shared.ts (1)
  • createConcurrencyTests (6-169)
packages/durably/tests/helpers/node-dialect.ts (1)
  • createNodeDialect (6-12)
packages/durably/tests/shared/storage.shared.ts (3)
packages/durably/src/durably.ts (2)
  • Durably (59-154)
  • createDurably (159-291)
packages/durably/src/index.ts (2)
  • Durably (7-7)
  • createDurably (6-6)
packages/durably/src/context.ts (1)
  • run (23-112)
packages/durably/tests/node/events.test.ts (2)
packages/durably/tests/shared/events.shared.ts (1)
  • createEventsTests (5-193)
packages/durably/tests/helpers/node-dialect.ts (1)
  • createNodeDialect (6-12)
packages/durably/tests/shared/concurrency.shared.ts (1)
packages/durably/src/durably.ts (2)
  • Durably (59-154)
  • createDurably (159-291)
packages/durably/tests/shared/job.shared.ts (3)
packages/durably/src/durably.ts (2)
  • Durably (59-154)
  • createDurably (159-291)
packages/durably/src/index.ts (2)
  • Durably (7-7)
  • createDurably (6-6)
packages/durably/src/context.ts (1)
  • run (23-112)
packages/durably/tests/browser/storage.test.ts (2)
packages/durably/tests/shared/storage.shared.ts (1)
  • createStorageTests (5-182)
packages/durably/tests/helpers/browser-dialect.ts (1)
  • createBrowserDialect (5-10)
packages/durably/tests/shared/log.shared.ts (3)
packages/durably/src/durably.ts (2)
  • Durably (59-154)
  • createDurably (159-291)
packages/durably/src/events.ts (1)
  • LogWriteEvent (81-88)
packages/durably/src/context.ts (1)
  • run (23-112)
packages/durably/tests/node/migrate.test.ts (2)
packages/durably/tests/shared/migrate.shared.ts (1)
  • createMigrateTests (6-89)
packages/durably/tests/helpers/node-dialect.ts (1)
  • createNodeDialect (6-12)
🪛 ast-grep (0.40.0)
examples/browser/src/dashboard.ts

[warning] 28-28: Direct modification of innerHTML or outerHTML properties detected. Modifying these properties with unsanitized user input can lead to XSS vulnerabilities. Use safe alternatives or sanitize content first.
Context: runsTbody.innerHTML = <tr><td colspan="5" class="empty-state">No runs yet</td></tr>
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://owasp.org/www-community/xss-filter-evasion-cheatsheet
- https://cwe.mitre.org/data/definitions/79.html

(dom-content-modification)


[warning] 33-50: Direct modification of innerHTML or outerHTML properties detected. Modifying these properties with unsanitized user input can lead to XSS vulnerabilities. Use safe alternatives or sanitize content first.
Context: runsTbody.innerHTML = runs
.map(
(run) => <tr data-id="${run.id}"> <td class="run-id">${run.id.slice(0, 8)}...</td> <td>${run.jobName}</td> <td><span class="status-badge status-${run.status}">${run.status}</span></td> <td>${formatDate(run.createdAt)}</td> <td> <button class="action-btn view-btn" data-id="${run.id}">View</button> ${run.status === 'failed' ?Retry: ''} ${run.status === 'running' || run.status === 'pending' ?Cancel: ''} ${run.status !== 'running' && run.status !== 'pending' ?Delete: ''} </td> </tr>,
)
.join('')
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://owasp.org/www-community/xss-filter-evasion-cheatsheet
- https://cwe.mitre.org/data/definitions/79.html

(dom-content-modification)


[warning] 87-107: Direct modification of innerHTML or outerHTML properties detected. Modifying these properties with unsanitized user input can lead to XSS vulnerabilities. Use safe alternatives or sanitize content first.
Context: detailsContent.innerHTML = <p><strong>ID:</strong> <span class="run-id">${run.id}</span></p> <p><strong>Job:</strong> ${run.jobName}</p> <p><strong>Status:</strong> <span class="status-badge status-${run.status}">${run.status}</span></p> <p><strong>Created:</strong> ${formatDate(run.createdAt)}</p> ${run.progress ?

Progress: ${run.progress.current}${run.progress.total ? /${run.progress.total} : ''} ${run.progress.message || ''}

: ''} ${run.error ?

Error: ${run.error}

: ''} ${run.output ?

Output:

${JSON.stringify(run.output, null, 2)}
: ''} <p><strong>Payload:</strong></p> <pre class="result">${JSON.stringify(run.payload, null, 2)}</pre> ${ steps.length > 0 ?

Steps:



    ${steps.map((s) => <li><span>${s.name}</span><span class="status-badge status-${s.status === 'completed' ? 'completed' : 'failed'}">${s.status}</span></li>).join('')}

: '' }
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://owasp.org/www-community/xss-filter-evasion-cheatsheet
- https://cwe.mitre.org/data/definitions/79.html

(dom-content-modification)


[warning] 28-28: Direct HTML content assignment detected. Modifying innerHTML, outerHTML, or using document.write with unsanitized content can lead to XSS vulnerabilities. Use secure alternatives like textContent or sanitize HTML with libraries like DOMPurify.
Context: runsTbody.innerHTML = <tr><td colspan="5" class="empty-state">No runs yet</td></tr>
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://www.dhairyashah.dev/posts/why-innerhtml-is-a-bad-idea-and-how-to-avoid-it/
- https://cwe.mitre.org/data/definitions/79.html

(unsafe-html-content-assignment)


[warning] 33-50: Direct HTML content assignment detected. Modifying innerHTML, outerHTML, or using document.write with unsanitized content can lead to XSS vulnerabilities. Use secure alternatives like textContent or sanitize HTML with libraries like DOMPurify.
Context: runsTbody.innerHTML = runs
.map(
(run) => <tr data-id="${run.id}"> <td class="run-id">${run.id.slice(0, 8)}...</td> <td>${run.jobName}</td> <td><span class="status-badge status-${run.status}">${run.status}</span></td> <td>${formatDate(run.createdAt)}</td> <td> <button class="action-btn view-btn" data-id="${run.id}">View</button> ${run.status === 'failed' ?Retry: ''} ${run.status === 'running' || run.status === 'pending' ?Cancel: ''} ${run.status !== 'running' && run.status !== 'pending' ?Delete: ''} </td> </tr>,
)
.join('')
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://www.dhairyashah.dev/posts/why-innerhtml-is-a-bad-idea-and-how-to-avoid-it/
- https://cwe.mitre.org/data/definitions/79.html

(unsafe-html-content-assignment)


[warning] 87-107: Direct HTML content assignment detected. Modifying innerHTML, outerHTML, or using document.write with unsanitized content can lead to XSS vulnerabilities. Use secure alternatives like textContent or sanitize HTML with libraries like DOMPurify.
Context: detailsContent.innerHTML = <p><strong>ID:</strong> <span class="run-id">${run.id}</span></p> <p><strong>Job:</strong> ${run.jobName}</p> <p><strong>Status:</strong> <span class="status-badge status-${run.status}">${run.status}</span></p> <p><strong>Created:</strong> ${formatDate(run.createdAt)}</p> ${run.progress ?

Progress: ${run.progress.current}${run.progress.total ? /${run.progress.total} : ''} ${run.progress.message || ''}

: ''} ${run.error ?

Error: ${run.error}

: ''} ${run.output ?

Output:

${JSON.stringify(run.output, null, 2)}
: ''} <p><strong>Payload:</strong></p> <pre class="result">${JSON.stringify(run.payload, null, 2)}</pre> ${ steps.length > 0 ?

Steps:



    ${steps.map((s) => <li><span>${s.name}</span><span class="status-badge status-${s.status === 'completed' ? 'completed' : 'failed'}">${s.status}</span></li>).join('')}

: '' }
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://www.dhairyashah.dev/posts/why-innerhtml-is-a-bad-idea-and-how-to-avoid-it/
- https://cwe.mitre.org/data/definitions/79.html

(unsafe-html-content-assignment)

🪛 GitHub Actions: CI
examples/node/basic.ts

[error] 8-8: Cannot find module '@coji/durably' or its corresponding type declarations. TS2307.

🪛 LanguageTool
website/api/events.md

[style] ~51-~51: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ... number // } }) #### `run:fail` Fired when a run fails. ts durably.on('ru...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)


[style] ~109-~109: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...number // } }) #### `step:fail` Fired when a step fails. ts durably.on('s...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)

website/api/create-durably.md

[style] ~47-~47: Consider using “who” when you are referring to a person instead of an object.
Context: ...ly.start(): void ``` Starts the worker that processes pending jobs. ### stop() ...

(THAT_WHO)

Comment thread .claude/hooks/format-on-edit.ts
Comment thread examples/browser/src/dashboard.ts
Comment thread examples/browser/src/dashboard.ts
Comment thread examples/node/basic.ts
Comment thread examples/react/src/Dashboard.tsx
Comment thread packages/durably/src/job.ts
Comment thread packages/durably/src/storage.ts
Comment thread packages/durably/tests/react/strict-mode.test.tsx
Comment thread website/.vitepress/theme/CopySourceButton.vue
- Add zod to installation commands (peer dependency)
- Update description to mention both Kysely and Zod as peer deps
- Add browser installation option with SQLocal

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
README.md (1)

29-66: Consider adding plugin and event system examples.

The usage example effectively demonstrates the core defineJob() API with context-based execution and Zod schemas. However, the PR delivers both a plugin architecture (durably.use()) and an event system for monitoring, which are mentioned in the Features section but not demonstrated. Adding brief code snippets showing event subscription or plugin usage would help users discover these capabilities early.

This is optional but would improve discoverability of the extensibility features noted in line 13.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e08dd1d and badbe6d.

📒 Files selected for processing (1)
  • README.md (2 hunks)
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: CR
Repo: coji/durably PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-20T14:01:28.808Z
Learning: Applies to **/*.{js,ts,jsx,tsx} : Use `durably.defineJob()` to define jobs that receive a context object and payload
Learnt from: CR
Repo: coji/durably PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-20T14:01:28.808Z
Learning: Applies to **/*.{js,ts,jsx,tsx} : Use the plugin architecture (`durably.use()`) for optional features like log persistence
📚 Learning: 2025-12-20T14:01:28.808Z
Learnt from: CR
Repo: coji/durably PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-20T14:01:28.808Z
Learning: Applies to **/*.{js,ts,jsx,tsx} : Use `durably.defineJob()` to define jobs that receive a context object and payload

Applied to files:

  • README.md
📚 Learning: 2025-12-20T14:01:28.808Z
Learnt from: CR
Repo: coji/durably PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-20T14:01:28.808Z
Learning: Applies to **/*.{js,ts,jsx,tsx} : Use the plugin architecture (`durably.use()`) for optional features like log persistence

Applied to files:

  • README.md
🔇 Additional comments (3)
README.md (3)

5-5: Verify external links are stable and correctly formatted.

The documentation link and live demo link should be validated to ensure they resolve correctly and remain stable as the project evolves. Consider adding link-checking to CI/docs workflows if not already present.

[scratchpad_end] -->


19-27: Installation section clearly covers multiple environments.

The three installation paths (better-sqlite3, libsql, SQLocal) are well-organized and appropriate for the cross-environment support delivered in this PR. Each option is clearly labeled and separated.


68-72: Documentation references are appropriately scoped.

The specification and streaming extension links are helpful, and the note that streaming is "conceptual, not yet implemented" manages expectations appropriately. This aligns well with the PR's current delivery scope.

Remove db.destroy() call in afterEach as it can cause "driver has already
been destroyed" errors when async operations like migrations are still pending.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add status check after event subscription to handle the case where the
run completes before the event listeners are registered. This prevents
the promise from hanging indefinitely when the worker processes the run
very quickly.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- browser: Add escapeHtml helper and sanitize user-controlled data in innerHTML
- react: Use stable index property for step list keys instead of array index

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant