@coji/durably 0.8.1 → 0.10.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.
@@ -1,3 +1,3 @@
1
- export { w as withLogPersistence } from '../index-4aPZWn8r.js';
1
+ export { H as withLogPersistence } from '../index-BjlCb0gP.js';
2
2
  import 'kysely';
3
3
  import 'zod';
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, payload) => {
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(payload.orgId)
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,17 +117,17 @@ 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
 
119
128
  ### step.progress(current, total?, message?)
120
129
 
121
- Updates progress information for the run.
130
+ 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.
122
131
 
123
132
  ```ts
124
133
  step.progress(50, 100, 'Processing items...')
@@ -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
- payload: { userId: string }
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
- payload: { userId: string }
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))
@@ -273,7 +294,9 @@ Create HTTP handlers for client/server architecture using Web Standard Request/R
273
294
  ```ts
274
295
  import { createDurablyHandler } from '@coji/durably'
275
296
 
276
- const handler = createDurablyHandler(durably)
297
+ const handler = createDurablyHandler(durably, {
298
+ sseThrottleMs: 100, // default: throttle progress SSE events (0 to disable)
299
+ })
277
300
 
278
301
  // Use the unified handle() method with automatic routing
279
302
  app.all('/api/durably/*', async (req) => {
@@ -292,6 +315,22 @@ app.post('/api/durably/cancel', (req) => handler.cancel(req))
292
315
  app.delete('/api/durably/run', (req) => handler.delete(req))
293
316
  ```
294
317
 
318
+ **Label filtering via query params:**
319
+
320
+ ```http
321
+ GET /runs?label.organizationId=org_123
322
+ GET /runs/subscribe?label.organizationId=org_123&label.env=prod
323
+ ```
324
+
325
+ **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:
326
+
327
+ ```ts
328
+ import { toClientRun } from '@coji/durably'
329
+
330
+ const run = await durably.getRun(runId)
331
+ const clientRun = toClientRun(run) // strips internal fields
332
+ ```
333
+
295
334
  **Handler Interface:**
296
335
 
297
336
  ```ts
@@ -316,6 +355,7 @@ interface TriggerRequest {
316
355
  input: Record<string, unknown>
317
356
  idempotencyKey?: string
318
357
  concurrencyKey?: string
358
+ labels?: Record<string, string>
319
359
  }
320
360
 
321
361
  interface TriggerResponse {
@@ -395,12 +435,13 @@ interface JobDefinition<TName, TInput, TOutput> {
395
435
  name: TName
396
436
  input: ZodType<TInput>
397
437
  output?: ZodType<TOutput>
398
- run: (step: StepContext, payload: TInput) => Promise<TOutput>
438
+ run: (step: StepContext, input: TInput) => Promise<TOutput>
399
439
  }
400
440
 
441
+ // AbortSignal is aborted when the run is cancelled
401
442
  interface StepContext {
402
443
  runId: string
403
- run<T>(name: string, fn: () => T | Promise<T>): Promise<T>
444
+ run<T>(name: string, fn: (signal: AbortSignal) => T | Promise<T>): Promise<T>
404
445
  progress(current: number, total?: number, message?: string): void
405
446
  log: {
406
447
  info(message: string, data?: unknown): void
@@ -413,10 +454,13 @@ interface Run<TOutput = unknown> {
413
454
  id: string
414
455
  jobName: string
415
456
  status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled'
416
- payload: unknown
457
+ input: unknown
458
+ labels: Record<string, string>
417
459
  output?: TOutput
418
460
  error?: string
419
461
  progress?: { current: number; total?: number; message?: string }
462
+ startedAt: string | null
463
+ completedAt: string | null
420
464
  createdAt: string
421
465
  updatedAt: string
422
466
  }
@@ -436,8 +480,17 @@ interface JobHandle<TName, TInput, TOutput> {
436
480
  interface TriggerOptions {
437
481
  idempotencyKey?: string
438
482
  concurrencyKey?: string
483
+ labels?: Record<string, string>
439
484
  timeout?: number
440
485
  }
486
+
487
+ interface RunFilter {
488
+ status?: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled'
489
+ jobName?: string
490
+ labels?: Record<string, string>
491
+ limit?: number
492
+ offset?: number
493
+ }
441
494
  ```
442
495
 
443
496
  ## License
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coji/durably",
3
- "version": "0.8.1",
3
+ "version": "0.10.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.3.11",
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.1",
55
- "@types/react": "^19.2.7",
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.2",
58
- "@vitest/browser": "^4.0.16",
59
- "@vitest/browser-playwright": "4.0.16",
60
- "jsdom": "^27.4.0",
61
- "kysely": "^0.28.9",
62
- "playwright": "^1.57.0",
63
- "prettier": "^3.7.4",
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.3",
66
- "react-dom": "^19.2.3",
67
- "sqlocal": "^0.16.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.16",
71
- "zod": "^4.3.5"
70
+ "vitest": "^4.0.18",
71
+ "zod": "^4.3.6"
72
72
  },
73
73
  "scripts": {
74
74
  "build": "tsup",