@coji/durably 0.14.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{index-CDCdrLgw.d.ts → index-CXH4ozmK.d.ts} +100 -16
- package/dist/index.d.ts +6 -4
- package/dist/index.js +1000 -599
- package/dist/index.js.map +1 -1
- package/dist/plugins/index.d.ts +1 -1
- package/docs/llms.md +99 -12
- package/package.json +5 -5
package/dist/plugins/index.d.ts
CHANGED
package/docs/llms.md
CHANGED
|
@@ -60,7 +60,8 @@ const dialect = new LibsqlDialect({ client })
|
|
|
60
60
|
// Option 1: With jobs (1-step initialization, returns typed instance)
|
|
61
61
|
const durably = createDurably({
|
|
62
62
|
dialect,
|
|
63
|
-
pollingIntervalMs: 1000, //
|
|
63
|
+
pollingIntervalMs: 1000, // Delay before polling again when idle (ms)
|
|
64
|
+
maxConcurrentRuns: 1, // Concurrent runs processed by the worker (default: 1)
|
|
64
65
|
leaseRenewIntervalMs: 5000, // Lease renewal interval (ms)
|
|
65
66
|
leaseMs: 30000, // Lease duration (ms); expired leases are reclaimed
|
|
66
67
|
preserveSteps: false, // Set to true to keep step output data after terminal state (default: false = cleanup)
|
|
@@ -123,6 +124,7 @@ await durably.init()
|
|
|
123
124
|
// Basic trigger (fire and forget)
|
|
124
125
|
const run = await syncUsers.trigger({ orgId: 'org_123' })
|
|
125
126
|
console.log(run.id, run.status) // "pending"
|
|
127
|
+
console.log(run.disposition) // "created"
|
|
126
128
|
|
|
127
129
|
// Wait for completion
|
|
128
130
|
const result = await syncUsers.triggerAndWait(
|
|
@@ -130,15 +132,27 @@ const result = await syncUsers.triggerAndWait(
|
|
|
130
132
|
{ timeout: 5000 },
|
|
131
133
|
)
|
|
132
134
|
console.log(result.output.syncedCount)
|
|
135
|
+
console.log(result.disposition) // "created"
|
|
133
136
|
|
|
134
137
|
// With idempotency key (prevents duplicate jobs)
|
|
135
|
-
await syncUsers.trigger(
|
|
138
|
+
const idempotentRun = await syncUsers.trigger(
|
|
136
139
|
{ orgId: 'org_123' },
|
|
137
140
|
{ idempotencyKey: 'webhook-event-456' },
|
|
138
141
|
)
|
|
142
|
+
console.log(idempotentRun.disposition) // "created" or "idempotent"
|
|
139
143
|
|
|
140
|
-
// With concurrency key (serializes execution)
|
|
144
|
+
// With concurrency key (serializes execution, max 1 pending per key)
|
|
141
145
|
await syncUsers.trigger({ orgId: 'org_123' }, { concurrencyKey: 'org_123' })
|
|
146
|
+
// Second trigger with same key throws ConflictError if a pending run exists
|
|
147
|
+
|
|
148
|
+
// With coalesce (skip duplicate pending runs gracefully)
|
|
149
|
+
const coalesced = await syncUsers.trigger(
|
|
150
|
+
{ orgId: 'org_123' },
|
|
151
|
+
{ concurrencyKey: 'org_123', coalesce: 'skip' },
|
|
152
|
+
)
|
|
153
|
+
if (coalesced.disposition === 'coalesced') {
|
|
154
|
+
console.log('Reused existing pending run:', coalesced.id)
|
|
155
|
+
}
|
|
142
156
|
|
|
143
157
|
// With labels (for filtering)
|
|
144
158
|
await syncUsers.trigger({ orgId: 'org_123' }, { labels: { source: 'browser' } })
|
|
@@ -184,6 +198,31 @@ step.log.error('Failed to connect', { error: err.message })
|
|
|
184
198
|
|
|
185
199
|
## Run Management
|
|
186
200
|
|
|
201
|
+
### Wait for Existing Run
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
// Wait for an existing run to complete (no new run created)
|
|
205
|
+
// Useful when Job A triggers Job B and returns B's run ID
|
|
206
|
+
const completedRun = await durably.waitForRun(runId)
|
|
207
|
+
console.log(completedRun.output) // available when completed
|
|
208
|
+
|
|
209
|
+
// With timeout and callbacks
|
|
210
|
+
const run = await durably.waitForRun(runId, {
|
|
211
|
+
timeout: 10000,
|
|
212
|
+
onProgress: (p) => console.log(`${p.current}/${p.total}`),
|
|
213
|
+
onLog: (l) => console.log(l.message),
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
// Same-process listeners settle waits immediately; if another runtime completes the run
|
|
217
|
+
// against the same storage, the wait falls back to storage polling. Optional
|
|
218
|
+
// `pollingIntervalMs` overrides the instance `createDurably({ pollingIntervalMs })` for this call only.
|
|
219
|
+
await durably.waitForRun(runId, { pollingIntervalMs: 2000 })
|
|
220
|
+
|
|
221
|
+
// Throws NotFoundError if run doesn't exist
|
|
222
|
+
// Throws CancelledError if run is cancelled
|
|
223
|
+
// Throws Error if run fails
|
|
224
|
+
```
|
|
225
|
+
|
|
187
226
|
### Get Run Status
|
|
188
227
|
|
|
189
228
|
```ts
|
|
@@ -210,6 +249,9 @@ const typedRun = await durably.getRun<MyRun>(runId)
|
|
|
210
249
|
// Get failed runs
|
|
211
250
|
const failedRuns = await durably.getRuns({ status: 'failed' })
|
|
212
251
|
|
|
252
|
+
// Get active runs (multiple statuses)
|
|
253
|
+
const activeRuns = await durably.getRuns({ status: ['pending', 'leased'] })
|
|
254
|
+
|
|
213
255
|
// Filter by job name with pagination
|
|
214
256
|
const runs = await durably.getRuns({
|
|
215
257
|
jobName: 'sync-users', // also accepts string[] for multiple jobs
|
|
@@ -279,7 +321,11 @@ Subscribe to job execution events. **Listeners run synchronously** in the worker
|
|
|
279
321
|
|
|
280
322
|
```ts
|
|
281
323
|
// Run lifecycle events
|
|
324
|
+
// Note: run:trigger is NOT emitted on idempotent hits (disposition: 'idempotent')
|
|
282
325
|
durably.on('run:trigger', (e) => console.log('Triggered:', e.runId))
|
|
326
|
+
durably.on('run:coalesced', (e) =>
|
|
327
|
+
console.log('Coalesced:', e.runId, 'skipped input:', e.skippedInput),
|
|
328
|
+
)
|
|
283
329
|
durably.on('run:leased', (e) => console.log('Leased:', e.runId))
|
|
284
330
|
durably.on('run:complete', (e) => console.log('Done:', e.output))
|
|
285
331
|
durably.on('run:fail', (e) => console.error('Failed:', e.error))
|
|
@@ -299,6 +345,25 @@ durably.on('step:cancel', (e) => console.log('Step cancelled:', e.stepName))
|
|
|
299
345
|
durably.on('log:write', (e) => console.log(`[${e.level}]`, e.message))
|
|
300
346
|
```
|
|
301
347
|
|
|
348
|
+
### Core event categories
|
|
349
|
+
|
|
350
|
+
The `DurablyEvent` union is grouped for callers who want lifecycle facts vs operational detail:
|
|
351
|
+
|
|
352
|
+
- **Domain** (`DomainEvent` / `DomainEventType`): `run:trigger`, `run:coalesced`, `run:complete`, `run:fail`, `run:cancel`, `run:delete`
|
|
353
|
+
- **Operational** (`OperationalEvent` / `OperationalEventType`): `run:leased`, `run:lease-renewed`, `run:progress`, `step:*`, `log:write`, `worker:error`
|
|
354
|
+
|
|
355
|
+
Use `isDomainEvent(event)` (checks `event.type` only) as a type guard.
|
|
356
|
+
|
|
357
|
+
```ts
|
|
358
|
+
import { isDomainEvent, type DurablyEvent } from '@coji/durably'
|
|
359
|
+
|
|
360
|
+
function handleEvent(event: DurablyEvent) {
|
|
361
|
+
if (isDomainEvent(event)) {
|
|
362
|
+
console.log('State change:', event.type, event.runId)
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
```
|
|
366
|
+
|
|
302
367
|
## Advanced APIs
|
|
303
368
|
|
|
304
369
|
### getJob
|
|
@@ -413,13 +478,13 @@ GET /runs?label.organizationId=org_123
|
|
|
413
478
|
GET /runs/subscribe?label.organizationId=org_123&label.env=prod
|
|
414
479
|
```
|
|
415
480
|
|
|
416
|
-
**Response Shape:** The `/runs` and `/run` endpoints return `ClientRun` objects (internal fields like `leaseOwner`, `leaseExpiresAt`, `idempotencyKey`, `concurrencyKey`, `updatedAt` are stripped). Use `toClientRun()` to apply the same projection in custom code:
|
|
481
|
+
**Response Shape:** The `/runs` and `/run` endpoints return `ClientRun` objects (internal fields like `leaseOwner`, `leaseExpiresAt`, `idempotencyKey`, `concurrencyKey`, `leaseGeneration`, `updatedAt` are stripped). Each response includes derived `isTerminal` and `isActive` booleans from `status` (terminal: completed, failed, or cancelled; active: pending or leased). Use `toClientRun()` to apply the same projection in custom code:
|
|
417
482
|
|
|
418
483
|
```ts
|
|
419
484
|
import { toClientRun } from '@coji/durably'
|
|
420
485
|
|
|
421
486
|
const run = await durably.getRun(runId)
|
|
422
|
-
const clientRun = toClientRun(run) // strips internal fields
|
|
487
|
+
const clientRun = toClientRun(run) // strips internal fields; adds isTerminal / isActive
|
|
423
488
|
```
|
|
424
489
|
|
|
425
490
|
**Handler Interface:**
|
|
@@ -477,11 +542,13 @@ interface TriggerRequest<TLabels> {
|
|
|
477
542
|
input: unknown
|
|
478
543
|
idempotencyKey?: string
|
|
479
544
|
concurrencyKey?: string
|
|
545
|
+
coalesce?: 'skip'
|
|
480
546
|
labels?: TLabels
|
|
481
547
|
}
|
|
482
548
|
|
|
483
549
|
interface TriggerResponse {
|
|
484
550
|
runId: string
|
|
551
|
+
disposition: Disposition
|
|
485
552
|
}
|
|
486
553
|
```
|
|
487
554
|
|
|
@@ -495,6 +562,10 @@ import { withLogPersistence } from '@coji/durably'
|
|
|
495
562
|
durably.use(withLogPersistence())
|
|
496
563
|
```
|
|
497
564
|
|
|
565
|
+
## SQLite WAL Maintenance
|
|
566
|
+
|
|
567
|
+
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.
|
|
568
|
+
|
|
498
569
|
## Browser Usage
|
|
499
570
|
|
|
500
571
|
```ts
|
|
@@ -586,6 +657,8 @@ interface Run<TLabels extends Record<string, string> = Record<string, string>> {
|
|
|
586
657
|
updatedAt: string
|
|
587
658
|
}
|
|
588
659
|
|
|
660
|
+
type Disposition = 'created' | 'idempotent' | 'coalesced'
|
|
661
|
+
|
|
589
662
|
interface TypedRun<
|
|
590
663
|
TOutput,
|
|
591
664
|
TLabels extends Record<string, string> = Record<string, string>,
|
|
@@ -593,6 +666,10 @@ interface TypedRun<
|
|
|
593
666
|
output: TOutput | null
|
|
594
667
|
}
|
|
595
668
|
|
|
669
|
+
type TriggerResult<TOutput, TLabels> = TypedRun<TOutput, TLabels> & {
|
|
670
|
+
disposition: Disposition
|
|
671
|
+
}
|
|
672
|
+
|
|
596
673
|
interface JobHandle<
|
|
597
674
|
TName extends string,
|
|
598
675
|
TInput,
|
|
@@ -603,14 +680,14 @@ interface JobHandle<
|
|
|
603
680
|
trigger(
|
|
604
681
|
input: TInput,
|
|
605
682
|
options?: TriggerOptions<TLabels>,
|
|
606
|
-
): Promise<
|
|
683
|
+
): Promise<TriggerResult<TOutput, TLabels>>
|
|
607
684
|
triggerAndWait(
|
|
608
685
|
input: TInput,
|
|
609
686
|
options?: TriggerAndWaitOptions<TLabels>,
|
|
610
|
-
): Promise<
|
|
687
|
+
): Promise<TriggerAndWaitResult<TOutput>>
|
|
611
688
|
batchTrigger(
|
|
612
689
|
inputs: BatchTriggerInput<TInput, TLabels>[],
|
|
613
|
-
): Promise<
|
|
690
|
+
): Promise<TriggerResult<TOutput, TLabels>[]>
|
|
614
691
|
getRun(id: string): Promise<TypedRun<TOutput, TLabels> | null>
|
|
615
692
|
getRuns(
|
|
616
693
|
filter?: Omit<RunFilter<TLabels>, 'jobName'>,
|
|
@@ -622,17 +699,27 @@ interface TriggerOptions<
|
|
|
622
699
|
> {
|
|
623
700
|
idempotencyKey?: string
|
|
624
701
|
concurrencyKey?: string
|
|
702
|
+
coalesce?: 'skip'
|
|
625
703
|
labels?: TLabels
|
|
626
704
|
}
|
|
627
705
|
|
|
628
|
-
interface
|
|
629
|
-
|
|
630
|
-
|
|
706
|
+
interface TriggerAndWaitResult<TOutput> {
|
|
707
|
+
id: string
|
|
708
|
+
output: TOutput
|
|
709
|
+
disposition: Disposition
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
interface WaitForRunOptions {
|
|
631
713
|
timeout?: number
|
|
632
714
|
onProgress?: (progress: ProgressData) => void | Promise<void>
|
|
633
715
|
onLog?: (log: LogData) => void | Promise<void>
|
|
634
716
|
}
|
|
635
717
|
|
|
718
|
+
interface TriggerAndWaitOptions<
|
|
719
|
+
TLabels extends Record<string, string> = Record<string, string>,
|
|
720
|
+
>
|
|
721
|
+
extends TriggerOptions<TLabels>, WaitForRunOptions {}
|
|
722
|
+
|
|
636
723
|
interface ProgressData {
|
|
637
724
|
current: number
|
|
638
725
|
total?: number
|
|
@@ -649,7 +736,7 @@ interface LogData {
|
|
|
649
736
|
interface RunFilter<
|
|
650
737
|
TLabels extends Record<string, string> = Record<string, string>,
|
|
651
738
|
> {
|
|
652
|
-
status?:
|
|
739
|
+
status?: RunStatus | RunStatus[]
|
|
653
740
|
jobName?: string | string[]
|
|
654
741
|
labels?: Partial<TLabels>
|
|
655
742
|
limit?: number
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coji/durably",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "Step-oriented resumable batch execution for Node.js and browsers using SQLite",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -23,13 +23,11 @@
|
|
|
23
23
|
"scripts": {
|
|
24
24
|
"build": "tsup",
|
|
25
25
|
"test": "pnpm test:node && pnpm test:react && pnpm test:browser",
|
|
26
|
-
"test:node": "vitest run --config vitest.config.ts
|
|
27
|
-
"test:node:postgres": "vitest run --config vitest.config.ts postgres",
|
|
28
|
-
"test:node:all": "vitest run --config vitest.config.ts",
|
|
26
|
+
"test:node": "vitest run --config vitest.config.ts",
|
|
29
27
|
"test:react": "vitest run --config vitest.react.config.ts",
|
|
30
28
|
"test:browser": "vitest run --config vitest.browser.config.ts",
|
|
31
29
|
"typecheck": "tsc --noEmit",
|
|
32
|
-
"lint": "biome lint .",
|
|
30
|
+
"lint": "biome lint --error-on-warnings .",
|
|
33
31
|
"lint:fix": "biome lint --write .",
|
|
34
32
|
"format": "prettier --experimental-cli --check .",
|
|
35
33
|
"format:fix": "prettier --experimental-cli --write ."
|
|
@@ -66,6 +64,7 @@
|
|
|
66
64
|
"@biomejs/biome": "^2.4.7",
|
|
67
65
|
"@libsql/client": "^0.17.0",
|
|
68
66
|
"@libsql/kysely-libsql": "^0.4.1",
|
|
67
|
+
"@testcontainers/postgresql": "11.13.0",
|
|
69
68
|
"@testing-library/react": "^16.3.2",
|
|
70
69
|
"@types/better-sqlite3": "^7.6.13",
|
|
71
70
|
"@types/pg": "^8.15.6",
|
|
@@ -83,6 +82,7 @@
|
|
|
83
82
|
"react": "^19.2.4",
|
|
84
83
|
"react-dom": "^19.2.4",
|
|
85
84
|
"sqlocal": "^0.17.0",
|
|
85
|
+
"testcontainers": "11.13.0",
|
|
86
86
|
"tsup": "^8.5.1",
|
|
87
87
|
"typescript": "^5.9.3",
|
|
88
88
|
"vitest": "^4.1.0",
|