@coji/durably 0.10.0 → 0.12.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/README.md CHANGED
@@ -12,7 +12,7 @@ Step-oriented resumable batch execution for Node.js and browsers using SQLite.
12
12
  npm install @coji/durably kysely zod better-sqlite3
13
13
  ```
14
14
 
15
- See the [Getting Started Guide](https://coji.github.io/durably/guide/getting-started) for other SQLite backends (libsql, SQLocal for browsers).
15
+ See the [Quick Start](https://coji.github.io/durably/guide/quick-start) for other SQLite backends (libsql, SQLocal for browsers).
16
16
 
17
17
  ## Quick Start
18
18
 
@@ -30,7 +30,7 @@ const myJob = defineJob({
30
30
  },
31
31
  })
32
32
 
33
- const durably = createDurably({ dialect }).register({ myJob })
33
+ const durably = createDurably({ dialect, jobs: { myJob } })
34
34
 
35
35
  await durably.init() // migrate + start
36
36
  await durably.jobs.myJob.trigger({ id: '123' })
@@ -70,13 +70,12 @@ interface RunDeleteEvent extends BaseEvent {
70
70
  labels: Record<string, string>;
71
71
  }
72
72
  /**
73
- * Run retry event (emitted when a failed run is retried)
73
+ * Progress data reported by step.progress()
74
74
  */
75
- interface RunRetryEvent extends BaseEvent {
76
- type: 'run:retry';
77
- runId: string;
78
- jobName: string;
79
- labels: Record<string, string>;
75
+ interface ProgressData {
76
+ current: number;
77
+ total?: number;
78
+ message?: string;
80
79
  }
81
80
  /**
82
81
  * Run progress event
@@ -85,11 +84,7 @@ interface RunProgressEvent extends BaseEvent {
85
84
  type: 'run:progress';
86
85
  runId: string;
87
86
  jobName: string;
88
- progress: {
89
- current: number;
90
- total?: number;
91
- message?: string;
92
- };
87
+ progress: ProgressData;
93
88
  labels: Record<string, string>;
94
89
  }
95
90
  /**
@@ -136,17 +131,24 @@ interface StepCancelEvent extends BaseEvent {
136
131
  stepIndex: number;
137
132
  labels: Record<string, string>;
138
133
  }
134
+ /**
135
+ * Log data reported by step.log
136
+ */
137
+ interface LogData {
138
+ level: 'info' | 'warn' | 'error';
139
+ message: string;
140
+ data?: unknown;
141
+ stepName?: string | null;
142
+ }
139
143
  /**
140
144
  * Log write event
141
145
  */
142
- interface LogWriteEvent extends BaseEvent {
146
+ interface LogWriteEvent extends BaseEvent, LogData {
143
147
  type: 'log:write';
144
148
  runId: string;
145
149
  jobName: string;
146
150
  labels: Record<string, string>;
147
151
  stepName: string | null;
148
- level: 'info' | 'warn' | 'error';
149
- message: string;
150
152
  data: unknown;
151
153
  }
152
154
  /**
@@ -161,7 +163,7 @@ interface WorkerErrorEvent extends BaseEvent {
161
163
  /**
162
164
  * All event types as discriminated union
163
165
  */
164
- type DurablyEvent = RunTriggerEvent | RunStartEvent | RunCompleteEvent | RunFailEvent | RunCancelEvent | RunDeleteEvent | RunRetryEvent | RunProgressEvent | StepStartEvent | StepCompleteEvent | StepFailEvent | StepCancelEvent | LogWriteEvent | WorkerErrorEvent;
166
+ type DurablyEvent = RunTriggerEvent | RunStartEvent | RunCompleteEvent | RunFailEvent | RunCancelEvent | RunDeleteEvent | RunProgressEvent | StepStartEvent | StepCompleteEvent | StepFailEvent | StepCancelEvent | LogWriteEvent | WorkerErrorEvent;
165
167
  /**
166
168
  * Event types for type-safe event names
167
169
  */
@@ -179,7 +181,7 @@ type EventInput<T extends EventType> = Omit<EventByType<T>, 'timestamp' | 'seque
179
181
  /**
180
182
  * All possible event inputs as a union (properly distributed)
181
183
  */
182
- type AnyEventInput = EventInput<'run:trigger'> | EventInput<'run:start'> | EventInput<'run:complete'> | EventInput<'run:fail'> | EventInput<'run:cancel'> | EventInput<'run:delete'> | EventInput<'run:retry'> | EventInput<'run:progress'> | EventInput<'step:start'> | EventInput<'step:complete'> | EventInput<'step:fail'> | EventInput<'step:cancel'> | EventInput<'log:write'> | EventInput<'worker:error'>;
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'>;
183
185
  /**
184
186
  * Event listener function
185
187
  */
@@ -248,17 +250,17 @@ interface Database {
248
250
  /**
249
251
  * Run data for creating a new run
250
252
  */
251
- interface CreateRunInput {
253
+ interface CreateRunInput<TLabels extends Record<string, string> = Record<string, string>> {
252
254
  jobName: string;
253
255
  input: unknown;
254
256
  idempotencyKey?: string;
255
257
  concurrencyKey?: string;
256
- labels?: Record<string, string>;
258
+ labels?: TLabels;
257
259
  }
258
260
  /**
259
261
  * Run data returned from storage
260
262
  */
261
- interface Run {
263
+ interface Run<TLabels extends Record<string, string> = Record<string, string>> {
262
264
  id: string;
263
265
  jobName: string;
264
266
  input: unknown;
@@ -274,7 +276,7 @@ interface Run {
274
276
  } | null;
275
277
  output: unknown | null;
276
278
  error: string | null;
277
- labels: Record<string, string>;
279
+ labels: TLabels;
278
280
  heartbeatAt: string;
279
281
  startedAt: string | null;
280
282
  completedAt: string | null;
@@ -301,10 +303,14 @@ interface UpdateRunInput {
301
303
  /**
302
304
  * Run filter options
303
305
  */
304
- interface RunFilter$1 {
306
+ interface RunFilter<TLabels extends Record<string, string> = Record<string, string>> {
305
307
  status?: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled';
306
- jobName?: string;
307
- labels?: Record<string, string>;
308
+ /** Filter by job name(s). Pass a string for one, or an array for multiple (OR). */
309
+ jobName?: string | string[];
310
+ /** Filter by labels (all specified labels must match) */
311
+ labels?: {
312
+ [K in keyof TLabels]?: TLabels[K];
313
+ };
308
314
  /** Maximum number of runs to return */
309
315
  limit?: number;
310
316
  /** Number of runs to skip (for pagination) */
@@ -362,11 +368,11 @@ interface Log {
362
368
  * A client-safe subset of Run, excluding internal fields like
363
369
  * heartbeatAt, idempotencyKey, concurrencyKey, and updatedAt.
364
370
  */
365
- type ClientRun = Omit<Run, 'idempotencyKey' | 'concurrencyKey' | 'heartbeatAt' | 'updatedAt'>;
371
+ type ClientRun<TLabels extends Record<string, string> = Record<string, string>> = Omit<Run<TLabels>, 'idempotencyKey' | 'concurrencyKey' | 'heartbeatAt' | 'updatedAt'>;
366
372
  /**
367
373
  * Project a full Run to a ClientRun by stripping internal fields.
368
374
  */
369
- declare function toClientRun(run: Run): ClientRun;
375
+ declare function toClientRun<TLabels extends Record<string, string> = Record<string, string>>(run: Run<TLabels>): ClientRun<TLabels>;
370
376
  /**
371
377
  * Storage interface for database operations
372
378
  */
@@ -376,9 +382,10 @@ interface Storage {
376
382
  updateRun(runId: string, data: UpdateRunInput): Promise<void>;
377
383
  deleteRun(runId: string): Promise<void>;
378
384
  getRun<T extends Run = Run>(runId: string): Promise<T | null>;
379
- getRuns<T extends Run = Run>(filter?: RunFilter$1): Promise<T[]>;
385
+ getRuns<T extends Run = Run>(filter?: RunFilter): Promise<T[]>;
380
386
  claimNextPendingRun(excludeConcurrencyKeys: string[]): Promise<Run | null>;
381
387
  createStep(input: CreateStepInput): Promise<Step>;
388
+ deleteSteps(runId: string): Promise<void>;
382
389
  getSteps(runId: string): Promise<Step[]>;
383
390
  getCompletedStep(runId: string, name: string): Promise<Step | null>;
384
391
  createLog(input: CreateLogInput): Promise<Log>;
@@ -411,39 +418,36 @@ interface StepContext {
411
418
  };
412
419
  }
413
420
  /**
414
- * Trigger options
421
+ * Trigger options for trigger() and batchTrigger()
415
422
  */
416
- interface TriggerOptions {
423
+ interface TriggerOptions<TLabels extends Record<string, string> = Record<string, string>> {
417
424
  idempotencyKey?: string;
418
425
  concurrencyKey?: string;
419
- labels?: Record<string, string>;
420
- /** Timeout in milliseconds for triggerAndWait() */
421
- timeout?: number;
426
+ labels?: TLabels;
422
427
  }
423
428
  /**
424
- * Run filter options
429
+ * Options for triggerAndWait() (extends TriggerOptions with wait-specific options)
425
430
  */
426
- interface RunFilter {
427
- status?: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled';
428
- jobName?: string;
429
- labels?: Record<string, string>;
430
- /** Maximum number of runs to return */
431
- limit?: number;
432
- /** Number of runs to skip (for pagination) */
433
- offset?: number;
431
+ interface TriggerAndWaitOptions<TLabels extends Record<string, string> = Record<string, string>> extends TriggerOptions<TLabels> {
432
+ /** Timeout in milliseconds */
433
+ timeout?: number;
434
+ /** Called when step.progress() is invoked during execution */
435
+ onProgress?: (progress: ProgressData) => void | Promise<void>;
436
+ /** Called when step.log is invoked during execution */
437
+ onLog?: (log: LogData) => void | Promise<void>;
434
438
  }
435
439
  /**
436
440
  * Typed run with output type
437
441
  */
438
- interface TypedRun<TOutput> extends Omit<Run, 'output'> {
442
+ interface TypedRun<TOutput, TLabels extends Record<string, string> = Record<string, string>> extends Omit<Run<TLabels>, 'output'> {
439
443
  output: TOutput | null;
440
444
  }
441
445
  /**
442
446
  * Batch trigger input - either just the input or input with options
443
447
  */
444
- type BatchTriggerInput<TInput> = TInput | {
448
+ type BatchTriggerInput<TInput, TLabels extends Record<string, string> = Record<string, string>> = TInput | {
445
449
  input: TInput;
446
- options?: TriggerOptions;
450
+ options?: TriggerOptions<TLabels>;
447
451
  };
448
452
  /**
449
453
  * Result of triggerAndWait
@@ -455,30 +459,30 @@ interface TriggerAndWaitResult<TOutput> {
455
459
  /**
456
460
  * Job handle returned by defineJob
457
461
  */
458
- interface JobHandle<TName extends string, TInput, TOutput> {
462
+ interface JobHandle<TName extends string, TInput, TOutput, TLabels extends Record<string, string> = Record<string, string>> {
459
463
  readonly name: TName;
460
464
  /**
461
465
  * Trigger a new run
462
466
  */
463
- trigger(input: TInput, options?: TriggerOptions): Promise<TypedRun<TOutput>>;
467
+ trigger(input: TInput, options?: TriggerOptions<TLabels>): Promise<TypedRun<TOutput, TLabels>>;
464
468
  /**
465
469
  * Trigger a new run and wait for completion
466
470
  * Returns the output directly, throws if the run fails
467
471
  */
468
- triggerAndWait(input: TInput, options?: TriggerOptions): Promise<TriggerAndWaitResult<TOutput>>;
472
+ triggerAndWait(input: TInput, options?: TriggerAndWaitOptions<TLabels>): Promise<TriggerAndWaitResult<TOutput>>;
469
473
  /**
470
474
  * Trigger multiple runs in a batch
471
475
  * All inputs are validated before any runs are created
472
476
  */
473
- batchTrigger(inputs: BatchTriggerInput<TInput>[]): Promise<TypedRun<TOutput>[]>;
477
+ batchTrigger(inputs: BatchTriggerInput<TInput, TLabels>[]): Promise<TypedRun<TOutput, TLabels>[]>;
474
478
  /**
475
479
  * Get a run by ID
476
480
  */
477
- getRun(id: string): Promise<TypedRun<TOutput> | null>;
481
+ getRun(id: string): Promise<TypedRun<TOutput, TLabels> | null>;
478
482
  /**
479
483
  * Get runs with optional filter
480
484
  */
481
- getRuns(filter?: Omit<RunFilter, 'jobName'>): Promise<TypedRun<TOutput>[]>;
485
+ getRuns(filter?: Omit<RunFilter<TLabels>, 'jobName'>): Promise<TypedRun<TOutput, TLabels>[]>;
482
486
  }
483
487
 
484
488
  /**
@@ -544,29 +548,47 @@ declare function defineJob<TName extends string, TInputSchema extends z.ZodType,
544
548
  /**
545
549
  * Options for creating a Durably instance
546
550
  */
547
- interface DurablyOptions {
551
+ interface DurablyOptions<TLabels extends Record<string, string> = Record<string, string>, TJobs extends Record<string, JobDefinition<string, any, any>> = Record<string, never>> {
548
552
  dialect: Dialect;
549
553
  pollingInterval?: number;
550
554
  heartbeatInterval?: number;
551
555
  staleThreshold?: number;
556
+ cleanupSteps?: boolean;
557
+ /**
558
+ * Zod schema for labels. When provided:
559
+ * - Labels are type-checked at compile time
560
+ * - Labels are validated at runtime on trigger()
561
+ */
562
+ labels?: z.ZodType<TLabels>;
563
+ /**
564
+ * Job definitions to register. Shorthand for calling .register() after creation.
565
+ * @example
566
+ * ```ts
567
+ * const durably = createDurably({
568
+ * dialect,
569
+ * jobs: { importCsv: importCsvJob, syncUsers: syncUsersJob },
570
+ * })
571
+ * ```
572
+ */
573
+ jobs?: TJobs;
552
574
  }
553
575
  /**
554
576
  * Plugin interface for extending Durably
555
577
  */
556
578
  interface DurablyPlugin {
557
579
  name: string;
558
- install(durably: Durably<any>): void;
580
+ install(durably: Durably<any, any>): void;
559
581
  }
560
582
  /**
561
583
  * Helper type to transform JobDefinition record to JobHandle record
562
584
  */
563
- type TransformToHandles<TJobs extends Record<string, JobDefinition<string, unknown, unknown>>> = {
564
- [K in keyof TJobs]: TJobs[K] extends JobDefinition<infer TName, infer TInput, infer TOutput> ? JobHandle<TName & string, TInput, TOutput> : never;
585
+ type TransformToHandles<TJobs extends Record<string, JobDefinition<string, unknown, unknown>>, TLabels extends Record<string, string> = Record<string, string>> = {
586
+ [K in keyof TJobs]: TJobs[K] extends JobDefinition<infer TName, infer TInput, infer TOutput> ? JobHandle<TName & string, TInput, TOutput, TLabels> : never;
565
587
  };
566
588
  /**
567
589
  * Durably instance with type-safe jobs
568
590
  */
569
- interface Durably<TJobs extends Record<string, JobHandle<string, unknown, unknown>> = Record<string, never>> {
591
+ interface Durably<TJobs extends Record<string, JobHandle<string, unknown, unknown, Record<string, string>>> = Record<string, never>, TLabels extends Record<string, string> = Record<string, string>> {
570
592
  /**
571
593
  * Registered job handles (type-safe)
572
594
  */
@@ -622,7 +644,7 @@ interface Durably<TJobs extends Record<string, JobHandle<string, unknown, unknow
622
644
  * // Usage: durably.jobs.importCsv.trigger({ rows: [...] })
623
645
  * ```
624
646
  */
625
- register<TNewJobs extends Record<string, JobDefinition<string, any, any>>>(jobDefs: TNewJobs): Durably<TJobs & TransformToHandles<TNewJobs>>;
647
+ register<TNewJobs extends Record<string, JobDefinition<string, any, any>>>(jobDefs: TNewJobs): Durably<TJobs & TransformToHandles<TNewJobs, TLabels>, TLabels>;
626
648
  /**
627
649
  * Start the worker polling loop
628
650
  */
@@ -632,10 +654,10 @@ interface Durably<TJobs extends Record<string, JobHandle<string, unknown, unknow
632
654
  */
633
655
  stop(): Promise<void>;
634
656
  /**
635
- * Retry a failed run by resetting it to pending
636
- * @throws Error if run is not in failed status
657
+ * Create a fresh run from a completed, failed, or cancelled run
658
+ * @throws Error if run is pending, running, or does not exist
637
659
  */
638
- retry(runId: string): Promise<void>;
660
+ retrigger(runId: string): Promise<Run<TLabels>>;
639
661
  /**
640
662
  * Cancel a pending or running run
641
663
  * @throws Error if run is already completed, failed, or cancelled
@@ -658,7 +680,7 @@ interface Durably<TJobs extends Record<string, JobHandle<string, unknown, unknow
658
680
  * const typedRun = await durably.getRun<MyRun>(runId)
659
681
  * ```
660
682
  */
661
- getRun<T extends Run = Run>(runId: string): Promise<T | null>;
683
+ getRun<T extends Run<TLabels> = Run<TLabels>>(runId: string): Promise<T | null>;
662
684
  /**
663
685
  * Get runs with optional filtering
664
686
  * @example
@@ -671,7 +693,7 @@ interface Durably<TJobs extends Record<string, JobHandle<string, unknown, unknow
671
693
  * const typedRuns = await durably.getRuns<MyRun>({ jobName: 'my-job' })
672
694
  * ```
673
695
  */
674
- getRuns<T extends Run = Run>(filter?: RunFilter$1): Promise<T[]>;
696
+ getRuns<T extends Run<TLabels> = Run<TLabels>>(filter?: RunFilter<TLabels>): Promise<T[]>;
675
697
  /**
676
698
  * Register a plugin
677
699
  */
@@ -680,7 +702,7 @@ interface Durably<TJobs extends Record<string, JobHandle<string, unknown, unknow
680
702
  * Get a registered job handle by name
681
703
  * Returns undefined if job is not registered
682
704
  */
683
- getJob<TName extends string = string>(name: TName): JobHandle<TName, Record<string, unknown>, unknown> | undefined;
705
+ getJob<TName extends string = string>(name: TName): JobHandle<TName, Record<string, unknown>, unknown, TLabels> | undefined;
684
706
  /**
685
707
  * Subscribe to events for a specific run
686
708
  * Returns a ReadableStream that can be used for SSE
@@ -690,11 +712,14 @@ interface Durably<TJobs extends Record<string, JobHandle<string, unknown, unknow
690
712
  /**
691
713
  * Create a Durably instance
692
714
  */
693
- declare function createDurably(options: DurablyOptions): Durably<Record<string, never>>;
715
+ declare function createDurably<TLabels extends Record<string, string> = Record<string, string>, TJobs extends Record<string, JobDefinition<string, any, any>> = Record<string, never>>(options: DurablyOptions<TLabels, TJobs> & {
716
+ jobs: TJobs;
717
+ }): Durably<TransformToHandles<TJobs, TLabels>, TLabels>;
718
+ declare function createDurably<TLabels extends Record<string, string> = Record<string, string>>(options: DurablyOptions<TLabels>): Durably<Record<string, never>, TLabels>;
694
719
 
695
720
  /**
696
721
  * Plugin that persists log events to the database
697
722
  */
698
723
  declare function withLogPersistence(): DurablyPlugin;
699
724
 
700
- export { type StepsTable as A, createDurably as B, type ClientRun as C, type Durably as D, type ErrorHandler as E, defineJob as F, toClientRun as G, withLogPersistence as H, type JobDefinition as J, type Log as L, type Run as R, type SchemaVersionsTable as S, type TriggerAndWaitResult as T, type WorkerErrorEvent as W, type Database as a, type DurablyEvent as b, type DurablyOptions as c, type DurablyPlugin as d, type EventType as e, type JobHandle as f, type JobInput as g, type JobOutput as h, type LogWriteEvent as i, type LogsTable as j, type RunCancelEvent as k, type RunCompleteEvent as l, type RunDeleteEvent as m, type RunFailEvent as n, type RunFilter$1 as o, type RunProgressEvent as p, type RunRetryEvent 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 };
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 };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { D as Durably } from './index-BjlCb0gP.js';
2
- export { C as ClientRun, a as Database, b as DurablyEvent, c as DurablyOptions, d as DurablyPlugin, E as ErrorHandler, e as EventType, J as JobDefinition, f as JobHandle, g as JobInput, h as JobOutput, L as Log, i as LogWriteEvent, j as LogsTable, R as Run, k as RunCancelEvent, l as RunCompleteEvent, m as RunDeleteEvent, n as RunFailEvent, o as RunFilter, p as RunProgressEvent, q as RunRetryEvent, 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 TriggerAndWaitResult, W as WorkerErrorEvent, B as createDurably, F as defineJob, G as toClientRun, H as withLogPersistence } from './index-BjlCb0gP.js';
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';
3
3
  import 'kysely';
4
4
  import 'zod';
5
5
 
@@ -12,15 +12,23 @@ declare class CancelledError extends Error {
12
12
  constructor(runId: string);
13
13
  }
14
14
 
15
+ /**
16
+ * Run operation types for onRunAccess
17
+ */
18
+ type RunOperation = 'read' | 'subscribe' | 'steps' | 'retrigger' | 'cancel' | 'delete';
19
+ /**
20
+ * Subscription filter — only fields that SSE subscriptions actually support.
21
+ */
22
+ type RunsSubscribeFilter<TLabels extends Record<string, string> = Record<string, string>> = Pick<RunFilter<TLabels>, 'jobName' | 'labels'>;
15
23
  /**
16
24
  * Request body for triggering a job
17
25
  */
18
- interface TriggerRequest {
26
+ interface TriggerRequest<TLabels extends Record<string, string> = Record<string, string>> {
19
27
  jobName: string;
20
- input: Record<string, unknown>;
28
+ input: unknown;
21
29
  idempotencyKey?: string;
22
30
  concurrencyKey?: string;
23
- labels?: Record<string, string>;
31
+ labels?: TLabels;
24
32
  }
25
33
  /**
26
34
  * Response for trigger endpoint
@@ -28,127 +36,69 @@ interface TriggerRequest {
28
36
  interface TriggerResponse {
29
37
  runId: string;
30
38
  }
39
+ /**
40
+ * Auth middleware configuration.
41
+ * When `auth` is set, `authenticate` is required.
42
+ * TContext is inferred from authenticate's return type.
43
+ * TLabels is inferred from the Durably instance.
44
+ */
45
+ interface AuthConfig<TContext, TLabels extends Record<string, string> = Record<string, string>> {
46
+ /** Authenticate every request. Return context or throw Response to reject. */
47
+ authenticate: (request: Request) => Promise<TContext> | TContext;
48
+ /** Guard before trigger. Called after body validation and job resolution. */
49
+ onTrigger?: (ctx: TContext, trigger: TriggerRequest<TLabels>) => Promise<void> | void;
50
+ /** Guard before run-level operations. Run is pre-fetched. */
51
+ onRunAccess?: (ctx: TContext, run: Run<TLabels>, info: {
52
+ operation: RunOperation;
53
+ }) => Promise<void> | void;
54
+ /** Scope runs list queries (GET /runs). */
55
+ scopeRuns?: (ctx: TContext, filter: RunFilter<TLabels>) => RunFilter<TLabels> | Promise<RunFilter<TLabels>>;
56
+ /** Scope runs subscribe stream (GET /runs/subscribe). Falls back to scopeRuns if not set. */
57
+ scopeRunsSubscribe?: (ctx: TContext, filter: RunsSubscribeFilter<TLabels>) => RunsSubscribeFilter<TLabels> | Promise<RunsSubscribeFilter<TLabels>>;
58
+ }
31
59
  /**
32
60
  * Handler interface for HTTP endpoints
33
61
  */
34
62
  interface DurablyHandler {
35
63
  /**
36
- * Handle all Durably HTTP requests with automatic routing
64
+ * Handle all Durably HTTP requests with automatic routing + auth
37
65
  *
38
66
  * Routes:
39
67
  * - GET {basePath}/subscribe?runId=xxx - SSE stream
40
68
  * - GET {basePath}/runs - List runs
69
+ * - GET {basePath}/runs/subscribe - SSE stream of run updates
41
70
  * - GET {basePath}/run?runId=xxx - Get single run
71
+ * - GET {basePath}/steps?runId=xxx - Get steps
42
72
  * - POST {basePath}/trigger - Trigger a job
43
- * - POST {basePath}/retry?runId=xxx - Retry a failed run
73
+ * - POST {basePath}/retrigger?runId=xxx - Create a fresh run from a terminal run
44
74
  * - POST {basePath}/cancel?runId=xxx - Cancel a run
45
- *
46
- * @param request - The incoming HTTP request
47
- * @param basePath - The base path to strip from the URL (e.g., '/api/durably')
48
- * @returns Response or null if route not matched
49
- *
50
- * @example
51
- * ```ts
52
- * // React Router / Remix
53
- * export async function loader({ request }) {
54
- * return durablyHandler.handle(request, '/api/durably')
55
- * }
56
- * export async function action({ request }) {
57
- * return durablyHandler.handle(request, '/api/durably')
58
- * }
59
- * ```
75
+ * - DELETE {basePath}/run?runId=xxx - Delete a run
60
76
  */
61
77
  handle(request: Request, basePath: string): Promise<Response>;
62
- /**
63
- * Handle job trigger request
64
- * Expects POST with JSON body: { jobName, input, idempotencyKey?, concurrencyKey? }
65
- * Returns JSON: { runId }
66
- */
67
- trigger(request: Request): Promise<Response>;
68
- /**
69
- * Handle subscription request
70
- * Expects GET with query param: runId
71
- * Returns SSE stream of events
72
- */
73
- subscribe(request: Request): Response;
74
- /**
75
- * Handle runs list request
76
- * Expects GET with optional query params: jobName, status, limit, offset
77
- * Returns JSON array of runs
78
- */
79
- runs(request: Request): Promise<Response>;
80
- /**
81
- * Handle single run request
82
- * Expects GET with query param: runId
83
- * Returns JSON run object or 404
84
- */
85
- run(request: Request): Promise<Response>;
86
- /**
87
- * Handle retry request
88
- * Expects POST with query param: runId
89
- * Returns JSON: { success: true }
90
- */
91
- retry(request: Request): Promise<Response>;
92
- /**
93
- * Handle cancel request
94
- * Expects POST with query param: runId
95
- * Returns JSON: { success: true }
96
- */
97
- cancel(request: Request): Promise<Response>;
98
- /**
99
- * Handle delete request
100
- * Expects DELETE with query param: runId
101
- * Returns JSON: { success: true }
102
- */
103
- delete(request: Request): Promise<Response>;
104
- /**
105
- * Handle steps request
106
- * Expects GET with query param: runId
107
- * Returns JSON array of steps
108
- */
109
- steps(request: Request): Promise<Response>;
110
- /**
111
- * Handle runs subscription request
112
- * Expects GET with optional query param: jobName
113
- * Returns SSE stream of run update notifications
114
- */
115
- runsSubscribe(request: Request): Response;
116
78
  }
117
79
  /**
118
80
  * Options for createDurablyHandler
119
81
  */
120
- interface CreateDurablyHandlerOptions {
82
+ interface CreateDurablyHandlerOptions<TContext = undefined, TLabels extends Record<string, string> = Record<string, string>> {
121
83
  /**
122
- * Called before handling each request.
84
+ * Called before handling each request (after authentication).
123
85
  * Use this to initialize Durably (migrate, start worker, etc.)
124
- *
125
- * @example
126
- * ```ts
127
- * const durablyHandler = createDurablyHandler(durably, {
128
- * onRequest: async () => {
129
- * await durably.migrate()
130
- * durably.start()
131
- * }
132
- * })
133
- * ```
134
86
  */
135
87
  onRequest?: () => Promise<void> | void;
136
88
  /**
137
89
  * Throttle interval in milliseconds for SSE progress events.
138
- * When set, `run:progress` events are throttled per run so the client
139
- * receives at most one progress update per throttle window.
140
- * The first and last progress events are always delivered.
141
- * Non-progress events are never throttled.
142
- *
143
- * Set to 0 to disable throttling.
144
90
  * @default 100
145
91
  */
146
92
  sseThrottleMs?: number;
93
+ /**
94
+ * Auth middleware. When set, authenticate is required and auth applies to ALL endpoints.
95
+ */
96
+ auth?: AuthConfig<TContext, TLabels>;
147
97
  }
148
98
  /**
149
99
  * Create HTTP handlers for Durably
150
100
  * Uses Web Standard Request/Response for framework-agnostic usage
151
101
  */
152
- declare function createDurablyHandler(durably: Durably, options?: CreateDurablyHandlerOptions): DurablyHandler;
102
+ declare function createDurablyHandler<TContext = undefined, TLabels extends Record<string, string> = Record<string, string>>(durably: Durably<any, TLabels>, options?: CreateDurablyHandlerOptions<TContext, TLabels>): DurablyHandler;
153
103
 
154
- export { CancelledError, type CreateDurablyHandlerOptions, Durably, type DurablyHandler, type TriggerRequest, type TriggerResponse, createDurablyHandler };
104
+ export { type AuthConfig, CancelledError, type CreateDurablyHandlerOptions, Durably, type DurablyHandler, Run, RunFilter, type RunOperation, type RunsSubscribeFilter, type TriggerRequest, type TriggerResponse, createDurablyHandler };