@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 +2 -2
- package/dist/{index-BjlCb0gP.d.ts → index-hM7-oiyj.d.ts} +88 -63
- package/dist/index.d.ts +46 -96
- package/dist/index.js +516 -415
- package/dist/index.js.map +1 -1
- package/dist/plugins/index.d.ts +1 -1
- package/docs/llms.md +179 -61
- package/package.json +2 -2
package/dist/plugins/index.d.ts
CHANGED
package/docs/llms.md
CHANGED
|
@@ -24,15 +24,30 @@ pnpm add @coji/durably kysely zod sqlocal
|
|
|
24
24
|
import { createDurably } from '@coji/durably'
|
|
25
25
|
import { LibsqlDialect } from '@libsql/kysely-libsql'
|
|
26
26
|
import { createClient } from '@libsql/client'
|
|
27
|
+
import { z } from 'zod'
|
|
27
28
|
|
|
28
29
|
const client = createClient({ url: 'file:local.db' })
|
|
29
30
|
const dialect = new LibsqlDialect({ client })
|
|
30
31
|
|
|
32
|
+
// Option 1: With jobs (1-step initialization, returns typed instance)
|
|
31
33
|
const durably = createDurably({
|
|
32
34
|
dialect,
|
|
33
35
|
pollingInterval: 1000, // Job polling interval (ms)
|
|
34
36
|
heartbeatInterval: 5000, // Heartbeat update interval (ms)
|
|
35
37
|
staleThreshold: 30000, // When to consider a job abandoned (ms)
|
|
38
|
+
cleanupSteps: true, // Delete step output data on terminal state (default: true)
|
|
39
|
+
// Optional: type-safe labels with Zod schema
|
|
40
|
+
// labels: z.object({ organizationId: z.string(), env: z.string() }),
|
|
41
|
+
jobs: {
|
|
42
|
+
syncUsers: syncUsersJob,
|
|
43
|
+
},
|
|
44
|
+
})
|
|
45
|
+
// durably.jobs.syncUsers is immediately available and type-safe
|
|
46
|
+
|
|
47
|
+
// Option 2: Without jobs (register later)
|
|
48
|
+
const durably = createDurably({ dialect })
|
|
49
|
+
const { syncUsers } = durably.register({
|
|
50
|
+
syncUsers: syncUsersJob,
|
|
36
51
|
})
|
|
37
52
|
```
|
|
38
53
|
|
|
@@ -60,11 +75,6 @@ const syncUsersJob = defineJob({
|
|
|
60
75
|
return { syncedCount: users.length }
|
|
61
76
|
},
|
|
62
77
|
})
|
|
63
|
-
|
|
64
|
-
// Register jobs with durably instance
|
|
65
|
-
const { syncUsers } = durably.register({
|
|
66
|
-
syncUsers: syncUsersJob,
|
|
67
|
-
})
|
|
68
78
|
```
|
|
69
79
|
|
|
70
80
|
### 3. Starting the Worker
|
|
@@ -173,7 +183,7 @@ const failedRuns = await durably.getRuns({ status: 'failed' })
|
|
|
173
183
|
|
|
174
184
|
// Filter by job name with pagination
|
|
175
185
|
const runs = await durably.getRuns({
|
|
176
|
-
jobName: 'sync-users',
|
|
186
|
+
jobName: 'sync-users', // also accepts string[] for multiple jobs
|
|
177
187
|
status: 'completed',
|
|
178
188
|
limit: 10,
|
|
179
189
|
offset: 0,
|
|
@@ -197,10 +207,12 @@ type MyRun = Run & {
|
|
|
197
207
|
const typedRuns = await durably.getRuns<MyRun>({ jobName: 'my-job' })
|
|
198
208
|
```
|
|
199
209
|
|
|
200
|
-
###
|
|
210
|
+
### Retrigger Failed Runs
|
|
201
211
|
|
|
202
212
|
```ts
|
|
203
|
-
|
|
213
|
+
// Creates a fresh run (new ID) with the same input/options
|
|
214
|
+
const newRun = await durably.retrigger(runId)
|
|
215
|
+
console.log(newRun.id) // new run ID
|
|
204
216
|
```
|
|
205
217
|
|
|
206
218
|
### Cancel Runs
|
|
@@ -227,7 +239,6 @@ durably.on('run:complete', (e) => console.log('Done:', e.output))
|
|
|
227
239
|
durably.on('run:fail', (e) => console.error('Failed:', e.error))
|
|
228
240
|
durably.on('run:cancel', (e) => console.log('Cancelled:', e.runId))
|
|
229
241
|
durably.on('run:delete', (e) => console.log('Deleted:', e.runId))
|
|
230
|
-
durably.on('run:retry', (e) => console.log('Retried:', e.runId))
|
|
231
242
|
durably.on('run:progress', (e) =>
|
|
232
243
|
console.log('Progress:', e.progress.current, '/', e.progress.total),
|
|
233
244
|
)
|
|
@@ -296,23 +307,57 @@ import { createDurablyHandler } from '@coji/durably'
|
|
|
296
307
|
|
|
297
308
|
const handler = createDurablyHandler(durably, {
|
|
298
309
|
sseThrottleMs: 100, // default: throttle progress SSE events (0 to disable)
|
|
310
|
+
onRequest: async () => {
|
|
311
|
+
// Called before each request (after auth) — useful for lazy init
|
|
312
|
+
await durably.init()
|
|
313
|
+
},
|
|
299
314
|
})
|
|
300
315
|
|
|
301
|
-
// Use the
|
|
316
|
+
// Use the handle() method with automatic routing
|
|
302
317
|
app.all('/api/durably/*', async (req) => {
|
|
303
318
|
return await handler.handle(req, '/api/durably')
|
|
304
319
|
})
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
**With auth middleware (multi-tenant):**
|
|
323
|
+
|
|
324
|
+
```ts
|
|
325
|
+
const handler = createDurablyHandler(durably, {
|
|
326
|
+
auth: {
|
|
327
|
+
// Required: authenticate every request. Throw Response to reject.
|
|
328
|
+
authenticate: async (request) => {
|
|
329
|
+
const session = await requireUser(request)
|
|
330
|
+
const orgId = await resolveCurrentOrgId(request, session.user.id)
|
|
331
|
+
return { orgId }
|
|
332
|
+
},
|
|
333
|
+
|
|
334
|
+
// Guard before trigger (called after body validation + job resolution)
|
|
335
|
+
onTrigger: async (ctx, { jobName, input, labels }) => {
|
|
336
|
+
if (labels?.organizationId !== ctx.orgId) {
|
|
337
|
+
throw new Response('Forbidden', { status: 403 })
|
|
338
|
+
}
|
|
339
|
+
},
|
|
340
|
+
|
|
341
|
+
// Guard before run-level operations (read, subscribe, steps, retrigger, cancel, delete)
|
|
342
|
+
onRunAccess: async (ctx, run, { operation }) => {
|
|
343
|
+
if (run.labels.organizationId !== ctx.orgId) {
|
|
344
|
+
throw new Response('Forbidden', { status: 403 })
|
|
345
|
+
}
|
|
346
|
+
},
|
|
305
347
|
|
|
306
|
-
//
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
348
|
+
// Scope runs list queries (GET /runs)
|
|
349
|
+
scopeRuns: async (ctx, filter) => ({
|
|
350
|
+
...filter,
|
|
351
|
+
labels: { ...filter.labels, organizationId: ctx.orgId },
|
|
352
|
+
}),
|
|
353
|
+
|
|
354
|
+
// Scope runs subscribe stream (GET /runs/subscribe). Falls back to scopeRuns if not set.
|
|
355
|
+
scopeRunsSubscribe: async (ctx, filter) => ({
|
|
356
|
+
...filter,
|
|
357
|
+
labels: { ...filter.labels, organizationId: ctx.orgId },
|
|
358
|
+
}),
|
|
359
|
+
},
|
|
360
|
+
})
|
|
316
361
|
```
|
|
317
362
|
|
|
318
363
|
**Label filtering via query params:**
|
|
@@ -335,27 +380,58 @@ const clientRun = toClientRun(run) // strips internal fields
|
|
|
335
380
|
|
|
336
381
|
```ts
|
|
337
382
|
interface DurablyHandler {
|
|
338
|
-
// Unified routing handler
|
|
339
383
|
handle(request: Request, basePath: string): Promise<Response>
|
|
384
|
+
}
|
|
340
385
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
retry(request: Request): Promise<Response> // POST /retry?runId=xxx
|
|
349
|
-
cancel(request: Request): Promise<Response> // POST /cancel?runId=xxx
|
|
350
|
-
delete(request: Request): Promise<Response> // DELETE /run?runId=xxx
|
|
386
|
+
interface CreateDurablyHandlerOptions<
|
|
387
|
+
TContext = undefined,
|
|
388
|
+
TLabels extends Record<string, string> = Record<string, string>,
|
|
389
|
+
> {
|
|
390
|
+
onRequest?: () => Promise<void> | void
|
|
391
|
+
sseThrottleMs?: number // default: 100
|
|
392
|
+
auth?: AuthConfig<TContext, TLabels>
|
|
351
393
|
}
|
|
352
394
|
|
|
353
|
-
interface
|
|
395
|
+
interface AuthConfig<
|
|
396
|
+
TContext,
|
|
397
|
+
TLabels extends Record<string, string> = Record<string, string>,
|
|
398
|
+
> {
|
|
399
|
+
authenticate: (request: Request) => Promise<TContext> | TContext
|
|
400
|
+
onTrigger?: (
|
|
401
|
+
ctx: TContext,
|
|
402
|
+
trigger: TriggerRequest<TLabels>,
|
|
403
|
+
) => Promise<void> | void
|
|
404
|
+
onRunAccess?: (
|
|
405
|
+
ctx: TContext,
|
|
406
|
+
run: Run<TLabels>,
|
|
407
|
+
info: { operation: RunOperation },
|
|
408
|
+
) => Promise<void> | void
|
|
409
|
+
scopeRuns?: (
|
|
410
|
+
ctx: TContext,
|
|
411
|
+
filter: RunFilter<TLabels>,
|
|
412
|
+
) => RunFilter<TLabels> | Promise<RunFilter<TLabels>>
|
|
413
|
+
scopeRunsSubscribe?: (
|
|
414
|
+
ctx: TContext,
|
|
415
|
+
filter: RunsSubscribeFilter<TLabels>,
|
|
416
|
+
) => RunsSubscribeFilter<TLabels> | Promise<RunsSubscribeFilter<TLabels>>
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
type RunOperation =
|
|
420
|
+
| 'read'
|
|
421
|
+
| 'subscribe'
|
|
422
|
+
| 'steps'
|
|
423
|
+
| 'retrigger'
|
|
424
|
+
| 'cancel'
|
|
425
|
+
| 'delete'
|
|
426
|
+
|
|
427
|
+
// RunsSubscribeFilter is Pick<RunFilter, 'jobName' | 'labels'>
|
|
428
|
+
|
|
429
|
+
interface TriggerRequest<TLabels> {
|
|
354
430
|
jobName: string
|
|
355
|
-
input:
|
|
431
|
+
input: unknown
|
|
356
432
|
idempotencyKey?: string
|
|
357
433
|
concurrencyKey?: string
|
|
358
|
-
labels?:
|
|
434
|
+
labels?: TLabels
|
|
359
435
|
}
|
|
360
436
|
|
|
361
437
|
interface TriggerResponse {
|
|
@@ -387,17 +463,15 @@ const durably = createDurably({
|
|
|
387
463
|
pollingInterval: 100,
|
|
388
464
|
heartbeatInterval: 500,
|
|
389
465
|
staleThreshold: 3000,
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
},
|
|
400
|
-
}),
|
|
466
|
+
jobs: {
|
|
467
|
+
myJob: defineJob({
|
|
468
|
+
name: 'my-job',
|
|
469
|
+
input: z.object({}),
|
|
470
|
+
run: async (step) => {
|
|
471
|
+
/* ... */
|
|
472
|
+
},
|
|
473
|
+
}),
|
|
474
|
+
},
|
|
401
475
|
})
|
|
402
476
|
|
|
403
477
|
// Initialize (same as Node.js)
|
|
@@ -450,44 +524,88 @@ interface StepContext {
|
|
|
450
524
|
}
|
|
451
525
|
}
|
|
452
526
|
|
|
453
|
-
|
|
527
|
+
// TLabels defaults to Record<string, string> when no labels schema is provided
|
|
528
|
+
interface Run<TLabels extends Record<string, string> = Record<string, string>> {
|
|
454
529
|
id: string
|
|
455
530
|
jobName: string
|
|
456
531
|
status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled'
|
|
457
532
|
input: unknown
|
|
458
|
-
labels:
|
|
459
|
-
output
|
|
460
|
-
error
|
|
461
|
-
progress
|
|
533
|
+
labels: TLabels
|
|
534
|
+
output: unknown | null
|
|
535
|
+
error: string | null
|
|
536
|
+
progress: { current: number; total?: number; message?: string } | null
|
|
462
537
|
startedAt: string | null
|
|
463
538
|
completedAt: string | null
|
|
464
539
|
createdAt: string
|
|
465
540
|
updatedAt: string
|
|
466
541
|
}
|
|
467
542
|
|
|
468
|
-
interface
|
|
543
|
+
interface TypedRun<
|
|
544
|
+
TOutput,
|
|
545
|
+
TLabels extends Record<string, string> = Record<string, string>,
|
|
546
|
+
> extends Omit<Run<TLabels>, 'output'> {
|
|
547
|
+
output: TOutput | null
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
interface JobHandle<
|
|
551
|
+
TName extends string,
|
|
552
|
+
TInput,
|
|
553
|
+
TOutput,
|
|
554
|
+
TLabels extends Record<string, string> = Record<string, string>,
|
|
555
|
+
> {
|
|
469
556
|
name: TName
|
|
470
|
-
trigger(
|
|
557
|
+
trigger(
|
|
558
|
+
input: TInput,
|
|
559
|
+
options?: TriggerOptions<TLabels>,
|
|
560
|
+
): Promise<TypedRun<TOutput, TLabels>>
|
|
471
561
|
triggerAndWait(
|
|
472
562
|
input: TInput,
|
|
473
|
-
options?:
|
|
563
|
+
options?: TriggerAndWaitOptions<TLabels>,
|
|
474
564
|
): Promise<{ id: string; output: TOutput }>
|
|
475
|
-
batchTrigger(
|
|
476
|
-
|
|
477
|
-
|
|
565
|
+
batchTrigger(
|
|
566
|
+
inputs: BatchTriggerInput<TInput, TLabels>[],
|
|
567
|
+
): Promise<TypedRun<TOutput, TLabels>[]>
|
|
568
|
+
getRun(id: string): Promise<TypedRun<TOutput, TLabels> | null>
|
|
569
|
+
getRuns(
|
|
570
|
+
filter?: Omit<RunFilter<TLabels>, 'jobName'>,
|
|
571
|
+
): Promise<TypedRun<TOutput, TLabels>[]>
|
|
478
572
|
}
|
|
479
573
|
|
|
480
|
-
interface TriggerOptions
|
|
574
|
+
interface TriggerOptions<
|
|
575
|
+
TLabels extends Record<string, string> = Record<string, string>,
|
|
576
|
+
> {
|
|
481
577
|
idempotencyKey?: string
|
|
482
578
|
concurrencyKey?: string
|
|
483
|
-
labels?:
|
|
579
|
+
labels?: TLabels
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
interface TriggerAndWaitOptions<
|
|
583
|
+
TLabels extends Record<string, string> = Record<string, string>,
|
|
584
|
+
> extends TriggerOptions<TLabels> {
|
|
484
585
|
timeout?: number
|
|
586
|
+
onProgress?: (progress: ProgressData) => void | Promise<void>
|
|
587
|
+
onLog?: (log: LogData) => void | Promise<void>
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
interface ProgressData {
|
|
591
|
+
current: number
|
|
592
|
+
total?: number
|
|
593
|
+
message?: string
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
interface LogData {
|
|
597
|
+
level: 'info' | 'warn' | 'error'
|
|
598
|
+
message: string
|
|
599
|
+
data?: unknown
|
|
600
|
+
stepName?: string | null
|
|
485
601
|
}
|
|
486
602
|
|
|
487
|
-
interface RunFilter
|
|
603
|
+
interface RunFilter<
|
|
604
|
+
TLabels extends Record<string, string> = Record<string, string>,
|
|
605
|
+
> {
|
|
488
606
|
status?: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled'
|
|
489
|
-
jobName?: string
|
|
490
|
-
labels?:
|
|
607
|
+
jobName?: string | string[]
|
|
608
|
+
labels?: Partial<TLabels>
|
|
491
609
|
limit?: number
|
|
492
610
|
offset?: number
|
|
493
611
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coji/durably",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.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",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"ulidx": "^2.4.1"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
|
-
"@biomejs/biome": "^2.4.
|
|
51
|
+
"@biomejs/biome": "^2.4.6",
|
|
52
52
|
"@libsql/client": "^0.17.0",
|
|
53
53
|
"@libsql/kysely-libsql": "^0.4.1",
|
|
54
54
|
"@testing-library/react": "^16.3.2",
|