@coji/durably 0.4.0 → 0.6.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 +15 -44
- package/dist/index.d.ts +237 -12
- package/dist/index.js +643 -45
- package/dist/index.js.map +1 -1
- package/docs/llms.md +129 -17
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -4,65 +4,36 @@ Step-oriented resumable batch execution for Node.js and browsers using SQLite.
|
|
|
4
4
|
|
|
5
5
|
**[Documentation](https://coji.github.io/durably/)** | **[GitHub](https://github.com/coji/durably)** | **[Live Demo](https://durably-demo.vercel.app)**
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
- Resumable batch processing with step-level persistence
|
|
10
|
-
- Works in both Node.js and browsers
|
|
11
|
-
- Uses SQLite for state management (better-sqlite3/libsql for Node.js, SQLite WASM for browsers)
|
|
12
|
-
- Minimal dependencies - just Kysely and Zod as peer dependencies
|
|
13
|
-
- Event system for monitoring and extensibility
|
|
14
|
-
- Type-safe input/output with Zod schemas
|
|
7
|
+
> **Note:** This package is ESM-only. CommonJS is not supported.
|
|
15
8
|
|
|
16
9
|
## Installation
|
|
17
10
|
|
|
18
11
|
```bash
|
|
19
|
-
# Node.js with better-sqlite3
|
|
20
12
|
npm install @coji/durably kysely zod better-sqlite3
|
|
21
|
-
|
|
22
|
-
# Node.js with libsql
|
|
23
|
-
npm install @coji/durably kysely zod @libsql/client @libsql/kysely-libsql
|
|
24
|
-
|
|
25
|
-
# Browser with SQLocal
|
|
26
|
-
npm install @coji/durably kysely zod sqlocal
|
|
27
13
|
```
|
|
28
14
|
|
|
29
|
-
|
|
15
|
+
See the [Getting Started Guide](https://coji.github.io/durably/guide/getting-started) for other SQLite backends (libsql, SQLocal for browsers).
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
30
18
|
|
|
31
19
|
```ts
|
|
32
|
-
import { createDurably } from '@coji/durably'
|
|
33
|
-
import SQLite from 'better-sqlite3'
|
|
34
|
-
import { SqliteDialect } from 'kysely'
|
|
20
|
+
import { createDurably, defineJob } from '@coji/durably'
|
|
35
21
|
import { z } from 'zod'
|
|
36
22
|
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const syncUsers = durably.defineJob(
|
|
44
|
-
{
|
|
45
|
-
name: 'sync-users',
|
|
46
|
-
input: z.object({ orgId: z.string() }),
|
|
47
|
-
output: z.object({ syncedCount: z.number() }),
|
|
48
|
-
},
|
|
49
|
-
async (step, payload) => {
|
|
50
|
-
const users = await step.run('fetch-users', async () => {
|
|
51
|
-
return api.fetchUsers(payload.orgId)
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
await step.run('save-to-db', async () => {
|
|
55
|
-
await db.upsertUsers(users)
|
|
23
|
+
const myJob = defineJob({
|
|
24
|
+
name: 'my-job',
|
|
25
|
+
input: z.object({ id: z.string() }),
|
|
26
|
+
run: async (step, payload) => {
|
|
27
|
+
await step.run('step-1', async () => {
|
|
28
|
+
/* ... */
|
|
56
29
|
})
|
|
57
|
-
|
|
58
|
-
return { syncedCount: users.length }
|
|
59
30
|
},
|
|
60
|
-
)
|
|
31
|
+
})
|
|
61
32
|
|
|
62
|
-
|
|
63
|
-
durably.start()
|
|
33
|
+
const durably = createDurably({ dialect }).register({ myJob })
|
|
64
34
|
|
|
65
|
-
await
|
|
35
|
+
await durably.init() // migrate + start
|
|
36
|
+
await durably.jobs.myJob.trigger({ id: '123' })
|
|
66
37
|
```
|
|
67
38
|
|
|
68
39
|
## Documentation
|
package/dist/index.d.ts
CHANGED
|
@@ -9,6 +9,15 @@ interface BaseEvent {
|
|
|
9
9
|
timestamp: string;
|
|
10
10
|
sequence: number;
|
|
11
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* Run trigger event (emitted when a job is triggered, before worker picks it up)
|
|
14
|
+
*/
|
|
15
|
+
interface RunTriggerEvent extends BaseEvent {
|
|
16
|
+
type: 'run:trigger';
|
|
17
|
+
runId: string;
|
|
18
|
+
jobName: string;
|
|
19
|
+
payload: unknown;
|
|
20
|
+
}
|
|
12
21
|
/**
|
|
13
22
|
* Run start event
|
|
14
23
|
*/
|
|
@@ -38,6 +47,35 @@ interface RunFailEvent extends BaseEvent {
|
|
|
38
47
|
error: string;
|
|
39
48
|
failedStepName: string;
|
|
40
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Run cancel event
|
|
52
|
+
*/
|
|
53
|
+
interface RunCancelEvent extends BaseEvent {
|
|
54
|
+
type: 'run:cancel';
|
|
55
|
+
runId: string;
|
|
56
|
+
jobName: string;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Run retry event (emitted when a failed run is retried)
|
|
60
|
+
*/
|
|
61
|
+
interface RunRetryEvent extends BaseEvent {
|
|
62
|
+
type: 'run:retry';
|
|
63
|
+
runId: string;
|
|
64
|
+
jobName: string;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Run progress event
|
|
68
|
+
*/
|
|
69
|
+
interface RunProgressEvent extends BaseEvent {
|
|
70
|
+
type: 'run:progress';
|
|
71
|
+
runId: string;
|
|
72
|
+
jobName: string;
|
|
73
|
+
progress: {
|
|
74
|
+
current: number;
|
|
75
|
+
total?: number;
|
|
76
|
+
message?: string;
|
|
77
|
+
};
|
|
78
|
+
}
|
|
41
79
|
/**
|
|
42
80
|
* Step start event
|
|
43
81
|
*/
|
|
@@ -94,7 +132,7 @@ interface WorkerErrorEvent extends BaseEvent {
|
|
|
94
132
|
/**
|
|
95
133
|
* All event types as discriminated union
|
|
96
134
|
*/
|
|
97
|
-
type DurablyEvent = RunStartEvent | RunCompleteEvent | RunFailEvent | StepStartEvent | StepCompleteEvent | StepFailEvent | LogWriteEvent | WorkerErrorEvent;
|
|
135
|
+
type DurablyEvent = RunTriggerEvent | RunStartEvent | RunCompleteEvent | RunFailEvent | RunCancelEvent | RunRetryEvent | RunProgressEvent | StepStartEvent | StepCompleteEvent | StepFailEvent | LogWriteEvent | WorkerErrorEvent;
|
|
98
136
|
/**
|
|
99
137
|
* Event types for type-safe event names
|
|
100
138
|
*/
|
|
@@ -112,7 +150,7 @@ type EventInput<T extends EventType> = Omit<EventByType<T>, 'timestamp' | 'seque
|
|
|
112
150
|
/**
|
|
113
151
|
* All possible event inputs as a union (properly distributed)
|
|
114
152
|
*/
|
|
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'>;
|
|
153
|
+
type AnyEventInput = EventInput<'run:trigger'> | EventInput<'run:start'> | EventInput<'run:complete'> | EventInput<'run:fail'> | EventInput<'run:cancel'> | EventInput<'run:retry'> | EventInput<'run:progress'> | EventInput<'step:start'> | EventInput<'step:complete'> | EventInput<'step:fail'> | EventInput<'log:write'> | EventInput<'worker:error'>;
|
|
116
154
|
/**
|
|
117
155
|
* Event listener function
|
|
118
156
|
*/
|
|
@@ -195,6 +233,7 @@ interface Run {
|
|
|
195
233
|
idempotencyKey: string | null;
|
|
196
234
|
concurrencyKey: string | null;
|
|
197
235
|
currentStepIndex: number;
|
|
236
|
+
stepCount: number;
|
|
198
237
|
progress: {
|
|
199
238
|
current: number;
|
|
200
239
|
total?: number;
|
|
@@ -336,8 +375,12 @@ interface TriggerOptions {
|
|
|
336
375
|
* Run filter options
|
|
337
376
|
*/
|
|
338
377
|
interface RunFilter {
|
|
339
|
-
status?: 'pending' | 'running' | 'completed' | 'failed';
|
|
378
|
+
status?: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled';
|
|
340
379
|
jobName?: string;
|
|
380
|
+
/** Maximum number of runs to return */
|
|
381
|
+
limit?: number;
|
|
382
|
+
/** Number of runs to skip (for pagination) */
|
|
383
|
+
offset?: number;
|
|
341
384
|
}
|
|
342
385
|
/**
|
|
343
386
|
* Typed run with output type
|
|
@@ -402,6 +445,22 @@ interface JobDefinition<TName extends string, TInput, TOutput> {
|
|
|
402
445
|
readonly output: z.ZodType<TOutput> | undefined;
|
|
403
446
|
readonly run: JobRunFunction<TInput, TOutput>;
|
|
404
447
|
}
|
|
448
|
+
/**
|
|
449
|
+
* Extract input type from a JobDefinition
|
|
450
|
+
* @example
|
|
451
|
+
* ```ts
|
|
452
|
+
* type Input = JobInput<typeof myJob> // { userId: string }
|
|
453
|
+
* ```
|
|
454
|
+
*/
|
|
455
|
+
type JobInput<T> = T extends JobDefinition<string, infer TInput, unknown> ? TInput : never;
|
|
456
|
+
/**
|
|
457
|
+
* Extract output type from a JobDefinition
|
|
458
|
+
* @example
|
|
459
|
+
* ```ts
|
|
460
|
+
* type Output = JobOutput<typeof myJob> // { count: number }
|
|
461
|
+
* ```
|
|
462
|
+
*/
|
|
463
|
+
type JobOutput<T> = T extends JobDefinition<string, unknown, infer TOutput> ? TOutput : never;
|
|
405
464
|
/**
|
|
406
465
|
* Configuration for defining a job
|
|
407
466
|
*/
|
|
@@ -446,12 +505,33 @@ interface DurablyOptions {
|
|
|
446
505
|
*/
|
|
447
506
|
interface DurablyPlugin {
|
|
448
507
|
name: string;
|
|
449
|
-
install(durably: Durably): void;
|
|
508
|
+
install(durably: Durably<any>): void;
|
|
450
509
|
}
|
|
451
510
|
/**
|
|
452
|
-
*
|
|
511
|
+
* Helper type to transform JobDefinition record to JobHandle record
|
|
453
512
|
*/
|
|
454
|
-
|
|
513
|
+
type TransformToHandles<TJobs extends Record<string, JobDefinition<string, unknown, unknown>>> = {
|
|
514
|
+
[K in keyof TJobs]: TJobs[K] extends JobDefinition<infer TName, infer TInput, infer TOutput> ? JobHandle<TName & string, TInput, TOutput> : never;
|
|
515
|
+
};
|
|
516
|
+
/**
|
|
517
|
+
* Durably instance with type-safe jobs
|
|
518
|
+
*/
|
|
519
|
+
interface Durably<TJobs extends Record<string, JobHandle<string, unknown, unknown>> = Record<string, never>> {
|
|
520
|
+
/**
|
|
521
|
+
* Registered job handles (type-safe)
|
|
522
|
+
*/
|
|
523
|
+
readonly jobs: TJobs;
|
|
524
|
+
/**
|
|
525
|
+
* Initialize Durably: run migrations and start the worker
|
|
526
|
+
* This is the recommended way to start Durably.
|
|
527
|
+
* Equivalent to calling migrate() then start().
|
|
528
|
+
* @example
|
|
529
|
+
* ```ts
|
|
530
|
+
* const durably = createDurably({ dialect }).register({ ... })
|
|
531
|
+
* await durably.init()
|
|
532
|
+
* ```
|
|
533
|
+
*/
|
|
534
|
+
init(): Promise<void>;
|
|
455
535
|
/**
|
|
456
536
|
* Run database migrations
|
|
457
537
|
* This is idempotent and safe to call multiple times
|
|
@@ -480,11 +560,19 @@ interface Durably {
|
|
|
480
560
|
*/
|
|
481
561
|
onError(handler: ErrorHandler): void;
|
|
482
562
|
/**
|
|
483
|
-
* Register
|
|
484
|
-
*
|
|
485
|
-
*
|
|
563
|
+
* Register job definitions and return a new Durably instance with type-safe jobs
|
|
564
|
+
* @example
|
|
565
|
+
* ```ts
|
|
566
|
+
* const durably = createDurably({ dialect })
|
|
567
|
+
* .register({
|
|
568
|
+
* importCsv: importCsvJob,
|
|
569
|
+
* syncUsers: syncUsersJob,
|
|
570
|
+
* })
|
|
571
|
+
* await durably.migrate()
|
|
572
|
+
* // Usage: durably.jobs.importCsv.trigger({ rows: [...] })
|
|
573
|
+
* ```
|
|
486
574
|
*/
|
|
487
|
-
register<
|
|
575
|
+
register<TNewJobs extends Record<string, JobDefinition<string, any, any>>>(jobDefs: TNewJobs): Durably<TJobs & TransformToHandles<TNewJobs>>;
|
|
488
576
|
/**
|
|
489
577
|
* Start the worker polling loop
|
|
490
578
|
*/
|
|
@@ -520,11 +608,21 @@ interface Durably {
|
|
|
520
608
|
* Register a plugin
|
|
521
609
|
*/
|
|
522
610
|
use(plugin: DurablyPlugin): void;
|
|
611
|
+
/**
|
|
612
|
+
* Get a registered job handle by name
|
|
613
|
+
* Returns undefined if job is not registered
|
|
614
|
+
*/
|
|
615
|
+
getJob<TName extends string = string>(name: TName): JobHandle<TName, Record<string, unknown>, unknown> | undefined;
|
|
616
|
+
/**
|
|
617
|
+
* Subscribe to events for a specific run
|
|
618
|
+
* Returns a ReadableStream that can be used for SSE
|
|
619
|
+
*/
|
|
620
|
+
subscribe(runId: string): ReadableStream<DurablyEvent>;
|
|
523
621
|
}
|
|
524
622
|
/**
|
|
525
623
|
* Create a Durably instance
|
|
526
624
|
*/
|
|
527
|
-
declare function createDurably(options: DurablyOptions): Durably
|
|
625
|
+
declare function createDurably(options: DurablyOptions): Durably<Record<string, never>>;
|
|
528
626
|
|
|
529
627
|
/**
|
|
530
628
|
* Plugin that persists log events to the database
|
|
@@ -540,4 +638,131 @@ declare class CancelledError extends Error {
|
|
|
540
638
|
constructor(runId: string);
|
|
541
639
|
}
|
|
542
640
|
|
|
543
|
-
|
|
641
|
+
/**
|
|
642
|
+
* Request body for triggering a job
|
|
643
|
+
*/
|
|
644
|
+
interface TriggerRequest {
|
|
645
|
+
jobName: string;
|
|
646
|
+
input: Record<string, unknown>;
|
|
647
|
+
idempotencyKey?: string;
|
|
648
|
+
concurrencyKey?: string;
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
651
|
+
* Response for trigger endpoint
|
|
652
|
+
*/
|
|
653
|
+
interface TriggerResponse {
|
|
654
|
+
runId: string;
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* Handler interface for HTTP endpoints
|
|
658
|
+
*/
|
|
659
|
+
interface DurablyHandler {
|
|
660
|
+
/**
|
|
661
|
+
* Handle all Durably HTTP requests with automatic routing
|
|
662
|
+
*
|
|
663
|
+
* Routes:
|
|
664
|
+
* - GET {basePath}/subscribe?runId=xxx - SSE stream
|
|
665
|
+
* - GET {basePath}/runs - List runs
|
|
666
|
+
* - GET {basePath}/run?runId=xxx - Get single run
|
|
667
|
+
* - POST {basePath}/trigger - Trigger a job
|
|
668
|
+
* - POST {basePath}/retry?runId=xxx - Retry a failed run
|
|
669
|
+
* - POST {basePath}/cancel?runId=xxx - Cancel a run
|
|
670
|
+
*
|
|
671
|
+
* @param request - The incoming HTTP request
|
|
672
|
+
* @param basePath - The base path to strip from the URL (e.g., '/api/durably')
|
|
673
|
+
* @returns Response or null if route not matched
|
|
674
|
+
*
|
|
675
|
+
* @example
|
|
676
|
+
* ```ts
|
|
677
|
+
* // React Router / Remix
|
|
678
|
+
* export async function loader({ request }) {
|
|
679
|
+
* return durablyHandler.handle(request, '/api/durably')
|
|
680
|
+
* }
|
|
681
|
+
* export async function action({ request }) {
|
|
682
|
+
* return durablyHandler.handle(request, '/api/durably')
|
|
683
|
+
* }
|
|
684
|
+
* ```
|
|
685
|
+
*/
|
|
686
|
+
handle(request: Request, basePath: string): Promise<Response>;
|
|
687
|
+
/**
|
|
688
|
+
* Handle job trigger request
|
|
689
|
+
* Expects POST with JSON body: { jobName, input, idempotencyKey?, concurrencyKey? }
|
|
690
|
+
* Returns JSON: { runId }
|
|
691
|
+
*/
|
|
692
|
+
trigger(request: Request): Promise<Response>;
|
|
693
|
+
/**
|
|
694
|
+
* Handle subscription request
|
|
695
|
+
* Expects GET with query param: runId
|
|
696
|
+
* Returns SSE stream of events
|
|
697
|
+
*/
|
|
698
|
+
subscribe(request: Request): Response;
|
|
699
|
+
/**
|
|
700
|
+
* Handle runs list request
|
|
701
|
+
* Expects GET with optional query params: jobName, status, limit, offset
|
|
702
|
+
* Returns JSON array of runs
|
|
703
|
+
*/
|
|
704
|
+
runs(request: Request): Promise<Response>;
|
|
705
|
+
/**
|
|
706
|
+
* Handle single run request
|
|
707
|
+
* Expects GET with query param: runId
|
|
708
|
+
* Returns JSON run object or 404
|
|
709
|
+
*/
|
|
710
|
+
run(request: Request): Promise<Response>;
|
|
711
|
+
/**
|
|
712
|
+
* Handle retry request
|
|
713
|
+
* Expects POST with query param: runId
|
|
714
|
+
* Returns JSON: { success: true }
|
|
715
|
+
*/
|
|
716
|
+
retry(request: Request): Promise<Response>;
|
|
717
|
+
/**
|
|
718
|
+
* Handle cancel request
|
|
719
|
+
* Expects POST with query param: runId
|
|
720
|
+
* Returns JSON: { success: true }
|
|
721
|
+
*/
|
|
722
|
+
cancel(request: Request): Promise<Response>;
|
|
723
|
+
/**
|
|
724
|
+
* Handle delete request
|
|
725
|
+
* Expects DELETE with query param: runId
|
|
726
|
+
* Returns JSON: { success: true }
|
|
727
|
+
*/
|
|
728
|
+
delete(request: Request): Promise<Response>;
|
|
729
|
+
/**
|
|
730
|
+
* Handle steps request
|
|
731
|
+
* Expects GET with query param: runId
|
|
732
|
+
* Returns JSON array of steps
|
|
733
|
+
*/
|
|
734
|
+
steps(request: Request): Promise<Response>;
|
|
735
|
+
/**
|
|
736
|
+
* Handle runs subscription request
|
|
737
|
+
* Expects GET with optional query param: jobName
|
|
738
|
+
* Returns SSE stream of run update notifications
|
|
739
|
+
*/
|
|
740
|
+
runsSubscribe(request: Request): Response;
|
|
741
|
+
}
|
|
742
|
+
/**
|
|
743
|
+
* Options for createDurablyHandler
|
|
744
|
+
*/
|
|
745
|
+
interface CreateDurablyHandlerOptions {
|
|
746
|
+
/**
|
|
747
|
+
* Called before handling each request.
|
|
748
|
+
* Use this to initialize Durably (migrate, start worker, etc.)
|
|
749
|
+
*
|
|
750
|
+
* @example
|
|
751
|
+
* ```ts
|
|
752
|
+
* const durablyHandler = createDurablyHandler(durably, {
|
|
753
|
+
* onRequest: async () => {
|
|
754
|
+
* await durably.migrate()
|
|
755
|
+
* durably.start()
|
|
756
|
+
* }
|
|
757
|
+
* })
|
|
758
|
+
* ```
|
|
759
|
+
*/
|
|
760
|
+
onRequest?: () => Promise<void> | void;
|
|
761
|
+
}
|
|
762
|
+
/**
|
|
763
|
+
* Create HTTP handlers for Durably
|
|
764
|
+
* Uses Web Standard Request/Response for framework-agnostic usage
|
|
765
|
+
*/
|
|
766
|
+
declare function createDurablyHandler(durably: Durably, options?: CreateDurablyHandlerOptions): DurablyHandler;
|
|
767
|
+
|
|
768
|
+
export { CancelledError, type Database, type Durably, type DurablyEvent, type DurablyHandler, type DurablyOptions, type DurablyPlugin, type ErrorHandler, type EventType, type JobDefinition, type JobHandle, type JobInput, type JobOutput, type Log, type LogWriteEvent, type LogsTable, type Run, type RunCompleteEvent, type RunFailEvent, type RunFilter$1 as RunFilter, type RunProgressEvent, type RunStartEvent, type RunsTable, type SchemaVersionsTable, type Step, type StepCompleteEvent, type StepContext, type StepFailEvent, type StepStartEvent, type StepsTable, type TriggerAndWaitResult, type TriggerRequest, type TriggerResponse, type WorkerErrorEvent, createDurably, createDurablyHandler, defineJob, withLogPersistence };
|