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