diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c577ea..f27a0b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/CLAUDE.md b/CLAUDE.md index bc83060..8112a84 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 @@ -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) diff --git a/packages/durably-react/docs/llms.md b/packages/durably-react/docs/llms.md index 4bf02b9..a81cdac 100644 --- a/packages/durably-react/docs/llms.md +++ b/packages/durably-react/docs/llms.md @@ -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
Status: {status}
diff --git a/packages/durably-react/package.json b/packages/durably-react/package.json index 46b5260..bda4c62 100644 --- a/packages/durably-react/package.json +++ b/packages/durably-react/package.json @@ -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", diff --git a/packages/durably/docs/llms.md b/packages/durably/docs/llms.md index eea118f..762b2d0 100644 --- a/packages/durably/docs/llms.md +++ b/packages/durably/docs/llms.md @@ -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 diff --git a/packages/durably/package.json b/packages/durably/package.json index 7ba88eb..76e4597 100644 --- a/packages/durably/package.json +++ b/packages/durably/package.json @@ -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", diff --git a/website/api/durably-react/fullstack.md b/website/api/durably-react/fullstack.md index 9e08872..10de7a2 100644 --- a/website/api/durably-react/fullstack.md +++ b/website/api/durably-react/fullstack.md @@ -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
Status: {status}
@@ -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 | --- diff --git a/website/guide/concepts.md b/website/guide/concepts.md index 39412b6..73e7c47 100644 --- a/website/guide/concepts.md +++ b/website/guide/concepts.md @@ -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) }) ``` diff --git a/website/guide/databases.md b/website/guide/databases.md index ef213aa..a90ce1a 100644 --- a/website/guide/databases.md +++ b/website/guide/databases.md @@ -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. diff --git a/website/public/llms.txt b/website/public/llms.txt index 1ac0a18..cc82715 100644 --- a/website/public/llms.txt +++ b/website/public/llms.txt @@ -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 @@ -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
Status: {status}