Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,15 @@ When API changes are made, update `packages/durably/docs/llms.md` to keep it in
## Core Concepts

- **Job**: Defined via `defineJob()` and registered via `jobs` option (or `.register()`), receives a step context and payload
- **Step**: Created via `step.run()`, each step's success state and return value is persisted
- **Step**: Created via `step.run()`, each step's success state and return value is persisted (cleaned up on terminal state by default, see `cleanupSteps`)
- **Run**: A job execution instance, created via `trigger()`, always persisted as `pending` before execution
- **Worker**: Polls for pending runs and executes them sequentially

## Key Design Decisions

- **ESM-only**: This library is ESM-only. CommonJS is not supported. Always use top-level `await` for async initialization (e.g., `await durably.migrate()`). Do not wrap in async IIFE or Promise chains.
- Single-threaded execution, no parallel run processing in minimal config
- No automatic retry - failures are immediate and explicit (`retry()` API for manual retry)
- No automatic retry - failures are immediate and explicit (`retrigger()` creates a fresh run with a new ID and returns it)
- Dialect injection pattern - Kysely dialect passed to `createDurably()` to abstract SQLite implementations
- Event system for extensibility (`run:start`, `run:complete`, `run:fail`, `step:*`, `log:write`)

Expand All @@ -44,6 +44,7 @@ Four tables: `durably_runs`, `durably_steps`, `durably_logs`, `durably_schema_ve
- `pollingInterval`: 1000ms
- `heartbeatInterval`: 5000ms
- `staleThreshold`: 30000ms (for detecting abandoned runs)
- `cleanupSteps`: true (deletes step output data when runs reach terminal state)
Comment thread
coderabbitai[bot] marked this conversation as resolved.

## Browser Constraints (by design)

Expand Down
10 changes: 5 additions & 5 deletions examples/fullstack-react-router/app/routes/_index/dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export function Dashboard() {

const {
cancel,
retry,
retrigger,
deleteRun,
getRun,
getSteps,
Expand All @@ -65,8 +65,8 @@ export function Dashboard() {
refresh()
}

const handleRetry = async (runId: string) => {
await retry(runId)
const handleRetrigger = async (runId: string) => {
await retrigger(runId)
refresh()
}

Expand Down Expand Up @@ -211,11 +211,11 @@ export function Dashboard() {
run.status === 'cancelled') && (
<button
type="button"
onClick={() => handleRetry(run.id)}
onClick={() => handleRetrigger(run.id)}
disabled={isActioning}
className="text-xs text-green-600 hover:text-green-800 disabled:cursor-not-allowed disabled:text-gray-400"
>
Retry
Retrigger
</button>
)}
{(run.status === 'running' ||
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* GET /api/durably/runs - List runs
* GET /api/durably/run?runId=xxx - Get single run
* POST /api/durably/trigger - Trigger a job
* POST /api/durably/retry?runId=xxx - Retry a failed run
* POST /api/durably/retrigger?runId=xxx - Create a fresh run from a terminal run
* POST /api/durably/cancel?runId=xxx - Cancel a run
*/

Expand Down
4 changes: 2 additions & 2 deletions examples/spa-react-router/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ This example demonstrates Durably running entirely in the browser using React Ro
- **SQLite WASM with OPFS** - Persistent storage in the browser
- **DurablyProvider** - React context for lifecycle management
- **Multiple jobs** - Image processing and data sync examples
- **Run history dashboard** - View, retry, cancel, and delete runs
- **Run history dashboard** - View, retrigger, cancel, and delete runs
- **Tailwind CSS** - Modern styling

## Getting Started
Expand Down Expand Up @@ -68,4 +68,4 @@ app/
1. Run a job and observe the progress
2. Reload the page during execution - it resumes automatically
3. Check the dashboard for run history
4. Try retry/cancel/delete actions
4. Try retrigger/cancel/delete actions
8 changes: 4 additions & 4 deletions examples/spa-react-router/app/routes/_index/dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,9 @@ export function Dashboard() {
}
}

const handleRetry = async (runId: string) => {
const handleRetrigger = async (runId: string) => {
if (!durably) return
await durably.retry(runId)
await durably.retrigger(runId)
refresh()
}

Expand Down Expand Up @@ -208,10 +208,10 @@ export function Dashboard() {
run.status === 'cancelled') && (
<button
type="button"
onClick={() => handleRetry(run.id)}
onClick={() => handleRetrigger(run.id)}
className="text-xs text-green-600 hover:text-green-800 disabled:cursor-not-allowed disabled:text-gray-400"
>
Retry
Retrigger
</button>
)}
{(run.status === 'running' ||
Expand Down
8 changes: 4 additions & 4 deletions examples/spa-vite-react/src/components/dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,9 @@ export function Dashboard() {
}
}

const handleRetry = async (runId: string) => {
const handleRetrigger = async (runId: string) => {
if (!durably) return
await durably.retry(runId)
await durably.retrigger(runId)
refresh()
}

Expand Down Expand Up @@ -205,10 +205,10 @@ export function Dashboard() {
run.status === 'cancelled') && (
<button
type="button"
onClick={() => handleRetry(run.id)}
onClick={() => handleRetrigger(run.id)}
className="text-xs text-green-600 hover:text-green-800 disabled:cursor-not-allowed disabled:text-gray-400"
>
Retry
Retrigger
</button>
)}
{(run.status === 'running' ||
Expand Down
10 changes: 5 additions & 5 deletions packages/durably-react/docs/llms.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ function LogViewer({ runId }: { runId: string }) {
// Cross-job hooks (built into the proxy)
function Dashboard() {
const { runs } = durably.useRuns({ pageSize: 10 })
const { retry, cancel } = durably.useRunActions()
const { retrigger, cancel } = durably.useRunActions()
}
```

Expand Down Expand Up @@ -257,22 +257,22 @@ function FilteredDashboard() {

### Fullstack useRunActions

Actions for runs (retry, cancel, delete):
Actions for runs (retrigger, cancel, delete):

```tsx
import { useRunActions } from '@coji/durably-react'

function RunActions({ runId, status }: { runId: string; status: string }) {
const { retry, cancel, deleteRun, getRun, getSteps, isLoading, error } =
const { retrigger, cancel, deleteRun, getRun, getSteps, isLoading, error } =
useRunActions({
api: '/api/durably',
})

return (
<div>
{(status === 'failed' || status === 'cancelled') && (
<button onClick={() => retry(runId)} disabled={isLoading}>
Retry
<button onClick={() => retrigger(runId)} disabled={isLoading}>
Retrigger
</button>
)}
{(status === 'pending' || status === 'running') && (
Expand Down
4 changes: 2 additions & 2 deletions packages/durably-react/src/client/create-durably.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export type DurablyClient<T> = {
) => UseRunsClientResult<TInput, TOutput>

/**
* Run actions: retry, cancel, delete, getRun, getSteps (cross-job).
* Run actions: retrigger, cancel, delete, getRun, getSteps (cross-job).
* The `api` option is pre-configured.
*/
useRunActions: () => UseRunActionsClientResult
Expand Down Expand Up @@ -89,7 +89,7 @@ export type DurablyClient<T> = {
* // Cross-job hooks
* function Dashboard() {
* const { runs, nextPage } = durably.useRuns({ pageSize: 10 })
* const { retry, cancel } = durably.useRunActions()
* const { retrigger, cancel } = durably.useRunActions()
* }
* ```
*/
Expand Down
25 changes: 15 additions & 10 deletions packages/durably-react/src/client/use-run-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ export interface UseRunActionsClientOptions {

export interface UseRunActionsClientResult {
/**
* Retry a failed or cancelled run
* Create a fresh run from a completed, failed, or cancelled run
*/
retry: (runId: string) => Promise<void>
retrigger: (runId: string) => Promise<string>
/**
* Cancel a pending or running run
*/
Expand Down Expand Up @@ -49,20 +49,20 @@ export interface UseRunActionsClientResult {
}

/**
* Hook for run actions (retry, cancel) via server API.
* Hook for run actions via server API.
*
* @example
* ```tsx
* function RunActions({ runId, status }: { runId: string; status: string }) {
* const { retry, cancel, isLoading, error } = useRunActions({
* const { retrigger, cancel, isLoading, error } = useRunActions({
* api: '/api/durably',
* })
*
* return (
* <div>
* {status === 'failed' && (
* <button onClick={() => retry(runId)} disabled={isLoading}>
* Retry
* <button onClick={() => retrigger(runId)} disabled={isLoading}>
* Run Again
* </button>
* )}
* {(status === 'pending' || status === 'running') && (
Expand All @@ -84,17 +84,17 @@ export function useRunActions(
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<string | null>(null)

const retry = useCallback(
const retrigger = useCallback(
async (runId: string) => {
setIsLoading(true)
setError(null)

try {
const url = `${api}/retry?runId=${encodeURIComponent(runId)}`
const url = `${api}/retrigger?runId=${encodeURIComponent(runId)}`
const response = await fetch(url, { method: 'POST' })

if (!response.ok) {
let errorMessage = `Failed to retry: ${response.statusText}`
let errorMessage = `Failed to retrigger: ${response.statusText}`
try {
const data = await response.json()
if (data.error) {
Expand All @@ -105,6 +105,11 @@ export function useRunActions(
}
throw new Error(errorMessage)
}
const data = (await response.json()) as { runId?: string }
if (!data.runId) {
throw new Error('Failed to retrigger: missing runId in response')
}
return data.runId
} catch (err) {
const message = err instanceof Error ? err.message : 'Unknown error'
setError(message)
Expand Down Expand Up @@ -253,7 +258,7 @@ export function useRunActions(
)

return {
retry,
retrigger,
cancel,
deleteRun,
getRun,
Expand Down
4 changes: 1 addition & 3 deletions packages/durably-react/src/client/use-runs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ type RunUpdateEvent =
| 'run:fail'
| 'run:cancel'
| 'run:delete'
| 'run:retry'
runId: string
jobName: string
}
Expand Down Expand Up @@ -319,8 +318,7 @@ export function useRuns<
data.type === 'run:complete' ||
data.type === 'run:fail' ||
data.type === 'run:cancel' ||
data.type === 'run:delete' ||
data.type === 'run:retry'
data.type === 'run:delete'
) {
refresh()
}
Expand Down
28 changes: 8 additions & 20 deletions packages/durably-react/src/hooks/use-job-run.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { useEffect, useRef } from 'react'
import { useDurably } from '../context'
import type { LogEntry, Progress, RunStatus } from '../types'
import { useRunSubscription } from './use-run-subscription'
Expand Down Expand Up @@ -68,30 +67,19 @@ export function useJobRun<TOutput = unknown>(

const subscription = useRunSubscription<TOutput>(durably, runId)

// Fetch initial state when runId changes
const fetchedRef = useRef<Set<string>>(new Set())

useEffect(() => {
if (!durably || !runId || fetchedRef.current.has(runId)) return

// Mark as fetched to avoid duplicate fetches
fetchedRef.current.add(runId)

// Try to fetch current run state
// Note: We need to use internal APIs or polling here
// For now, we rely on event-based updates
}, [durably, runId])
// If we have a runId but no status yet, treat as pending
const effectiveStatus = subscription.status ?? (runId ? 'pending' : null)

return {
status: subscription.status,
status: effectiveStatus,
output: subscription.output,
error: subscription.error,
logs: subscription.logs,
progress: subscription.progress,
isRunning: subscription.status === 'running',
isPending: subscription.status === 'pending',
isCompleted: subscription.status === 'completed',
isFailed: subscription.status === 'failed',
isCancelled: subscription.status === 'cancelled',
isRunning: effectiveStatus === 'running',
isPending: effectiveStatus === 'pending',
isCompleted: effectiveStatus === 'completed',
isFailed: effectiveStatus === 'failed',
isCancelled: effectiveStatus === 'cancelled',
}
}
7 changes: 0 additions & 7 deletions packages/durably-react/src/hooks/use-job-subscription.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,13 +154,6 @@ export function useJobSubscription<TOutput = unknown>(
}),
)

unsubscribes.push(
durably.on('run:retry', (event) => {
if (event.runId !== currentRunIdRef.current) return
dispatch({ type: 'run:retry' })
}),
)

unsubscribes.push(
durably.on('run:progress', (event) => {
if (event.runId !== currentRunIdRef.current) return
Expand Down
1 change: 0 additions & 1 deletion packages/durably-react/src/hooks/use-runs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,6 @@ export function useRuns<
durably.on('run:fail', refresh),
durably.on('run:cancel', refresh),
durably.on('run:delete', refresh),
durably.on('run:retry', refresh),
durably.on('run:progress', refresh),
durably.on('step:start', refresh),
durably.on('step:complete', refresh),
Expand Down
7 changes: 0 additions & 7 deletions packages/durably-react/src/shared/durably-event-subscriber.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,6 @@ export function createDurablyEventSubscriber(
}),
)

unsubscribes.push(
durably.on('run:retry', (event) => {
if (event.runId !== runId) return
onEvent({ type: 'run:retry' })
}),
)

unsubscribes.push(
durably.on('run:progress', (event) => {
if (event.runId !== runId) return
Expand Down
1 change: 0 additions & 1 deletion packages/durably-react/src/shared/event-subscriber.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ export type SubscriptionEvent<TOutput = unknown> =
| { type: 'run:complete'; output: TOutput }
| { type: 'run:fail'; error: string }
| { type: 'run:cancel' }
| { type: 'run:retry' }
| { type: 'run:progress'; progress: Progress }
| {
type: 'log:write'
Expand Down
3 changes: 0 additions & 3 deletions packages/durably-react/src/shared/sse-event-subscriber.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,6 @@ export function createSSEEventSubscriber(apiBaseUrl: string): EventSubscriber {
case 'run:cancel':
onEvent({ type: 'run:cancel' })
break
case 'run:retry':
onEvent({ type: 'run:retry' })
break
case 'run:progress':
onEvent({ type: 'run:progress', progress: data.progress })
break
Expand Down
Loading