@absurd-sqlite/bun-worker 0.1.0-alpha.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 +22 -0
- package/bun.lock +134 -0
- package/package.json +51 -0
- package/src/index.ts +63 -0
- package/src/sqlite.ts +169 -0
- package/test/basic.test.ts +505 -0
- package/test/events.test.ts +207 -0
- package/test/hooks.test.ts +350 -0
- package/test/idempotent.test.ts +195 -0
- package/test/index.test.ts +63 -0
- package/test/retry.test.ts +389 -0
- package/test/run.test.ts +101 -0
- package/test/setup.ts +650 -0
- package/test/sqlite.test.ts +89 -0
- package/test/step.test.ts +259 -0
- package/test/wait-for.ts +21 -0
- package/test/worker.test.ts +194 -0
- package/tsconfig.build.json +12 -0
- package/tsconfig.json +13 -0
package/test/setup.ts
ADDED
|
@@ -0,0 +1,650 @@
|
|
|
1
|
+
import { afterAll } from "bun:test";
|
|
2
|
+
import { Database } from "bun:sqlite";
|
|
3
|
+
import { existsSync, mkdtempSync, readdirSync, rmSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import {
|
|
8
|
+
Absurd,
|
|
9
|
+
type AbsurdHooks,
|
|
10
|
+
type JsonValue,
|
|
11
|
+
} from "absurd-sdk";
|
|
12
|
+
|
|
13
|
+
import { BunSqliteConnection } from "../src/sqlite";
|
|
14
|
+
|
|
15
|
+
configureBunSqlite();
|
|
16
|
+
|
|
17
|
+
// Database row types matching the SQLite schema
|
|
18
|
+
export interface TaskRow {
|
|
19
|
+
task_id: string;
|
|
20
|
+
task_name: string;
|
|
21
|
+
params: JsonValue;
|
|
22
|
+
headers: JsonValue | null;
|
|
23
|
+
retry_strategy: JsonValue | null;
|
|
24
|
+
max_attempts: number | null;
|
|
25
|
+
cancellation: JsonValue | null;
|
|
26
|
+
enqueue_at: Date;
|
|
27
|
+
first_started_at: Date | null;
|
|
28
|
+
state:
|
|
29
|
+
| "pending"
|
|
30
|
+
| "running"
|
|
31
|
+
| "sleeping"
|
|
32
|
+
| "completed"
|
|
33
|
+
| "failed"
|
|
34
|
+
| "cancelled";
|
|
35
|
+
attempts: number;
|
|
36
|
+
last_attempt_run: string | null;
|
|
37
|
+
completed_payload: JsonValue | null;
|
|
38
|
+
cancelled_at: Date | null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface RunRow {
|
|
42
|
+
run_id: string;
|
|
43
|
+
task_id: string;
|
|
44
|
+
attempt: number;
|
|
45
|
+
state:
|
|
46
|
+
| "pending"
|
|
47
|
+
| "running"
|
|
48
|
+
| "sleeping"
|
|
49
|
+
| "completed"
|
|
50
|
+
| "failed"
|
|
51
|
+
| "cancelled";
|
|
52
|
+
claimed_by: string | null;
|
|
53
|
+
claim_expires_at: Date | null;
|
|
54
|
+
available_at: Date;
|
|
55
|
+
wake_event: string | null;
|
|
56
|
+
event_payload: JsonValue | null;
|
|
57
|
+
started_at: Date | null;
|
|
58
|
+
completed_at: Date | null;
|
|
59
|
+
failed_at: Date | null;
|
|
60
|
+
result: JsonValue | null;
|
|
61
|
+
failure_reason: JsonValue | null;
|
|
62
|
+
created_at: Date;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface SqliteFixture {
|
|
66
|
+
db: Database;
|
|
67
|
+
conn: BunSqliteConnection;
|
|
68
|
+
dbPath: string;
|
|
69
|
+
cleanup: () => void;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const fixtures: SqliteFixture[] = [];
|
|
73
|
+
|
|
74
|
+
afterAll(() => {
|
|
75
|
+
for (const fixture of fixtures) {
|
|
76
|
+
fixture.db.close();
|
|
77
|
+
fixture.cleanup();
|
|
78
|
+
}
|
|
79
|
+
fixtures.length = 0;
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
export interface TestContext {
|
|
83
|
+
absurd: Absurd;
|
|
84
|
+
pool: BunSqliteConnection;
|
|
85
|
+
queueName: string;
|
|
86
|
+
dbPath: string;
|
|
87
|
+
cleanupTasks(): Promise<void>;
|
|
88
|
+
getQueueStorageState(
|
|
89
|
+
queueName: string
|
|
90
|
+
): Promise<{ exists: boolean; tables: string[] }>;
|
|
91
|
+
getTask(taskID: string): Promise<TaskRow | null>;
|
|
92
|
+
getRun(runID: string): Promise<RunRow | null>;
|
|
93
|
+
getRuns(taskID: string): Promise<RunRow[]>;
|
|
94
|
+
setFakeNow(ts: Date | null): Promise<void>;
|
|
95
|
+
sleep(ms: number): Promise<void>;
|
|
96
|
+
getRemainingTasksCount(): Promise<number>;
|
|
97
|
+
getRemainingEventsCount(): Promise<number>;
|
|
98
|
+
getWaitsCount(): Promise<number>;
|
|
99
|
+
getCheckpoint(
|
|
100
|
+
taskID: string,
|
|
101
|
+
checkpointName: string
|
|
102
|
+
): Promise<{
|
|
103
|
+
checkpoint_name: string;
|
|
104
|
+
state: JsonValue;
|
|
105
|
+
owner_run_id: string;
|
|
106
|
+
} | null>;
|
|
107
|
+
scheduleRun(runID: string, wakeAt: Date): Promise<void>;
|
|
108
|
+
completeRun(runID: string, payload: JsonValue): Promise<void>;
|
|
109
|
+
cleanupTasksByTTL(ttlSeconds: number, limit: number): Promise<number>;
|
|
110
|
+
cleanupEventsByTTL(ttlSeconds: number, limit: number): Promise<number>;
|
|
111
|
+
setTaskCheckpointState(
|
|
112
|
+
taskID: string,
|
|
113
|
+
stepName: string,
|
|
114
|
+
state: JsonValue,
|
|
115
|
+
runID: string,
|
|
116
|
+
extendClaimBySeconds: number | null
|
|
117
|
+
): Promise<void>;
|
|
118
|
+
awaitEventInternal(
|
|
119
|
+
taskID: string,
|
|
120
|
+
runID: string,
|
|
121
|
+
stepName: string,
|
|
122
|
+
eventName: string,
|
|
123
|
+
timeoutSeconds: number | null
|
|
124
|
+
): Promise<void>;
|
|
125
|
+
extendClaim(runID: string, extendBySeconds: number): Promise<void>;
|
|
126
|
+
expectCancelledError(promise: Promise<unknown>): Promise<void>;
|
|
127
|
+
createClient(options?: { queueName?: string; hooks?: AbsurdHooks }): Absurd;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function randomName(prefix = "test"): string {
|
|
131
|
+
return `${prefix}_${Math.random().toString(36).substring(7)}`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const testDir = fileURLToPath(new URL(".", import.meta.url));
|
|
135
|
+
const repoRoot = join(testDir, "../../..");
|
|
136
|
+
const extensionBase = join(repoRoot, "target/release/libabsurd");
|
|
137
|
+
|
|
138
|
+
function resolveExtensionPath(base: string): string {
|
|
139
|
+
const platformExt =
|
|
140
|
+
process.platform === "win32"
|
|
141
|
+
? ".dll"
|
|
142
|
+
: process.platform === "darwin"
|
|
143
|
+
? ".dylib"
|
|
144
|
+
: ".so";
|
|
145
|
+
const candidates = buildExtensionCandidates(base, platformExt);
|
|
146
|
+
for (const candidate of candidates) {
|
|
147
|
+
if (existsSync(candidate)) {
|
|
148
|
+
return candidate;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
throw new Error(
|
|
152
|
+
`SQLite extension not found at ${base} (expected ${platformExt})`
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
let cachedExtensionPath: string | null = null;
|
|
157
|
+
|
|
158
|
+
function getExtensionPath(): string {
|
|
159
|
+
if (!cachedExtensionPath) {
|
|
160
|
+
cachedExtensionPath = resolveExtensionPath(extensionBase);
|
|
161
|
+
}
|
|
162
|
+
return cachedExtensionPath;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function loadExtension(db: Database): void {
|
|
166
|
+
(db as unknown as { loadExtension(path: string): void }).loadExtension(
|
|
167
|
+
getExtensionPath()
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function createFixture(): SqliteFixture {
|
|
172
|
+
const tempDir = mkdtempSync(join(tmpdir(), "absurd-sqlite-"));
|
|
173
|
+
const dbPath = join(tempDir, "absurd.db");
|
|
174
|
+
const db = new Database(dbPath);
|
|
175
|
+
loadExtension(db);
|
|
176
|
+
db.query("select absurd_apply_migrations()").get();
|
|
177
|
+
const conn = new BunSqliteConnection(db);
|
|
178
|
+
|
|
179
|
+
const cleanup = () => {
|
|
180
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const fixture = { db, conn, dbPath, cleanup };
|
|
184
|
+
fixtures.push(fixture);
|
|
185
|
+
return fixture;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function buildExtensionCandidates(base: string, platformExt: string): string[] {
|
|
189
|
+
const candidates = new Set<string>();
|
|
190
|
+
const envPath = process.env.ABSURD_SQLITE_EXTENSION_PATH;
|
|
191
|
+
if (envPath) {
|
|
192
|
+
candidates.add(envPath);
|
|
193
|
+
}
|
|
194
|
+
candidates.add(base);
|
|
195
|
+
candidates.add(`${base}${platformExt}`);
|
|
196
|
+
|
|
197
|
+
return Array.from(candidates);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function configureBunSqlite(): void {
|
|
201
|
+
if (process.platform !== "darwin") {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const customSQLite = resolveCustomSQLitePath();
|
|
206
|
+
if (!customSQLite) {
|
|
207
|
+
throw new Error(
|
|
208
|
+
"Bun's SQLite build on macOS does not support extensions. " +
|
|
209
|
+
"Install sqlite via Homebrew and set ABSURD_SQLITE_CUSTOM_SQLITE_PATH " +
|
|
210
|
+
"to the libsqlite3.dylib path."
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
Database.setCustomSQLite(customSQLite);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function resolveCustomSQLitePath(): string | null {
|
|
217
|
+
const envPath = process.env.ABSURD_SQLITE_CUSTOM_SQLITE_PATH;
|
|
218
|
+
if (envPath && existsSync(envPath)) {
|
|
219
|
+
return envPath;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const prefixes = [
|
|
223
|
+
process.env.HOMEBREW_PREFIX,
|
|
224
|
+
"/opt/homebrew",
|
|
225
|
+
"/usr/local",
|
|
226
|
+
].filter(Boolean) as string[];
|
|
227
|
+
|
|
228
|
+
for (const prefix of prefixes) {
|
|
229
|
+
const optPath = join(prefix, "opt", "sqlite", "lib", "libsqlite3.dylib");
|
|
230
|
+
if (existsSync(optPath)) {
|
|
231
|
+
return optPath;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const cellarPath = join(prefix, "Cellar", "sqlite");
|
|
235
|
+
if (!existsSync(cellarPath)) {
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
try {
|
|
239
|
+
const entries = readdirSync(cellarPath, { withFileTypes: true });
|
|
240
|
+
for (const entry of entries) {
|
|
241
|
+
if (!entry.isDirectory()) {
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
const candidate = join(
|
|
245
|
+
cellarPath,
|
|
246
|
+
entry.name,
|
|
247
|
+
"lib",
|
|
248
|
+
"libsqlite3.dylib"
|
|
249
|
+
);
|
|
250
|
+
if (existsSync(candidate)) {
|
|
251
|
+
return candidate;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
} catch {
|
|
255
|
+
// fall through to other prefixes
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export async function createTestAbsurd(
|
|
263
|
+
queueName: string = "default"
|
|
264
|
+
): Promise<TestContext> {
|
|
265
|
+
const fixture = createFixture();
|
|
266
|
+
const absurd = new Absurd({
|
|
267
|
+
db: fixture.conn,
|
|
268
|
+
queueName,
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
await absurd.createQueue(queueName);
|
|
272
|
+
|
|
273
|
+
return {
|
|
274
|
+
absurd,
|
|
275
|
+
pool: fixture.conn,
|
|
276
|
+
queueName,
|
|
277
|
+
dbPath: fixture.dbPath,
|
|
278
|
+
cleanupTasks: () => cleanupTasks(fixture.conn, queueName),
|
|
279
|
+
getQueueStorageState: (targetQueueName: string) =>
|
|
280
|
+
getQueueStorageState(fixture.conn, targetQueueName),
|
|
281
|
+
getTask: (taskID: string) => getTask(fixture.conn, taskID, queueName),
|
|
282
|
+
getRun: (runID: string) => getRun(fixture.conn, runID, queueName),
|
|
283
|
+
getRuns: (taskID: string) => getRuns(fixture.conn, taskID, queueName),
|
|
284
|
+
setFakeNow: (ts: Date | null) => setFakeNow(fixture.conn, ts),
|
|
285
|
+
sleep: (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
286
|
+
getRemainingTasksCount: () =>
|
|
287
|
+
getRemainingTasksCount(fixture.conn, queueName),
|
|
288
|
+
getRemainingEventsCount: () =>
|
|
289
|
+
getRemainingEventsCount(fixture.conn, queueName),
|
|
290
|
+
getWaitsCount: () => getWaitsCount(fixture.conn, queueName),
|
|
291
|
+
getCheckpoint: (taskID: string, checkpointName: string) =>
|
|
292
|
+
getCheckpoint(fixture.conn, taskID, checkpointName, queueName),
|
|
293
|
+
scheduleRun: (runID: string, wakeAt: Date) =>
|
|
294
|
+
scheduleRun(fixture.conn, runID, wakeAt, queueName),
|
|
295
|
+
completeRun: (runID: string, payload: JsonValue) =>
|
|
296
|
+
completeRun(fixture.conn, runID, payload, queueName),
|
|
297
|
+
cleanupTasksByTTL: (ttlSeconds: number, limit: number) =>
|
|
298
|
+
cleanupTasksByTTL(fixture.conn, ttlSeconds, limit, queueName),
|
|
299
|
+
cleanupEventsByTTL: (ttlSeconds: number, limit: number) =>
|
|
300
|
+
cleanupEventsByTTL(fixture.conn, ttlSeconds, limit, queueName),
|
|
301
|
+
setTaskCheckpointState: (
|
|
302
|
+
taskID: string,
|
|
303
|
+
stepName: string,
|
|
304
|
+
state: JsonValue,
|
|
305
|
+
runID: string,
|
|
306
|
+
extendClaimBySeconds: number | null
|
|
307
|
+
) =>
|
|
308
|
+
setTaskCheckpointState(
|
|
309
|
+
fixture.conn,
|
|
310
|
+
taskID,
|
|
311
|
+
stepName,
|
|
312
|
+
state,
|
|
313
|
+
runID,
|
|
314
|
+
extendClaimBySeconds,
|
|
315
|
+
queueName
|
|
316
|
+
),
|
|
317
|
+
awaitEventInternal: (
|
|
318
|
+
taskID: string,
|
|
319
|
+
runID: string,
|
|
320
|
+
stepName: string,
|
|
321
|
+
eventName: string,
|
|
322
|
+
timeoutSeconds: number | null
|
|
323
|
+
) =>
|
|
324
|
+
awaitEventInternal(
|
|
325
|
+
fixture.conn,
|
|
326
|
+
taskID,
|
|
327
|
+
runID,
|
|
328
|
+
stepName,
|
|
329
|
+
eventName,
|
|
330
|
+
timeoutSeconds,
|
|
331
|
+
queueName
|
|
332
|
+
),
|
|
333
|
+
extendClaim: (runID: string, extendBySeconds: number) =>
|
|
334
|
+
extendClaim(fixture.conn, runID, extendBySeconds, queueName),
|
|
335
|
+
expectCancelledError: (promise: Promise<unknown>) =>
|
|
336
|
+
expectCancelledError(promise),
|
|
337
|
+
createClient: (options) => {
|
|
338
|
+
const client = new Absurd({
|
|
339
|
+
db: fixture.conn,
|
|
340
|
+
queueName: options?.queueName ?? queueName,
|
|
341
|
+
hooks: options?.hooks,
|
|
342
|
+
});
|
|
343
|
+
return client;
|
|
344
|
+
},
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async function setFakeNow(
|
|
349
|
+
conn: BunSqliteConnection,
|
|
350
|
+
ts: Date | null
|
|
351
|
+
): Promise<void> {
|
|
352
|
+
if (ts === null) {
|
|
353
|
+
await conn.exec("select absurd.set_fake_now(null)");
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
await conn.exec("select absurd.set_fake_now($1)", [ts.getTime()]);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
async function cleanupTasks(
|
|
360
|
+
conn: BunSqliteConnection,
|
|
361
|
+
queue: string
|
|
362
|
+
): Promise<void> {
|
|
363
|
+
const tables = [
|
|
364
|
+
"absurd_tasks",
|
|
365
|
+
"absurd_runs",
|
|
366
|
+
"absurd_events",
|
|
367
|
+
"absurd_waits",
|
|
368
|
+
"absurd_checkpoints",
|
|
369
|
+
];
|
|
370
|
+
for (const table of tables) {
|
|
371
|
+
await conn.exec(`DELETE FROM ${table} WHERE queue_name = $1`, [queue]);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
async function getQueueStorageState(
|
|
376
|
+
conn: BunSqliteConnection,
|
|
377
|
+
queue: string
|
|
378
|
+
): Promise<{ exists: boolean; tables: string[] }> {
|
|
379
|
+
const { rows } = await conn.query<{ count: number }>(
|
|
380
|
+
`SELECT COUNT(*) AS count FROM absurd_queues WHERE queue_name = $1`,
|
|
381
|
+
[queue]
|
|
382
|
+
);
|
|
383
|
+
const tableRows = await conn.query<{ name: string }>(
|
|
384
|
+
`SELECT name FROM sqlite_master WHERE type = 'table' AND name LIKE 'absurd_%'`
|
|
385
|
+
);
|
|
386
|
+
return {
|
|
387
|
+
exists: rows[0]?.count > 0,
|
|
388
|
+
tables: tableRows.rows.map((row) => row.name),
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
async function getTask(
|
|
393
|
+
conn: BunSqliteConnection,
|
|
394
|
+
taskID: string,
|
|
395
|
+
queue: string
|
|
396
|
+
): Promise<TaskRow | null> {
|
|
397
|
+
const { rows } = await conn.query<TaskRow>(
|
|
398
|
+
`SELECT task_id,
|
|
399
|
+
task_name,
|
|
400
|
+
json(params) as params,
|
|
401
|
+
json(headers) as headers,
|
|
402
|
+
json(retry_strategy) as retry_strategy,
|
|
403
|
+
max_attempts,
|
|
404
|
+
json(cancellation) as cancellation,
|
|
405
|
+
enqueue_at,
|
|
406
|
+
first_started_at,
|
|
407
|
+
state,
|
|
408
|
+
attempts,
|
|
409
|
+
last_attempt_run,
|
|
410
|
+
json(completed_payload) as completed_payload,
|
|
411
|
+
cancelled_at
|
|
412
|
+
FROM absurd_tasks
|
|
413
|
+
WHERE task_id = $1 AND queue_name = $2`,
|
|
414
|
+
[taskID, queue]
|
|
415
|
+
);
|
|
416
|
+
return rows.length > 0 ? rows[0] : null;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
async function getRun(
|
|
420
|
+
conn: BunSqliteConnection,
|
|
421
|
+
runID: string,
|
|
422
|
+
queue: string
|
|
423
|
+
): Promise<RunRow | null> {
|
|
424
|
+
const { rows } = await conn.query<RunRow>(
|
|
425
|
+
`SELECT run_id,
|
|
426
|
+
task_id,
|
|
427
|
+
attempt,
|
|
428
|
+
state,
|
|
429
|
+
claimed_by,
|
|
430
|
+
claim_expires_at,
|
|
431
|
+
available_at,
|
|
432
|
+
wake_event,
|
|
433
|
+
json(event_payload) as event_payload,
|
|
434
|
+
started_at,
|
|
435
|
+
completed_at,
|
|
436
|
+
failed_at,
|
|
437
|
+
json(result) as result,
|
|
438
|
+
json(failure_reason) as failure_reason,
|
|
439
|
+
created_at
|
|
440
|
+
FROM absurd_runs
|
|
441
|
+
WHERE run_id = $1 AND queue_name = $2`,
|
|
442
|
+
[runID, queue]
|
|
443
|
+
);
|
|
444
|
+
return rows.length > 0 ? rows[0] : null;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
async function getRuns(
|
|
448
|
+
conn: BunSqliteConnection,
|
|
449
|
+
taskID: string,
|
|
450
|
+
queue: string
|
|
451
|
+
): Promise<RunRow[]> {
|
|
452
|
+
const { rows } = await conn.query<RunRow>(
|
|
453
|
+
`SELECT run_id,
|
|
454
|
+
task_id,
|
|
455
|
+
attempt,
|
|
456
|
+
state,
|
|
457
|
+
claimed_by,
|
|
458
|
+
claim_expires_at,
|
|
459
|
+
available_at,
|
|
460
|
+
wake_event,
|
|
461
|
+
json(event_payload) as event_payload,
|
|
462
|
+
started_at,
|
|
463
|
+
completed_at,
|
|
464
|
+
failed_at,
|
|
465
|
+
json(result) as result,
|
|
466
|
+
json(failure_reason) as failure_reason,
|
|
467
|
+
created_at
|
|
468
|
+
FROM absurd_runs
|
|
469
|
+
WHERE task_id = $1 AND queue_name = $2
|
|
470
|
+
ORDER BY attempt`,
|
|
471
|
+
[taskID, queue]
|
|
472
|
+
);
|
|
473
|
+
return rows;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
async function getRemainingTasksCount(
|
|
477
|
+
conn: BunSqliteConnection,
|
|
478
|
+
queue: string
|
|
479
|
+
): Promise<number> {
|
|
480
|
+
const { rows } = await conn.query<{ count: number }>(
|
|
481
|
+
`SELECT COUNT(*) AS count FROM absurd_tasks WHERE queue_name = $1`,
|
|
482
|
+
[queue]
|
|
483
|
+
);
|
|
484
|
+
return Number(rows[0]?.count ?? 0);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
async function getRemainingEventsCount(
|
|
488
|
+
conn: BunSqliteConnection,
|
|
489
|
+
queue: string
|
|
490
|
+
): Promise<number> {
|
|
491
|
+
const { rows } = await conn.query<{ count: number }>(
|
|
492
|
+
`SELECT COUNT(*) AS count FROM absurd_events WHERE queue_name = $1`,
|
|
493
|
+
[queue]
|
|
494
|
+
);
|
|
495
|
+
return Number(rows[0]?.count ?? 0);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
async function getWaitsCount(
|
|
499
|
+
conn: BunSqliteConnection,
|
|
500
|
+
queue: string
|
|
501
|
+
): Promise<number> {
|
|
502
|
+
const { rows } = await conn.query<{ count: number }>(
|
|
503
|
+
`SELECT COUNT(*) AS count FROM absurd_waits WHERE queue_name = $1`,
|
|
504
|
+
[queue]
|
|
505
|
+
);
|
|
506
|
+
return Number(rows[0]?.count ?? 0);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
async function getCheckpoint(
|
|
510
|
+
conn: BunSqliteConnection,
|
|
511
|
+
taskID: string,
|
|
512
|
+
checkpointName: string,
|
|
513
|
+
queue: string
|
|
514
|
+
): Promise<{
|
|
515
|
+
checkpoint_name: string;
|
|
516
|
+
state: JsonValue;
|
|
517
|
+
owner_run_id: string;
|
|
518
|
+
} | null> {
|
|
519
|
+
const { rows } = await conn.query<{
|
|
520
|
+
checkpoint_name: string;
|
|
521
|
+
state: JsonValue;
|
|
522
|
+
owner_run_id: string;
|
|
523
|
+
}>(
|
|
524
|
+
`SELECT checkpoint_name,
|
|
525
|
+
json(state) as state,
|
|
526
|
+
owner_run_id
|
|
527
|
+
FROM absurd_checkpoints
|
|
528
|
+
WHERE task_id = $1
|
|
529
|
+
AND checkpoint_name = $2
|
|
530
|
+
AND queue_name = $3`,
|
|
531
|
+
[taskID, checkpointName, queue]
|
|
532
|
+
);
|
|
533
|
+
return rows.length > 0 ? rows[0] : null;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
async function scheduleRun(
|
|
537
|
+
conn: BunSqliteConnection,
|
|
538
|
+
runID: string,
|
|
539
|
+
wakeAt: Date,
|
|
540
|
+
queue: string
|
|
541
|
+
): Promise<void> {
|
|
542
|
+
await conn.exec(`SELECT absurd.schedule_run($1, $2, $3)`, [
|
|
543
|
+
queue,
|
|
544
|
+
runID,
|
|
545
|
+
wakeAt,
|
|
546
|
+
]);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
async function completeRun(
|
|
550
|
+
conn: BunSqliteConnection,
|
|
551
|
+
runID: string,
|
|
552
|
+
payload: JsonValue,
|
|
553
|
+
queue: string
|
|
554
|
+
): Promise<void> {
|
|
555
|
+
await conn.exec(`SELECT absurd.complete_run($1, $2, $3)`, [
|
|
556
|
+
queue,
|
|
557
|
+
runID,
|
|
558
|
+
JSON.stringify(payload),
|
|
559
|
+
]);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
async function cleanupTasksByTTL(
|
|
563
|
+
conn: BunSqliteConnection,
|
|
564
|
+
ttlSeconds: number,
|
|
565
|
+
limit: number,
|
|
566
|
+
queue: string
|
|
567
|
+
): Promise<number> {
|
|
568
|
+
const { rows } = await conn.query<{ count: number }>(
|
|
569
|
+
`SELECT absurd.cleanup_tasks($1, $2, $3) AS count`,
|
|
570
|
+
[queue, ttlSeconds, limit]
|
|
571
|
+
);
|
|
572
|
+
return Number(rows[0]?.count ?? 0);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
async function cleanupEventsByTTL(
|
|
576
|
+
conn: BunSqliteConnection,
|
|
577
|
+
ttlSeconds: number,
|
|
578
|
+
limit: number,
|
|
579
|
+
queue: string
|
|
580
|
+
): Promise<number> {
|
|
581
|
+
const { rows } = await conn.query<{ count: number }>(
|
|
582
|
+
`SELECT absurd.cleanup_events($1, $2, $3) AS count`,
|
|
583
|
+
[queue, ttlSeconds, limit]
|
|
584
|
+
);
|
|
585
|
+
return Number(rows[0]?.count ?? 0);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
async function setTaskCheckpointState(
|
|
589
|
+
conn: BunSqliteConnection,
|
|
590
|
+
taskID: string,
|
|
591
|
+
stepName: string,
|
|
592
|
+
state: JsonValue,
|
|
593
|
+
runID: string,
|
|
594
|
+
extendClaimBySeconds: number | null,
|
|
595
|
+
queue: string
|
|
596
|
+
): Promise<void> {
|
|
597
|
+
await conn.exec(
|
|
598
|
+
`SELECT absurd.set_task_checkpoint_state($1, $2, $3, $4, $5, $6)`,
|
|
599
|
+
[
|
|
600
|
+
queue,
|
|
601
|
+
taskID,
|
|
602
|
+
stepName,
|
|
603
|
+
JSON.stringify(state),
|
|
604
|
+
runID,
|
|
605
|
+
extendClaimBySeconds,
|
|
606
|
+
]
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
async function awaitEventInternal(
|
|
611
|
+
conn: BunSqliteConnection,
|
|
612
|
+
taskID: string,
|
|
613
|
+
runID: string,
|
|
614
|
+
stepName: string,
|
|
615
|
+
eventName: string,
|
|
616
|
+
timeoutSeconds: number | null,
|
|
617
|
+
queue: string
|
|
618
|
+
): Promise<void> {
|
|
619
|
+
await conn.query(
|
|
620
|
+
`SELECT should_suspend, json(payload) as payload
|
|
621
|
+
FROM absurd.await_event($1, $2, $3, $4, $5, $6)`,
|
|
622
|
+
[queue, taskID, runID, stepName, eventName, timeoutSeconds]
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
async function extendClaim(
|
|
627
|
+
conn: BunSqliteConnection,
|
|
628
|
+
runID: string,
|
|
629
|
+
extendBySeconds: number,
|
|
630
|
+
queue: string
|
|
631
|
+
): Promise<void> {
|
|
632
|
+
await conn.exec(`SELECT absurd.extend_claim($1, $2, $3)`, [
|
|
633
|
+
queue,
|
|
634
|
+
runID,
|
|
635
|
+
extendBySeconds,
|
|
636
|
+
]);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
async function expectCancelledError(promise: Promise<unknown>): Promise<void> {
|
|
640
|
+
try {
|
|
641
|
+
await promise;
|
|
642
|
+
} catch (err: any) {
|
|
643
|
+
const message = String(err?.message ?? "");
|
|
644
|
+
if (message.toLowerCase().includes("cancelled")) {
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
throw err;
|
|
648
|
+
}
|
|
649
|
+
throw new Error("Expected cancellation error");
|
|
650
|
+
}
|