@coji/durably 0.2.0 → 0.3.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.
Files changed (3) hide show
  1. package/README.md +4 -0
  2. package/docs/llms.md +302 -0
  3. package/package.json +2 -1
package/README.md CHANGED
@@ -69,6 +69,10 @@ await syncUsers.trigger({ orgId: 'org_123' })
69
69
 
70
70
  For full documentation, visit [coji.github.io/durably](https://coji.github.io/durably/).
71
71
 
72
+ ### For LLMs / AI Agents
73
+
74
+ This package includes `docs/llms.md` with API documentation optimized for LLMs and coding agents. You can read it directly from `node_modules/@coji/durably/docs/llms.md` or access it at [coji.github.io/durably/llms.txt](https://coji.github.io/durably/llms.txt).
75
+
72
76
  ## License
73
77
 
74
78
  MIT
package/docs/llms.md ADDED
@@ -0,0 +1,302 @@
1
+ # Durably - LLM Documentation
2
+
3
+ > Step-oriented resumable batch execution for Node.js and browsers using SQLite.
4
+
5
+ ## Overview
6
+
7
+ Durably is a minimal workflow engine that persists step results to SQLite. If a job is interrupted (server restart, browser tab close, crash), it automatically resumes from the last successful step.
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ # Node.js with libsql (recommended)
13
+ npm install @coji/durably kysely zod @libsql/client @libsql/kysely-libsql
14
+
15
+ # Browser with SQLocal
16
+ npm install @coji/durably kysely zod sqlocal
17
+ ```
18
+
19
+ ## Core Concepts
20
+
21
+ ### 1. Durably Instance
22
+
23
+ ```ts
24
+ import { createDurably } from '@coji/durably'
25
+ import { LibsqlDialect } from '@libsql/kysely-libsql'
26
+ import { createClient } from '@libsql/client'
27
+
28
+ const client = createClient({ url: 'file:local.db' })
29
+ const dialect = new LibsqlDialect({ client })
30
+
31
+ const durably = createDurably({
32
+ dialect,
33
+ pollingInterval: 1000, // Job polling interval (ms)
34
+ heartbeatInterval: 5000, // Heartbeat update interval (ms)
35
+ staleThreshold: 30000, // When to consider a job abandoned (ms)
36
+ })
37
+ ```
38
+
39
+ ### 2. Job Definition
40
+
41
+ ```ts
42
+ import { z } from 'zod'
43
+
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) => {
51
+ // Step 1: Fetch users (result is persisted)
52
+ const users = await step.run('fetch-users', async () => {
53
+ return await api.fetchUsers(payload.orgId)
54
+ })
55
+
56
+ // Step 2: Save to database
57
+ await step.run('save-to-db', async () => {
58
+ await db.upsertUsers(users)
59
+ })
60
+
61
+ return { syncedCount: users.length }
62
+ },
63
+ )
64
+ ```
65
+
66
+ ### 3. Starting the Worker
67
+
68
+ ```ts
69
+ // Run migrations (creates tables if needed)
70
+ await durably.migrate()
71
+
72
+ // Start the worker (polls for pending jobs)
73
+ durably.start()
74
+ ```
75
+
76
+ ### 4. Triggering Jobs
77
+
78
+ ```ts
79
+ // Basic trigger (fire and forget)
80
+ const run = await syncUsers.trigger({ orgId: 'org_123' })
81
+ console.log(run.id, run.status) // "pending"
82
+
83
+ // Wait for completion
84
+ const result = await syncUsers.triggerAndWait(
85
+ { orgId: 'org_123' },
86
+ { timeout: 5000 },
87
+ )
88
+ console.log(result.output.syncedCount)
89
+
90
+ // With idempotency key (prevents duplicate jobs)
91
+ await syncUsers.trigger(
92
+ { orgId: 'org_123' },
93
+ { idempotencyKey: 'webhook-event-456' },
94
+ )
95
+
96
+ // With concurrency key (serializes execution)
97
+ await syncUsers.trigger({ orgId: 'org_123' }, { concurrencyKey: 'org_123' })
98
+ ```
99
+
100
+ ## Step Context API
101
+
102
+ The `step` object provides these methods:
103
+
104
+ ### step.run(name, fn)
105
+
106
+ Executes a step and persists its result. On resume, returns cached result without re-executing.
107
+
108
+ ```ts
109
+ const result = await step.run('step-name', async () => {
110
+ return await someAsyncOperation()
111
+ })
112
+ ```
113
+
114
+ ### step.progress(current, total?, message?)
115
+
116
+ Updates progress information for the run.
117
+
118
+ ```ts
119
+ step.progress(50, 100, 'Processing items...')
120
+ ```
121
+
122
+ ### step.log
123
+
124
+ Structured logging within jobs.
125
+
126
+ ```ts
127
+ step.log.info('Starting process', { userId: '123' })
128
+ step.log.warn('Rate limit approaching')
129
+ step.log.error('Failed to connect', { error: err.message })
130
+ ```
131
+
132
+ ## Run Management
133
+
134
+ ### Get Run Status
135
+
136
+ ```ts
137
+ // Via job handle (type-safe output)
138
+ const run = await syncUsers.getRun(runId)
139
+ if (run?.status === 'completed') {
140
+ console.log(run.output.syncedCount)
141
+ }
142
+
143
+ // Via durably instance (cross-job)
144
+ const run = await durably.getRun(runId)
145
+ ```
146
+
147
+ ### Query Runs
148
+
149
+ ```ts
150
+ // Get failed runs
151
+ const failedRuns = await durably.getRuns({ status: 'failed' })
152
+
153
+ // Filter by job name with pagination
154
+ const runs = await durably.getRuns({
155
+ jobName: 'sync-users',
156
+ status: 'completed',
157
+ limit: 10,
158
+ offset: 0,
159
+ })
160
+ ```
161
+
162
+ ### Retry Failed Runs
163
+
164
+ ```ts
165
+ await durably.retry(runId)
166
+ ```
167
+
168
+ ### Cancel Runs
169
+
170
+ ```ts
171
+ await durably.cancel(runId)
172
+ ```
173
+
174
+ ### Delete Runs
175
+
176
+ ```ts
177
+ await durably.deleteRun(runId)
178
+ ```
179
+
180
+ ## Events
181
+
182
+ Subscribe to job execution events:
183
+
184
+ ```ts
185
+ durably.on('run:start', (e) => console.log('Started:', e.runId))
186
+ durably.on('run:complete', (e) => console.log('Done:', e.output))
187
+ durably.on('run:fail', (e) => console.error('Failed:', e.error))
188
+
189
+ durably.on('step:start', (e) => console.log('Step:', e.stepName))
190
+ 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
+ )
194
+
195
+ durably.on('log:write', (e) => console.log(`[${e.level}]`, e.message))
196
+ ```
197
+
198
+ ## Plugins
199
+
200
+ ### Log Persistence
201
+
202
+ ```ts
203
+ import { withLogPersistence } from '@coji/durably/plugins'
204
+
205
+ durably.use(withLogPersistence())
206
+ ```
207
+
208
+ ## Browser Usage
209
+
210
+ ```ts
211
+ import { createDurably } from '@coji/durably'
212
+ import { SQLocalKysely } from 'sqlocal/kysely'
213
+ import { z } from 'zod'
214
+
215
+ const { dialect } = new SQLocalKysely('app.sqlite3')
216
+
217
+ const durably = createDurably({
218
+ dialect,
219
+ pollingInterval: 100,
220
+ heartbeatInterval: 500,
221
+ staleThreshold: 3000,
222
+ })
223
+
224
+ // Same API as Node.js
225
+ const myJob = durably.defineJob(/* ... */)
226
+
227
+ await durably.migrate()
228
+ durably.start()
229
+ ```
230
+
231
+ ## Run Lifecycle
232
+
233
+ ```text
234
+ trigger() → pending → running → completed
235
+ ↘ ↗
236
+ → failed
237
+ ```
238
+
239
+ - **pending**: Waiting for worker to pick up
240
+ - **running**: Worker is executing steps
241
+ - **completed**: All steps finished successfully
242
+ - **failed**: A step threw an error
243
+ - **cancelled**: Manually cancelled via `cancel()`
244
+
245
+ ## Resumability
246
+
247
+ When a job resumes after interruption:
248
+
249
+ 1. Worker polls for pending/stale runs
250
+ 2. Job function is re-executed from the beginning
251
+ 3. `step.run()` checks SQLite for cached results
252
+ 4. Completed steps return cached values immediately (no re-execution)
253
+ 5. Execution continues from the first incomplete step
254
+
255
+ ## Type Definitions
256
+
257
+ ```ts
258
+ interface StepContext {
259
+ runId: string
260
+ run<T>(name: string, fn: () => T | Promise<T>): Promise<T>
261
+ progress(current: number, total?: number, message?: string): void
262
+ log: {
263
+ info(message: string, data?: unknown): void
264
+ warn(message: string, data?: unknown): void
265
+ error(message: string, data?: unknown): void
266
+ }
267
+ }
268
+
269
+ interface Run<TOutput = unknown> {
270
+ id: string
271
+ jobName: string
272
+ status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled'
273
+ payload: unknown
274
+ output?: TOutput
275
+ error?: string
276
+ progress?: { current: number; total?: number; message?: string }
277
+ createdAt: string
278
+ updatedAt: string
279
+ }
280
+
281
+ interface JobHandle<TName, TInput, TOutput> {
282
+ name: TName
283
+ trigger(input: TInput, options?: TriggerOptions): Promise<Run<TOutput>>
284
+ triggerAndWait(
285
+ input: TInput,
286
+ options?: TriggerOptions,
287
+ ): Promise<{ id: string; output: TOutput }>
288
+ batchTrigger(inputs: BatchTriggerInput<TInput>[]): Promise<Run<TOutput>[]>
289
+ getRun(id: string): Promise<Run<TOutput> | null>
290
+ getRuns(filter?: RunFilter): Promise<Run<TOutput>[]>
291
+ }
292
+
293
+ interface TriggerOptions {
294
+ idempotencyKey?: string
295
+ concurrencyKey?: string
296
+ timeout?: number
297
+ }
298
+ ```
299
+
300
+ ## License
301
+
302
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coji/durably",
3
- "version": "0.2.0",
3
+ "version": "0.3.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",
@@ -17,6 +17,7 @@
17
17
  },
18
18
  "files": [
19
19
  "dist",
20
+ "docs",
20
21
  "README.md"
21
22
  ],
22
23
  "keywords": [