Releases: coji/durably
Releases · coji/durably
v0.14.0
Highlights
- New exports:
DurablyError,NotFoundError,ValidationError,ConflictErrorerror 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
onErrorinstead 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-renewedevent, error classes to API reference - PostgreSQL example and database comparison guide (#114-#120)
See CHANGELOG.md for full details.
v0.13.0
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) preserveStepsreplacescleanupSteps: 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_labelsreplaces 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, })
retainRunsoption: 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:
activeLeaseGuardsubquery inclaimNextprevents 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)
purgeRunsandretainRunsin 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
Breaking Changes
@coji/durably
- Rename
retry()toretrigger(): Creates a fresh run (new ID) with the same input instead of resetting the original run. ReturnsPromise<Run>instead ofPromise<void>- await durably.retry(runId) + const newRun = await durably.retrigger(runId)
- Remove
run:retryevent andRunRetryEventtype: Sinceretrigger()creates a fresh run (not a mutation of the original), it emitsrun:triggerwith the new run's ID. Consumers should listen forrun:triggerand useevent.runIdto track the retriggered execution cleanupStepsenabled by default: Step output data is automatically deleted fromdurably_stepswhen runs reach terminal state. SetcleanupSteps: falseto preserve step data- HTTP endpoint rename:
POST /retry→POST /retrigger, returns{ success: true, runId: string }
@coji/durably-react
useRunActions().retrigger()returns new run ID:retrigger(runId)now returnsPromise<string>instead ofPromise<void>
Added
@coji/durably
cleanupStepsoption for controlling automatic step data cleanupjobRegistrycheck inretrigger()— throws"Unknown job"immediately if the job is no longer registered
Internal
- Extract
getRunOrThrow()helper indurably.ts(#92) - Extract
executeAction()helper inuseRunActions.ts(#92)
Full Changelog: v0.11.0...v0.12.0
v0.11.0
Breaking Changes
@coji/durably
- Type-safe labels via
TLabelsgeneric:createDurably()now accepts alabelsZod schema.TLabelsis inferred and flows throughtrigger(),getRuns(),Run, and all auth hooks (#75) - Auth middleware for
createDurablyHandler: Newauthoption withauthenticate,onTrigger,onRunAccess,scopeRuns, andscopeRunsSubscribehooks (#76)
@coji/durably-react
- Redesign:
createDurablyproxy 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/onLogcallbacks fortriggerAndWait()(#71)jobNamearray filter ingetRuns():getRuns({ jobName: ['a', 'b'] })(#73)- Multiple
jobNamefilter in HTTP handler:GET /runs?jobName=a&jobName=b(#74)
@coji/durably-react
jobNamearray filter inuseRuns()for both fullstack and SPA modes (#74)DurablyClienttype: Per-job hooks + cross-job utilities (#82)
Fixed
@coji/durably-react
- Stabilize
jobNamein browseruseRunsto 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
Added
@coji/durably
- SSE throttling for
step.progress(): AddsseThrottleMsoption tocreateDurablyHandler()(default: 100ms) that throttlesrun:progressSSE events per run. First and last progress events are always delivered immediately. Non-progress events are never throttled. Set to0to disable (#63) - Export
CreateDurablyHandlerOptionstype for typed handler configuration
v0.9.0
Breaking Changes
@coji/durably
- Rename
payloadtoinput: Job run function parameter andRun.payloadfield renamed toinputrun: async (step, payload) => {}→run: async (step, input) => {}Run.payload→Run.input
- Add
startedAt/completedAtto Run type - Unify
ClientRuntype: Server responses now strip internal fields consistently
Added
@coji/durably
- Kubernetes-style labels for run filtering: Add labels via
trigger(), filter withgetRuns({ labels }) - AbortSignal in
step.run()for cooperative cancellation step:cancelevent: Cancelled steps emitstep:cancelinstead ofstep:failrun:deleteevent for React hooks auto-refresh- Label key validation to prevent injection issues
@coji/durably-react
realtimeoption for client-modeuseRuns: Control SSE subscription (default:true)run:deleteandrun:triggerinDurablyEventtypestep:cancelevent handling in both browser and client mode
Fixed
@coji/durably
- Worker run claim TOCTOU race condition
- Preserve
started_aton stale run re-claim - Deterministic run ordering with monotonic ULID
- Include
jobNameandlabelsinlog:writeevents - Export
RunTriggerEventandRunRetryEvent
@coji/durably-react
- Initialize
isLoadingastrueinuseRuns - Add
step:failrefresh in browser-modeuseRuns - Add
labelsto step events in client-mode - Add
stepName/labelstolog:writeDurablyEvent 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()totalparameter (optional, not required) - Remove non-existent
isReadyfrom hook docs - Add
step:cancelto all examples and guides
Full Changelog: v0.8.1...v0.9.0
v0.8.1
What's Changed
@coji/durably-react
isCancelledstate boolean for client-modeuseJob: Now consistent with browser-mode API- Previously missing from client-mode hook, causing inconsistency between modes
Tooling
- Added
release-checkskill for pre-release integrity verification
Full Changelog: v0.8.0...v0.8.1
v0.8.0
Added
@coji/durably-react
autoResumeandfollowLatestoptions for client-modeuseJob: Match browser-mode APIautoResume(default:true): Auto-resume running/pending jobs on mountfollowLatest(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
What's New
@coji/durably
- Generic type parameter for
getRun<T>()andgetRuns<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
useRunshook: Multiple ways to get type-safe run accessuseRuns<TRun>(options)- Pass type parameter for dashboards with multiple job typesuseRuns(jobDefinition, options?)- Pass JobDefinition to infer types and auto-filter by jobNameuseRuns(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
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()
- Added ULID-based secondary sort key to
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
- Extracted SSE utilities to
@coji/durably-react
- Internal code organization improvements (no API changes)
- Extracted shared subscription logic to
shared/directory - Created
useSubscriptionhook for unified state management - Extracted
useAutoResumeanduseJobSubscriptionhooks - Unified event subscriber patterns between browser and client modes
- Extracted shared subscription logic to
Full Changelog: v0.6.0...v0.6.1