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
34 changes: 34 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,40 @@ and this project adheres to [Semantic Versioning](https://semver.org/).

## [Unreleased]

## [0.15.0] - 2026-03-29

### Breaking Changes

#### @coji/durably

- **`trigger()` return type changed to `TriggerResult`**: Returns `{ run, disposition }` where `disposition` is `'created' | 'idempotent' | 'coalesced'`. Add `coalesce: 'skip'` option to reuse an existing pending run instead of throwing. `concurrencyKey` now enforces max 1 pending run per key (#148)

#### @coji/durably-react

- **`useRunActions()` no longer returns `isLoading` / `error`**: Use React 19 `useTransition` for per-button pending UI and `try/catch` for error handling. Peer dependency raised to React 19+ (#179)

### Added

#### @coji/durably

- **`waitForRun(runId, options?)`**: Wait for an existing run to reach a terminal state, with `timeout`, `onProgress`, and `onLog` callbacks. Uses event-first resolution with storage polling fallback for cross-process observation (#151, #169)
- **`maxConcurrentRuns` option**: Enable parallel run processing in the worker. Defaults to `1` (sequential) (#173)
- **Event classification**: Events classified as Domain or Operational with `isDomainEvent()` type guard (#169)
- **Automatic WAL checkpoint**: Periodic `PRAGMA wal_checkpoint(TRUNCATE)` during idle maintenance for local SQLite backends, preventing unbounded WAL file growth. Probed at `migrate()` time; skipped for Turso, PostgreSQL, and browser environments (#181)

#### @coji/durably-react

- **`isTerminal` / `isActive` on run objects and hooks**: Derived status booleans replace manual status enumeration (#179)
- **`useRuns` status array filter**: `status` option accepts `RunStatus | RunStatus[]` to filter by multiple statuses (#154)
- **`createJobHooks().useRun()` lifecycle callbacks**: `onStart`, `onComplete`, and `onFail` options for per-run lifecycle handling (#155)
- **`createJobHooks()` options forwarding**: All hook options forwarded transparently via `Omit` (#179)

### Fixed

#### @coji/durably

- **`TypedRun` / `TypedClientRun` output type**: Default `output` type no longer includes `undefined`, removing unnecessary undefined checks (#145)

## [0.14.0] - 2026-03-16

### Added
Expand Down
6 changes: 4 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,9 @@ Regenerate `llms.txt` after editing any `llms.md`. Regenerate the OG image whene

- **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 (cleaned up on terminal state by default, see `preserveSteps`)
- **Run**: A job execution instance, created via `trigger()`, always persisted as `pending` before execution
- **Worker**: Polls for pending runs and executes them sequentially
- **Run**: A job execution instance, created via `trigger()` (returns `TriggerResult` with `disposition`: `'created' | 'idempotent' | 'coalesced'`), always persisted as `pending` before execution. Use `coalesce: 'skip'` to reuse an existing pending run with the same `concurrencyKey`
- **Worker**: Polls for pending runs and executes them (sequentially by default, or concurrently via `maxConcurrentRuns`)
- **waitForRun**: `durably.waitForRun(runId, options?)` waits for a run to reach terminal state, with `timeout`, `onProgress`, `onLog` callbacks. Uses events with storage polling fallback

## Key Design Decisions

Expand All @@ -61,6 +62,7 @@ Five tables: `durably_runs`, `durably_run_labels`, `durably_steps`, `durably_log
- `leaseRenewIntervalMs`: 5000ms
- `leaseMs`: 30000ms (lease duration; expired leases are reclaimed)
- `preserveSteps`: false (deletes step output data when runs reach terminal state)
- `maxConcurrentRuns`: 1 (concurrent runs per worker; increase for I/O-bound jobs)
- `retainRuns`: undefined (no automatic cleanup; set e.g. `'30d'` to auto-delete terminal runs)

## Browser Constraints (by design)
Expand Down
3 changes: 3 additions & 0 deletions packages/durably-react/docs/llms.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,9 @@ function Component({ runId }: { runId: string }) {
}>({
api: '/api/durably',
runId,
onStart: () => console.log('Run started'),
onComplete: () => console.log('Run completed'),
onFail: () => console.log('Run failed'),
})

return <div>Status: {status}</div>
Expand Down
2 changes: 1 addition & 1 deletion packages/durably-react/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@coji/durably-react",
"version": "0.14.0",
"version": "0.15.0",
"description": "React bindings for Durably - step-oriented resumable batch execution",
"type": "module",
"main": "./dist/index.js",
Expand Down
4 changes: 4 additions & 0 deletions packages/durably/docs/llms.md
Original file line number Diff line number Diff line change
Expand Up @@ -562,6 +562,10 @@ import { withLogPersistence } from '@coji/durably'
durably.use(withLogPersistence())
```

## SQLite WAL Maintenance

For local SQLite backends using WAL mode, Durably automatically runs periodic WAL checkpoints (`PRAGMA wal_checkpoint(TRUNCATE)`) during idle maintenance to prevent unbounded WAL file growth. This is probed at `migrate()` time and only enabled when the backend supports it — automatically skipped for Turso (remote libSQL), PostgreSQL, and browser (OPFS) backends.

## Browser Usage

```ts
Expand Down
2 changes: 1 addition & 1 deletion packages/durably/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@coji/durably",
"version": "0.14.0",
"version": "0.15.0",
"description": "Step-oriented resumable batch execution for Node.js and browsers using SQLite",
"type": "module",
"main": "./dist/index.js",
Expand Down
14 changes: 10 additions & 4 deletions website/api/durably-react/fullstack.md
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,9 @@ function Component({ runId }: { runId: string }) {
}>({
api: '/api/durably',
runId,
onStart: () => console.log('Run started'),
onComplete: () => console.log('Run completed'),
onFail: () => console.log('Run failed'),
})

return <div>Status: {status}</div>
Expand All @@ -257,10 +260,13 @@ function Component({ runId }: { runId: string }) {

### Options

| Option | Type | Description |
| ------- | -------- | -------------------------- |
| `api` | `string` | API base path |
| `runId` | `string` | The run ID to subscribe to |
| Option | Type | Description |
| ------------ | ------------ | ------------------------------------------------- |
| `api` | `string` | API base path |
| `runId` | `string` | The run ID to subscribe to |
| `onStart` | `() => void` | Called when the run transitions to pending/leased |
| `onComplete` | `() => void` | Called when the run completes |
| `onFail` | `() => void` | Called when the run fails |

---

Expand Down
1 change: 1 addition & 0 deletions website/guide/concepts.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ createDurably({
dialect,
leaseRenewIntervalMs: 5000, // Renew lease every 5s (default)
leaseMs: 30000, // Lease duration — stale after 30s without renewal (default)
maxConcurrentRuns: 1, // Concurrent runs per worker (default: 1, increase for I/O-bound jobs)
})
```

Expand Down
6 changes: 6 additions & 0 deletions website/guide/databases.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,12 @@ const dialect = new SqliteDialect({
- Good for one-off scripts and CLI tools
- No remote support (use libSQL if you might need Turso later)

## SQLite WAL Maintenance

For local SQLite backends using WAL (Write-Ahead Logging) mode, Durably automatically runs `PRAGMA wal_checkpoint(TRUNCATE)` during idle maintenance to prevent unbounded WAL file growth. This is throttled to at most once per 60 seconds and only runs when the worker is idle.

At `migrate()` time, Durably probes whether WAL checkpointing is supported. Checkpointing is enabled only for local file-backed SQLite — it is automatically skipped for Turso (remote libSQL), PostgreSQL, and browser (OPFS) backends.

## PostgreSQL

**For multi-worker production deployments.** The recommended backend for running multiple workers concurrently, with advisory locks and `FOR UPDATE SKIP LOCKED` for strong concurrency guarantees.
Expand Down
7 changes: 7 additions & 0 deletions website/public/llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -562,6 +562,10 @@ import { withLogPersistence } from '@coji/durably'
durably.use(withLogPersistence())
```

## SQLite WAL Maintenance

For local SQLite backends using WAL mode, Durably automatically runs periodic WAL checkpoints (`PRAGMA wal_checkpoint(TRUNCATE)`) during idle maintenance to prevent unbounded WAL file growth. This is probed at `migrate()` time and only enabled when the backend supports it — automatically skipped for Turso (remote libSQL), PostgreSQL, and browser (OPFS) backends.

## Browser Usage

```ts
Expand Down Expand Up @@ -932,6 +936,9 @@ function Component({ runId }: { runId: string }) {
}>({
api: '/api/durably',
runId,
onStart: () => console.log('Run started'),
onComplete: () => console.log('Run completed'),
onFail: () => console.log('Run failed'),
})

return <div>Status: {status}</div>
Expand Down
Loading