@coji/durably 0.8.0 → 0.9.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 +1 -1
- package/dist/{index-4aPZWn8r.d.ts → index-BjlCb0gP.d.ts} +69 -19
- package/dist/index.d.ts +3 -2
- package/dist/index.js +250 -80
- package/dist/index.js.map +1 -1
- package/dist/plugins/index.d.ts +1 -1
- package/docs/llms.md +61 -10
- package/package.json +16 -16
package/dist/plugins/index.d.ts
CHANGED
package/docs/llms.md
CHANGED
|
@@ -46,10 +46,10 @@ const syncUsersJob = defineJob({
|
|
|
46
46
|
name: 'sync-users',
|
|
47
47
|
input: z.object({ orgId: z.string() }),
|
|
48
48
|
output: z.object({ syncedCount: z.number() }),
|
|
49
|
-
run: async (step,
|
|
49
|
+
run: async (step, input) => {
|
|
50
50
|
// Step 1: Fetch users (result is persisted)
|
|
51
51
|
const users = await step.run('fetch-users', async () => {
|
|
52
|
-
return await api.fetchUsers(
|
|
52
|
+
return await api.fetchUsers(input.orgId)
|
|
53
53
|
})
|
|
54
54
|
|
|
55
55
|
// Step 2: Save to database
|
|
@@ -100,6 +100,15 @@ await syncUsers.trigger(
|
|
|
100
100
|
|
|
101
101
|
// With concurrency key (serializes execution)
|
|
102
102
|
await syncUsers.trigger({ orgId: 'org_123' }, { concurrencyKey: 'org_123' })
|
|
103
|
+
|
|
104
|
+
// With labels (for filtering)
|
|
105
|
+
await syncUsers.trigger({ orgId: 'org_123' }, { labels: { source: 'browser' } })
|
|
106
|
+
|
|
107
|
+
// Labels for multi-tenancy
|
|
108
|
+
await syncUsers.trigger(
|
|
109
|
+
{ orgId: 'org_123' },
|
|
110
|
+
{ labels: { organizationId: 'org_123', env: 'prod' } },
|
|
111
|
+
)
|
|
103
112
|
```
|
|
104
113
|
|
|
105
114
|
## Step Context API
|
|
@@ -108,11 +117,11 @@ The `step` object provides these methods:
|
|
|
108
117
|
|
|
109
118
|
### step.run(name, fn)
|
|
110
119
|
|
|
111
|
-
Executes a step and persists its result. On resume, returns cached result without re-executing.
|
|
120
|
+
Executes a step and persists its result. On resume, returns cached result without re-executing. The callback receives an `AbortSignal` that is aborted when the run is cancelled, enabling cooperative cancellation of long-running steps.
|
|
112
121
|
|
|
113
122
|
```ts
|
|
114
|
-
const result = await step.run('step-name', async () => {
|
|
115
|
-
return await someAsyncOperation()
|
|
123
|
+
const result = await step.run('step-name', async (signal) => {
|
|
124
|
+
return await someAsyncOperation({ signal })
|
|
116
125
|
})
|
|
117
126
|
```
|
|
118
127
|
|
|
@@ -150,7 +159,7 @@ const run = await durably.getRun(runId)
|
|
|
150
159
|
|
|
151
160
|
// Via durably instance (typed with generic parameter)
|
|
152
161
|
type MyRun = Run & {
|
|
153
|
-
|
|
162
|
+
input: { userId: string }
|
|
154
163
|
output: { count: number } | null
|
|
155
164
|
}
|
|
156
165
|
const typedRun = await durably.getRun<MyRun>(runId)
|
|
@@ -170,9 +179,19 @@ const runs = await durably.getRuns({
|
|
|
170
179
|
offset: 0,
|
|
171
180
|
})
|
|
172
181
|
|
|
182
|
+
// Filter by labels
|
|
183
|
+
const browserRuns = await durably.getRuns({
|
|
184
|
+
labels: { source: 'browser' },
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
// Filter by labels (multi-tenancy)
|
|
188
|
+
const orgRuns = await durably.getRuns({
|
|
189
|
+
labels: { organizationId: 'org_123' },
|
|
190
|
+
})
|
|
191
|
+
|
|
173
192
|
// Typed getRuns with generic parameter
|
|
174
193
|
type MyRun = Run & {
|
|
175
|
-
|
|
194
|
+
input: { userId: string }
|
|
176
195
|
output: { count: number } | null
|
|
177
196
|
}
|
|
178
197
|
const typedRuns = await durably.getRuns<MyRun>({ jobName: 'my-job' })
|
|
@@ -207,6 +226,7 @@ durably.on('run:start', (e) => console.log('Started:', e.runId))
|
|
|
207
226
|
durably.on('run:complete', (e) => console.log('Done:', e.output))
|
|
208
227
|
durably.on('run:fail', (e) => console.error('Failed:', e.error))
|
|
209
228
|
durably.on('run:cancel', (e) => console.log('Cancelled:', e.runId))
|
|
229
|
+
durably.on('run:delete', (e) => console.log('Deleted:', e.runId))
|
|
210
230
|
durably.on('run:retry', (e) => console.log('Retried:', e.runId))
|
|
211
231
|
durably.on('run:progress', (e) =>
|
|
212
232
|
console.log('Progress:', e.progress.current, '/', e.progress.total),
|
|
@@ -216,6 +236,7 @@ durably.on('run:progress', (e) =>
|
|
|
216
236
|
durably.on('step:start', (e) => console.log('Step:', e.stepName))
|
|
217
237
|
durably.on('step:complete', (e) => console.log('Step done:', e.stepName))
|
|
218
238
|
durably.on('step:fail', (e) => console.error('Step failed:', e.stepName))
|
|
239
|
+
durably.on('step:cancel', (e) => console.log('Step cancelled:', e.stepName))
|
|
219
240
|
|
|
220
241
|
// Log events
|
|
221
242
|
durably.on('log:write', (e) => console.log(`[${e.level}]`, e.message))
|
|
@@ -292,6 +313,22 @@ app.post('/api/durably/cancel', (req) => handler.cancel(req))
|
|
|
292
313
|
app.delete('/api/durably/run', (req) => handler.delete(req))
|
|
293
314
|
```
|
|
294
315
|
|
|
316
|
+
**Label filtering via query params:**
|
|
317
|
+
|
|
318
|
+
```http
|
|
319
|
+
GET /runs?label.organizationId=org_123
|
|
320
|
+
GET /runs/subscribe?label.organizationId=org_123&label.env=prod
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
**Response Shape:** The `/runs` and `/run` endpoints return `ClientRun` objects (internal fields like `heartbeatAt`, `idempotencyKey`, `concurrencyKey`, `updatedAt` are stripped). Use `toClientRun()` to apply the same projection in custom code:
|
|
324
|
+
|
|
325
|
+
```ts
|
|
326
|
+
import { toClientRun } from '@coji/durably'
|
|
327
|
+
|
|
328
|
+
const run = await durably.getRun(runId)
|
|
329
|
+
const clientRun = toClientRun(run) // strips internal fields
|
|
330
|
+
```
|
|
331
|
+
|
|
295
332
|
**Handler Interface:**
|
|
296
333
|
|
|
297
334
|
```ts
|
|
@@ -316,6 +353,7 @@ interface TriggerRequest {
|
|
|
316
353
|
input: Record<string, unknown>
|
|
317
354
|
idempotencyKey?: string
|
|
318
355
|
concurrencyKey?: string
|
|
356
|
+
labels?: Record<string, string>
|
|
319
357
|
}
|
|
320
358
|
|
|
321
359
|
interface TriggerResponse {
|
|
@@ -395,12 +433,13 @@ interface JobDefinition<TName, TInput, TOutput> {
|
|
|
395
433
|
name: TName
|
|
396
434
|
input: ZodType<TInput>
|
|
397
435
|
output?: ZodType<TOutput>
|
|
398
|
-
run: (step: StepContext,
|
|
436
|
+
run: (step: StepContext, input: TInput) => Promise<TOutput>
|
|
399
437
|
}
|
|
400
438
|
|
|
439
|
+
// AbortSignal is aborted when the run is cancelled
|
|
401
440
|
interface StepContext {
|
|
402
441
|
runId: string
|
|
403
|
-
run<T>(name: string, fn: () => T | Promise<T>): Promise<T>
|
|
442
|
+
run<T>(name: string, fn: (signal: AbortSignal) => T | Promise<T>): Promise<T>
|
|
404
443
|
progress(current: number, total?: number, message?: string): void
|
|
405
444
|
log: {
|
|
406
445
|
info(message: string, data?: unknown): void
|
|
@@ -413,10 +452,13 @@ interface Run<TOutput = unknown> {
|
|
|
413
452
|
id: string
|
|
414
453
|
jobName: string
|
|
415
454
|
status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled'
|
|
416
|
-
|
|
455
|
+
input: unknown
|
|
456
|
+
labels: Record<string, string>
|
|
417
457
|
output?: TOutput
|
|
418
458
|
error?: string
|
|
419
459
|
progress?: { current: number; total?: number; message?: string }
|
|
460
|
+
startedAt: string | null
|
|
461
|
+
completedAt: string | null
|
|
420
462
|
createdAt: string
|
|
421
463
|
updatedAt: string
|
|
422
464
|
}
|
|
@@ -436,8 +478,17 @@ interface JobHandle<TName, TInput, TOutput> {
|
|
|
436
478
|
interface TriggerOptions {
|
|
437
479
|
idempotencyKey?: string
|
|
438
480
|
concurrencyKey?: string
|
|
481
|
+
labels?: Record<string, string>
|
|
439
482
|
timeout?: number
|
|
440
483
|
}
|
|
484
|
+
|
|
485
|
+
interface RunFilter {
|
|
486
|
+
status?: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled'
|
|
487
|
+
jobName?: string
|
|
488
|
+
labels?: Record<string, string>
|
|
489
|
+
limit?: number
|
|
490
|
+
offset?: number
|
|
491
|
+
}
|
|
441
492
|
```
|
|
442
493
|
|
|
443
494
|
## License
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coji/durably",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.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,27 +48,27 @@
|
|
|
48
48
|
"ulidx": "^2.4.1"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
|
-
"@biomejs/biome": "^2.
|
|
51
|
+
"@biomejs/biome": "^2.4.5",
|
|
52
52
|
"@libsql/client": "^0.17.0",
|
|
53
53
|
"@libsql/kysely-libsql": "^0.4.1",
|
|
54
|
-
"@testing-library/react": "^16.3.
|
|
55
|
-
"@types/react": "^19.2.
|
|
54
|
+
"@testing-library/react": "^16.3.2",
|
|
55
|
+
"@types/react": "^19.2.14",
|
|
56
56
|
"@types/react-dom": "^19.2.3",
|
|
57
|
-
"@vitejs/plugin-react": "^5.1.
|
|
58
|
-
"@vitest/browser": "^4.0.
|
|
59
|
-
"@vitest/browser-playwright": "4.0.
|
|
60
|
-
"jsdom": "^
|
|
61
|
-
"kysely": "^0.28.
|
|
62
|
-
"playwright": "^1.
|
|
63
|
-
"prettier": "^3.
|
|
57
|
+
"@vitejs/plugin-react": "^5.1.4",
|
|
58
|
+
"@vitest/browser": "^4.0.18",
|
|
59
|
+
"@vitest/browser-playwright": "4.0.18",
|
|
60
|
+
"jsdom": "^28.1.0",
|
|
61
|
+
"kysely": "^0.28.11",
|
|
62
|
+
"playwright": "^1.58.2",
|
|
63
|
+
"prettier": "^3.8.1",
|
|
64
64
|
"prettier-plugin-organize-imports": "^4.3.0",
|
|
65
|
-
"react": "^19.2.
|
|
66
|
-
"react-dom": "^19.2.
|
|
67
|
-
"sqlocal": "^0.
|
|
65
|
+
"react": "^19.2.4",
|
|
66
|
+
"react-dom": "^19.2.4",
|
|
67
|
+
"sqlocal": "^0.17.0",
|
|
68
68
|
"tsup": "^8.5.1",
|
|
69
69
|
"typescript": "^5.9.3",
|
|
70
|
-
"vitest": "^4.0.
|
|
71
|
-
"zod": "^4.3.
|
|
70
|
+
"vitest": "^4.0.18",
|
|
71
|
+
"zod": "^4.3.6"
|
|
72
72
|
},
|
|
73
73
|
"scripts": {
|
|
74
74
|
"build": "tsup",
|