@coji/durably 0.4.0 → 0.6.1

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.d.ts CHANGED
@@ -1,543 +1,142 @@
1
- import { Dialect, Kysely } from 'kysely';
2
- import { z } from 'zod';
1
+ import { D as Durably } from './index-CHQw-b_O.js';
2
+ export { p as Database, g as DurablyEvent, a as DurablyOptions, b as DurablyPlugin, E as ErrorHandler, h as EventType, J as JobDefinition, n as JobHandle, e as JobInput, f as JobOutput, u as Log, L as LogWriteEvent, q as LogsTable, v as Run, R as RunCompleteEvent, i as RunFailEvent, x as RunFilter, j as RunProgressEvent, k as RunStartEvent, r as RunsTable, s as SchemaVersionsTable, y as Step, S as StepCompleteEvent, o as StepContext, l as StepFailEvent, m as StepStartEvent, t as StepsTable, T as TriggerAndWaitResult, W as WorkerErrorEvent, c as createDurably, d as defineJob, w as withLogPersistence } from './index-CHQw-b_O.js';
3
+ import 'kysely';
4
+ import 'zod';
3
5
 
4
6
  /**
5
- * Base event interface
6
- */
7
- interface BaseEvent {
8
- type: string;
9
- timestamp: string;
10
- sequence: number;
11
- }
12
- /**
13
- * Run start event
14
- */
15
- interface RunStartEvent extends BaseEvent {
16
- type: 'run:start';
17
- runId: string;
18
- jobName: string;
19
- payload: unknown;
20
- }
21
- /**
22
- * Run complete event
23
- */
24
- interface RunCompleteEvent extends BaseEvent {
25
- type: 'run:complete';
26
- runId: string;
27
- jobName: string;
28
- output: unknown;
29
- duration: number;
30
- }
31
- /**
32
- * Run fail event
33
- */
34
- interface RunFailEvent extends BaseEvent {
35
- type: 'run:fail';
36
- runId: string;
37
- jobName: string;
38
- error: string;
39
- failedStepName: string;
40
- }
41
- /**
42
- * Step start event
43
- */
44
- interface StepStartEvent extends BaseEvent {
45
- type: 'step:start';
46
- runId: string;
47
- jobName: string;
48
- stepName: string;
49
- stepIndex: number;
50
- }
51
- /**
52
- * Step complete event
53
- */
54
- interface StepCompleteEvent extends BaseEvent {
55
- type: 'step:complete';
56
- runId: string;
57
- jobName: string;
58
- stepName: string;
59
- stepIndex: number;
60
- output: unknown;
61
- duration: number;
62
- }
63
- /**
64
- * Step fail event
65
- */
66
- interface StepFailEvent extends BaseEvent {
67
- type: 'step:fail';
68
- runId: string;
69
- jobName: string;
70
- stepName: string;
71
- stepIndex: number;
72
- error: string;
73
- }
74
- /**
75
- * Log write event
76
- */
77
- interface LogWriteEvent extends BaseEvent {
78
- type: 'log:write';
79
- runId: string;
80
- stepName: string | null;
81
- level: 'info' | 'warn' | 'error';
82
- message: string;
83
- data: unknown;
84
- }
85
- /**
86
- * Worker error event (internal errors like heartbeat failures)
87
- */
88
- interface WorkerErrorEvent extends BaseEvent {
89
- type: 'worker:error';
90
- error: string;
91
- context: string;
92
- runId?: string;
93
- }
94
- /**
95
- * All event types as discriminated union
96
- */
97
- type DurablyEvent = RunStartEvent | RunCompleteEvent | RunFailEvent | StepStartEvent | StepCompleteEvent | StepFailEvent | LogWriteEvent | WorkerErrorEvent;
98
- /**
99
- * Event types for type-safe event names
100
- */
101
- type EventType = DurablyEvent['type'];
102
- /**
103
- * Extract event by type
104
- */
105
- type EventByType<T extends EventType> = Extract<DurablyEvent, {
106
- type: T;
107
- }>;
108
- /**
109
- * Event input (without auto-generated fields)
110
- */
111
- type EventInput<T extends EventType> = Omit<EventByType<T>, 'timestamp' | 'sequence'>;
112
- /**
113
- * All possible event inputs as a union (properly distributed)
114
- */
115
- type AnyEventInput = EventInput<'run:start'> | EventInput<'run:complete'> | EventInput<'run:fail'> | EventInput<'step:start'> | EventInput<'step:complete'> | EventInput<'step:fail'> | EventInput<'log:write'> | EventInput<'worker:error'>;
116
- /**
117
- * Event listener function
118
- */
119
- type EventListener<T extends EventType> = (event: EventByType<T>) => void;
120
- /**
121
- * Unsubscribe function returned by on()
122
- */
123
- type Unsubscribe = () => void;
124
- /**
125
- * Error handler function for listener exceptions
126
- */
127
- type ErrorHandler = (error: Error, event: DurablyEvent) => void;
128
-
129
- /**
130
- * Database schema types for Durably
7
+ * Error thrown when a run is cancelled during execution.
8
+ * The worker catches this error and treats it specially - it does not
9
+ * mark the run as failed, as the run status is already 'cancelled'.
131
10
  */
132
- interface RunsTable {
133
- id: string;
134
- job_name: string;
135
- payload: string;
136
- status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled';
137
- idempotency_key: string | null;
138
- concurrency_key: string | null;
139
- current_step_index: number;
140
- progress: string | null;
141
- output: string | null;
142
- error: string | null;
143
- heartbeat_at: string;
144
- created_at: string;
145
- updated_at: string;
146
- }
147
- interface StepsTable {
148
- id: string;
149
- run_id: string;
150
- name: string;
151
- index: number;
152
- status: 'completed' | 'failed';
153
- output: string | null;
154
- error: string | null;
155
- started_at: string;
156
- completed_at: string | null;
157
- }
158
- interface LogsTable {
159
- id: string;
160
- run_id: string;
161
- step_name: string | null;
162
- level: 'info' | 'warn' | 'error';
163
- message: string;
164
- data: string | null;
165
- created_at: string;
166
- }
167
- interface SchemaVersionsTable {
168
- version: number;
169
- applied_at: string;
170
- }
171
- interface Database {
172
- durably_runs: RunsTable;
173
- durably_steps: StepsTable;
174
- durably_logs: LogsTable;
175
- durably_schema_versions: SchemaVersionsTable;
11
+ declare class CancelledError extends Error {
12
+ constructor(runId: string);
176
13
  }
177
14
 
178
15
  /**
179
- * Run data for creating a new run
16
+ * Request body for triggering a job
180
17
  */
181
- interface CreateRunInput {
18
+ interface TriggerRequest {
182
19
  jobName: string;
183
- payload: unknown;
20
+ input: Record<string, unknown>;
184
21
  idempotencyKey?: string;
185
22
  concurrencyKey?: string;
186
23
  }
187
24
  /**
188
- * Run data returned from storage
189
- */
190
- interface Run {
191
- id: string;
192
- jobName: string;
193
- payload: unknown;
194
- status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled';
195
- idempotencyKey: string | null;
196
- concurrencyKey: string | null;
197
- currentStepIndex: number;
198
- progress: {
199
- current: number;
200
- total?: number;
201
- message?: string;
202
- } | null;
203
- output: unknown | null;
204
- error: string | null;
205
- heartbeatAt: string;
206
- createdAt: string;
207
- updatedAt: string;
208
- }
209
- /**
210
- * Run update data
211
- */
212
- interface UpdateRunInput {
213
- status?: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled';
214
- currentStepIndex?: number;
215
- progress?: {
216
- current: number;
217
- total?: number;
218
- message?: string;
219
- } | null;
220
- output?: unknown;
221
- error?: string | null;
222
- heartbeatAt?: string;
223
- }
224
- /**
225
- * Run filter options
226
- */
227
- interface RunFilter$1 {
228
- status?: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled';
229
- jobName?: string;
230
- /** Maximum number of runs to return */
231
- limit?: number;
232
- /** Number of runs to skip (for pagination) */
233
- offset?: number;
234
- }
235
- /**
236
- * Step data for creating a new step
25
+ * Response for trigger endpoint
237
26
  */
238
- interface CreateStepInput {
27
+ interface TriggerResponse {
239
28
  runId: string;
240
- name: string;
241
- index: number;
242
- status: 'completed' | 'failed';
243
- output?: unknown;
244
- error?: string;
245
- startedAt: string;
246
29
  }
247
30
  /**
248
- * Step data returned from storage
31
+ * Handler interface for HTTP endpoints
249
32
  */
250
- interface Step {
251
- id: string;
252
- runId: string;
253
- name: string;
254
- index: number;
255
- status: 'completed' | 'failed';
256
- output: unknown | null;
257
- error: string | null;
258
- startedAt: string;
259
- completedAt: string | null;
260
- }
261
- /**
262
- * Log data for creating a new log
263
- */
264
- interface CreateLogInput {
265
- runId: string;
266
- stepName: string | null;
267
- level: 'info' | 'warn' | 'error';
268
- message: string;
269
- data?: unknown;
270
- }
271
- /**
272
- * Log data returned from storage
273
- */
274
- interface Log {
275
- id: string;
276
- runId: string;
277
- stepName: string | null;
278
- level: 'info' | 'warn' | 'error';
279
- message: string;
280
- data: unknown | null;
281
- createdAt: string;
282
- }
283
- /**
284
- * Storage interface for database operations
285
- */
286
- interface Storage {
287
- createRun(input: CreateRunInput): Promise<Run>;
288
- batchCreateRuns(inputs: CreateRunInput[]): Promise<Run[]>;
289
- updateRun(runId: string, data: UpdateRunInput): Promise<void>;
290
- deleteRun(runId: string): Promise<void>;
291
- getRun(runId: string): Promise<Run | null>;
292
- getRuns(filter?: RunFilter$1): Promise<Run[]>;
293
- getNextPendingRun(excludeConcurrencyKeys: string[]): Promise<Run | null>;
294
- createStep(input: CreateStepInput): Promise<Step>;
295
- getSteps(runId: string): Promise<Step[]>;
296
- getCompletedStep(runId: string, name: string): Promise<Step | null>;
297
- createLog(input: CreateLogInput): Promise<Log>;
298
- getLogs(runId: string): Promise<Log[]>;
299
- }
300
-
301
- /**
302
- * Step context passed to the job function
303
- */
304
- interface StepContext {
33
+ interface DurablyHandler {
305
34
  /**
306
- * The ID of the current run
35
+ * Handle all Durably HTTP requests with automatic routing
36
+ *
37
+ * Routes:
38
+ * - GET {basePath}/subscribe?runId=xxx - SSE stream
39
+ * - GET {basePath}/runs - List runs
40
+ * - GET {basePath}/run?runId=xxx - Get single run
41
+ * - POST {basePath}/trigger - Trigger a job
42
+ * - POST {basePath}/retry?runId=xxx - Retry a failed run
43
+ * - POST {basePath}/cancel?runId=xxx - Cancel a run
44
+ *
45
+ * @param request - The incoming HTTP request
46
+ * @param basePath - The base path to strip from the URL (e.g., '/api/durably')
47
+ * @returns Response or null if route not matched
48
+ *
49
+ * @example
50
+ * ```ts
51
+ * // React Router / Remix
52
+ * export async function loader({ request }) {
53
+ * return durablyHandler.handle(request, '/api/durably')
54
+ * }
55
+ * export async function action({ request }) {
56
+ * return durablyHandler.handle(request, '/api/durably')
57
+ * }
58
+ * ```
307
59
  */
308
- readonly runId: string;
60
+ handle(request: Request, basePath: string): Promise<Response>;
309
61
  /**
310
- * Execute a step with automatic persistence and replay
62
+ * Handle job trigger request
63
+ * Expects POST with JSON body: { jobName, input, idempotencyKey?, concurrencyKey? }
64
+ * Returns JSON: { runId }
311
65
  */
312
- run<T>(name: string, fn: () => T | Promise<T>): Promise<T>;
66
+ trigger(request: Request): Promise<Response>;
313
67
  /**
314
- * Report progress for the current run
68
+ * Handle subscription request
69
+ * Expects GET with query param: runId
70
+ * Returns SSE stream of events
315
71
  */
316
- progress(current: number, total?: number, message?: string): void;
72
+ subscribe(request: Request): Response;
317
73
  /**
318
- * Log a message
74
+ * Handle runs list request
75
+ * Expects GET with optional query params: jobName, status, limit, offset
76
+ * Returns JSON array of runs
319
77
  */
320
- log: {
321
- info(message: string, data?: unknown): void;
322
- warn(message: string, data?: unknown): void;
323
- error(message: string, data?: unknown): void;
324
- };
325
- }
326
- /**
327
- * Trigger options
328
- */
329
- interface TriggerOptions {
330
- idempotencyKey?: string;
331
- concurrencyKey?: string;
332
- /** Timeout in milliseconds for triggerAndWait() */
333
- timeout?: number;
334
- }
335
- /**
336
- * Run filter options
337
- */
338
- interface RunFilter {
339
- status?: 'pending' | 'running' | 'completed' | 'failed';
340
- jobName?: string;
341
- }
342
- /**
343
- * Typed run with output type
344
- */
345
- interface TypedRun<TOutput> extends Omit<Run, 'output'> {
346
- output: TOutput | null;
347
- }
348
- /**
349
- * Batch trigger input - either just the input or input with options
350
- */
351
- type BatchTriggerInput<TInput> = TInput | {
352
- input: TInput;
353
- options?: TriggerOptions;
354
- };
355
- /**
356
- * Result of triggerAndWait
357
- */
358
- interface TriggerAndWaitResult<TOutput> {
359
- id: string;
360
- output: TOutput;
361
- }
362
- /**
363
- * Job handle returned by defineJob
364
- */
365
- interface JobHandle<TName extends string, TInput, TOutput> {
366
- readonly name: TName;
78
+ runs(request: Request): Promise<Response>;
367
79
  /**
368
- * Trigger a new run
80
+ * Handle single run request
81
+ * Expects GET with query param: runId
82
+ * Returns JSON run object or 404
369
83
  */
370
- trigger(input: TInput, options?: TriggerOptions): Promise<TypedRun<TOutput>>;
84
+ run(request: Request): Promise<Response>;
371
85
  /**
372
- * Trigger a new run and wait for completion
373
- * Returns the output directly, throws if the run fails
86
+ * Handle retry request
87
+ * Expects POST with query param: runId
88
+ * Returns JSON: { success: true }
374
89
  */
375
- triggerAndWait(input: TInput, options?: TriggerOptions): Promise<TriggerAndWaitResult<TOutput>>;
90
+ retry(request: Request): Promise<Response>;
376
91
  /**
377
- * Trigger multiple runs in a batch
378
- * All inputs are validated before any runs are created
92
+ * Handle cancel request
93
+ * Expects POST with query param: runId
94
+ * Returns JSON: { success: true }
379
95
  */
380
- batchTrigger(inputs: BatchTriggerInput<TInput>[]): Promise<TypedRun<TOutput>[]>;
96
+ cancel(request: Request): Promise<Response>;
381
97
  /**
382
- * Get a run by ID
98
+ * Handle delete request
99
+ * Expects DELETE with query param: runId
100
+ * Returns JSON: { success: true }
383
101
  */
384
- getRun(id: string): Promise<TypedRun<TOutput> | null>;
102
+ delete(request: Request): Promise<Response>;
385
103
  /**
386
- * Get runs with optional filter
104
+ * Handle steps request
105
+ * Expects GET with query param: runId
106
+ * Returns JSON array of steps
387
107
  */
388
- getRuns(filter?: Omit<RunFilter, 'jobName'>): Promise<TypedRun<TOutput>[]>;
389
- }
390
-
391
- /**
392
- * Job run function type
393
- */
394
- type JobRunFunction<TInput, TOutput> = (step: StepContext, payload: TInput) => Promise<TOutput>;
395
- /**
396
- * Job definition - a standalone description of a job
397
- * This is the result of calling defineJob() and can be passed to durably.register()
398
- */
399
- interface JobDefinition<TName extends string, TInput, TOutput> {
400
- readonly name: TName;
401
- readonly input: z.ZodType<TInput>;
402
- readonly output: z.ZodType<TOutput> | undefined;
403
- readonly run: JobRunFunction<TInput, TOutput>;
404
- }
405
- /**
406
- * Configuration for defining a job
407
- */
408
- interface DefineJobConfig<TName extends string, TInputSchema extends z.ZodType, TOutputSchema extends z.ZodType | undefined> {
409
- name: TName;
410
- input: TInputSchema;
411
- output?: TOutputSchema;
412
- run: JobRunFunction<z.infer<TInputSchema>, TOutputSchema extends z.ZodType ? z.infer<TOutputSchema> : void>;
413
- }
414
- /**
415
- * Define a job - creates a JobDefinition that can be registered with durably.register()
416
- *
417
- * @example
418
- * ```ts
419
- * import { defineJob } from '@coji/durably'
420
- * import { z } from 'zod'
421
- *
422
- * export const syncUsers = defineJob({
423
- * name: 'sync-users',
424
- * input: z.object({ orgId: z.string() }),
425
- * output: z.object({ syncedCount: z.number() }),
426
- * run: async (step, payload) => {
427
- * const users = await step.run('fetch-users', () => fetchUsers(payload.orgId))
428
- * return { syncedCount: users.length }
429
- * },
430
- * })
431
- * ```
432
- */
433
- declare function defineJob<TName extends string, TInputSchema extends z.ZodType, TOutputSchema extends z.ZodType | undefined = undefined>(config: DefineJobConfig<TName, TInputSchema, TOutputSchema>): JobDefinition<TName, z.infer<TInputSchema>, TOutputSchema extends z.ZodType ? z.infer<TOutputSchema> : void>;
434
-
435
- /**
436
- * Options for creating a Durably instance
437
- */
438
- interface DurablyOptions {
439
- dialect: Dialect;
440
- pollingInterval?: number;
441
- heartbeatInterval?: number;
442
- staleThreshold?: number;
443
- }
444
- /**
445
- * Plugin interface for extending Durably
446
- */
447
- interface DurablyPlugin {
448
- name: string;
449
- install(durably: Durably): void;
108
+ steps(request: Request): Promise<Response>;
109
+ /**
110
+ * Handle runs subscription request
111
+ * Expects GET with optional query param: jobName
112
+ * Returns SSE stream of run update notifications
113
+ */
114
+ runsSubscribe(request: Request): Response;
450
115
  }
451
116
  /**
452
- * Durably instance
117
+ * Options for createDurablyHandler
453
118
  */
454
- interface Durably {
455
- /**
456
- * Run database migrations
457
- * This is idempotent and safe to call multiple times
458
- */
459
- migrate(): Promise<void>;
460
- /**
461
- * Get the underlying Kysely database instance
462
- * Useful for testing and advanced use cases
463
- */
464
- readonly db: Kysely<Database>;
465
- /**
466
- * Storage layer for database operations
467
- */
468
- readonly storage: Storage;
469
- /**
470
- * Register an event listener
471
- * @returns Unsubscribe function
472
- */
473
- on<T extends EventType>(type: T, listener: EventListener<T>): Unsubscribe;
474
- /**
475
- * Emit an event (auto-assigns timestamp and sequence)
476
- */
477
- emit(event: AnyEventInput): void;
478
- /**
479
- * Register an error handler for listener exceptions
480
- */
481
- onError(handler: ErrorHandler): void;
482
- /**
483
- * Register a job definition and return a job handle
484
- * Same JobDefinition can be registered multiple times (idempotent)
485
- * Different JobDefinitions with the same name will throw an error
486
- */
487
- register<TName extends string, TInput, TOutput>(jobDef: JobDefinition<TName, TInput, TOutput>): JobHandle<TName, TInput, TOutput>;
488
- /**
489
- * Start the worker polling loop
490
- */
491
- start(): void;
492
- /**
493
- * Stop the worker after current run completes
494
- */
495
- stop(): Promise<void>;
496
- /**
497
- * Retry a failed run by resetting it to pending
498
- * @throws Error if run is not in failed status
499
- */
500
- retry(runId: string): Promise<void>;
501
- /**
502
- * Cancel a pending or running run
503
- * @throws Error if run is already completed, failed, or cancelled
504
- */
505
- cancel(runId: string): Promise<void>;
119
+ interface CreateDurablyHandlerOptions {
506
120
  /**
507
- * Delete a completed, failed, or cancelled run and its associated steps and logs
508
- * @throws Error if run is pending or running, or does not exist
121
+ * Called before handling each request.
122
+ * Use this to initialize Durably (migrate, start worker, etc.)
123
+ *
124
+ * @example
125
+ * ```ts
126
+ * const durablyHandler = createDurablyHandler(durably, {
127
+ * onRequest: async () => {
128
+ * await durably.migrate()
129
+ * durably.start()
130
+ * }
131
+ * })
132
+ * ```
509
133
  */
510
- deleteRun(runId: string): Promise<void>;
511
- /**
512
- * Get a run by ID (returns unknown output type)
513
- */
514
- getRun(runId: string): Promise<Run | null>;
515
- /**
516
- * Get runs with optional filtering
517
- */
518
- getRuns(filter?: RunFilter$1): Promise<Run[]>;
519
- /**
520
- * Register a plugin
521
- */
522
- use(plugin: DurablyPlugin): void;
134
+ onRequest?: () => Promise<void> | void;
523
135
  }
524
136
  /**
525
- * Create a Durably instance
137
+ * Create HTTP handlers for Durably
138
+ * Uses Web Standard Request/Response for framework-agnostic usage
526
139
  */
527
- declare function createDurably(options: DurablyOptions): Durably;
528
-
529
- /**
530
- * Plugin that persists log events to the database
531
- */
532
- declare function withLogPersistence(): DurablyPlugin;
533
-
534
- /**
535
- * Error thrown when a run is cancelled during execution.
536
- * The worker catches this error and treats it specially - it does not
537
- * mark the run as failed, as the run status is already 'cancelled'.
538
- */
539
- declare class CancelledError extends Error {
540
- constructor(runId: string);
541
- }
140
+ declare function createDurablyHandler(durably: Durably, options?: CreateDurablyHandlerOptions): DurablyHandler;
542
141
 
543
- export { CancelledError, type Database, type Durably, type DurablyEvent, type DurablyOptions, type DurablyPlugin, type ErrorHandler, type EventType, type JobDefinition, type JobHandle, type Log, type LogWriteEvent, type LogsTable, type Run, type RunCompleteEvent, type RunFailEvent, type RunFilter$1 as RunFilter, type RunStartEvent, type RunsTable, type SchemaVersionsTable, type Step, type StepCompleteEvent, type StepContext, type StepFailEvent, type StepStartEvent, type StepsTable, type TriggerAndWaitResult, type WorkerErrorEvent, createDurably, defineJob, withLogPersistence };
142
+ export { CancelledError, Durably, type DurablyHandler, type TriggerRequest, type TriggerResponse, createDurablyHandler };