@coji/durably 0.3.0 → 0.6.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/docs/llms.md CHANGED
@@ -10,10 +10,10 @@ Durably is a minimal workflow engine that persists step results to SQLite. If a
10
10
 
11
11
  ```bash
12
12
  # Node.js with libsql (recommended)
13
- npm install @coji/durably kysely zod @libsql/client @libsql/kysely-libsql
13
+ pnpm add @coji/durably kysely zod @libsql/client @libsql/kysely-libsql
14
14
 
15
15
  # Browser with SQLocal
16
- npm install @coji/durably kysely zod sqlocal
16
+ pnpm add @coji/durably kysely zod sqlocal
17
17
  ```
18
18
 
19
19
  ## Core Concepts
@@ -39,15 +39,14 @@ const durably = createDurably({
39
39
  ### 2. Job Definition
40
40
 
41
41
  ```ts
42
+ import { defineJob } from '@coji/durably'
42
43
  import { z } from 'zod'
43
44
 
44
- const syncUsers = durably.defineJob(
45
- {
46
- name: 'sync-users',
47
- input: z.object({ orgId: z.string() }),
48
- output: z.object({ syncedCount: z.number() }),
49
- },
50
- async (step, payload) => {
45
+ const syncUsersJob = defineJob({
46
+ name: 'sync-users',
47
+ input: z.object({ orgId: z.string() }),
48
+ output: z.object({ syncedCount: z.number() }),
49
+ run: async (step, payload) => {
51
50
  // Step 1: Fetch users (result is persisted)
52
51
  const users = await step.run('fetch-users', async () => {
53
52
  return await api.fetchUsers(payload.orgId)
@@ -60,17 +59,23 @@ const syncUsers = durably.defineJob(
60
59
 
61
60
  return { syncedCount: users.length }
62
61
  },
63
- )
62
+ })
63
+
64
+ // Register jobs with durably instance
65
+ const { syncUsers } = durably.register({
66
+ syncUsers: syncUsersJob,
67
+ })
64
68
  ```
65
69
 
66
70
  ### 3. Starting the Worker
67
71
 
68
72
  ```ts
69
- // Run migrations (creates tables if needed)
70
- await durably.migrate()
73
+ // Initialize: runs migrations and starts the worker
74
+ await durably.init()
71
75
 
72
- // Start the worker (polls for pending jobs)
73
- durably.start()
76
+ // Or separately if needed:
77
+ // await durably.migrate() // Run migrations only
78
+ // durably.start() // Start worker only
74
79
  ```
75
80
 
76
81
  ### 4. Triggering Jobs
@@ -182,25 +187,134 @@ await durably.deleteRun(runId)
182
187
  Subscribe to job execution events:
183
188
 
184
189
  ```ts
190
+ // Run lifecycle events
191
+ durably.on('run:trigger', (e) => console.log('Triggered:', e.runId))
185
192
  durably.on('run:start', (e) => console.log('Started:', e.runId))
186
193
  durably.on('run:complete', (e) => console.log('Done:', e.output))
187
194
  durably.on('run:fail', (e) => console.error('Failed:', e.error))
195
+ durably.on('run:cancel', (e) => console.log('Cancelled:', e.runId))
196
+ durably.on('run:retry', (e) => console.log('Retried:', e.runId))
197
+ durably.on('run:progress', (e) =>
198
+ console.log('Progress:', e.progress.current, '/', e.progress.total),
199
+ )
188
200
 
201
+ // Step events
189
202
  durably.on('step:start', (e) => console.log('Step:', e.stepName))
190
203
  durably.on('step:complete', (e) => console.log('Step done:', e.stepName))
191
- durably.on('step:skip', (e) =>
192
- console.log('Step skipped (cached):', e.stepName),
193
- )
204
+ durably.on('step:fail', (e) => console.error('Step failed:', e.stepName))
194
205
 
206
+ // Log events
195
207
  durably.on('log:write', (e) => console.log(`[${e.level}]`, e.message))
196
208
  ```
197
209
 
210
+ ## Advanced APIs
211
+
212
+ ### getJob
213
+
214
+ Get a registered job by name:
215
+
216
+ ```ts
217
+ const job = durably.getJob('sync-users')
218
+ if (job) {
219
+ const run = await job.trigger({ orgId: 'org_123' })
220
+ }
221
+ ```
222
+
223
+ ### subscribe
224
+
225
+ Subscribe to events for a specific run as a ReadableStream:
226
+
227
+ ```ts
228
+ const stream = durably.subscribe(runId)
229
+ const reader = stream.getReader()
230
+
231
+ while (true) {
232
+ const { done, value } = await reader.read()
233
+ if (done) break
234
+
235
+ switch (value.type) {
236
+ case 'run:start':
237
+ console.log('Started')
238
+ break
239
+ case 'run:complete':
240
+ console.log('Completed:', value.output)
241
+ break
242
+ case 'run:fail':
243
+ console.error('Failed:', value.error)
244
+ break
245
+ case 'run:progress':
246
+ console.log('Progress:', value.progress)
247
+ break
248
+ case 'log:write':
249
+ console.log(`[${value.level}]`, value.message)
250
+ break
251
+ }
252
+ }
253
+ ```
254
+
255
+ ### createDurablyHandler
256
+
257
+ Create HTTP handlers for client/server architecture using Web Standard Request/Response:
258
+
259
+ ```ts
260
+ import { createDurablyHandler } from '@coji/durably'
261
+
262
+ const handler = createDurablyHandler(durably)
263
+
264
+ // Use the unified handle() method with automatic routing
265
+ app.all('/api/durably/*', async (req) => {
266
+ return await handler.handle(req, '/api/durably')
267
+ })
268
+
269
+ // Or use individual endpoints
270
+ app.post('/api/durably/trigger', (req) => handler.trigger(req))
271
+ app.get('/api/durably/subscribe', (req) => handler.subscribe(req))
272
+ app.get('/api/durably/runs', (req) => handler.runs(req))
273
+ app.get('/api/durably/run', (req) => handler.run(req))
274
+ app.get('/api/durably/steps', (req) => handler.steps(req))
275
+ app.get('/api/durably/runs/subscribe', (req) => handler.runsSubscribe(req))
276
+ app.post('/api/durably/retry', (req) => handler.retry(req))
277
+ app.post('/api/durably/cancel', (req) => handler.cancel(req))
278
+ app.delete('/api/durably/run', (req) => handler.delete(req))
279
+ ```
280
+
281
+ **Handler Interface:**
282
+
283
+ ```ts
284
+ interface DurablyHandler {
285
+ // Unified routing handler
286
+ handle(request: Request, basePath: string): Promise<Response>
287
+
288
+ // Individual endpoints
289
+ trigger(request: Request): Promise<Response> // POST /trigger
290
+ subscribe(request: Request): Response // GET /subscribe?runId=xxx (SSE)
291
+ runs(request: Request): Promise<Response> // GET /runs
292
+ run(request: Request): Promise<Response> // GET /run?runId=xxx
293
+ steps(request: Request): Promise<Response> // GET /steps?runId=xxx
294
+ runsSubscribe(request: Request): Response // GET /runs/subscribe (SSE)
295
+ retry(request: Request): Promise<Response> // POST /retry?runId=xxx
296
+ cancel(request: Request): Promise<Response> // POST /cancel?runId=xxx
297
+ delete(request: Request): Promise<Response> // DELETE /run?runId=xxx
298
+ }
299
+
300
+ interface TriggerRequest {
301
+ jobName: string
302
+ input: Record<string, unknown>
303
+ idempotencyKey?: string
304
+ concurrencyKey?: string
305
+ }
306
+
307
+ interface TriggerResponse {
308
+ runId: string
309
+ }
310
+ ```
311
+
198
312
  ## Plugins
199
313
 
200
314
  ### Log Persistence
201
315
 
202
316
  ```ts
203
- import { withLogPersistence } from '@coji/durably/plugins'
317
+ import { withLogPersistence } from '@coji/durably'
204
318
 
205
319
  durably.use(withLogPersistence())
206
320
  ```
@@ -208,7 +322,7 @@ durably.use(withLogPersistence())
208
322
  ## Browser Usage
209
323
 
210
324
  ```ts
211
- import { createDurably } from '@coji/durably'
325
+ import { createDurably, defineJob } from '@coji/durably'
212
326
  import { SQLocalKysely } from 'sqlocal/kysely'
213
327
  import { z } from 'zod'
214
328
 
@@ -222,10 +336,18 @@ const durably = createDurably({
222
336
  })
223
337
 
224
338
  // Same API as Node.js
225
- const myJob = durably.defineJob(/* ... */)
339
+ const { myJob } = durably.register({
340
+ myJob: defineJob({
341
+ name: 'my-job',
342
+ input: z.object({}),
343
+ run: async (step) => {
344
+ /* ... */
345
+ },
346
+ }),
347
+ })
226
348
 
227
- await durably.migrate()
228
- durably.start()
349
+ // Initialize (same as Node.js)
350
+ await durably.init()
229
351
  ```
230
352
 
231
353
  ## Run Lifecycle
@@ -255,6 +377,13 @@ When a job resumes after interruption:
255
377
  ## Type Definitions
256
378
 
257
379
  ```ts
380
+ interface JobDefinition<TName, TInput, TOutput> {
381
+ name: TName
382
+ input: ZodType<TInput>
383
+ output?: ZodType<TOutput>
384
+ run: (step: StepContext, payload: TInput) => Promise<TOutput>
385
+ }
386
+
258
387
  interface StepContext {
259
388
  runId: string
260
389
  run<T>(name: string, fn: () => T | Promise<T>): Promise<T>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coji/durably",
3
- "version": "0.3.0",
3
+ "version": "0.6.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",
@@ -41,8 +41,8 @@
41
41
  },
42
42
  "homepage": "https://github.com/coji/durably#readme",
43
43
  "peerDependencies": {
44
- "kysely": ">=0.27.0",
45
- "zod": ">=4.0.0"
44
+ "kysely": "^0.27.0",
45
+ "zod": "^4.0.0"
46
46
  },
47
47
  "dependencies": {
48
48
  "ulidx": "^2.4.1"
@@ -68,7 +68,7 @@
68
68
  "tsup": "^8.5.1",
69
69
  "typescript": "^5.9.3",
70
70
  "vitest": "^4.0.16",
71
- "zod": "^4.2.1"
71
+ "zod": "^4.3.4"
72
72
  },
73
73
  "scripts": {
74
74
  "build": "tsup",