@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.
- package/README.md +4 -0
- package/docs/llms.md +302 -0
- 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.
|
|
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": [
|