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