@coji/durably 0.12.0 → 0.13.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.
@@ -20,13 +20,23 @@ interface RunTriggerEvent extends BaseEvent {
20
20
  labels: Record<string, string>;
21
21
  }
22
22
  /**
23
- * Run start event
23
+ * Run leased event
24
24
  */
25
- interface RunStartEvent extends BaseEvent {
26
- type: 'run:start';
25
+ interface RunLeasedEvent extends BaseEvent {
26
+ type: 'run:leased';
27
27
  runId: string;
28
28
  jobName: string;
29
29
  input: unknown;
30
+ leaseOwner: string;
31
+ leaseExpiresAt: string;
32
+ labels: Record<string, string>;
33
+ }
34
+ interface RunLeaseRenewedEvent extends BaseEvent {
35
+ type: 'run:lease-renewed';
36
+ runId: string;
37
+ jobName: string;
38
+ leaseOwner: string;
39
+ leaseExpiresAt: string;
30
40
  labels: Record<string, string>;
31
41
  }
32
42
  /**
@@ -72,7 +82,7 @@ interface RunDeleteEvent extends BaseEvent {
72
82
  /**
73
83
  * Progress data reported by step.progress()
74
84
  */
75
- interface ProgressData {
85
+ interface ProgressData$1 {
76
86
  current: number;
77
87
  total?: number;
78
88
  message?: string;
@@ -84,7 +94,7 @@ interface RunProgressEvent extends BaseEvent {
84
94
  type: 'run:progress';
85
95
  runId: string;
86
96
  jobName: string;
87
- progress: ProgressData;
97
+ progress: ProgressData$1;
88
98
  labels: Record<string, string>;
89
99
  }
90
100
  /**
@@ -163,7 +173,7 @@ interface WorkerErrorEvent extends BaseEvent {
163
173
  /**
164
174
  * All event types as discriminated union
165
175
  */
166
- type DurablyEvent = RunTriggerEvent | RunStartEvent | RunCompleteEvent | RunFailEvent | RunCancelEvent | RunDeleteEvent | RunProgressEvent | StepStartEvent | StepCompleteEvent | StepFailEvent | StepCancelEvent | LogWriteEvent | WorkerErrorEvent;
176
+ type DurablyEvent = RunTriggerEvent | RunLeasedEvent | RunLeaseRenewedEvent | RunCompleteEvent | RunFailEvent | RunCancelEvent | RunDeleteEvent | RunProgressEvent | StepStartEvent | StepCompleteEvent | StepFailEvent | StepCancelEvent | LogWriteEvent | WorkerErrorEvent;
167
177
  /**
168
178
  * Event types for type-safe event names
169
179
  */
@@ -181,7 +191,7 @@ type EventInput<T extends EventType> = Omit<EventByType<T>, 'timestamp' | 'seque
181
191
  /**
182
192
  * All possible event inputs as a union (properly distributed)
183
193
  */
184
- type AnyEventInput = EventInput<'run:trigger'> | EventInput<'run:start'> | EventInput<'run:complete'> | EventInput<'run:fail'> | EventInput<'run:cancel'> | EventInput<'run:delete'> | EventInput<'run:progress'> | EventInput<'step:start'> | EventInput<'step:complete'> | EventInput<'step:fail'> | EventInput<'step:cancel'> | EventInput<'log:write'> | EventInput<'worker:error'>;
194
+ type AnyEventInput = EventInput<'run:trigger'> | EventInput<'run:leased'> | EventInput<'run:lease-renewed'> | EventInput<'run:complete'> | EventInput<'run:fail'> | EventInput<'run:cancel'> | EventInput<'run:delete'> | EventInput<'run:progress'> | EventInput<'step:start'> | EventInput<'step:complete'> | EventInput<'step:fail'> | EventInput<'step:cancel'> | EventInput<'log:write'> | EventInput<'worker:error'>;
185
195
  /**
186
196
  * Event listener function
187
197
  */
@@ -202,7 +212,7 @@ interface RunsTable {
202
212
  id: string;
203
213
  job_name: string;
204
214
  input: string;
205
- status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled';
215
+ status: 'pending' | 'leased' | 'completed' | 'failed' | 'cancelled';
206
216
  idempotency_key: string | null;
207
217
  concurrency_key: string | null;
208
218
  current_step_index: number;
@@ -210,7 +220,9 @@ interface RunsTable {
210
220
  output: string | null;
211
221
  error: string | null;
212
222
  labels: string;
213
- heartbeat_at: string;
223
+ lease_owner: string | null;
224
+ lease_expires_at: string | null;
225
+ lease_generation: number;
214
226
  started_at: string | null;
215
227
  completed_at: string | null;
216
228
  created_at: string;
@@ -236,17 +248,24 @@ interface LogsTable {
236
248
  data: string | null;
237
249
  created_at: string;
238
250
  }
251
+ interface RunLabelsTable {
252
+ run_id: string;
253
+ key: string;
254
+ value: string;
255
+ }
239
256
  interface SchemaVersionsTable {
240
257
  version: number;
241
258
  applied_at: string;
242
259
  }
243
260
  interface Database {
244
261
  durably_runs: RunsTable;
262
+ durably_run_labels: RunLabelsTable;
245
263
  durably_steps: StepsTable;
246
264
  durably_logs: LogsTable;
247
265
  durably_schema_versions: SchemaVersionsTable;
248
266
  }
249
267
 
268
+ type RunStatus = 'pending' | 'leased' | 'completed' | 'failed' | 'cancelled';
250
269
  /**
251
270
  * Run data for creating a new run
252
271
  */
@@ -264,7 +283,7 @@ interface Run<TLabels extends Record<string, string> = Record<string, string>> {
264
283
  id: string;
265
284
  jobName: string;
266
285
  input: unknown;
267
- status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled';
286
+ status: RunStatus;
268
287
  idempotencyKey: string | null;
269
288
  concurrencyKey: string | null;
270
289
  currentStepIndex: number;
@@ -277,34 +296,19 @@ interface Run<TLabels extends Record<string, string> = Record<string, string>> {
277
296
  output: unknown | null;
278
297
  error: string | null;
279
298
  labels: TLabels;
280
- heartbeatAt: string;
299
+ leaseOwner: string | null;
300
+ leaseExpiresAt: string | null;
301
+ leaseGeneration: number;
281
302
  startedAt: string | null;
282
303
  completedAt: string | null;
283
304
  createdAt: string;
284
305
  updatedAt: string;
285
306
  }
286
- /**
287
- * Run update data
288
- */
289
- interface UpdateRunInput {
290
- status?: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled';
291
- currentStepIndex?: number;
292
- progress?: {
293
- current: number;
294
- total?: number;
295
- message?: string;
296
- } | null;
297
- output?: unknown;
298
- error?: string | null;
299
- heartbeatAt?: string;
300
- startedAt?: string;
301
- completedAt?: string;
302
- }
303
307
  /**
304
308
  * Run filter options
305
309
  */
306
310
  interface RunFilter<TLabels extends Record<string, string> = Record<string, string>> {
307
- status?: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled';
311
+ status?: RunStatus;
308
312
  /** Filter by job name(s). Pass a string for one, or an array for multiple (OR). */
309
313
  jobName?: string | string[];
310
314
  /** Filter by labels (all specified labels must match) */
@@ -317,10 +321,9 @@ interface RunFilter<TLabels extends Record<string, string> = Record<string, stri
317
321
  offset?: number;
318
322
  }
319
323
  /**
320
- * Step data for creating a new step
324
+ * Step data for persisting a step checkpoint
321
325
  */
322
326
  interface CreateStepInput {
323
- runId: string;
324
327
  name: string;
325
328
  index: number;
326
329
  status: 'completed' | 'failed' | 'cancelled';
@@ -364,33 +367,73 @@ interface Log {
364
367
  data: unknown | null;
365
368
  createdAt: string;
366
369
  }
370
+ interface ProgressData {
371
+ current: number;
372
+ total?: number;
373
+ message?: string;
374
+ }
375
+ type DatabaseBackend = 'generic' | 'postgres';
367
376
  /**
368
- * A client-safe subset of Run, excluding internal fields like
369
- * heartbeatAt, idempotencyKey, concurrencyKey, and updatedAt.
370
- */
371
- type ClientRun<TLabels extends Record<string, string> = Record<string, string>> = Omit<Run<TLabels>, 'idempotencyKey' | 'concurrencyKey' | 'heartbeatAt' | 'updatedAt'>;
372
- /**
373
- * Project a full Run to a ClientRun by stripping internal fields.
377
+ * Data for updating a run
374
378
  */
375
- declare function toClientRun<TLabels extends Record<string, string> = Record<string, string>>(run: Run<TLabels>): ClientRun<TLabels>;
379
+ interface UpdateRunData {
380
+ status?: RunStatus;
381
+ currentStepIndex?: number;
382
+ progress?: ProgressData | null;
383
+ output?: unknown;
384
+ error?: string | null;
385
+ leaseOwner?: string | null;
386
+ leaseExpiresAt?: string | null;
387
+ startedAt?: string;
388
+ completedAt?: string;
389
+ }
376
390
  /**
377
- * Storage interface for database operations
391
+ * Unified storage interface used by the runtime.
378
392
  */
379
- interface Storage {
380
- createRun(input: CreateRunInput): Promise<Run>;
381
- batchCreateRuns(inputs: CreateRunInput[]): Promise<Run[]>;
382
- updateRun(runId: string, data: UpdateRunInput): Promise<void>;
393
+ interface Store<TLabels extends Record<string, string> = Record<string, string>> {
394
+ enqueue(input: CreateRunInput<TLabels>): Promise<Run<TLabels>>;
395
+ enqueueMany(inputs: CreateRunInput<TLabels>[]): Promise<Run<TLabels>[]>;
396
+ getRun<T extends Run<TLabels> = Run<TLabels>>(runId: string): Promise<T | null>;
397
+ getRuns<T extends Run<TLabels> = Run<TLabels>>(filter?: RunFilter<TLabels>): Promise<T[]>;
398
+ updateRun(runId: string, data: UpdateRunData): Promise<void>;
383
399
  deleteRun(runId: string): Promise<void>;
384
- getRun<T extends Run = Run>(runId: string): Promise<T | null>;
385
- getRuns<T extends Run = Run>(filter?: RunFilter): Promise<T[]>;
386
- claimNextPendingRun(excludeConcurrencyKeys: string[]): Promise<Run | null>;
387
- createStep(input: CreateStepInput): Promise<Step>;
388
- deleteSteps(runId: string): Promise<void>;
400
+ claimNext(workerId: string, now: string, leaseMs: number): Promise<Run<TLabels> | null>;
401
+ renewLease(runId: string, leaseGeneration: number, now: string, leaseMs: number): Promise<boolean>;
402
+ releaseExpiredLeases(now: string): Promise<number>;
403
+ completeRun(runId: string, leaseGeneration: number, output: unknown, completedAt: string): Promise<boolean>;
404
+ failRun(runId: string, leaseGeneration: number, error: string, completedAt: string): Promise<boolean>;
405
+ cancelRun(runId: string, now: string): Promise<boolean>;
406
+ /**
407
+ * Atomically persist a step checkpoint, guarded by lease generation.
408
+ * Inserts the step record and advances currentStepIndex (for completed
409
+ * steps only) in a single transaction. Returns null if the generation
410
+ * does not match (lease was lost).
411
+ */
412
+ persistStep(runId: string, leaseGeneration: number, input: CreateStepInput): Promise<Step | null>;
389
413
  getSteps(runId: string): Promise<Step[]>;
390
414
  getCompletedStep(runId: string, name: string): Promise<Step | null>;
415
+ deleteSteps(runId: string): Promise<void>;
416
+ updateProgress(runId: string, leaseGeneration: number, progress: ProgressData | null): Promise<void>;
417
+ purgeRuns(options: {
418
+ olderThan: string;
419
+ limit?: number;
420
+ }): Promise<number>;
391
421
  createLog(input: CreateLogInput): Promise<Log>;
392
422
  getLogs(runId: string): Promise<Log[]>;
393
423
  }
424
+ /**
425
+ * A client-safe subset of Run, excluding internal fields like
426
+ * leaseOwner, leaseExpiresAt, idempotencyKey, concurrencyKey, and updatedAt.
427
+ */
428
+ type ClientRun<TLabels extends Record<string, string> = Record<string, string>> = Omit<Run<TLabels>, 'idempotencyKey' | 'concurrencyKey' | 'leaseOwner' | 'leaseExpiresAt' | 'leaseGeneration' | 'updatedAt'>;
429
+ /**
430
+ * Project a full Run to a ClientRun by stripping internal fields.
431
+ */
432
+ declare function toClientRun<TLabels extends Record<string, string> = Record<string, string>>(run: Run<TLabels>): ClientRun<TLabels>;
433
+ /**
434
+ * Create a Kysely-based Store implementation
435
+ */
436
+ declare function createKyselyStore(db: Kysely<Database>, backend?: DatabaseBackend): Store<Record<string, string>>;
394
437
 
395
438
  /**
396
439
  * Step context passed to the job function
@@ -400,6 +443,18 @@ interface StepContext {
400
443
  * The ID of the current run
401
444
  */
402
445
  readonly runId: string;
446
+ /**
447
+ * AbortSignal for cooperative cancellation or lease-loss handling.
448
+ */
449
+ readonly signal: AbortSignal;
450
+ /**
451
+ * Whether this execution should stop cooperatively.
452
+ */
453
+ isAborted(): boolean;
454
+ /**
455
+ * Throw if execution has been cancelled or lease ownership was lost.
456
+ */
457
+ throwIfAborted(): void;
403
458
  /**
404
459
  * Execute a step with automatic persistence and replay
405
460
  */
@@ -432,7 +487,7 @@ interface TriggerAndWaitOptions<TLabels extends Record<string, string> = Record<
432
487
  /** Timeout in milliseconds */
433
488
  timeout?: number;
434
489
  /** Called when step.progress() is invoked during execution */
435
- onProgress?: (progress: ProgressData) => void | Promise<void>;
490
+ onProgress?: (progress: ProgressData$1) => void | Promise<void>;
436
491
  /** Called when step.log is invoked during execution */
437
492
  onLog?: (log: LogData) => void | Promise<void>;
438
493
  }
@@ -550,10 +605,15 @@ declare function defineJob<TName extends string, TInputSchema extends z.ZodType,
550
605
  */
551
606
  interface DurablyOptions<TLabels extends Record<string, string> = Record<string, string>, TJobs extends Record<string, JobDefinition<string, any, any>> = Record<string, never>> {
552
607
  dialect: Dialect;
553
- pollingInterval?: number;
554
- heartbeatInterval?: number;
555
- staleThreshold?: number;
556
- cleanupSteps?: boolean;
608
+ /**
609
+ * Browser-local singleton key used to detect multiple runtimes against the same local database in one tab.
610
+ * When omitted, Durably will use browser-local dialect metadata if available.
611
+ */
612
+ singletonKey?: string;
613
+ pollingIntervalMs?: number;
614
+ leaseRenewIntervalMs?: number;
615
+ leaseMs?: number;
616
+ preserveSteps?: boolean;
557
617
  /**
558
618
  * Zod schema for labels. When provided:
559
619
  * - Labels are type-checked at compile time
@@ -571,6 +631,12 @@ interface DurablyOptions<TLabels extends Record<string, string> = Record<string,
571
631
  * ```
572
632
  */
573
633
  jobs?: TJobs;
634
+ /**
635
+ * Auto-delete terminal runs older than the specified duration.
636
+ * Only runs in terminal states (completed, failed, cancelled) are purged.
637
+ * @example '30d' (30 days), '24h' (24 hours), '60m' (60 minutes)
638
+ */
639
+ retainRuns?: string;
574
640
  }
575
641
  /**
576
642
  * Plugin interface for extending Durably
@@ -617,7 +683,7 @@ interface Durably<TJobs extends Record<string, JobHandle<string, unknown, unknow
617
683
  /**
618
684
  * Storage layer for database operations
619
685
  */
620
- readonly storage: Storage;
686
+ readonly storage: Store<TLabels>;
621
687
  /**
622
688
  * Register an event listener
623
689
  * @returns Unsubscribe function
@@ -645,10 +711,25 @@ interface Durably<TJobs extends Record<string, JobHandle<string, unknown, unknow
645
711
  * ```
646
712
  */
647
713
  register<TNewJobs extends Record<string, JobDefinition<string, any, any>>>(jobDefs: TNewJobs): Durably<TJobs & TransformToHandles<TNewJobs, TLabels>, TLabels>;
714
+ /**
715
+ * Process a single claimable run.
716
+ */
717
+ processOne(options?: {
718
+ workerId?: string;
719
+ }): Promise<boolean>;
720
+ /**
721
+ * Process runs until the queue appears idle.
722
+ */
723
+ processUntilIdle(options?: {
724
+ workerId?: string;
725
+ maxRuns?: number;
726
+ }): Promise<number>;
648
727
  /**
649
728
  * Start the worker polling loop
650
729
  */
651
- start(): void;
730
+ start(options?: {
731
+ workerId?: string;
732
+ }): void;
652
733
  /**
653
734
  * Stop the worker after current run completes
654
735
  */
@@ -668,6 +749,15 @@ interface Durably<TJobs extends Record<string, JobHandle<string, unknown, unknow
668
749
  * @throws Error if run is pending or running, or does not exist
669
750
  */
670
751
  deleteRun(runId: string): Promise<void>;
752
+ /**
753
+ * Delete terminal runs older than the specified cutoff.
754
+ * Only runs in terminal states (completed, failed, cancelled) are purged.
755
+ * @returns Number of deleted runs
756
+ */
757
+ purgeRuns(options: {
758
+ olderThan: Date;
759
+ limit?: number;
760
+ }): Promise<number>;
671
761
  /**
672
762
  * Get a run by ID
673
763
  * @example
@@ -722,4 +812,4 @@ declare function createDurably<TLabels extends Record<string, string> = Record<s
722
812
  */
723
813
  declare function withLogPersistence(): DurablyPlugin;
724
814
 
725
- export { type StepsTable as A, type BatchTriggerInput as B, type ClientRun as C, type Durably as D, type ErrorHandler as E, type TriggerAndWaitResult as F, type TriggerOptions as G, createDurably as H, defineJob as I, type JobDefinition as J, toClientRun as K, type Log as L, withLogPersistence as M, type ProgressData as P, type Run as R, type SchemaVersionsTable as S, type TriggerAndWaitOptions as T, type WorkerErrorEvent as W, type RunFilter as a, type Database as b, type DurablyEvent as c, type DurablyOptions as d, type DurablyPlugin as e, type EventType as f, type JobHandle as g, type JobInput as h, type JobOutput as i, type LogData as j, type LogWriteEvent as k, type LogsTable as l, type RunCancelEvent as m, type RunCompleteEvent as n, type RunDeleteEvent as o, type RunFailEvent as p, type RunProgressEvent as q, type RunStartEvent as r, type RunTriggerEvent as s, type RunsTable as t, type Step as u, type StepCancelEvent as v, type StepCompleteEvent as w, type StepContext as x, type StepFailEvent as y, type StepStartEvent as z };
815
+ export { type StepFailEvent as A, type BatchTriggerInput as B, type ClientRun as C, type Durably as D, type ErrorHandler as E, type StepStartEvent as F, type StepsTable as G, type Store as H, type TriggerAndWaitResult as I, type JobDefinition as J, type TriggerOptions as K, type Log as L, createDurably as M, createKyselyStore as N, defineJob as O, type ProgressData$1 as P, toClientRun as Q, type Run as R, type SchemaVersionsTable as S, type TriggerAndWaitOptions as T, type UpdateRunData as U, withLogPersistence as V, type WorkerErrorEvent as W, type RunFilter as a, type Database as b, type DurablyEvent as c, type DurablyOptions as d, type DurablyPlugin as e, type EventType as f, type JobHandle as g, type JobInput as h, type JobOutput as i, type LogData as j, type LogWriteEvent as k, type LogsTable as l, type RunCancelEvent as m, type RunCompleteEvent as n, type RunDeleteEvent as o, type RunFailEvent as p, type RunLeaseRenewedEvent as q, type RunLeasedEvent as r, type RunProgressEvent as s, type RunStatus as t, type RunTriggerEvent as u, type RunsTable as v, type Step as w, type StepCancelEvent as x, type StepCompleteEvent as y, type StepContext as z };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { R as Run, a as RunFilter, D as Durably } from './index-hM7-oiyj.js';
2
- export { B as BatchTriggerInput, C as ClientRun, b as Database, c as DurablyEvent, d as DurablyOptions, e as DurablyPlugin, E as ErrorHandler, f as EventType, J as JobDefinition, g as JobHandle, h as JobInput, i as JobOutput, L as Log, j as LogData, k as LogWriteEvent, l as LogsTable, P as ProgressData, m as RunCancelEvent, n as RunCompleteEvent, o as RunDeleteEvent, p as RunFailEvent, q as RunProgressEvent, r as RunStartEvent, s as RunTriggerEvent, t as RunsTable, S as SchemaVersionsTable, u as Step, v as StepCancelEvent, w as StepCompleteEvent, x as StepContext, y as StepFailEvent, z as StepStartEvent, A as StepsTable, T as TriggerAndWaitOptions, F as TriggerAndWaitResult, G as TriggerOptions, W as WorkerErrorEvent, H as createDurably, I as defineJob, K as toClientRun, M as withLogPersistence } from './index-hM7-oiyj.js';
1
+ import { R as Run, a as RunFilter, D as Durably } from './index-DWsJlgyh.js';
2
+ export { B as BatchTriggerInput, C as ClientRun, b as Database, c as DurablyEvent, d as DurablyOptions, e as DurablyPlugin, E as ErrorHandler, f as EventType, J as JobDefinition, g as JobHandle, h as JobInput, i as JobOutput, L as Log, j as LogData, k as LogWriteEvent, l as LogsTable, P as ProgressData, m as RunCancelEvent, n as RunCompleteEvent, o as RunDeleteEvent, p as RunFailEvent, q as RunLeaseRenewedEvent, r as RunLeasedEvent, s as RunProgressEvent, t as RunStatus, u as RunTriggerEvent, v as RunsTable, S as SchemaVersionsTable, w as Step, x as StepCancelEvent, y as StepCompleteEvent, z as StepContext, A as StepFailEvent, F as StepStartEvent, G as StepsTable, H as Store, T as TriggerAndWaitOptions, I as TriggerAndWaitResult, K as TriggerOptions, U as UpdateRunData, W as WorkerErrorEvent, M as createDurably, N as createKyselyStore, O as defineJob, Q as toClientRun, V as withLogPersistence } from './index-DWsJlgyh.js';
3
3
  import 'kysely';
4
4
  import 'zod';
5
5
 
@@ -11,6 +11,12 @@ import 'zod';
11
11
  declare class CancelledError extends Error {
12
12
  constructor(runId: string);
13
13
  }
14
+ /**
15
+ * Error thrown when a worker loses lease ownership during execution.
16
+ */
17
+ declare class LeaseLostError extends Error {
18
+ constructor(runId: string);
19
+ }
14
20
 
15
21
  /**
16
22
  * Run operation types for onRunAccess
@@ -101,4 +107,4 @@ interface CreateDurablyHandlerOptions<TContext = undefined, TLabels extends Reco
101
107
  */
102
108
  declare function createDurablyHandler<TContext = undefined, TLabels extends Record<string, string> = Record<string, string>>(durably: Durably<any, TLabels>, options?: CreateDurablyHandlerOptions<TContext, TLabels>): DurablyHandler;
103
109
 
104
- export { type AuthConfig, CancelledError, type CreateDurablyHandlerOptions, Durably, type DurablyHandler, Run, RunFilter, type RunOperation, type RunsSubscribeFilter, type TriggerRequest, type TriggerResponse, createDurablyHandler };
110
+ export { type AuthConfig, CancelledError, type CreateDurablyHandlerOptions, Durably, type DurablyHandler, LeaseLostError, Run, RunFilter, type RunOperation, type RunsSubscribeFilter, type TriggerRequest, type TriggerResponse, createDurablyHandler };