@coji/durably 0.9.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 +50 -89
- package/dist/index.js +470 -243
- package/dist/index.js.map +1 -1
- package/dist/plugins/index.d.ts +1 -1
- package/docs/llms.md +178 -60
- 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
|
|
@@ -127,7 +136,7 @@ const result = await step.run('step-name', async (signal) => {
|
|
|
127
136
|
|
|
128
137
|
### step.progress(current, total?, message?)
|
|
129
138
|
|
|
130
|
-
Updates progress information for the run.
|
|
139
|
+
Updates progress information for the run. Call freely in loops — SSE delivery is throttled by `sseThrottleMs` (default 100ms) so clients receive smooth updates without flooding.
|
|
131
140
|
|
|
132
141
|
```ts
|
|
133
142
|
step.progress(50, 100, 'Processing items...')
|
|
@@ -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,
|
|
@@ -294,23 +303,59 @@ Create HTTP handlers for client/server architecture using Web Standard Request/R
|
|
|
294
303
|
```ts
|
|
295
304
|
import { createDurablyHandler } from '@coji/durably'
|
|
296
305
|
|
|
297
|
-
const handler = createDurablyHandler(durably
|
|
306
|
+
const handler = createDurablyHandler(durably, {
|
|
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
|
+
},
|
|
312
|
+
})
|
|
298
313
|
|
|
299
|
-
// Use the
|
|
314
|
+
// Use the handle() method with automatic routing
|
|
300
315
|
app.all('/api/durably/*', async (req) => {
|
|
301
316
|
return await handler.handle(req, '/api/durably')
|
|
302
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
|
+
},
|
|
303
345
|
|
|
304
|
-
//
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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
|
+
})
|
|
314
359
|
```
|
|
315
360
|
|
|
316
361
|
**Label filtering via query params:**
|
|
@@ -333,27 +378,58 @@ const clientRun = toClientRun(run) // strips internal fields
|
|
|
333
378
|
|
|
334
379
|
```ts
|
|
335
380
|
interface DurablyHandler {
|
|
336
|
-
// Unified routing handler
|
|
337
381
|
handle(request: Request, basePath: string): Promise<Response>
|
|
382
|
+
}
|
|
338
383
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
retry(request: Request): Promise<Response> // POST /retry?runId=xxx
|
|
347
|
-
cancel(request: Request): Promise<Response> // POST /cancel?runId=xxx
|
|
348
|
-
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>
|
|
349
391
|
}
|
|
350
392
|
|
|
351
|
-
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> {
|
|
352
428
|
jobName: string
|
|
353
|
-
input:
|
|
429
|
+
input: unknown
|
|
354
430
|
idempotencyKey?: string
|
|
355
431
|
concurrencyKey?: string
|
|
356
|
-
labels?:
|
|
432
|
+
labels?: TLabels
|
|
357
433
|
}
|
|
358
434
|
|
|
359
435
|
interface TriggerResponse {
|
|
@@ -385,17 +461,15 @@ const durably = createDurably({
|
|
|
385
461
|
pollingInterval: 100,
|
|
386
462
|
heartbeatInterval: 500,
|
|
387
463
|
staleThreshold: 3000,
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
},
|
|
398
|
-
}),
|
|
464
|
+
jobs: {
|
|
465
|
+
myJob: defineJob({
|
|
466
|
+
name: 'my-job',
|
|
467
|
+
input: z.object({}),
|
|
468
|
+
run: async (step) => {
|
|
469
|
+
/* ... */
|
|
470
|
+
},
|
|
471
|
+
}),
|
|
472
|
+
},
|
|
399
473
|
})
|
|
400
474
|
|
|
401
475
|
// Initialize (same as Node.js)
|
|
@@ -448,44 +522,88 @@ interface StepContext {
|
|
|
448
522
|
}
|
|
449
523
|
}
|
|
450
524
|
|
|
451
|
-
|
|
525
|
+
// TLabels defaults to Record<string, string> when no labels schema is provided
|
|
526
|
+
interface Run<TLabels extends Record<string, string> = Record<string, string>> {
|
|
452
527
|
id: string
|
|
453
528
|
jobName: string
|
|
454
529
|
status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled'
|
|
455
530
|
input: unknown
|
|
456
|
-
labels:
|
|
457
|
-
output
|
|
458
|
-
error
|
|
459
|
-
progress
|
|
531
|
+
labels: TLabels
|
|
532
|
+
output: unknown | null
|
|
533
|
+
error: string | null
|
|
534
|
+
progress: { current: number; total?: number; message?: string } | null
|
|
460
535
|
startedAt: string | null
|
|
461
536
|
completedAt: string | null
|
|
462
537
|
createdAt: string
|
|
463
538
|
updatedAt: string
|
|
464
539
|
}
|
|
465
540
|
|
|
466
|
-
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
|
+
> {
|
|
467
554
|
name: TName
|
|
468
|
-
trigger(
|
|
555
|
+
trigger(
|
|
556
|
+
input: TInput,
|
|
557
|
+
options?: TriggerOptions<TLabels>,
|
|
558
|
+
): Promise<TypedRun<TOutput, TLabels>>
|
|
469
559
|
triggerAndWait(
|
|
470
560
|
input: TInput,
|
|
471
|
-
options?:
|
|
561
|
+
options?: TriggerAndWaitOptions<TLabels>,
|
|
472
562
|
): Promise<{ id: string; output: TOutput }>
|
|
473
|
-
batchTrigger(
|
|
474
|
-
|
|
475
|
-
|
|
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>[]>
|
|
476
570
|
}
|
|
477
571
|
|
|
478
|
-
interface TriggerOptions
|
|
572
|
+
interface TriggerOptions<
|
|
573
|
+
TLabels extends Record<string, string> = Record<string, string>,
|
|
574
|
+
> {
|
|
479
575
|
idempotencyKey?: string
|
|
480
576
|
concurrencyKey?: string
|
|
481
|
-
labels?:
|
|
577
|
+
labels?: TLabels
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
interface TriggerAndWaitOptions<
|
|
581
|
+
TLabels extends Record<string, string> = Record<string, string>,
|
|
582
|
+
> extends TriggerOptions<TLabels> {
|
|
482
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
|
|
483
599
|
}
|
|
484
600
|
|
|
485
|
-
interface RunFilter
|
|
601
|
+
interface RunFilter<
|
|
602
|
+
TLabels extends Record<string, string> = Record<string, string>,
|
|
603
|
+
> {
|
|
486
604
|
status?: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled'
|
|
487
|
-
jobName?: string
|
|
488
|
-
labels?:
|
|
605
|
+
jobName?: string | string[]
|
|
606
|
+
labels?: Partial<TLabels>
|
|
489
607
|
limit?: number
|
|
490
608
|
offset?: number
|
|
491
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",
|