ai-database 2.0.1 → 2.1.1
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/CHANGELOG.md +43 -0
- package/dist/actions.d.ts +247 -0
- package/dist/actions.d.ts.map +1 -0
- package/dist/actions.js +260 -0
- package/dist/actions.js.map +1 -0
- package/dist/ai-promise-db.d.ts +34 -2
- package/dist/ai-promise-db.d.ts.map +1 -1
- package/dist/ai-promise-db.js +511 -66
- package/dist/ai-promise-db.js.map +1 -1
- package/dist/constants.d.ts +16 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +16 -0
- package/dist/constants.js.map +1 -0
- package/dist/events.d.ts +153 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +154 -0
- package/dist/events.js.map +1 -0
- package/dist/index.d.ts +8 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -1
- package/dist/index.js.map +1 -1
- package/dist/memory-provider.d.ts +144 -2
- package/dist/memory-provider.d.ts.map +1 -1
- package/dist/memory-provider.js +569 -13
- package/dist/memory-provider.js.map +1 -1
- package/dist/schema/cascade.d.ts +96 -0
- package/dist/schema/cascade.d.ts.map +1 -0
- package/dist/schema/cascade.js +528 -0
- package/dist/schema/cascade.js.map +1 -0
- package/dist/schema/index.d.ts +197 -0
- package/dist/schema/index.d.ts.map +1 -0
- package/dist/schema/index.js +1211 -0
- package/dist/schema/index.js.map +1 -0
- package/dist/schema/parse.d.ts +225 -0
- package/dist/schema/parse.d.ts.map +1 -0
- package/dist/schema/parse.js +732 -0
- package/dist/schema/parse.js.map +1 -0
- package/dist/schema/provider.d.ts +176 -0
- package/dist/schema/provider.d.ts.map +1 -0
- package/dist/schema/provider.js +258 -0
- package/dist/schema/provider.js.map +1 -0
- package/dist/schema/resolve.d.ts +87 -0
- package/dist/schema/resolve.d.ts.map +1 -0
- package/dist/schema/resolve.js +474 -0
- package/dist/schema/resolve.js.map +1 -0
- package/dist/schema/semantic.d.ts +53 -0
- package/dist/schema/semantic.d.ts.map +1 -0
- package/dist/schema/semantic.js +247 -0
- package/dist/schema/semantic.js.map +1 -0
- package/dist/schema/types.d.ts +528 -0
- package/dist/schema/types.d.ts.map +1 -0
- package/dist/schema/types.js +9 -0
- package/dist/schema/types.js.map +1 -0
- package/dist/schema.d.ts +24 -867
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +41 -1124
- package/dist/schema.js.map +1 -1
- package/dist/semantic.d.ts +175 -0
- package/dist/semantic.d.ts.map +1 -0
- package/dist/semantic.js +338 -0
- package/dist/semantic.js.map +1 -0
- package/dist/types.d.ts +14 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +13 -4
- package/.turbo/turbo-build.log +0 -5
- package/TESTING.md +0 -410
- package/TEST_SUMMARY.md +0 -250
- package/TODO.md +0 -128
- package/src/ai-promise-db.ts +0 -1243
- package/src/authorization.ts +0 -1102
- package/src/durable-clickhouse.ts +0 -596
- package/src/durable-promise.ts +0 -582
- package/src/execution-queue.ts +0 -608
- package/src/index.test.ts +0 -868
- package/src/index.ts +0 -337
- package/src/linguistic.ts +0 -404
- package/src/memory-provider.test.ts +0 -1036
- package/src/memory-provider.ts +0 -1119
- package/src/schema.test.ts +0 -1254
- package/src/schema.ts +0 -2296
- package/src/tests.ts +0 -725
- package/src/types.ts +0 -1177
- package/test/README.md +0 -153
- package/test/edge-cases.test.ts +0 -646
- package/test/provider-resolution.test.ts +0 -402
- package/tsconfig.json +0 -9
- package/vitest.config.ts +0 -19
|
@@ -1,596 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ClickHouse-backed Durable Promise Provider
|
|
3
|
-
*
|
|
4
|
-
* Uses @mdxdb/clickhouse as the persistence layer for DurablePromise.
|
|
5
|
-
* Provides full durability, crash recovery, and observability.
|
|
6
|
-
*
|
|
7
|
-
* @packageDocumentation
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import type { ExecutionPriority, DurablePromiseOptions, BatchScheduler } from './durable-promise.js'
|
|
11
|
-
import { DurablePromise, getCurrentContext, setBatchScheduler } from './durable-promise.js'
|
|
12
|
-
import { Semaphore } from './memory-provider.js'
|
|
13
|
-
|
|
14
|
-
// =============================================================================
|
|
15
|
-
// Types
|
|
16
|
-
// =============================================================================
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* ClickHouse action row structure (minimal fields needed)
|
|
20
|
-
*/
|
|
21
|
-
interface ActionRow {
|
|
22
|
-
id: string
|
|
23
|
-
ns: string
|
|
24
|
-
actor: string
|
|
25
|
-
act: string
|
|
26
|
-
action: string
|
|
27
|
-
activity: string
|
|
28
|
-
object: string
|
|
29
|
-
objectData: Record<string, unknown>
|
|
30
|
-
status: 'pending' | 'active' | 'completed' | 'failed' | 'cancelled'
|
|
31
|
-
progress: number
|
|
32
|
-
total: number
|
|
33
|
-
result: Record<string, unknown>
|
|
34
|
-
error: string
|
|
35
|
-
data: Record<string, unknown>
|
|
36
|
-
meta: Record<string, unknown>
|
|
37
|
-
priority: number
|
|
38
|
-
batch: string
|
|
39
|
-
batchIndex: number
|
|
40
|
-
batchTotal: number
|
|
41
|
-
dependencies: string[]
|
|
42
|
-
scheduledAt: string | null
|
|
43
|
-
startedAt: string | null
|
|
44
|
-
completedAt: string | null
|
|
45
|
-
createdAt: string
|
|
46
|
-
updatedAt: string
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Executor interface for ClickHouse operations
|
|
51
|
-
*/
|
|
52
|
-
export interface ClickHouseExecutor {
|
|
53
|
-
query<T = unknown>(sql: string): Promise<T[]>
|
|
54
|
-
command(sql: string): Promise<void>
|
|
55
|
-
insert<T>(table: string, values: T[]): Promise<void>
|
|
56
|
-
close(): Promise<void>
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Configuration for the ClickHouse durable provider
|
|
61
|
-
*/
|
|
62
|
-
export interface ClickHouseDurableConfig {
|
|
63
|
-
/** ClickHouse executor instance */
|
|
64
|
-
executor: ClickHouseExecutor
|
|
65
|
-
|
|
66
|
-
/** Default namespace for actions */
|
|
67
|
-
namespace?: string
|
|
68
|
-
|
|
69
|
-
/** Concurrency limits by priority tier */
|
|
70
|
-
concurrency?: {
|
|
71
|
-
priority?: number
|
|
72
|
-
standard?: number
|
|
73
|
-
flex?: number
|
|
74
|
-
batch?: number
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/** Batch window in milliseconds */
|
|
78
|
-
batchWindow?: number
|
|
79
|
-
|
|
80
|
-
/** Maximum batch size before auto-flush */
|
|
81
|
-
maxBatchSize?: number
|
|
82
|
-
|
|
83
|
-
/** Poll interval for checking action status (ms) */
|
|
84
|
-
pollInterval?: number
|
|
85
|
-
|
|
86
|
-
/** Auto-recover pending actions on start */
|
|
87
|
-
autoRecover?: boolean
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Priority tier to numeric priority mapping
|
|
92
|
-
* Lower number = higher priority
|
|
93
|
-
*/
|
|
94
|
-
const PRIORITY_MAP: Record<ExecutionPriority, number> = {
|
|
95
|
-
priority: 1,
|
|
96
|
-
standard: 5,
|
|
97
|
-
flex: 7,
|
|
98
|
-
batch: 9,
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Numeric priority to tier mapping
|
|
103
|
-
*/
|
|
104
|
-
const PRIORITY_REVERSE: Record<number, ExecutionPriority> = {
|
|
105
|
-
1: 'priority',
|
|
106
|
-
5: 'standard',
|
|
107
|
-
7: 'flex',
|
|
108
|
-
9: 'batch',
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// =============================================================================
|
|
112
|
-
// ClickHouse Durable Provider
|
|
113
|
-
// =============================================================================
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* ClickHouse-backed provider for durable promises
|
|
117
|
-
*
|
|
118
|
-
* @example
|
|
119
|
-
* ```ts
|
|
120
|
-
* import { createClickHouseDatabase } from '@mdxdb/clickhouse'
|
|
121
|
-
* import { ClickHouseDurableProvider } from 'ai-database'
|
|
122
|
-
*
|
|
123
|
-
* const db = await createClickHouseDatabase({ url: 'http://localhost:8123' })
|
|
124
|
-
* const provider = new ClickHouseDurableProvider({
|
|
125
|
-
* executor: db.getExecutor(),
|
|
126
|
-
* namespace: 'myapp.example.com',
|
|
127
|
-
* })
|
|
128
|
-
*
|
|
129
|
-
* // Use context for automatic persistence
|
|
130
|
-
* await provider.withContext({ priority: 'batch' }, async () => {
|
|
131
|
-
* const result = await ai.generate({ prompt: 'Hello' })
|
|
132
|
-
* })
|
|
133
|
-
*
|
|
134
|
-
* // Flush batched operations
|
|
135
|
-
* await provider.flush()
|
|
136
|
-
* ```
|
|
137
|
-
*/
|
|
138
|
-
export class ClickHouseDurableProvider implements BatchScheduler {
|
|
139
|
-
private readonly executor: ClickHouseExecutor
|
|
140
|
-
private readonly namespace: string
|
|
141
|
-
private readonly semaphores: Record<ExecutionPriority, Semaphore>
|
|
142
|
-
private readonly config: Required<Omit<ClickHouseDurableConfig, 'executor'>>
|
|
143
|
-
|
|
144
|
-
// Batch queue
|
|
145
|
-
private readonly batchQueue: Map<string, DurablePromise<unknown>> = new Map()
|
|
146
|
-
private batchTimer: ReturnType<typeof setTimeout> | null = null
|
|
147
|
-
|
|
148
|
-
// Tracking
|
|
149
|
-
private pendingCount = 0
|
|
150
|
-
private activeCount = 0
|
|
151
|
-
private completedCount = 0
|
|
152
|
-
private failedCount = 0
|
|
153
|
-
|
|
154
|
-
constructor(config: ClickHouseDurableConfig) {
|
|
155
|
-
this.executor = config.executor
|
|
156
|
-
this.namespace = config.namespace ?? 'default'
|
|
157
|
-
|
|
158
|
-
this.config = {
|
|
159
|
-
namespace: this.namespace,
|
|
160
|
-
concurrency: {
|
|
161
|
-
priority: config.concurrency?.priority ?? 50,
|
|
162
|
-
standard: config.concurrency?.standard ?? 20,
|
|
163
|
-
flex: config.concurrency?.flex ?? 10,
|
|
164
|
-
batch: config.concurrency?.batch ?? 1000,
|
|
165
|
-
},
|
|
166
|
-
batchWindow: config.batchWindow ?? 60000,
|
|
167
|
-
maxBatchSize: config.maxBatchSize ?? 10000,
|
|
168
|
-
pollInterval: config.pollInterval ?? 1000,
|
|
169
|
-
autoRecover: config.autoRecover ?? true,
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// Initialize semaphores
|
|
173
|
-
this.semaphores = {
|
|
174
|
-
priority: new Semaphore(this.config.concurrency.priority!),
|
|
175
|
-
standard: new Semaphore(this.config.concurrency.standard!),
|
|
176
|
-
flex: new Semaphore(this.config.concurrency.flex!),
|
|
177
|
-
batch: new Semaphore(this.config.concurrency.batch!),
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// Register as global batch scheduler
|
|
181
|
-
setBatchScheduler(this)
|
|
182
|
-
|
|
183
|
-
// Auto-recover on init
|
|
184
|
-
if (this.config.autoRecover) {
|
|
185
|
-
this.recover().catch(console.error)
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// ===========================================================================
|
|
190
|
-
// Action Creation
|
|
191
|
-
// ===========================================================================
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* Create an action in ClickHouse
|
|
195
|
-
*/
|
|
196
|
-
async createAction(options: {
|
|
197
|
-
id: string
|
|
198
|
-
method: string
|
|
199
|
-
args?: unknown[]
|
|
200
|
-
priority: ExecutionPriority
|
|
201
|
-
actor?: string
|
|
202
|
-
dependsOn?: string[]
|
|
203
|
-
deferUntil?: Date
|
|
204
|
-
meta?: Record<string, unknown>
|
|
205
|
-
}): Promise<ActionRow> {
|
|
206
|
-
const now = new Date().toISOString()
|
|
207
|
-
const verb = this.parseVerb(options.method)
|
|
208
|
-
|
|
209
|
-
const action: Partial<ActionRow> = {
|
|
210
|
-
id: options.id,
|
|
211
|
-
ns: this.namespace,
|
|
212
|
-
actor: options.actor ?? getCurrentContext()?.actor ?? 'system',
|
|
213
|
-
act: verb,
|
|
214
|
-
action: `${verb}s`,
|
|
215
|
-
activity: `${verb}ing`,
|
|
216
|
-
object: options.method,
|
|
217
|
-
objectData: {
|
|
218
|
-
method: options.method,
|
|
219
|
-
args: options.args,
|
|
220
|
-
},
|
|
221
|
-
status: 'pending',
|
|
222
|
-
progress: 0,
|
|
223
|
-
total: 1,
|
|
224
|
-
result: {},
|
|
225
|
-
error: '',
|
|
226
|
-
data: {},
|
|
227
|
-
meta: options.meta ?? {},
|
|
228
|
-
priority: PRIORITY_MAP[options.priority],
|
|
229
|
-
batch: '',
|
|
230
|
-
batchIndex: 0,
|
|
231
|
-
batchTotal: 0,
|
|
232
|
-
dependencies: options.dependsOn ?? [],
|
|
233
|
-
scheduledAt: options.deferUntil?.toISOString() ?? null,
|
|
234
|
-
startedAt: null,
|
|
235
|
-
completedAt: null,
|
|
236
|
-
createdAt: now,
|
|
237
|
-
updatedAt: now,
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
await this.executor.insert('Actions', [action])
|
|
241
|
-
this.pendingCount++
|
|
242
|
-
|
|
243
|
-
return action as ActionRow
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
/**
|
|
247
|
-
* Update action status
|
|
248
|
-
*/
|
|
249
|
-
async updateAction(
|
|
250
|
-
id: string,
|
|
251
|
-
updates: Partial<Pick<ActionRow, 'status' | 'progress' | 'result' | 'error' | 'startedAt' | 'completedAt'>>
|
|
252
|
-
): Promise<void> {
|
|
253
|
-
const now = new Date().toISOString()
|
|
254
|
-
|
|
255
|
-
// Get existing action
|
|
256
|
-
const rows = await this.executor.query<ActionRow>(
|
|
257
|
-
`SELECT * FROM Actions FINAL WHERE id = '${this.escapeString(id)}' AND ns = '${this.namespace}' ORDER BY updatedAt DESC LIMIT 1`
|
|
258
|
-
)
|
|
259
|
-
|
|
260
|
-
if (rows.length === 0) {
|
|
261
|
-
throw new Error(`Action not found: ${id}`)
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
const existing = rows[0]!
|
|
265
|
-
|
|
266
|
-
// Track status changes
|
|
267
|
-
if (updates.status && updates.status !== existing.status) {
|
|
268
|
-
if (existing.status === 'pending') this.pendingCount--
|
|
269
|
-
if (existing.status === 'active') this.activeCount--
|
|
270
|
-
|
|
271
|
-
if (updates.status === 'active') this.activeCount++
|
|
272
|
-
if (updates.status === 'completed') this.completedCount++
|
|
273
|
-
if (updates.status === 'failed') this.failedCount++
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
// Insert new row with updates (ReplacingMergeTree will handle dedup)
|
|
277
|
-
await this.executor.insert('Actions', [{
|
|
278
|
-
...existing,
|
|
279
|
-
...updates,
|
|
280
|
-
updatedAt: now,
|
|
281
|
-
}])
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
/**
|
|
285
|
-
* Get action by ID
|
|
286
|
-
*/
|
|
287
|
-
async getAction(id: string): Promise<ActionRow | null> {
|
|
288
|
-
const rows = await this.executor.query<ActionRow>(
|
|
289
|
-
`SELECT * FROM Actions FINAL WHERE id = '${this.escapeString(id)}' AND ns = '${this.namespace}' ORDER BY updatedAt DESC LIMIT 1`
|
|
290
|
-
)
|
|
291
|
-
return rows[0] ?? null
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
/**
|
|
295
|
-
* List actions by status
|
|
296
|
-
*/
|
|
297
|
-
async listActions(options: {
|
|
298
|
-
status?: ActionRow['status'] | ActionRow['status'][]
|
|
299
|
-
priority?: ExecutionPriority
|
|
300
|
-
limit?: number
|
|
301
|
-
} = {}): Promise<ActionRow[]> {
|
|
302
|
-
const conditions: string[] = [`ns = '${this.namespace}'`]
|
|
303
|
-
|
|
304
|
-
if (options.status) {
|
|
305
|
-
if (Array.isArray(options.status)) {
|
|
306
|
-
const statuses = options.status.map(s => `'${s}'`).join(', ')
|
|
307
|
-
conditions.push(`status IN (${statuses})`)
|
|
308
|
-
} else {
|
|
309
|
-
conditions.push(`status = '${options.status}'`)
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
if (options.priority) {
|
|
314
|
-
conditions.push(`priority = ${PRIORITY_MAP[options.priority]}`)
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
const limit = options.limit ? `LIMIT ${options.limit}` : ''
|
|
318
|
-
|
|
319
|
-
return this.executor.query<ActionRow>(
|
|
320
|
-
`SELECT * FROM Actions FINAL WHERE ${conditions.join(' AND ')} ORDER BY createdAt ASC ${limit}`
|
|
321
|
-
)
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
// ===========================================================================
|
|
325
|
-
// Batch Scheduler Interface
|
|
326
|
-
// ===========================================================================
|
|
327
|
-
|
|
328
|
-
/**
|
|
329
|
-
* Add a promise to the batch queue
|
|
330
|
-
*/
|
|
331
|
-
enqueue(promise: DurablePromise<unknown>): void {
|
|
332
|
-
this.batchQueue.set(promise.actionId, promise)
|
|
333
|
-
this.startBatchTimer()
|
|
334
|
-
|
|
335
|
-
if (this.batchQueue.size >= this.config.maxBatchSize) {
|
|
336
|
-
this.flush()
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
/**
|
|
341
|
-
* Get pending count
|
|
342
|
-
*/
|
|
343
|
-
get pending(): number {
|
|
344
|
-
return this.batchQueue.size + this.pendingCount
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
/**
|
|
348
|
-
* Flush all pending batch operations
|
|
349
|
-
*/
|
|
350
|
-
async flush(): Promise<void> {
|
|
351
|
-
if (this.batchTimer) {
|
|
352
|
-
clearTimeout(this.batchTimer)
|
|
353
|
-
this.batchTimer = null
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
const promises = Array.from(this.batchQueue.values())
|
|
357
|
-
this.batchQueue.clear()
|
|
358
|
-
|
|
359
|
-
if (promises.length === 0) return
|
|
360
|
-
|
|
361
|
-
// Group by method prefix (provider)
|
|
362
|
-
const groups = new Map<string, DurablePromise<unknown>[]>()
|
|
363
|
-
for (const promise of promises) {
|
|
364
|
-
const provider = promise.method.split('.')[0] ?? 'default'
|
|
365
|
-
const existing = groups.get(provider) ?? []
|
|
366
|
-
existing.push(promise)
|
|
367
|
-
groups.set(provider, existing)
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
// Update batch metadata in ClickHouse
|
|
371
|
-
for (const [provider, batch] of groups) {
|
|
372
|
-
const batchId = crypto.randomUUID()
|
|
373
|
-
|
|
374
|
-
for (let i = 0; i < batch.length; i++) {
|
|
375
|
-
await this.executor.insert('Actions', [{
|
|
376
|
-
id: batch[i]!.actionId,
|
|
377
|
-
ns: this.namespace,
|
|
378
|
-
batch: batchId,
|
|
379
|
-
batchIndex: i,
|
|
380
|
-
batchTotal: batch.length,
|
|
381
|
-
updatedAt: new Date().toISOString(),
|
|
382
|
-
}])
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
console.log(`Batch ${batchId}: ${batch.length} ${provider} operations queued`)
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
// Execute non-batch immediately with concurrency control
|
|
389
|
-
await Promise.all(
|
|
390
|
-
promises.map(async (promise) => {
|
|
391
|
-
await this.semaphores.batch.run(async () => {
|
|
392
|
-
try {
|
|
393
|
-
await promise
|
|
394
|
-
} catch {
|
|
395
|
-
// Error is handled by the promise itself
|
|
396
|
-
}
|
|
397
|
-
})
|
|
398
|
-
})
|
|
399
|
-
)
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
private startBatchTimer(): void {
|
|
403
|
-
if (this.batchTimer) return
|
|
404
|
-
|
|
405
|
-
this.batchTimer = setTimeout(async () => {
|
|
406
|
-
this.batchTimer = null
|
|
407
|
-
await this.flush()
|
|
408
|
-
}, this.config.batchWindow)
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
// ===========================================================================
|
|
412
|
-
// Recovery
|
|
413
|
-
// ===========================================================================
|
|
414
|
-
|
|
415
|
-
/**
|
|
416
|
-
* Recover pending/active actions from ClickHouse after crash
|
|
417
|
-
*/
|
|
418
|
-
async recover(): Promise<number> {
|
|
419
|
-
const actions = await this.listActions({
|
|
420
|
-
status: ['pending', 'active'],
|
|
421
|
-
})
|
|
422
|
-
|
|
423
|
-
console.log(`Recovering ${actions.length} actions from ClickHouse`)
|
|
424
|
-
|
|
425
|
-
let recovered = 0
|
|
426
|
-
for (const action of actions) {
|
|
427
|
-
// Mark active as failed (we don't know if it completed)
|
|
428
|
-
if (action.status === 'active') {
|
|
429
|
-
await this.updateAction(action.id, {
|
|
430
|
-
status: 'failed',
|
|
431
|
-
error: 'Recovered after crash - execution interrupted',
|
|
432
|
-
completedAt: new Date().toISOString(),
|
|
433
|
-
})
|
|
434
|
-
recovered++
|
|
435
|
-
}
|
|
436
|
-
// Pending actions can be retried
|
|
437
|
-
else if (action.status === 'pending') {
|
|
438
|
-
this.pendingCount++
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
return recovered
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
/**
|
|
446
|
-
* Retry failed actions
|
|
447
|
-
*/
|
|
448
|
-
async retryFailed(filter?: { method?: string; since?: Date }): Promise<number> {
|
|
449
|
-
const conditions: string[] = [
|
|
450
|
-
`ns = '${this.namespace}'`,
|
|
451
|
-
`status = 'failed'`,
|
|
452
|
-
]
|
|
453
|
-
|
|
454
|
-
if (filter?.method) {
|
|
455
|
-
conditions.push(`object = '${this.escapeString(filter.method)}'`)
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
if (filter?.since) {
|
|
459
|
-
conditions.push(`completedAt > '${filter.since.toISOString()}'`)
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
const failed = await this.executor.query<ActionRow>(
|
|
463
|
-
`SELECT * FROM Actions FINAL WHERE ${conditions.join(' AND ')}`
|
|
464
|
-
)
|
|
465
|
-
|
|
466
|
-
for (const action of failed) {
|
|
467
|
-
await this.updateAction(action.id, {
|
|
468
|
-
status: 'pending',
|
|
469
|
-
error: '',
|
|
470
|
-
completedAt: null,
|
|
471
|
-
})
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
return failed.length
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
// ===========================================================================
|
|
478
|
-
// Context
|
|
479
|
-
// ===========================================================================
|
|
480
|
-
|
|
481
|
-
/**
|
|
482
|
-
* Create a DurablePromise with ClickHouse persistence
|
|
483
|
-
*/
|
|
484
|
-
createPromise<T>(options: Omit<DurablePromiseOptions<T>, 'provider'>): DurablePromise<T> {
|
|
485
|
-
// The provider will be used via context
|
|
486
|
-
return new DurablePromise({
|
|
487
|
-
...options,
|
|
488
|
-
// Inject ourselves as the context provider
|
|
489
|
-
})
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
// ===========================================================================
|
|
493
|
-
// Stats
|
|
494
|
-
// ===========================================================================
|
|
495
|
-
|
|
496
|
-
/**
|
|
497
|
-
* Get current statistics
|
|
498
|
-
*/
|
|
499
|
-
async getStats(): Promise<{
|
|
500
|
-
pending: number
|
|
501
|
-
active: number
|
|
502
|
-
completed: number
|
|
503
|
-
failed: number
|
|
504
|
-
byPriority: Record<ExecutionPriority, { pending: number; active: number; completed: number }>
|
|
505
|
-
batchQueue: number
|
|
506
|
-
}> {
|
|
507
|
-
// Query ClickHouse for accurate counts
|
|
508
|
-
const statusCounts = await this.executor.query<{ status: string; priority: number; count: string }>(
|
|
509
|
-
`SELECT status, priority, count() as count FROM Actions FINAL WHERE ns = '${this.namespace}' GROUP BY status, priority`
|
|
510
|
-
)
|
|
511
|
-
|
|
512
|
-
const byPriority: Record<ExecutionPriority, { pending: number; active: number; completed: number }> = {
|
|
513
|
-
priority: { pending: 0, active: 0, completed: 0 },
|
|
514
|
-
standard: { pending: 0, active: 0, completed: 0 },
|
|
515
|
-
flex: { pending: 0, active: 0, completed: 0 },
|
|
516
|
-
batch: { pending: 0, active: 0, completed: 0 },
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
let pending = 0
|
|
520
|
-
let active = 0
|
|
521
|
-
let completed = 0
|
|
522
|
-
let failed = 0
|
|
523
|
-
|
|
524
|
-
for (const row of statusCounts) {
|
|
525
|
-
const count = parseInt(row.count, 10)
|
|
526
|
-
const tier = PRIORITY_REVERSE[row.priority] ?? 'standard'
|
|
527
|
-
|
|
528
|
-
if (row.status === 'pending') {
|
|
529
|
-
pending += count
|
|
530
|
-
byPriority[tier].pending += count
|
|
531
|
-
} else if (row.status === 'active') {
|
|
532
|
-
active += count
|
|
533
|
-
byPriority[tier].active += count
|
|
534
|
-
} else if (row.status === 'completed') {
|
|
535
|
-
completed += count
|
|
536
|
-
byPriority[tier].completed += count
|
|
537
|
-
} else if (row.status === 'failed') {
|
|
538
|
-
failed += count
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
return {
|
|
543
|
-
pending,
|
|
544
|
-
active,
|
|
545
|
-
completed,
|
|
546
|
-
failed,
|
|
547
|
-
byPriority,
|
|
548
|
-
batchQueue: this.batchQueue.size,
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
// ===========================================================================
|
|
553
|
-
// Helpers
|
|
554
|
-
// ===========================================================================
|
|
555
|
-
|
|
556
|
-
private parseVerb(method: string): string {
|
|
557
|
-
const parts = method.split('.')
|
|
558
|
-
return parts[parts.length - 1] ?? 'process'
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
private escapeString(str: string): string {
|
|
562
|
-
return str.replace(/\\/g, '\\\\').replace(/'/g, "\\'")
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
// ===========================================================================
|
|
566
|
-
// Cleanup
|
|
567
|
-
// ===========================================================================
|
|
568
|
-
|
|
569
|
-
/**
|
|
570
|
-
* Close the provider
|
|
571
|
-
*/
|
|
572
|
-
async close(): Promise<void> {
|
|
573
|
-
if (this.batchTimer) {
|
|
574
|
-
clearTimeout(this.batchTimer)
|
|
575
|
-
this.batchTimer = null
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
// Flush any remaining batch
|
|
579
|
-
await this.flush()
|
|
580
|
-
|
|
581
|
-
setBatchScheduler(null)
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
// =============================================================================
|
|
586
|
-
// Factory
|
|
587
|
-
// =============================================================================
|
|
588
|
-
|
|
589
|
-
/**
|
|
590
|
-
* Create a ClickHouse durable provider
|
|
591
|
-
*/
|
|
592
|
-
export function createClickHouseDurableProvider(
|
|
593
|
-
config: ClickHouseDurableConfig
|
|
594
|
-
): ClickHouseDurableProvider {
|
|
595
|
-
return new ClickHouseDurableProvider(config)
|
|
596
|
-
}
|