Skip to content

feat: add @coji/durably-react package#11

Merged
coji merged 102 commits into
mainfrom
feature/durably-react
Jan 3, 2026
Merged

feat: add @coji/durably-react package#11
coji merged 102 commits into
mainfrom
feature/durably-react

Conversation

@coji
Copy link
Copy Markdown
Owner

@coji coji commented Dec 24, 2025

Summary

Add @coji/durably-react package with React bindings for Durably, plus significant enhancements to the core package for real-time event streaming and type-safe APIs.

Breaking Changes

  • register() API simplified: registerAll() renamed to register(), old single-job signature removed
    // New
    const durably = createDurably({ dialect }).register({ myJob })
    await durably.jobs.myJob.trigger({ id: '123' })
    
    // Old (removed)
    const job = durably.register(jobDef)

New Features

@coji/durably (Core)

  • New events for run lifecycle:
    • run:trigger: Emitted when a job is triggered (before worker picks it up)
    • run:cancel: Emitted when a run is cancelled
    • run:retry: Emitted when a failed/cancelled run is retried
  • Type-safe durably.jobs property: Access registered jobs with full type inference
  • Retry from cancelled state: retry() now works on both failed and cancelled runs
  • stepCount added to Run type: Run now includes stepCount property reflecting the number of completed steps
    • Computed dynamically via JOIN query (no schema change required)
    • Available in getRun(), getRuns(), getNextPendingRun()

@coji/durably/server

  • New endpoints:
    • GET /steps?runId=xxx: Get steps for a run
    • DELETE /run?runId=xxx: Delete a run
  • SSE event streaming: /runs/subscribe now streams run:trigger, run:cancel, run:retry events

@coji/durably-react

Browser-complete mode - Run Durably entirely in the browser with SQLite WASM:

import { DurablyProvider, useJob } from '@coji/durably-react'

<Suspense fallback={<div>Loading...</div>}>
  <DurablyProvider durably={durablyPromise}>
    <App />
  </DurablyProvider>
</Suspense>

// In components
const { trigger, status, progress, logs, isRunning } = useJob(myJob)

Server-connected mode - Connect to a Durably server via HTTP/SSE:

import { useJob } from '@coji/durably-react/client'

const { trigger, status, output, isRunning } = useJob<TInput, TOutput>({
  api: '/api/durably',
  jobName: 'my-job',
})

Hooks provided:

  • useJob(jobDef, options?) - Trigger jobs and track status with real-time updates
  • useJobRun({ runId }) - Subscribe to a specific run's status
  • useJobLogs({ runId, maxLogs? }) - Stream logs from a run
  • useRuns(options?) - List runs with filtering, pagination, and real-time updates
  • useRunActions({ api }) - Actions: retry, cancel, deleteRun, getRun, getSteps
  • useDurably() - Direct access to Durably instance

Type-safe client factories:

  • createDurablyClient<typeof jobs>({ api }) - Type-safe client for all registered jobs
  • createJobHooks<typeof job>({ api, jobName }) - Per-job hook factory

Examples

  • examples/browser-vite-react - Browser-complete mode with Vite
  • examples/browser-react-router-spa - React Router 7 SPA with clientLoader
  • examples/fullstack-react-router - Full-stack with server-connected mode

All examples include unified dashboard UI with:

  • Run list with pagination: View, Retry, Cancel, Delete actions
  • Step progress display: Current step index / total steps
  • Progress bar: Real-time progress tracking
  • Job tabs: Switch between Image Processing and Data Sync jobs
  • Run details modal: Full run info, steps list, output, and errors

Test Coverage

  • 404 tests total across all packages
  • Browser tests (Playwright) and Node tests (Vitest)
  • Comprehensive coverage for provider, hooks, subscriptions, and server handler

Documentation

  • Updated all README files to match v0.6.0 API
  • Updated llms.md files for LLM/AI agent consumption
  • Updated specification docs (spec.md, spec-react.md)
  • Updated website documentation

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Added React integration package with hooks for job triggering, run monitoring, and real-time updates
    • Introduced server HTTP handler for remote Durably access via REST and SSE endpoints
    • Added job subscription API for real-time event streaming
    • Browser-only and fullstack example applications with React Router
    • New run lifecycle events for better observability
  • Breaking Changes

    • Job registration API changed from single-job to multi-job object-based pattern
    • Run status enum expanded to include "cancelled" state
  • Documentation

    • Restructured guides for server, fullstack, and browser-only setups
    • Added comprehensive React integration documentation
    • New Node.js server and browser-only examples

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

coji and others added 3 commits December 24, 2025 00:33
- Update spec-react.md with dialectFactory code examples
- Replace emoji with Japanese text (悪い例/良い例)
- Add detailed implementation plan for @coji/durably-react

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add two operation modes: browser-complete and server-integration
- Browser-complete: DurablyProvider + dialectFactory (same as before)
- Server-integration: @coji/durably-react/client (lightweight, no @coji/durably dependency)
- Server-side: createDurablyHandler with Web Standard API (Request/Response)
- Define SSE event format for real-time updates
- Add durably.subscribe(runId) and durably.getJob(jobName) as new requirements

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

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

vercel Bot commented Dec 24, 2025

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

Project Deployment Review Updated (UTC)
durably-demo Ready Ready Preview Jan 3, 2026 6:20am

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Dec 24, 2025

Caution

Review failed

The pull request is closed.

📝 Walkthrough

Walkthrough

This pull request introduces a comprehensive redesign of the Durably API alongside a new React integration package. Core changes include shifting job registration to object-based syntax, adding type-safe event subscriptions, introducing a new @coji/durably-react package with browser and server-connected modes, creating a server HTTP handler, and adding extensive documentation and example applications. The changes establish a foundation for both in-browser and full-stack job execution patterns.

Changes

Cohort / File(s) Summary
Core API Redesign
packages/durably/src/durably.ts, packages/durably/src/job.ts, packages/durably/src/events.ts, packages/durably/src/define-job.ts
Job registration refactored from single-argument to object-based syntax (register({ job }) returning named handles). New methods: getJob(), subscribe(runId) returning ReadableStream<DurablyEvent>. New event types: RunTriggerEvent, RunCancelEvent, RunRetryEvent. RunStatus extended to include 'cancelled'. Type utilities JobInput<T>, JobOutput<T> added.
Server Handler
packages/durably/src/server.ts, packages/durably/src/index.ts
New createDurablyHandler(durably) factory exposing HTTP routes: trigger (POST), subscribe (SSE), runs/run (GET), retry/cancel/delete (POST/DELETE). Request/response types: TriggerRequest, TriggerResponse, RunsRequest, DurablyHandler. Handler validates jobName, enforces pagination (limit/offset), streams SSE events.
Storage & Run Enhancements
packages/durably/src/storage.ts
Added stepCount to Run interface. Updated queries to count and include step counts per run. Pagination parameters limit and offset added to RunFilter.
React Integration (Browser Mode)
packages/durably-react/src/context.tsx, packages/durably-react/src/index.ts, packages/durably-react/src/types.ts
New DurablyProvider context component accepting durably instance/Promise. Hook useDurably() accesses context. Browser-mode hooks: useJob, useJobRun, useJobLogs, useRuns. Re-exported types from shared layer.
React Integration (Client/Server Mode)
packages/durably-react/src/client/*.ts
Server-connected surface at @coji/durably-react/client. Factories: createDurablyClient(), createJobHooks(). Hooks: useJob, useJobRun, useJobLogs, useRuns, useRunActions with HTTP/SSE backends. Utility hook useSSESubscription() for SSE event handling. Type-safe API via generic type parameters.
React Hook Internals
packages/durably-react/src/hooks/use-run-subscription.ts
Browser-only useRunSubscription() hook subscribing to Durably events. Manages status, output, error, logs (with optional max cap), progress. Handles run:* and log:write events locally. Methods: clearLogs(), reset().
React Testing
packages/durably-react/tests/browser/*.test.tsx, packages/durably-react/tests/client/*.test.tsx, packages/durably-react/tests/helpers/*.ts
Comprehensive test suites for browser hooks (useJob, useJobRun, useJobLogs, useRuns), client hooks with mocked SSE/fetch, and helper utilities (createTestDurably, createBrowserDialect, createMockEventSource). Tests validate lifecycle, state transitions, pagination, real-time updates, error handling.
Package Configuration
packages/durably-react/package.json, packages/durably-react/tsconfig.json, packages/durably-react/tsup.config.ts, packages/durably-react/vitest.config.ts
New durably-react package manifest with dual export points: main (browser mode) and ./client (server-connected). TypeScript, Vitest, and tsup build configs. Coverage via V8 reporter. Browser tests via Playwright/Chromium.
Core Testing Updates
packages/durably/tests/shared/*.shared.ts, packages/durably/tests/node/*.test.ts
All test suites refactored to use new object-based register({ job }) API. Added comprehensive createServerTests() factory validating HTTP routes, SSE streaming, pagination, filtering, error handling, and event lifecycle.
Example: Browser React Router SPA
examples/browser-react-router-spa/*
New SPA example using React Router v7, SQLite WASM (OPFS), DurablyProvider, and browser-mode hooks. Three jobs (processImage, dataSync, importCsv). Components: Job trigger forms, progress display, run dashboard with pagination and actions. No server backend; client-only persistence.
Example: Browser Vite + React
examples/browser-vite-react/*
Simplified browser example with Vite, React, Tailwind. Two jobs (processImage, dataSync). Dashboard, forms, and progress components. Uses browser-mode Durably with SQLite WASM.
Example: Fullstack React Router
examples/fullstack-react-router/*
Full-stack example with React Router v7, server-side Durably, and client-side server-connected hooks. Server: SSR with Durably handler routes; Client: triggers and monitors jobs via HTTP/SSE. Three jobs with step-based workflows. Docker support.
Example: Server Node.js
examples/server-node/*
Pure Node.js example with Turso/LibSQL dialect. Single processImage job. Demonstrates synchronous job triggering, result handling, and database stats.
Documentation Updates
docs/spec-react.md, docs/spec.md, docs/spec-streaming.md, packages/durably/README.md, packages/durably-react/README.md, packages/durably-react/docs/llms.md, website/api/*.md, website/guide/*.md, CLAUDE.md, CHANGELOG.md
Comprehensive specification of browser vs. server operation modes, new React APIs, server handler endpoints, type definitions, and usage patterns. Updated guides and API docs reflect new registration syntax, getJob/subscribe methods, and React integration. Removed old browser and React guides.
Project Configuration & Tooling
.prettierignore, biome.json, turbo.json, package.json, packages/durably/package.json, .gitignore
Added Turbo orchestration config. Updated root and package-level package.json scripts to use turbo run. New .prettierignore and biome.json entries. Added .turbo and coverage to .gitignore. Version bumps for zod dependency.
Removed Examples
examples/browser/*, examples/react/*
Deleted old browser and React example projects; functionality superseded by new examples (browser-react-router-spa, fullstack-react-router).
Website Configuration
website/.vitepress/config.ts
Updated sidebar: "Platforms" → "Usage", "API Reference" → "Core API", item links updated (Node.js → Server, Browser → Full-Stack, React → Browser-Only). Added new "React API" sidebar group.

Sequence Diagram(s)

sequenceDiagram
    participant App as React App
    participant Provider as DurablyProvider
    participant Hook as useJob Hook
    participant Durably as Durably Instance
    participant EventEmitter as Event System
    participant Storage as Storage Layer

    App->>Provider: Wrap with DurablyProvider<br/>(durably instance)
    Provider->>Hook: useDurably() context<br/>access
    
    rect rgb(200, 220, 255)
    Note over App,Storage: Browser Mode: Trigger Job
    App->>Hook: trigger(jobInput)
    Hook->>Durably: jobs.myJob.trigger(input)
    Durably->>Storage: createRun(jobName, input)
    Storage-->>Durably: runId, pending status
    Durably->>EventEmitter: emit run:trigger
    EventEmitter-->>Hook: subscription update
    Hook-->>App: { runId, isPending: true }
    end

    rect rgb(200, 255, 220)
    Note over App,Storage: Real-Time Event Stream
    Durably->>Storage: getNextPendingRun()
    Durably->>Durably: execute steps
    Durably->>EventEmitter: emit run:start
    EventEmitter-->>Hook: event received
    Hook->>Hook: update status: running
    Hook-->>App: { status: 'running' }
    
    Durably->>Durably: step.run() → execute
    Durably->>EventEmitter: emit run:progress
    EventEmitter-->>Hook: progress event
    Hook-->>App: { progress: { current, total } }
    
    Durably->>Storage: updateRun(completed)
    Durably->>EventEmitter: emit run:complete
    EventEmitter-->>Hook: completion event
    Hook-->>App: { status: 'completed',<br/>output, isCompleted: true }
    end
Loading
sequenceDiagram
    participant Client as React Client
    participant Form as Job Trigger Form
    participant HTTP as HTTP/SSE Server
    participant Handler as DurablyHandler
    participant Durably as Durably Instance
    participant Storage as Storage Layer

    Client->>Form: click Trigger
    Form->>HTTP: POST /api/durably/trigger<br/>{ jobName, input }
    HTTP->>Handler: handle(request)
    Handler->>Handler: parse jobName
    Handler->>Durably: getJob(jobName)
    Durably-->>Handler: JobHandle
    Handler->>Handler: job.trigger(input)
    Handler->>Storage: createRun
    Storage-->>Handler: { runId }
    Handler-->>HTTP: 200 { runId }
    HTTP-->>Form: response
    Form-->>Client: runId, enable SSE

    rect rgb(255, 220, 200)
    Note over Client,Storage: Server-Connected Mode: SSE Subscribe
    Client->>HTTP: GET /api/durably/subscribe?runId=<br/>(EventSource)
    HTTP->>Handler: subscribe(request)
    Handler->>Durably: subscribe(runId)
    Durably-->>Handler: ReadableStream<DurablyEvent>
    Handler-->>HTTP: SSE stream (run:start, run:progress...)
    HTTP-->>Client: event: run:start
    Client->>Client: update hook state
    HTTP-->>Client: event: run:progress
    Client->>Client: update progress
    HTTP-->>Client: event: run:complete
    Client->>Client: final state, close SSE
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~65 minutes

Possibly related PRs

  • PR #3 (Durably Core Extensions): Introduces foundational changes to the Durably interface—getJob(), subscribe(runId) returning ReadableStream<DurablyEvent>, new event types (RunTriggerEvent, RunCancelEvent, RunRetryEvent), and RunStatus extension to include 'cancelled'—all of which are directly consumed by the React bindings and server handler in this PR.

  • PR #9 (Register API Redesign): The main PR continues and expands the register API redesign—converting from register(job) to register({ jobName: job }) with type-safe job handle returns—which is foundational to all new usage patterns across examples and tests in this PR.

  • PR #2 (Specs & Tooling Migration): Both PRs refactor documentation specs (docs/spec.md, docs/spec-streaming.md) and update website navigation to reflect new API surfaces, operation modes (browser vs. server), and the introduction of the durably-react package with distinct usage patterns.

Poem

🐰 A rabbit hops with glee,
Jobs now register wild and free,
With useJob hooks to dance and sway,
And SSE streams lighting the way,
From browser to server, persistence takes flight!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 32.31% 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 title 'feat: add @coji/durably-react package' clearly and concisely summarizes the main change—introducing a new React bindings package for Durably.

📜 Recent review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2b965dd and 767f461.

⛔ Files ignored due to path filters (4)
  • website/public/images/fullstack-architecture.svg is excluded by !**/*.svg
  • website/public/images/getting-started-overview.svg is excluded by !**/*.svg
  • website/public/images/job-lifecycle.svg is excluded by !**/*.svg
  • website/public/images/resumability.svg is excluded by !**/*.svg
📒 Files selected for processing (61)
  • CHANGELOG.md
  • examples/browser-react-router-spa/app/lib/durably.ts
  • examples/browser-react-router-spa/app/routes/_index/dashboard.tsx
  • examples/browser-vite-react/src/App.tsx
  • examples/browser-vite-react/src/components/dashboard.tsx
  • examples/browser-vite-react/src/lib/durably.ts
  • examples/fullstack-react-router/app/lib/durably.server.ts
  • examples/fullstack-react-router/app/routes/_index/dashboard.tsx
  • examples/server-node/basic.ts
  • packages/durably-react/README.md
  • packages/durably-react/docs/llms.md
  • packages/durably-react/package.json
  • packages/durably-react/src/client/use-job-logs.ts
  • packages/durably-react/src/client/use-job-run.ts
  • packages/durably-react/src/client/use-job.ts
  • packages/durably-react/src/client/use-runs.ts
  • packages/durably-react/src/context.tsx
  • packages/durably-react/src/hooks/use-job-logs.ts
  • packages/durably-react/src/hooks/use-job-run.ts
  • packages/durably-react/src/hooks/use-job.ts
  • packages/durably-react/src/hooks/use-runs.ts
  • packages/durably-react/tests/browser/provider.test.tsx
  • packages/durably-react/tests/browser/use-job-logs.test.tsx
  • packages/durably-react/tests/browser/use-job-run.test.tsx
  • packages/durably-react/tests/browser/use-job.test.tsx
  • packages/durably-react/tests/browser/use-runs.test.tsx
  • packages/durably-react/tests/client/create-durably-client.test.tsx
  • packages/durably-react/tests/client/create-job-hooks.test.tsx
  • packages/durably-react/tests/client/use-job-logs.test.tsx
  • packages/durably-react/tests/client/use-job-run.test.tsx
  • packages/durably-react/tests/client/use-job.test.tsx
  • packages/durably-react/tests/helpers/create-test-durably.ts
  • packages/durably-react/tests/types.test.ts
  • packages/durably/README.md
  • packages/durably/docs/llms.md
  • packages/durably/src/durably.ts
  • packages/durably/src/server.ts
  • website/.vitepress/config.ts
  • website/api/create-durably.md
  • website/api/define-job.md
  • website/api/durably-react/browser.md
  • website/api/durably-react/client.md
  • website/api/durably-react/index.md
  • website/api/durably-react/types.md
  • website/api/http-handler.md
  • website/api/index.md
  • website/api/step.md
  • website/guide/background-sync.md
  • website/guide/concepts.md
  • website/guide/csv-import.md
  • website/guide/deployment.md
  • website/guide/events.md
  • website/guide/getting-started.md
  • website/guide/index.md
  • website/guide/jobs-and-steps.md
  • website/guide/offline-app.md
  • website/guide/resumability.md
  • website/package.json
  • website/public/llms.txt
  • website/public/llms.txt
  • website/scripts/generate-llms.js

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.

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

🧹 Nitpick comments (1)
docs/implementation-plan-react.md (1)

118-120: Update documentation files per learnings: include Phase 6 guidance on llms.md.

The implementation plan's Phase 6 (Documentation and Examples) references updating website/api/events.md, website/guide/events.md, and packages/durably/docs/llms.md. Based on learnings, packages/durably/docs/llms.md is bundled in the npm package and symlinked for web access, so it's critical to keep it synchronized. However, the Phase 0 documentation updates (lines 117–120) mention the same llms.md file. Ensure that the Phase 0 and Phase 6 doc updates are coordinated to avoid duplication.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 76e368a and cd0c4d3.

📒 Files selected for processing (2)
  • docs/implementation-plan-react.md
  • docs/spec-react.md
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-12-22T12:46:22.679Z
Learnt from: CR
Repo: coji/durably PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-22T12:46:22.679Z
Learning: Applies to packages/durably/docs/llms.md : Update `packages/durably/docs/llms.md` to keep it in sync with API changes, as this file is bundled in the npm package for coding agents to read from `node_modules` and symlinked to `website/public/llms.txt` for web access

Applied to files:

  • docs/spec-react.md
🪛 markdownlint-cli2 (0.18.1)
docs/spec-react.md

13-13: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


429-429: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


504-504: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🔇 Additional comments (4)
docs/implementation-plan-react.md (1)

457-499: Phase 0 scope appears aligned with spec but verify core event completeness.

The implementation plan correctly identifies that run:progress does not exist (line 484–486) and must be added in Phase 0. However, the spec-react.md (lines 347–355) defines a broader DurablyEvent union including run:start, run:complete, run:fail, step:start, step:complete, and log:write. Verify whether these events already exist in the core and are simply not listed in the "requirements" section, or whether the spec anticipates them as future additions beyond Phase 0.

docs/spec-react.md (3)

503-517: Verify DurablyEvent type union is comprehensive and includes all described events.

The type definition at lines 347–355 defines DurablyEvent as a union of 7 event types (run:start, run:complete, run:fail, run:progress, step:start, step:complete, log:write). Verify that all of these event types are either already implemented in the core or scheduled for Phase 0. The implementation-plan-react.md Phase 0 (lines 80–127) only mentions adding run:progress, but the spec references other events without clarifying their existence status.


383-385: Verify lightweight client package doesn't require @coji/durably for server-integration mode.

The documentation claims (line 382–384) that the server-integration mode client package is lightweight and does not require @coji/durably as a dependency. Verify that the planned @coji/durably-react/client module implementation will not accidentally create a hard dependency on the core package, as this is a stated goal for the dual-mode architecture.


542-560: Align core requirements section with implementation-plan Phase 0 scope.

Lines 549–560 list three core requirements: subscribe(), getJob(), and createDurablyHandler(). Verify that the implementation-plan Phase 0 (which focuses on run:progress event) adequately covers these or clarifies when getJob() and createDurablyHandler() should be implemented. The spec presents these as new requirements simultaneously; the plan should make the phasing explicit.

Comment thread docs/spec-react.md
Comment thread docs/spec-react.md
Comment thread docs/spec-react.md
Address CodeRabbit review feedback:
- Add 'text' language identifier to package structure block
- Add 'text' language identifier to SSE event format block

🤖 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: 1

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cd0c4d3 and 1ced540.

📒 Files selected for processing (1)
  • docs/spec-react.md
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: CR
Repo: coji/durably PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-22T12:46:22.679Z
Learning: Applies to packages/durably/docs/llms.md : Update `packages/durably/docs/llms.md` to keep it in sync with API changes, as this file is bundled in the npm package for coding agents to read from `node_modules` and symlinked to `website/public/llms.txt` for web access
📚 Learning: 2025-12-22T12:46:22.679Z
Learnt from: CR
Repo: coji/durably PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-22T12:46:22.679Z
Learning: Applies to packages/durably/docs/llms.md : Update `packages/durably/docs/llms.md` to keep it in sync with API changes, as this file is bundled in the npm package for coding agents to read from `node_modules` and symlinked to `website/public/llms.txt` for web access

Applied to files:

  • docs/spec-react.md
🪛 markdownlint-cli2 (0.18.1)
docs/spec-react.md

363-363: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

Comment thread docs/spec-react.md Outdated
@coji
Copy link
Copy Markdown
Owner Author

coji commented Dec 24, 2025

CodeRabbit Review Response

Comment 1 & 2 (Line 13, 289): Fixed in 1ced540 - added text language identifier to package structure and SSE event format code blocks.

Comment 3 (Line 429): This is not a code block - it's a markdown bullet list under the '### Strict Mode 対応' heading at line 535. No change needed.

Address code review feedback:
- Fix useJobRun signature to use object form: useJobRun({ runId })
- Add 400 Bad Request handling for missing runId in server example
- Fix SSE event samples to include jobName in all events
- Add trigger/triggerAndWait return type definitions
- Add initialRunId to client useJob options table
- Fix log:write level type to use union instead of string
- Add security (auth/CORS/CSRF) out of scope note
- Add cancel API to future extensions section

🤖 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)
docs/spec-react.md (1)

383-388: Add language identifier to the dependencies code block.

The fenced code block at line 383 lacks a language identifier. For consistency with the similar package structure block earlier and to pass markdown linting, add the text identifier.

🔎 Proposed fix
-```
+```text
 @coji/durably-react
 ├── @coji/durably  (peer dependency)
 ├── react          (peer dependency, >= 18.0.0)
 └── react-dom      (peer dependency, >= 18.0.0)

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used**: defaults

**Review profile**: CHILL

**Plan**: Pro

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 1ced54023d3a8bb45e82b615b449353dc011564b and 2af91fc9f8b67000482998266a55fb2d08a53206.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `docs/spec-react.md`

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧠 Learnings (1)</summary>

<details>
<summary>📚 Learning: 2025-12-22T12:46:22.687Z</summary>

Learnt from: CR
Repo: coji/durably PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-22T12:46:22.687Z
Learning: Applies to packages/durably/docs/llms.md : Update packages/durably/docs/llms.md to keep it in sync with API changes, as this file is bundled in the npm package for coding agents to read from node_modules and symlinked to website/public/llms.txt for web access


**Applied to files:**
- `docs/spec-react.md`

</details>

</details><details>
<summary>🪛 markdownlint-cli2 (0.18.1)</summary>

<details>
<summary>docs/spec-react.md</summary>

383-383: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

</details>

</details>

</details>

<details>
<summary>🔇 Additional comments (2)</summary><blockquote>

<details>
<summary>docs/spec-react.md (2)</summary><blockquote>

`193-505`: **API specifications and examples are clear and comprehensive.**

The dual-mode architecture is well-articulated with consistent signatures across browser and server modes. Type definitions align with examples, and practical use cases (batch processing, AI agent, reconnection handling) effectively demonstrate both patterns.

---

`541-623`: **Implementation guidance and future extensions are well-scoped.**

The internal implementation hints for both modes provide clear direction, Strict Mode handling is properly addressed, and future extensions are clearly marked and deferred without cluttering the current specification.

</blockquote></details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->

- Unify useJob return values (add all fields to server mode example)
- Unify useJobRun return values (add error, logs to server mode)
- Add useJobLogs to server mode with api/runId/maxLogs options
- Add jobName to step:start, step:complete, log:write events for consistency
- Add DurablyOptions type definition (pollingInterval, heartbeatInterval, staleThreshold)
- Separate options tables for useJob, useJobRun, useJobLogs in server mode
- Add 'text' language identifier to dependency tree code block

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
coji and others added 12 commits December 28, 2025 22:59
- Merge StrictMode test into Phase 3 (remove Phase 6)
- Add Phase 17 for shared logic extraction
- Add detailed test code for Phase 24-26 (server mode hooks)
- Add Phase 28 for type inference tests
- Update summary table
- Fix duplicate Phase 18 (useJobRun and useJobLogs)
- Renumber phases 19-30 → 20-31
- Update summary table to reflect 31 total phases

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

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

- Phase 1: Package setup with dual entry points (index.ts, client.ts)
- Phase 2: Type definitions (RunStatus, Progress, LogEntry, DurablyEvent)
- Phase 3-5: DurablyProvider with StrictMode support
  - dialectFactory for Kysely dialect injection
  - autoStart/autoMigrate options
  - Proper cleanup on unmount
  - Error handling

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

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

- Phase 6: trigger function to execute jobs
- Phase 7: status subscription (pending/running/completed/failed)
- Phase 8: output retrieval on completion
- Phase 9: error handling on failure
- Phase 10: progress updates during execution
- Phase 11: log collection
- Phase 12: boolean helpers (isRunning, isPending, isCompleted, isFailed)
- Phase 13: triggerAndWait for sync-style usage
- Phase 14: reset function to clear state
- Phase 15: initialRunId for reconnection scenarios
- Phase 16: proper cleanup on unmount

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…ription logic (Phase 17-19)

- Extract common run subscription logic into use-run-subscription.ts
- Add useJobRun hook for subscribing to existing runs by ID
- Add useJobLogs hook for subscribing to logs with maxLogs limit
- Add tests for both new hooks

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

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

- Add getJob method to retrieve registered job by name
- Add subscribe method returning ReadableStream for SSE support
- Create createDurablyHandler for HTTP endpoints (trigger + subscribe)
- Export DurablyHandler types from index

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

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

- Add useSSESubscription for shared SSE event handling
- Add client-mode useJob with fetch trigger and EventSource subscription
- Add client-mode useJobRun for subscribing to existing runs via SSE
- Add client-mode useJobLogs for log subscription via SSE
- Add MockEventSource test utility for SSE testing
- 25 new tests covering all client-mode scenarios

🤖 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: 2

🧹 Nitpick comments (10)
docs/implementation-plan-react.md (1)

22-38: Add language identifier to code block.

The fenced code block for the directory structure is missing a language identifier. Consider adding text or plaintext for consistency with other code blocks in this document.

🔎 Suggested fix
-```
+```text
 packages/durably-react/
 ├── src/
 │   ├── index.ts              # ブラウザ完結モード
packages/durably/src/server.ts (1)

46-55: Add input validation for malformed requests.

The request.json() call will throw on malformed JSON, but the error will be caught generically as a 500 error. Consider validating the parsed body structure to provide clearer error messages.

🔎 Suggested improvement
     async trigger(request: Request): Promise<Response> {
       try {
-        const body = (await request.json()) as TriggerRequest
+        let body: TriggerRequest
+        try {
+          body = (await request.json()) as TriggerRequest
+        } catch {
+          return new Response(
+            JSON.stringify({ error: 'Invalid JSON body' }),
+            { status: 400, headers: { 'Content-Type': 'application/json' } },
+          )
+        }
 
-        if (!body.jobName) {
+        if (!body.jobName || typeof body.jobName !== 'string') {
           return new Response(
             JSON.stringify({ error: 'jobName is required' }),
             { status: 400, headers: { 'Content-Type': 'application/json' } },
           )
         }
packages/durably/tests/node/core-extensions.test.ts (1)

192-199: Consider verifying SSE stream content.

The test only checks the Content-Type header but doesn't verify that actual SSE-formatted events are sent through the stream. While this is sufficient for basic validation, consider adding a follow-up test that reads from the SSE stream and validates event formatting.

packages/durably-react/tests/browser/use-job-logs.test.tsx (2)

138-168: Fixed timeout could cause flakiness.

Line 164 uses a fixed setTimeout(500) to wait for job completion. Consider using waitFor with a condition to check for completion or a specific log count instead, as fixed timeouts can be flaky in CI environments.

🔎 Suggested improvement
-    // Wait for job to complete
-    await new Promise((r) => setTimeout(r, 500))
+    // Wait for logs to accumulate
+    await waitFor(
+      () => {
+        expect(result.current.logs.length).toBeGreaterThan(0)
+      },
+      { timeout: 2000 }
+    )
 
     // Should have at most 5 logs
     expect(result.current.logs.length).toBeLessThanOrEqual(5)

170-212: Fixed timeout could cause flakiness.

Lines 196 uses another fixed setTimeout(500) wait. Similar to the previous test, consider using waitFor with appropriate conditions for more reliable test execution.

packages/durably-react/src/hooks/use-job-run.ts (1)

68-80: Placeholder effect may need future implementation.

The effect at lines 71-80 is currently a placeholder with a TODO comment indicating that fetching initial run state is not yet implemented. The hook currently relies entirely on event-based updates, which means:

  1. If a run completes before subscription starts, the status may never update
  2. The fetchedRef is maintained but not meaningfully used

This is acceptable if runs are expected to be long-running (as the test jobs suggest), but consider documenting this limitation or implementing initial state fetching via durably.getRun(runId) in a future update.

Would you like me to generate an implementation that fetches initial run state to handle already-completed runs?

packages/durably-react/src/context.tsx (1)

53-126: Consider stabilizing options and onReady with refs or memoization guidance.

Including options and onReady in the dependency array (line 119) means if the consumer passes inline objects or callbacks, the effect will re-run on every render. While initializedRef prevents re-initialization, this could still trigger unnecessary effect cleanup/setup cycles.

Consider documenting that consumers should memoize these props, or use refs internally to track their latest values without triggering re-runs.

🔎 Alternative pattern using refs for latest values
  const initializedRef = useRef(false)
  const instanceRef = useRef<Durably | null>(null)
+ const optionsRef = useRef(options)
+ const onReadyRef = useRef(onReady)
+ optionsRef.current = options
+ onReadyRef.current = onReady

  useEffect(() => {
    // Prevent double initialization in StrictMode
    if (initializedRef.current) {
      // If already initialized, just use the existing instance
      if (instanceRef.current) {
        setDurably(instanceRef.current)
        setIsReady(true)
      }
      return
    }

    initializedRef.current = true
    let cleanedUp = false

    async function init() {
      try {
        const dialect = dialectFactory()
-       const instance = createDurably({ dialect, ...options })
+       const instance = createDurably({ dialect, ...optionsRef.current })
        instanceRef.current = instance

        if (cleanedUp) return

        if (autoMigrate) {
          await instance.migrate()
          if (cleanedUp) return
        }

        if (autoStart) {
          instance.start()
        }

        if (cleanedUp) return

        setDurably(instance)
        setIsReady(true)
-       onReady?.(instance)
+       onReadyRef.current?.(instance)
      } catch (err) {
        if (cleanedUp) return
        setError(err instanceof Error ? err : new Error(String(err)))
      }
    }

    init()

    return () => {
      cleanedUp = true
      if (instanceRef.current) {
        instanceRef.current.stop()
      }
    }
- }, [dialectFactory, options, autoStart, autoMigrate, onReady])
+ }, [dialectFactory, autoStart, autoMigrate])
packages/durably-react/src/hooks/use-run-subscription.ts (1)

90-111: Consider deduplicating log entry creation logic with useJob.

The log entry creation pattern (lines 94-102) is duplicated in use-job.ts (lines 143-151). Consider extracting a shared helper function or reusing useRunSubscription in useJob to reduce duplication.

🔎 Example helper extraction
// In a shared utils file or at the top of use-run-subscription.ts
export function createLogEntry(event: {
  runId: string
  stepName: string | null
  level: 'info' | 'warn' | 'error'
  message: string
  data: unknown
}): LogEntry {
  return {
    id: crypto.randomUUID(),
    runId: event.runId,
    stepName: event.stepName,
    level: event.level,
    message: event.message,
    data: event.data,
    timestamp: new Date().toISOString(),
  }
}
packages/durably-react/src/hooks/use-job.ts (2)

238-256: triggerAndWait uses polling instead of event-based completion.

The implementation polls every 50ms to check completion status, which is inefficient compared to the event-based approach used in the core package (packages/durably/src/job.ts lines 223-293). This creates unnecessary database queries and delays.

Consider subscribing to run:complete and run:fail events for immediate notification, similar to how the core triggerAndWait is implemented.

🔎 Event-based implementation
  const triggerAndWait = useCallback(
    async (input: TInput): Promise<{ runId: string; output: TOutput }> => {
      const jobHandle = jobHandleRef.current
      if (!jobHandle || !durably) {
        throw new Error('Job not ready')
      }

      // Reset state
      setOutput(null)
      setError(null)
      setLogs([])
      setProgress(null)

      const run = await jobHandle.trigger(input)
      setCurrentRunId(run.id)
      setStatus('pending')

      // Wait for completion
      return new Promise((resolve, reject) => {
+       let resolved = false
+
+       const cleanup = () => {
+         if (resolved) return
+         resolved = true
+         unsubscribeComplete()
+         unsubscribeFail()
+       }
+
+       const unsubscribeComplete = durably.on('run:complete', (event) => {
+         if (event.runId === run.id && !resolved) {
+           cleanup()
+           resolve({ runId: run.id, output: event.output as TOutput })
+         }
+       })
+
+       const unsubscribeFail = durably.on('run:fail', (event) => {
+         if (event.runId === run.id && !resolved) {
+           cleanup()
+           reject(new Error(event.error))
+         }
+       })
+
+       // Check current status after subscribing (race condition mitigation)
+       jobHandle.getRun(run.id).then((currentRun) => {
+         if (resolved || !currentRun) return
+         if (currentRun.status === 'completed') {
+           cleanup()
+           resolve({ runId: run.id, output: currentRun.output as TOutput })
+         } else if (currentRun.status === 'failed') {
+           cleanup()
+           reject(new Error(currentRun.error || 'Run failed'))
+         }
+       })
-       const checkCompletion = async () => {
-         const updatedRun = await jobHandle.getRun(run.id)
-         if (!updatedRun) {
-           reject(new Error('Run not found'))
-           return
-         }
-
-         if (updatedRun.status === 'completed') {
-           resolve({ runId: run.id, output: updatedRun.output as TOutput })
-         } else if (updatedRun.status === 'failed') {
-           reject(new Error(updatedRun.error ?? 'Job failed'))
-         } else {
-           // Still running, check again
-           setTimeout(checkCompletion, 50)
-         }
-       }
-       checkCompletion()
      })
    },
    [durably],
  )

97-176: Consider reusing useRunSubscription to reduce code duplication.

The event subscription logic (lines 108-154) duplicates useRunSubscription. Since useRunSubscription is documented as "shared by useJob, useJobRun, and useJobLogs," consider composing useJob with useRunSubscription internally.

🔎 Composition approach
export function useJob<...>(...): UseJobResult<TInput, TOutput> {
  const { durably, isReady: isDurablyReady } = useDurably()
  const [currentRunId, setCurrentRunId] = useState<string | null>(
    options?.initialRunId ?? null,
  )
  
  // Reuse shared subscription logic
  const {
    status,
    output,
    error,
    logs,
    progress,
    reset: resetSubscription,
  } = useRunSubscription<TOutput>(durably, currentRunId)
  
  const jobHandleRef = useRef<JobHandle<TName, TInput, TOutput> | null>(null)
  
  // Register job only
  useEffect(() => {
    if (!durably || !isDurablyReady) return
    jobHandleRef.current = durably.register(jobDefinition)
  }, [durably, isDurablyReady, jobDefinition])
  
  // ... trigger, triggerAndWait, reset implementation
}
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between afb9629 and 97bfae7.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (23)
  • docs/implementation-plan-react.md
  • docs/spec-react.md
  • packages/durably-react/package.json
  • packages/durably-react/src/client.ts
  • packages/durably-react/src/context.tsx
  • packages/durably-react/src/hooks/use-job-logs.ts
  • packages/durably-react/src/hooks/use-job-run.ts
  • packages/durably-react/src/hooks/use-job.ts
  • packages/durably-react/src/hooks/use-run-subscription.ts
  • packages/durably-react/src/index.ts
  • packages/durably-react/src/types.ts
  • packages/durably-react/tests/browser/provider.test.tsx
  • packages/durably-react/tests/browser/use-job-logs.test.tsx
  • packages/durably-react/tests/browser/use-job-run.test.tsx
  • packages/durably-react/tests/browser/use-job.test.tsx
  • packages/durably-react/tests/helpers/browser-dialect.ts
  • packages/durably-react/tsconfig.json
  • packages/durably-react/tsup.config.ts
  • packages/durably-react/vitest.config.ts
  • packages/durably/src/durably.ts
  • packages/durably/src/index.ts
  • packages/durably/src/server.ts
  • packages/durably/tests/node/core-extensions.test.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/durably-react/src/client.ts
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-12-22T12:46:22.687Z
Learnt from: CR
Repo: coji/durably PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-22T12:46:22.687Z
Learning: Applies to packages/durably/docs/llms.md : Update `packages/durably/docs/llms.md` to keep it in sync with API changes, as this file is bundled in the npm package for coding agents to read from `node_modules` and symlinked to `website/public/llms.txt` for web access

Applied to files:

  • packages/durably/src/index.ts
  • docs/implementation-plan-react.md
  • packages/durably-react/tsconfig.json
  • packages/durably-react/package.json
  • docs/spec-react.md
📚 Learning: 2025-12-22T12:46:22.687Z
Learnt from: CR
Repo: coji/durably PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-22T12:46:22.687Z
Learning: Use the dialect injection pattern - pass Kysely dialect to `createDurably()` to abstract SQLite implementations across Node.js and browser environments

Applied to files:

  • packages/durably-react/tests/helpers/browser-dialect.ts
  • packages/durably-react/src/context.tsx
📚 Learning: 2025-12-22T12:46:22.687Z
Learnt from: CR
Repo: coji/durably PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-22T12:46:22.687Z
Learning: Job definitions must be created via `durably.defineJob()`, receiving a step context and payload

Applied to files:

  • packages/durably/src/durably.ts
🧬 Code graph analysis (9)
packages/durably-react/src/hooks/use-job-logs.ts (3)
packages/durably-react/src/index.ts (4)
  • UseJobLogsOptions (9-9)
  • UseJobLogsResult (9-9)
  • LogEntry (12-12)
  • useJobLogs (8-8)
packages/durably-react/src/hooks/use-run-subscription.ts (1)
  • useRunSubscription (37-141)
packages/durably/src/context.ts (1)
  • runId (19-21)
packages/durably/src/server.ts (2)
packages/durably/src/durably.ts (1)
  • Durably (54-153)
packages/durably/src/events.ts (1)
  • AnyEventInput (148-157)
packages/durably-react/src/hooks/use-job.ts (4)
packages/durably-react/src/index.ts (7)
  • UseJobOptions (7-7)
  • UseJobResult (7-7)
  • RunStatus (12-12)
  • LogEntry (12-12)
  • Progress (12-12)
  • useJob (6-6)
  • useDurably (4-4)
packages/durably/src/context.ts (3)
  • error (150-159)
  • progress (114-125)
  • run (23-112)
examples/react/src/lib/durably.ts (1)
  • durably (20-25)
packages/durably/src/job.ts (1)
  • triggerAndWait (224-294)
packages/durably-react/src/hooks/use-job-run.ts (2)
packages/durably-react/src/hooks/use-run-subscription.ts (1)
  • useRunSubscription (37-141)
packages/durably/src/context.ts (1)
  • runId (19-21)
packages/durably-react/src/context.tsx (3)
packages/durably-react/src/index.ts (4)
  • DurablyProviderOptions (5-5)
  • DurablyProviderProps (5-5)
  • DurablyProvider (4-4)
  • useDurably (4-4)
examples/react/src/lib/durably.ts (1)
  • durably (20-25)
packages/durably/src/context.ts (1)
  • error (150-159)
packages/durably/tests/node/core-extensions.test.ts (3)
packages/durably/src/durably.ts (2)
  • Durably (54-153)
  • createDurably (158-381)
packages/durably/src/index.ts (5)
  • Durably (7-7)
  • createDurably (6-6)
  • defineJob (10-10)
  • DurablyEvent (18-18)
  • createDurablyHandler (51-51)
packages/durably/src/server.ts (1)
  • createDurablyHandler (44-132)
packages/durably-react/tests/browser/provider.test.tsx (4)
packages/durably-react/src/index.ts (1)
  • DurablyProvider (4-4)
packages/durably-react/src/context.tsx (1)
  • DurablyProvider (53-126)
packages/durably-react/tests/helpers/browser-dialect.ts (1)
  • createBrowserDialect (5-10)
examples/react/src/lib/durably.ts (1)
  • durably (20-25)
packages/durably-react/tests/browser/use-job-logs.test.tsx (2)
packages/durably/src/context.ts (2)
  • runId (19-21)
  • run (23-112)
packages/durably-react/src/hooks/use-job-logs.ts (1)
  • useJobLogs (35-46)
packages/durably-react/src/hooks/use-run-subscription.ts (1)
packages/durably/src/context.ts (3)
  • runId (19-21)
  • error (150-159)
  • progress (114-125)
🪛 markdownlint-cli2 (0.18.1)
docs/implementation-plan-react.md

22-22: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🔇 Additional comments (33)
packages/durably-react/tsconfig.json (1)

1-19: LGTM!

The TypeScript configuration is well-structured for a React library package. The settings appropriately target ES2022 with bundler module resolution, include DOM types for browser APIs, and enable strict type checking with declaration generation for consumers.

packages/durably-react/tests/helpers/browser-dialect.ts (1)

5-10: Good test isolation pattern.

The unique database naming strategy using timestamp and counter ensures proper test isolation. Based on learnings, this correctly implements the dialect injection pattern for browser environments.

Consider whether test databases should be cleaned up after test runs to prevent OPFS file accumulation, though this may be acceptable for ephemeral test environments.

packages/durably-react/tsup.config.ts (1)

1-13: LGTM!

The build configuration correctly sets up dual entry points for browser-complete (index) and server-integration (client) modes, aligning with the package.json exports. Externalizing React and @coji/durably is appropriate given the peer/optional dependency structure.

packages/durably-react/vitest.config.ts (2)

8-18: Good COOP/COEP configuration for OPFS support.

The custom middleware correctly sets the required Cross-Origin headers to enable SharedArrayBuffer and OPFS functionality needed by sqlocal in browser tests.


20-28: Appropriate browser test configuration.

The retry policy of 2 is sensible for browser tests which can be flaky due to timing. Playwright with headless Chromium provides reliable browser automation for CI environments.

packages/durably-react/tests/browser/use-job.test.tsx (4)

57-71: Good test cleanup pattern.

The instance tracking and cleanup in afterEach ensures proper resource cleanup. The 200ms delay helps ensure async operations complete before the next test starts, which is important for browser-based tests with SQLite.


73-84: Well-designed test wrapper factory.

The createWrapper factory correctly creates a fresh DurablyProvider with a unique dialect for each test, ensuring isolation. The onReady callback for instance tracking enables proper cleanup.


294-309: Cleanup test validates no memory leaks.

The unmount-while-running test with a short delay afterward is a reasonable approach to verify that the hook properly unsubscribes on unmount without causing errors. This pattern catches common memory leak issues in subscription-based hooks.


86-98: Return type is correct and consistent.

The trigger function is properly typed as (input: TInput) => Promise<{ runId: string }> in the UseJobResult interface (line 21 of use-job.ts), and the implementation (lines 198-218) correctly returns { runId: run.id }. The test expectations match the actual implementation.

packages/durably-react/package.json (2)

52-60: Good peer dependency structure.

The peer dependencies correctly require React 18+ while making @coji/durably optional. This enables the server-integration mode (./client entry) to work without bundling the full Durably dependency.


80-80: Zod version alignment confirmed. Both packages/durably-react and packages/durably use Zod ^4.2.1, ensuring consistent type definitions for job schemas across packages.

docs/implementation-plan-react.md (1)

1-1062: Comprehensive TDD implementation plan.

The 31-phase implementation plan is well-structured, covering both browser-complete and server-integration modes with clear test-first examples. The phased approach with explicit completion conditions provides a solid roadmap for the package development.

packages/durably/src/server.ts (2)

99-119: SSE stream implementation looks correct.

The transformation from ReadableStream to SSE format is properly implemented with correct data: prefixing and double newline delimiters. Error handling propagates stream errors appropriately.


122-130: Note: Connection: keep-alive header.

The Connection: keep-alive header is appropriate for HTTP/1.1 but may be ignored or cause warnings in HTTP/2+ environments. This is generally acceptable as browsers and servers handle this gracefully.

packages/durably/src/index.ts (1)

49-52: LGTM! Server API surface properly exposed.

The new exports cleanly expose the server-side handler functionality and types, completing the public API surface for server integration as described in the PR objectives.

packages/durably-react/tests/browser/use-job-run.test.tsx (2)

111-147: Test pattern is well-structured.

The combined hook pattern (useTriggerAndSubscribe) effectively tests the subscription lifecycle by triggering a job and then subscribing to it. The test properly waits for readiness before proceeding.


134-136: Non-null assertion is safe in this context.

The non-null assertion on result.current.durably! is appropriate here since the test has already verified isReady is true via waitFor, which guarantees durably is initialized.

packages/durably/tests/node/core-extensions.test.ts (1)

74-92: Stream consumption pattern is correct.

The test properly consumes the ReadableStream by reading until done and collecting events. This correctly validates that events are emitted through the stream.

packages/durably-react/src/hooks/use-job-logs.ts (1)

1-46: LGTM! Clean hook abstraction.

The useJobLogs hook is a well-designed wrapper that focuses on log collection by delegating to useRunSubscription and exposing only log-related functionality. The implementation is straightforward and maintains proper separation of concerns.

packages/durably-react/src/hooks/use-job-run.ts (1)

82-93: Boolean helper flags are well-designed.

The derived boolean flags (isRunning, isPending, isCompleted, isFailed) provide a clean, UI-friendly API for checking run status without string comparisons.

packages/durably/src/durably.ts (1)

197-209: LGTM! Clean job retrieval implementation.

The getJob method properly retrieves a registered job from the registry and returns undefined for unknown jobs. The type casting is appropriate given the generic nature of the registry.

packages/durably-react/tests/browser/provider.test.tsx (3)

63-86: Excellent StrictMode validation.

This test effectively validates that the provider doesn't double-initialize in React StrictMode by tracking dialectFactory calls. This is critical for preventing duplicate Durably instances and resource leaks.


132-166: Cleanup test uses acceptable pattern.

The test wraps the stop() method with a spy to verify cleanup on unmount. While this is somewhat intrusive, it's an acceptable testing pattern when there's no other way to verify internal cleanup behavior.


168-188: Good error handling coverage.

The test validates that initialization errors are properly surfaced through the useDurably hook state, ensuring consumers can detect and handle initialization failures.

packages/durably-react/src/index.ts (1)

1-12: LGTM!

Clean barrel export with proper separation of runtime exports and type-only exports. The public API surface is well-organized and aligns with the documented specification.

packages/durably-react/src/context.tsx (1)

128-134: LGTM!

The useDurably hook correctly enforces usage within the provider and returns a well-typed context value.

packages/durably-react/src/types.ts (1)

1-60: LGTM!

Well-structured type definitions with a proper discriminated union for DurablyEvent. The types align with the specification and provide good type safety for event handling.

packages/durably-react/src/hooks/use-run-subscription.ts (1)

37-141: LGTM!

The subscription hook correctly manages event subscriptions with proper cleanup, runId filtering via refs, and log truncation logic. The reset and clearLogs utilities are useful additions.

docs/spec-react.md (4)

3-27: LGTM!

Clear overview with a helpful comparison table distinguishing browser-complete and server-integration modes. The package structure documentation aligns with the implementation.


372-379: Inconsistency: maxLogs default value differs from implementation.

The spec states maxLogs defaults to 100 (line 378), but use-run-subscription.ts (line 52) defaults to 0 (unlimited). Please align the documentation with the implementation or vice versa.

🔎 Suggested fix (if implementation is correct)
 | `maxLogs`  | `number` | -    | 保持する最大ログ数(デフォルト: 100) |
+| `maxLogs`  | `number` | -    | 保持する最大ログ数(デフォルト: 無制限) |

Or update the implementation to match the spec if 100 is the intended default.


103-158: LGTM!

Server integration pattern is well-documented with both the helper-based approach (createDurablyHandler) and manual implementation examples. The SSE endpoint contract is clear.


382-419: LGTM!

Type definitions in the spec are comprehensive and align with the implementation in types.ts. The discriminated union for DurablyEvent properly documents all SSE event types.

packages/durably-react/src/hooks/use-job.ts (1)

156-169: Duplicate state hydration with second useEffect.

The state hydration for initialRunId (lines 157-168) is duplicated by the second useEffect (lines 179-196). The first effect already handles initial state hydration when options?.initialRunId is provided, making the second effect potentially redundant or causing duplicate fetches.

Consider removing the second useEffect or clarifying its purpose if it handles a different scenario (e.g., when currentRunId changes independently of initialRunId).

🔎 Suggested simplification

If the second effect is only needed for initialRunId scenarios, it's redundant. If it's meant to handle manual currentRunId changes (which aren't exposed currently), the condition should be different:

  // Update state when currentRunId changes (for initialRunId scenario)
  useEffect(() => {
-   if (!durably || !currentRunId) return
+   // Skip if this is the initial mount handled by the first effect
+   if (!durably || !currentRunId || currentRunId === options?.initialRunId) return

    const jobHandle = jobHandleRef.current
-   if (jobHandle && options?.initialRunId) {
+   if (jobHandle) {
      jobHandle.getRun(currentRunId).then((run) => {
        // ...
      })
    }
- }, [durably, currentRunId, options?.initialRunId])
+ }, [durably, currentRunId])

Or simply remove the second effect if it's fully redundant.

Likely an incorrect or invalid review comment.

Comment thread packages/durably/src/durably.ts
Comment thread packages/durably/src/server.ts
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: 1

♻️ Duplicate comments (4)
examples/browser-vite-react/src/components/dashboard.tsx (4)

24-34: Error handling concern previously raised.


36-53: Error handling concern previously raised.


137-156: Division by zero concern previously raised.


230-244: Modal accessibility concern previously raised.

🧹 Nitpick comments (3)
examples/browser-vite-react/src/components/dashboard.tsx (1)

304-327: Consider using s.index as the list key instead of s.name.

Step names may not be unique within a run (e.g., multiple calls to a step with the same name). Since you already have index available from the mapping on line 31, using it as the key is more reliable and avoids potential React reconciliation issues.

🔎 Suggested change
 {steps.map((s) => (
   <li
-    key={s.name}
+    key={s.index}
     className="flex justify-between items-center p-2"
   >
examples/fullstack-react-router/app/routes/_index/data-sync-progress.tsx (1)

1-44: LGTM! Consider optional refactoring to reduce duplication.

The component correctly uses useJobRun to subscribe to real-time progress updates and forwards all state to RunProgress. The error ?? null fallback ensures type safety.

This component is nearly identical to ImageProcessingProgress (only differing in the intent check at line 14). If you expect to add more job types, consider extracting a generic JobProgress component that accepts the intent as a prop to reduce duplication.

Optional: Generic JobProgress component
interface JobProgressProps {
  intent: string
}

export function JobProgress({ intent }: JobProgressProps) {
  const actionData = useActionData<typeof action>()
  const runId = actionData?.intent === intent ? actionData.runId : null

  const {
    progress,
    output,
    error,
    logs,
    isPending,
    isRunning,
    isCompleted,
    isFailed,
    isCancelled,
  } = useJobRun({
    api: '/api/durably',
    runId,
  })

  return (
    <RunProgress
      progress={progress}
      output={output}
      error={error ?? null}
      logs={logs}
      isPending={isPending}
      isRunning={isRunning}
      isCompleted={isCompleted}
      isFailed={isFailed}
      isCancelled={isCancelled}
    />
  )
}

Then use as: <JobProgress intent="sync" /> and <JobProgress intent="image" />

examples/fullstack-react-router/app/routes/_index.tsx (1)

72-95: Consider adding ARIA attributes for better accessibility.

The tab interface would benefit from proper ARIA attributes to improve keyboard navigation and screen reader support.

🔎 Suggested accessibility improvements
-            <div className="flex border-b border-gray-200 mb-4">
+            <div className="flex border-b border-gray-200 mb-4" role="tablist" aria-label="Job selection">
               <button
                 type="button"
                 onClick={() => setActiveJob('image')}
+                role="tab"
+                aria-selected={activeJob === 'image'}
+                aria-controls="image-job-panel"
                 className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${
                   activeJob === 'image'
                     ? 'border-blue-600 text-blue-600'
                     : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
                 }`}
               >
                 Image Processing
               </button>
               <button
                 type="button"
                 onClick={() => setActiveJob('sync')}
+                role="tab"
+                aria-selected={activeJob === 'sync'}
+                aria-controls="sync-job-panel"
                 className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${
                   activeJob === 'sync'
                     ? 'border-blue-600 text-blue-600'
                     : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
                 }`}
               >
                 Data Sync
               </button>
             </div>

Then add corresponding id attributes to the form sections:

-            {activeJob === 'image' ? (
-              <ImageProcessingForm />
+            {activeJob === 'image' ? (
+              <div id="image-job-panel" role="tabpanel" aria-labelledby="image-tab">
+                <ImageProcessingForm />
+              </div>
             ) : (
-              <DataSyncForm />
+              <div id="sync-job-panel" role="tabpanel" aria-labelledby="sync-tab">
+                <DataSyncForm />
+              </div>
             )}
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d6b133f and 563ef71.

📒 Files selected for processing (9)
  • examples/browser-react-router-spa/app/routes/_index/dashboard.tsx
  • examples/browser-vite-react/src/components/dashboard.tsx
  • examples/fullstack-react-router/app/lib/durably.server.ts
  • examples/fullstack-react-router/app/routes/_index.tsx
  • examples/fullstack-react-router/app/routes/_index/data-sync-form.tsx
  • examples/fullstack-react-router/app/routes/_index/data-sync-progress.tsx
  • examples/fullstack-react-router/app/routes/_index/image-processing-form.tsx
  • examples/fullstack-react-router/app/routes/_index/image-processing-progress.tsx
  • examples/fullstack-react-router/app/routes/_index/run-progress.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • examples/fullstack-react-router/app/lib/durably.server.ts
  • examples/browser-react-router-spa/app/routes/_index/dashboard.tsx
🧰 Additional context used
🧬 Code graph analysis (5)
examples/fullstack-react-router/app/routes/_index/image-processing-progress.tsx (4)
examples/fullstack-react-router/app/routes/_index.tsx (1)
  • action (28-46)
packages/durably/src/context.ts (3)
  • runId (19-21)
  • progress (114-125)
  • error (150-159)
examples/fullstack-react-router/app/routes/_index/run-progress.tsx (1)
  • RunProgress (21-125)
packages/durably/src/worker.ts (1)
  • isRunning (282-284)
examples/fullstack-react-router/app/routes/_index/data-sync-form.tsx (2)
examples/fullstack-react-router/app/routes/_index.tsx (1)
  • action (28-46)
packages/durably/src/context.ts (1)
  • runId (19-21)
examples/fullstack-react-router/app/routes/_index/data-sync-progress.tsx (4)
examples/fullstack-react-router/app/routes/_index.tsx (1)
  • action (28-46)
packages/durably/src/context.ts (3)
  • runId (19-21)
  • progress (114-125)
  • error (150-159)
examples/fullstack-react-router/app/routes/_index/run-progress.tsx (1)
  • RunProgress (21-125)
packages/durably/src/worker.ts (1)
  • isRunning (282-284)
examples/fullstack-react-router/app/routes/_index/image-processing-form.tsx (2)
examples/fullstack-react-router/app/routes/_index.tsx (1)
  • action (28-46)
packages/durably/src/context.ts (1)
  • runId (19-21)
examples/fullstack-react-router/app/routes/_index/run-progress.tsx (4)
packages/durably-react/src/client.ts (1)
  • LogEntry (47-47)
packages/durably-react/src/client/index.ts (1)
  • LogEntry (47-47)
packages/durably/src/worker.ts (1)
  • isRunning (282-284)
packages/durably/src/context.ts (2)
  • progress (114-125)
  • error (150-159)
🔇 Additional comments (12)
examples/browser-vite-react/src/components/dashboard.tsx (5)

1-22: LGTM!

Clean imports and well-typed state management. The hook usage follows the expected patterns for browser-only mode.


55-63: LGTM!

Clean utility function and well-organized status-to-class mapping covering all expected states.


65-136: LGTM!

Good semantic HTML structure with proper table markup, loading indicator, and conditional empty state handling.


160-200: LGTM!

Action buttons correctly conditionally render based on run status with appropriate disabled styling.


207-228: LGTM!

Clean pagination implementation with proper disabled states and accessible button markup.

examples/fullstack-react-router/app/routes/_index/run-progress.tsx (3)

1-19: LGTM!

The component structure, imports, and interface definition are well-designed. The props clearly separate state concerns (progress, output, error, logs) from status flags (isPending, isRunning, etc.), making the component easy to use and test.


69-122: LGTM!

The conditional rendering for completed, failed, cancelled, and logs sections is well-structured. The color coding (green for success, red for error, gray for cancelled) provides clear visual feedback. The logs section appropriately handles different log levels with distinct styling.


46-67: Guard against division by zero when total is explicitly zero.

Line 59's progress bar calculation uses (progress.total || 1) as a fallback, but this only handles null/undefined. If progress.total is explicitly set to 0, the calculation becomes progress.current / 0, resulting in Infinity% or NaN% for the width style.

🔎 Proposed fix
           <div
             className="bg-blue-600 h-2 rounded-full transition-all duration-200"
             style={{
-              width: `${(progress.current / (progress.total || 1)) * 100}%`,
+              width: `${progress.total ? (progress.current / progress.total) * 100 : 0}%`,
             }}
           />

Likely an incorrect or invalid review comment.

examples/fullstack-react-router/app/routes/_index/image-processing-form.tsx (1)

1-64: LGTM!

The form implementation follows React Router best practices:

  • Uses useActionData and useNavigation for proper form state management
  • Disables the submit button during submission to prevent duplicate requests
  • Provides immediate feedback with the "Submitting..." label and triggered runId display
  • Input validation is set up with type="number" and min/max attributes

The Tailwind v4 styling is correctly applied with the new disabled: variant syntax.

examples/fullstack-react-router/app/routes/_index/data-sync-form.tsx (1)

1-47: LGTM!

The form follows the same solid patterns as ImageProcessingForm:

  • Proper state management with React Router hooks
  • Submit button disabled during submission
  • Clear user feedback for the triggered job
  • Type-safe action data handling

The implementation is consistent across the example application, making the codebase predictable and maintainable.

examples/fullstack-react-router/app/routes/_index/image-processing-progress.tsx (1)

1-44: LGTM!

The component correctly integrates useJobRun with React Router's action data to display real-time progress for image processing jobs. The pattern is consistent with DataSyncProgress, providing a unified developer experience across different job types in the example application.

examples/fullstack-react-router/app/routes/_index.tsx (1)

105-109: Verify that progress components correctly track triggered jobs.

The action function returns { intent, runId } after triggering a job, but the progress components (ImageProcessingProgress, DataSyncProgress) don't receive this runId as a prop.

Please verify:

  • Are these components designed to automatically track the most recent run via SSE subscriptions?
  • Or should they receive the runId from the action result to display the specific job that was just triggered?

If they should receive the runId, you'll need to use useActionData() to access the action result and pass it down.

Example: If runId should be passed to progress components
+import { useActionData } from 'react-router'
+
 export default function Index() {
   const [activeJob, setActiveJob] = useState<'image' | 'sync'>('image')
+  const actionData = useActionData<typeof action>()

   return (
     <div className="min-h-screen bg-gray-50">
       {/* ... */}
       
       {/* Progress Display */}
       {activeJob === 'image' ? (
-        <ImageProcessingProgress />
+        <ImageProcessingProgress 
+          runId={actionData?.intent === 'image' ? actionData.runId : undefined}
+        />
       ) : (
-        <DataSyncProgress />
+        <DataSyncProgress 
+          runId={actionData?.intent === 'sync' ? actionData.runId : undefined}
+        />
       )}
     </div>
   )
 }

Comment thread examples/fullstack-react-router/app/routes/_index.tsx
…tion

When both run.progress.current and run.progress.total are 0, the
previous calculation resulted in NaN. Now properly handles edge cases:
- If total exists, calculate percentage normally
- If no total but current > 0, show 100% (indeterminate progress)
- If both are 0, show 0%

🤖 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

♻️ Duplicate comments (3)
examples/browser-react-router-spa/app/routes/_index/dashboard.tsx (3)

24-34: Acknowledged: Example code with intentional simplicity.

Per previous discussion and learnings, error handling is intentionally omitted here to keep focus on demonstrating the Durably API. The implementation correctly fetches run details and steps.


36-53: Acknowledged: Action handlers follow example code pattern.

Per previous discussion and learnings, these handlers prioritize clarity over comprehensive error handling. The implementation correctly demonstrates the Durably API for retry, cancel, and delete operations.


137-156: Progress bar fix verified.

The three-case logic correctly handles all edge cases as confirmed in commit 2b965dd:

  • Normal percentage when total exists
  • 100% when no total but current > 0 (indeterminate progress)
  • 0% when both are 0

This eliminates the previous NaN issue.

🧹 Nitpick comments (3)
examples/browser-vite-react/src/components/dashboard.tsx (1)

304-327: Consider using step index as key and expanding status styling.

Two minor observations:

  1. Line 310: Using s.name as the React key assumes step names are unique. Using s.index would be more reliable:

    - key={s.name}
    + key={s.index}
  2. Lines 316-318: The binary styling (completed = green, everything else = red) may misrepresent steps with pending or running status as failures. Consider expanding the status handling for visual accuracy:

🔎 Optional: Expanded step status styling
 <span
-  className={`px-2 py-0.5 rounded-full text-xs font-medium ${
-    s.status === 'completed'
-      ? 'bg-green-100 text-green-800'
-      : 'bg-red-100 text-red-800'
-  }`}
+  className={`px-2 py-0.5 rounded-full text-xs font-medium ${
+    s.status === 'completed'
+      ? 'bg-green-100 text-green-800'
+      : s.status === 'failed'
+        ? 'bg-red-100 text-red-800'
+        : 'bg-gray-100 text-gray-800'
+  }`}
 >
examples/browser-react-router-spa/app/routes/_index/dashboard.tsx (2)

308-324: Consider using step index as React key.

Using s.name as the key could cause issues if step names are duplicated within a run. The step index is already available and guaranteed unique.

Also, the status styling only distinguishes between completed (green) and everything else (red). Consider expanding to handle pending status appropriately.

🔎 Proposed improvement
                     {steps.map((s) => (
                       <li
-                        key={s.name}
+                        key={s.index}
                         className="flex justify-between items-center p-2"
                       >
                         <span className="text-gray-800">{s.name}</span>
                         <span
-                          className={`px-2 py-0.5 rounded-full text-xs font-medium ${
-                            s.status === 'completed'
-                              ? 'bg-green-100 text-green-800'
-                              : 'bg-red-100 text-red-800'
-                          }`}
+                          className={`px-2 py-0.5 rounded-full text-xs font-medium ${
+                            s.status === 'completed'
+                              ? 'bg-green-100 text-green-800'
+                              : s.status === 'pending'
+                                ? 'bg-yellow-100 text-yellow-800'
+                                : 'bg-red-100 text-red-800'
+                          }`}
                         >
                           {s.status}
                         </span>
                       </li>
                     ))}

230-244: Optional: Add Escape key handling for modal.

Users commonly expect to close modals with the Escape key. This is a minor UX improvement.

🔎 Proposed improvement

Add an effect to handle Escape key:

import { useState, useEffect } from 'react'

// Inside Dashboard component:
useEffect(() => {
  const handleEscape = (e: KeyboardEvent) => {
    if (e.key === 'Escape') setSelectedRun(null)
  }
  if (selectedRun) {
    document.addEventListener('keydown', handleEscape)
    return () => document.removeEventListener('keydown', handleEscape)
  }
}, [selectedRun])
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 563ef71 and 2b965dd.

📒 Files selected for processing (3)
  • examples/browser-react-router-spa/app/routes/_index/dashboard.tsx
  • examples/browser-vite-react/src/components/dashboard.tsx
  • examples/fullstack-react-router/app/routes/_index/dashboard.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • examples/fullstack-react-router/app/routes/_index/dashboard.tsx
🧰 Additional context used
🧠 Learnings (6)
📓 Common learnings
Learnt from: coji
Repo: coji/durably PR: 11
File: examples/browser-vite-react/src/components/dashboard.tsx:24-34
Timestamp: 2026-01-02T14:03:31.517Z
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/fullstack-react-router/app/routes/_index.tsx:28-46
Timestamp: 2026-01-02T14:03:32.230Z
Learning: For example code in the examples/ directory of the coji/durably repository, simplicity and readability are prioritized over comprehensive error handling to keep the focus on demonstrating the Durably API usage patterns.
📚 Learning: 2026-01-02T14:03:31.517Z
Learnt from: coji
Repo: coji/durably PR: 11
File: examples/browser-vite-react/src/components/dashboard.tsx:24-34
Timestamp: 2026-01-02T14:03:31.517Z
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.

Applied to files:

  • examples/browser-react-router-spa/app/routes/_index/dashboard.tsx
📚 Learning: 2026-01-02T14:03:25.739Z
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.

Applied to files:

  • examples/browser-react-router-spa/app/routes/_index/dashboard.tsx
  • examples/browser-vite-react/src/components/dashboard.tsx
📚 Learning: 2026-01-02T14:03:32.230Z
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.

Applied to files:

  • examples/browser-react-router-spa/app/routes/_index/dashboard.tsx
  • examples/browser-vite-react/src/components/dashboard.tsx
📚 Learning: 2026-01-02T14:03:31.683Z
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.

Applied to files:

  • examples/browser-react-router-spa/app/routes/_index/dashboard.tsx
  • examples/browser-vite-react/src/components/dashboard.tsx
📚 Learning: 2026-01-02T14:03:31.517Z
Learnt from: coji
Repo: coji/durably PR: 11
File: examples/browser-vite-react/src/components/dashboard.tsx:24-34
Timestamp: 2026-01-02T14:03:31.517Z
Learning: In example code directories (e.g., examples/browser-vite-react, examples/fullstack-react-router) error handling may be omitted to keep code simple and readable and to focus on demonstrating the Durably API. This is intentional for examples, not a production best practice. When reviewing code in these example paths, verify that the lack of error handling is confined to examples and that the surrounding documentation clearly states this is for illustration. Do not apply this omission as a general guidance for production code.

Applied to files:

  • examples/browser-vite-react/src/components/dashboard.tsx
🧬 Code graph analysis (2)
examples/browser-react-router-spa/app/routes/_index/dashboard.tsx (3)
packages/durably-react/src/client/use-runs.ts (1)
  • useRuns (125-264)
packages/durably/src/storage.ts (1)
  • Run (18-33)
packages/durably/src/server.ts (3)
  • steps (420-445)
  • run (305-337)
  • runs (277-303)
examples/browser-vite-react/src/components/dashboard.tsx (3)
packages/durably-react/src/client/use-runs.ts (1)
  • useRuns (125-264)
packages/durably/src/storage.ts (1)
  • Run (18-33)
packages/durably/src/server.ts (2)
  • run (305-337)
  • runs (277-303)
🔇 Additional comments (8)
examples/browser-vite-react/src/components/dashboard.tsx (5)

1-10: LGTM!

Clean imports with a clear JSDoc header describing the component's purpose. The imports are minimal and appropriate for the component's functionality.


12-22: LGTM!

The hook usage is clean and the local state management for modal data is appropriately typed.


24-53: Clear demonstration of Durably API usage.

The handlers effectively showcase the core Durably operations: fetching run details, retrying failed/cancelled runs, cancelling active runs, and deleting completed runs. Based on learnings, the simplified error handling is intentional for example code clarity.


55-63: LGTM!

The status-to-class mapping covers all defined run statuses from the Run interface, and the fallback in the template handles any edge cases gracefully.


65-228: Well-structured table with proper conditional rendering.

The progress bar calculation at line 144 correctly handles the edge cases:

  • When total exists: standard percentage calculation
  • When no total but current > 0: shows 100%
  • When both are 0: shows 0%

The action buttons are appropriately gated by run status, and pagination controls have correct disabled states.

examples/browser-react-router-spa/app/routes/_index/dashboard.tsx (3)

1-22: LGTM!

The imports and component initialization are well-structured. The use of useDurably and useRuns hooks aligns correctly with the new @coji/durably-react browser bindings, and the state management for run details and steps is appropriately typed.


55-63: LGTM!

The status classes correctly include all run statuses including the new cancelled status. The fallback in line 123 (statusClasses[run.status] || 'bg-gray-100 text-gray-800') provides graceful handling for any unexpected status values.


207-227: LGTM!

Pagination controls are correctly implemented with proper disabled states based on page and hasMore values from the useRuns hook. The 1-indexed display (Page {page + 1}) is user-friendly.

coji and others added 10 commits January 3, 2026 11:59
Add init() method to Durably class that combines migrate() and start()
for simpler initialization. Restructure website documentation with
new use-case focused guides and split React API docs into multiple files.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Both start() and init() are idempotent, so browser code can safely use
init() even when DurablyProvider also calls start() internally. This
provides a consistent initialization API across server and browser.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…nReady/isReady

Since users now call init() before passing durably to DurablyProvider,
the provider no longer needs to manage initialization state. Removed:
- autoStart prop (init() already starts the worker)
- onReady callback (no async initialization)
- isReady/error from context and hooks (always ready after init())

This simplifies the API and reduces unnecessary state management.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Update createTestDurably to use init() instead of migrate()
- Add autoStart option to createTestDurably for tests that need worker control
- Remove isReady type assertions from types.test.ts
- Update tests that require worker not started to use autoStart: false

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Reorganize 0.6.0 changelog for clarity with per-package sections
- Add DurablyProvider simplification to breaking changes
- Document init() method addition
- Update READMEs to use init() instead of migrate()+start()

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add step:start, step:complete, step:fail, log:write events to SSE /runs/subscribe
- Subscribe to step:start, step:complete, run:progress in browser useRuns hook
- Handle step events in client useRuns for real-time updates
- Fix dashboard display: show stepCount only (not currentStepIndex/stepCount)
- Require React 19+ in peerDependencies (uses React.use() hook)

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add React 19+ requirement note to durably-react index
- Document real-time event subscriptions in useRuns (browser mode)
- Document SSE event subscriptions in useRuns (client mode)
- Update CHANGELOG with complete SSE event list

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add hierarchical sidebar menus for all major API sections
- Move Core Concepts from Reference to Introduction section
- Add Events submenu (Run/Step/Log/Worker events)
- Add Quick Reference page as API entry point
- Extract HTTP Handler to separate page
- Rename Browser-Complete Mode → Browser Hooks
- Rename Server-Connected Mode → Server Hooks
- Add mode selection guide to React Hooks overview
- Add explanations before code blocks in guide pages
- Change npm to pnpm in installation instructions

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
durably:
- Change npm to pnpm in installation
- Fix createDurablyHandler import path (@coji/durably)
- Fix withLogPersistence import path (@coji/durably)

durably-react:
- Add React 19+ requirement
- Rename Browser-Complete Mode → Browser Hooks
- Rename Server-Connected Mode → Server Hooks
- Simplify DurablyProvider props (durably, fallback only)
- Remove isReady from useDurably, useJob, useJobRun return types
- Add createDurablyClient as recommended approach
- Update Server Handler setup with handle() method
- Change npm to pnpm

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add script to concatenate llms.md from both @coji/durably and
@coji/durably-react into a single llms.txt file.

- Add scripts/generate-llms.js
- Run generate:llms before dev and build
- Replace symlink with generated file

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

2 participants