feat: add @coji/durably-react package#11
Conversation
- 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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Caution Review failedThe pull request is closed. 📝 WalkthroughWalkthroughThis 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 Changes
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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~65 minutes Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
📜 Recent review detailsConfiguration used: defaults Review profile: CHILL Plan: Pro ⛔ Files ignored due to path filters (4)
📒 Files selected for processing (61)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 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, andpackages/durably/docs/llms.md. Based on learnings,packages/durably/docs/llms.mdis 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
📒 Files selected for processing (2)
docs/implementation-plan-react.mddocs/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:progressdoes not exist (line 484–486) and must be added in Phase 0. However, the spec-react.md (lines 347–355) defines a broaderDurablyEventunion includingrun:start,run:complete,run:fail,step:start,step:complete, andlog: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
DurablyEventas 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 addingrun: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(), andcreateDurablyHandler(). Verify that the implementation-plan Phase 0 (which focuses onrun:progressevent) adequately covers these or clarifies whengetJob()andcreateDurablyHandler()should be implemented. The spec presents these as new requirements simultaneously; the plan should make the phasing explicit.
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>
There was a problem hiding this comment.
Actionable comments posted: 1
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 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)
CodeRabbit Review ResponseComment 1 & 2 (Line 13, 289): Fixed in 1ced540 - added 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>
There was a problem hiding this comment.
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
textidentifier.🔎 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 : Updatepackages/durably/docs/llms.mdto keep it in sync with API changes, as this file is bundled in the npm package for coding agents to read fromnode_modulesand symlinked towebsite/public/llms.txtfor 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>
- 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>
…g and event source mock
…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>
There was a problem hiding this comment.
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
textorplaintextfor 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 usingwaitForwith 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 usingwaitForwith 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:
- If a run completes before subscription starts, the status may never update
- The
fetchedRefis maintained but not meaningfully usedThis 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 stabilizingoptionsandonReadywith refs or memoization guidance.Including
optionsandonReadyin the dependency array (line 119) means if the consumer passes inline objects or callbacks, the effect will re-run on every render. WhileinitializedRefprevents 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 withuseJob.The log entry creation pattern (lines 94-102) is duplicated in
use-job.ts(lines 143-151). Consider extracting a shared helper function or reusinguseRunSubscriptioninuseJobto 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:triggerAndWaituses 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.tslines 223-293). This creates unnecessary database queries and delays.Consider subscribing to
run:completeandrun:failevents for immediate notification, similar to how the coretriggerAndWaitis 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 reusinguseRunSubscriptionto reduce code duplication.The event subscription logic (lines 108-154) duplicates
useRunSubscription. SinceuseRunSubscriptionis documented as "shared by useJob, useJobRun, and useJobLogs," consider composinguseJobwithuseRunSubscriptioninternally.🔎 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
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (23)
docs/implementation-plan-react.mddocs/spec-react.mdpackages/durably-react/package.jsonpackages/durably-react/src/client.tspackages/durably-react/src/context.tsxpackages/durably-react/src/hooks/use-job-logs.tspackages/durably-react/src/hooks/use-job-run.tspackages/durably-react/src/hooks/use-job.tspackages/durably-react/src/hooks/use-run-subscription.tspackages/durably-react/src/index.tspackages/durably-react/src/types.tspackages/durably-react/tests/browser/provider.test.tsxpackages/durably-react/tests/browser/use-job-logs.test.tsxpackages/durably-react/tests/browser/use-job-run.test.tsxpackages/durably-react/tests/browser/use-job.test.tsxpackages/durably-react/tests/helpers/browser-dialect.tspackages/durably-react/tsconfig.jsonpackages/durably-react/tsup.config.tspackages/durably-react/vitest.config.tspackages/durably/src/durably.tspackages/durably/src/index.tspackages/durably/src/server.tspackages/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.tsdocs/implementation-plan-react.mdpackages/durably-react/tsconfig.jsonpackages/durably-react/package.jsondocs/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.tspackages/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/durablyis 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
afterEachensures 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
createWrapperfactory correctly creates a fresh DurablyProvider with a unique dialect for each test, ensuring isolation. TheonReadycallback 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
triggerfunction is properly typed as(input: TInput) => Promise<{ runId: string }>in theUseJobResultinterface (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/durablyoptional. This enables the server-integration mode (./cliententry) to work without bundling the full Durably dependency.
80-80: Zod version alignment confirmed. Bothpackages/durably-reactandpackages/durablyuse 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-aliveheader.The
Connection: keep-aliveheader 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 verifiedisReadyis true viawaitFor, 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
useJobLogshook is a well-designed wrapper that focuses on log collection by delegating touseRunSubscriptionand 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
getJobmethod properly retrieves a registered job from the registry and returnsundefinedfor 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
dialectFactorycalls. 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
useDurablyhook 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
useDurablyhook 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
resetandclearLogsutilities 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:maxLogsdefault value differs from implementation.The spec states
maxLogsdefaults to 100 (line 378), butuse-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 forDurablyEventproperly 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 seconduseEffect(lines 179-196). The first effect already handles initial state hydration whenoptions?.initialRunIdis provided, making the second effect potentially redundant or causing duplicate fetches.Consider removing the second
useEffector clarifying its purpose if it handles a different scenario (e.g., whencurrentRunIdchanges independently ofinitialRunId).🔎 Suggested simplification
If the second effect is only needed for
initialRunIdscenarios, it's redundant. If it's meant to handle manualcurrentRunIdchanges (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.
There was a problem hiding this comment.
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 usings.indexas the list key instead ofs.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
indexavailable 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
useJobRunto subscribe to real-time progress updates and forwards all state toRunProgress. Theerror ?? nullfallback 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 genericJobProgresscomponent 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
idattributes 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
📒 Files selected for processing (9)
examples/browser-react-router-spa/app/routes/_index/dashboard.tsxexamples/browser-vite-react/src/components/dashboard.tsxexamples/fullstack-react-router/app/lib/durably.server.tsexamples/fullstack-react-router/app/routes/_index.tsxexamples/fullstack-react-router/app/routes/_index/data-sync-form.tsxexamples/fullstack-react-router/app/routes/_index/data-sync-progress.tsxexamples/fullstack-react-router/app/routes/_index/image-processing-form.tsxexamples/fullstack-react-router/app/routes/_index/image-processing-progress.tsxexamples/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 handlesnull/undefined. Ifprogress.totalis explicitly set to0, the calculation becomesprogress.current / 0, resulting inInfinity%orNaN%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
useActionDataanduseNavigationfor 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"andmin/maxattributesThe 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
useJobRunwith React Router's action data to display real-time progress for image processing jobs. The pattern is consistent withDataSyncProgress, 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
actionfunction returns{ intent, runId }after triggering a job, but the progress components (ImageProcessingProgress,DataSyncProgress) don't receive thisrunIdas a prop.Please verify:
- Are these components designed to automatically track the most recent run via SSE subscriptions?
- Or should they receive the
runIdfrom the action result to display the specific job that was just triggered?If they should receive the
runId, you'll need to useuseActionData()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> ) }
…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>
There was a problem hiding this comment.
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
totalexists- 100% when no
totalbutcurrent > 0(indeterminate progress)- 0% when both are 0
This eliminates the previous
NaNissue.
🧹 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:
Line 310: Using
s.nameas the React key assumes step names are unique. Usings.indexwould be more reliable:- key={s.name} + key={s.index}Lines 316-318: The binary styling (completed = green, everything else = red) may misrepresent steps with
pendingorrunningstatus 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.nameas 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 handlependingstatus 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
📒 Files selected for processing (3)
examples/browser-react-router-spa/app/routes/_index/dashboard.tsxexamples/browser-vite-react/src/components/dashboard.tsxexamples/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.tsxexamples/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.tsxexamples/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.tsxexamples/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
Runinterface, 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
totalexists: standard percentage calculation- When no
totalbutcurrent > 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
useDurablyanduseRunshooks aligns correctly with the new@coji/durably-reactbrowser 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
cancelledstatus. 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
pageandhasMorevalues from theuseRunshook. The 1-indexed display (Page {page + 1}) is user-friendly.
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>
Summary
Add
@coji/durably-reactpackage 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 toregister(), old single-job signature removedNew Features
@coji/durably (Core)
run:trigger: Emitted when a job is triggered (before worker picks it up)run:cancel: Emitted when a run is cancelledrun:retry: Emitted when a failed/cancelled run is retrieddurably.jobsproperty: Access registered jobs with full type inferenceretry()now works on bothfailedandcancelledrunsstepCountadded toRuntype: Run now includesstepCountproperty reflecting the number of completed stepsgetRun(),getRuns(),getNextPendingRun()@coji/durably/server
GET /steps?runId=xxx: Get steps for a runDELETE /run?runId=xxx: Delete a run/runs/subscribenow streamsrun:trigger,run:cancel,run:retryevents@coji/durably-react
Browser-complete mode - Run Durably entirely in the browser with SQLite WASM:
Server-connected mode - Connect to a Durably server via HTTP/SSE:
Hooks provided:
useJob(jobDef, options?)- Trigger jobs and track status with real-time updatesuseJobRun({ runId })- Subscribe to a specific run's statususeJobLogs({ runId, maxLogs? })- Stream logs from a runuseRuns(options?)- List runs with filtering, pagination, and real-time updatesuseRunActions({ api })- Actions: retry, cancel, deleteRun, getRun, getStepsuseDurably()- Direct access to Durably instanceType-safe client factories:
createDurablyClient<typeof jobs>({ api })- Type-safe client for all registered jobscreateJobHooks<typeof job>({ api, jobName })- Per-job hook factoryExamples
examples/browser-vite-react- Browser-complete mode with Viteexamples/browser-react-router-spa- React Router 7 SPA with clientLoaderexamples/fullstack-react-router- Full-stack with server-connected modeAll examples include unified dashboard UI with:
Test Coverage
Documentation
llms.mdfiles for LLM/AI agent consumptionspec.md,spec-react.md)🤖 Generated with Claude Code
Summary by CodeRabbit
Release Notes
New Features
Breaking Changes
Documentation
✏️ Tip: You can customize this high-level summary in your review settings.