Skip to content

Releases: coji/durably

v0.14.0

16 Mar 13:54
26c9e7f

Choose a tag to compare

Highlights

  • New exports: DurablyError, NotFoundError, ValidationError, ConflictError error classes for programmatic error handling
  • Idle maintenance: processOne() is now pure claim-execute-return; lease normalization and auto-purge run only during idle cycles
  • Async listener safety: Rejected promises from async event listeners are forwarded to onError instead of being silently dropped
  • PostgreSQL perf: Skip write mutex, denormalize step_count

Fixed

  • Prevent completed step from being written to cancelled run (#128)
  • Return proper HTTP status codes and stop coercing input (#129)
  • Separate maintenance from processOne to fix idle contract (#134)
  • Catch rejected promises from async event listeners (#137)

Performance

  • Skip write mutex for PostgreSQL backend (#130)
  • Denormalize step_count and remove labels JSON fallback (#132)

Documentation

  • Document synchronous event listener behavior (#135)
  • Add run:lease-renewed event, error classes to API reference
  • PostgreSQL example and database comparison guide (#114-#120)

See CHANGELOG.md for full details.

v0.13.0

16 Mar 08:37
900da33

Choose a tag to compare

Breaking Changes

@coji/durably

  • Lease-based runtime model: Complete rewrite of the execution engine. Runs are now claimed via leases with fencing tokens (lease_generation), enabling safe recovery from worker crashes. Expired leases are automatically reclaimed (#101)
  • preserveSteps replaces cleanupSteps: Option renamed and default flipped for clarity. preserveSteps: false (default) deletes step data on terminal state — same behavior as before (#101)
  • Labels normalized into indexed table: durably_run_labels replaces JSON-in-column storage for efficient label filtering. Migration is automatic (#103)
  • retrigger() validates input against current schema: Throws if the original run's input doesn't match the current job's input schema (#104)

Added

@coji/durably

  • purgeRuns() API: Batch-delete terminal runs older than a cutoff date. Cascade-deletes associated steps, logs, and labels (#109)
    const deleted = await durably.purgeRuns({
      olderThan: new Date(Date.now() - 30 * 86400000),
      limit: 500,
    })
  • retainRuns option: Auto-delete terminal runs during idle worker polling. Supports '30d', '24h', '60m' duration formats (#109)
  • PostgreSQL support: Kysely PostgreSQL dialect now supported alongside SQLite/libSQL (#101)
  • Concurrency key enforcement via SQL: activeLeaseGuard subquery in claimNext prevents duplicate leases for the same concurrency key without JS-side filtering (#101)

Fixed

@coji/durably

  • SSE subscribe race condition: Subscribe to events before reading DB state to prevent missed updates (#108)
  • SSE reconnection: Only dispatch error on permanent CLOSED state, not transient disconnects (#108)

Performance

  • Remove getRuns({ status: 'leased' }) from polling hot path: claimNext's SQL subquery already handles concurrency exclusion, eliminating a full table scan + JOIN + JSON parse per poll cycle (#109)

Documentation

  • Deployment guide with mode comparison (#104)
  • purgeRuns and retainRuns in API reference, llms.md, and CLAUDE.md (#109)
  • Runtime rearchitecture RFC (#97, #99)

Dependencies

  • vite 7→8, @vitejs/plugin-react 5→6, jsdom 28→29, vitest 4.0→4.1
  • Replace vite-tsconfig-paths plugin with native resolve.tsconfigPaths

Full Changelog: v0.12.0...v0.13.0

v0.12.0

07 Mar 06:27
56943e7

Choose a tag to compare

Breaking Changes

@coji/durably

  • Rename retry() to retrigger(): Creates a fresh run (new ID) with the same input instead of resetting the original run. Returns Promise<Run> instead of Promise<void>
    - await durably.retry(runId)
    + const newRun = await durably.retrigger(runId)
  • Remove run:retry event and RunRetryEvent type: Since retrigger() creates a fresh run (not a mutation of the original), it emits run:trigger with the new run's ID. Consumers should listen for run:trigger and use event.runId to track the retriggered execution
  • cleanupSteps enabled by default: Step output data is automatically deleted from durably_steps when runs reach terminal state. Set cleanupSteps: false to preserve step data
  • HTTP endpoint rename: POST /retryPOST /retrigger, returns { success: true, runId: string }

@coji/durably-react

  • useRunActions().retrigger() returns new run ID: retrigger(runId) now returns Promise<string> instead of Promise<void>

Added

@coji/durably

  • cleanupSteps option for controlling automatic step data cleanup
  • jobRegistry check in retrigger() — throws "Unknown job" immediately if the job is no longer registered

Internal

  • Extract getRunOrThrow() helper in durably.ts (#92)
  • Extract executeAction() helper in useRunActions.ts (#92)

Full Changelog: v0.11.0...v0.12.0

v0.11.0

06 Mar 16:06
04902e5

Choose a tag to compare

Breaking Changes

@coji/durably

  • Type-safe labels via TLabels generic: createDurably() now accepts a labels Zod schema. TLabels is inferred and flows through trigger(), getRuns(), Run, and all auth hooks (#75)
  • Auth middleware for createDurablyHandler: New auth option with authenticate, onTrigger, onRunAccess, scopeRuns, and scopeRunsSubscribe hooks (#76)

@coji/durably-react

  • Redesign: createDurably proxy pattern: New recommended API for fullstack mode. Import from @coji/durably-react (root) instead of @coji/durably-react/client (#82)
    import { createDurably } from '@coji/durably-react'
    import type { durably } from './durably.server'
    const durablyClient = createDurably<typeof durably>({ api: '/api/durably' })
    durablyClient.importCsv.useRun(runId)
    durablyClient.useRuns({ pageSize: 10 })

Added

@coji/durably

  • onProgress/onLog callbacks for triggerAndWait() (#71)
  • jobName array filter in getRuns(): getRuns({ jobName: ['a', 'b'] }) (#73)
  • Multiple jobName filter in HTTP handler: GET /runs?jobName=a&jobName=b (#74)

@coji/durably-react

  • jobName array filter in useRuns() for both fullstack and SPA modes (#74)
  • DurablyClient type: Per-job hooks + cross-job utilities (#82)

Fixed

@coji/durably-react

  • Stabilize jobName in browser useRuns to prevent unnecessary re-renders (#74)

Documentation

  • Overhaul guide documentation for beginners (#83)
  • New guides: Quick Start, Server/Fullstack/SPA Mode, Error Handling, Auth, Multi-Tenant, Deployment

Full Changelog: v0.10.0...v0.11.0

v0.10.0

05 Mar 12:54
adfdf91

Choose a tag to compare

Added

@coji/durably

  • SSE throttling for step.progress(): Add sseThrottleMs option to createDurablyHandler() (default: 100ms) that throttles run:progress SSE events per run. First and last progress events are always delivered immediately. Non-progress events are never throttled. Set to 0 to disable (#63)
  • Export CreateDurablyHandlerOptions type for typed handler configuration

v0.9.0

05 Mar 05:29
998f240

Choose a tag to compare

Breaking Changes

@coji/durably

  • Rename payload to input: Job run function parameter and Run.payload field renamed to input
    • run: async (step, payload) => {}run: async (step, input) => {}
    • Run.payloadRun.input
  • Add startedAt/completedAt to Run type
  • Unify ClientRun type: Server responses now strip internal fields consistently

Added

@coji/durably

  • Kubernetes-style labels for run filtering: Add labels via trigger(), filter with getRuns({ labels })
  • AbortSignal in step.run() for cooperative cancellation
  • step:cancel event: Cancelled steps emit step:cancel instead of step:fail
  • run:delete event for React hooks auto-refresh
  • Label key validation to prevent injection issues

@coji/durably-react

  • realtime option for client-mode useRuns: Control SSE subscription (default: true)
  • run:delete and run:trigger in DurablyEvent type
  • step:cancel event handling in both browser and client mode

Fixed

@coji/durably

  • Worker run claim TOCTOU race condition
  • Preserve started_at on stale run re-claim
  • Deterministic run ordering with monotonic ULID
  • Include jobName and labels in log:write events
  • Export RunTriggerEvent and RunRetryEvent

@coji/durably-react

  • Initialize isLoading as true in useRuns
  • Add step:fail refresh in browser-mode useRuns
  • Add labels to step events in client-mode
  • Add stepName/labels to log:write DurablyEvent type

Changed

@coji/durably

  • Consolidate migrations v1-v3 into single v1
  • Optimize step boundary cancellation with in-memory signal

Documentation

  • SSR incompatibility warning for createDurablyClient
  • Fix step.progress() total parameter (optional, not required)
  • Remove non-existent isReady from hook docs
  • Add step:cancel to all examples and guides

Full Changelog: v0.8.1...v0.9.0

v0.8.1

18 Jan 03:39
b466318

Choose a tag to compare

What's Changed

@coji/durably-react

  • isCancelled state boolean for client-mode useJob: Now consistent with browser-mode API
    • Previously missing from client-mode hook, causing inconsistency between modes

Tooling

  • Added release-check skill for pre-release integrity verification

Full Changelog: v0.8.0...v0.8.1

v0.8.0

11 Jan 09:40
f4dd783

Choose a tag to compare

Added

@coji/durably-react

  • autoResume and followLatest options for client-mode useJob: Match browser-mode API
    • autoResume (default: true): Auto-resume running/pending jobs on mount
    • followLatest (default: true): Switch to tracking new runs via SSE
    const { trigger, status } = useJob({
      api: '/api/durably',
      jobName: 'my-job',
      autoResume: true,   // Auto-resume existing jobs
      followLatest: true, // Track new runs from other tabs
    })

Full Changelog: v0.7.0...v0.8.0

v0.7.0

04 Jan 02:38
09767b9

Choose a tag to compare

What's New

@coji/durably

  • Generic type parameter for getRun<T>() and getRuns<T>(): Type-safe run retrieval
    // Untyped (returns Run)
    const run = await durably.getRun(runId)
    
    // Typed (returns custom type)
    type MyRun = Run & { payload: { userId: string }; output: { count: number } | null }
    const typedRun = await durably.getRun<MyRun>(runId)

@coji/durably-react

  • Generic type support for useRuns hook: Multiple ways to get type-safe run access
    • useRuns<TRun>(options) - Pass type parameter for dashboards with multiple job types
    • useRuns(jobDefinition, options?) - Pass JobDefinition to infer types and auto-filter by jobName
    • useRuns(options?) - Untyped usage for simple cases
    • New TypedRun<TInput, TOutput> type for browser hooks
    • New TypedClientRun<TInput, TOutput> type for client hooks

Example Usage

import { useRuns, TypedRun } from '@coji/durably-react'
import type { JobInput, JobOutput } from '@coji/durably'

// Define union type for all jobs in dashboard
type DashboardRun =
  | TypedRun<JobInput<typeof dataSyncJob>, JobOutput<typeof dataSyncJob>>
  | TypedRun<JobInput<typeof importCsvJob>, JobOutput<typeof importCsvJob>>

function Dashboard() {
  const { runs } = useRuns<DashboardRun>({ pageSize: 10 })
  // runs are typed as DashboardRun[]

  const showDetails = async (runId: string) => {
    const run = await durably.getRun<DashboardRun>(runId)
    // run is typed as DashboardRun | null
  }
}

Full Changelog: v0.6.1...v0.7.0

v0.6.1

03 Jan 12:14
10af60a

Choose a tag to compare

What's Changed

Fixed

@coji/durably

  • Run ordering now deterministic when multiple runs are created within the same millisecond
    • Added ULID-based secondary sort key to getNextPendingRun()

Changed

@coji/durably

  • Internal code organization improvements (no API changes)
    • Extracted SSE utilities to sse.ts
    • Extracted HTTP response helpers to http.ts
    • Centralized error handling in errors.ts

@coji/durably-react

  • Internal code organization improvements (no API changes)
    • Extracted shared subscription logic to shared/ directory
    • Created useSubscription hook for unified state management
    • Extracted useAutoResume and useJobSubscription hooks
    • Unified event subscriber patterns between browser and client modes

Full Changelog: v0.6.0...v0.6.1