@absurd-sqlite/bun-worker 0.2.0 → 0.2.2-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/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +42 -2
- package/dist/sqlite.d.ts +3 -0
- package/dist/sqlite.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +5 -4
- package/src/sqlite.ts +52 -3
- package/test/run.test.ts +4 -1
- package/test/sqlite.test.ts +71 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { AbsurdClient } from "@absurd-sqlite/sdk";
|
|
2
|
+
export type { AbsurdClient } from "@absurd-sqlite/sdk";
|
|
2
3
|
/**
|
|
3
4
|
* Register tasks and perform any one-time setup before the worker starts.
|
|
4
5
|
*/
|
|
5
|
-
export type SetupFunction = (absurd:
|
|
6
|
+
export type SetupFunction = (absurd: AbsurdClient) => void | Promise<void>;
|
|
6
7
|
/**
|
|
7
8
|
* Boots a worker using Bun's SQLite driver and Absurd's task engine.
|
|
8
9
|
*
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAIvD,YAAY,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEvD;;GAEG;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,MAAM,EAAE,YAAY,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAE3E;;;;;;GAMG;AACH,wBAA8B,GAAG,CAAC,aAAa,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CA2C7E"}
|
package/dist/index.js
CHANGED
|
@@ -5308,6 +5308,8 @@ import { Database } from "bun:sqlite";
|
|
|
5308
5308
|
// src/sqlite.ts
|
|
5309
5309
|
class BunSqliteConnection {
|
|
5310
5310
|
db;
|
|
5311
|
+
maxRetries = 5;
|
|
5312
|
+
baseRetryDelayMs = 50;
|
|
5311
5313
|
constructor(db) {
|
|
5312
5314
|
this.db = db;
|
|
5313
5315
|
}
|
|
@@ -5315,13 +5317,28 @@ class BunSqliteConnection {
|
|
|
5315
5317
|
const { sql: sqliteQuery, paramOrder } = rewritePostgresQuery(sql);
|
|
5316
5318
|
const sqliteParams = rewritePostgresParams(normalizeParams(params), paramOrder);
|
|
5317
5319
|
const statement = this.db.query(sqliteQuery);
|
|
5318
|
-
const rows = statement.all(...sqliteParams).map((row) => decodeRowValues(row));
|
|
5320
|
+
const rows = await this.runWithRetry(() => statement.all(...sqliteParams).map((row) => decodeRowValues(row)));
|
|
5319
5321
|
return { rows };
|
|
5320
5322
|
}
|
|
5321
5323
|
async exec(sql, params) {
|
|
5322
5324
|
const { sql: sqliteQuery, paramOrder } = rewritePostgresQuery(sql);
|
|
5323
5325
|
const sqliteParams = rewritePostgresParams(normalizeParams(params), paramOrder);
|
|
5324
|
-
this.db.query(sqliteQuery)
|
|
5326
|
+
const statement = this.db.query(sqliteQuery);
|
|
5327
|
+
await this.runWithRetry(() => statement.run(...sqliteParams));
|
|
5328
|
+
}
|
|
5329
|
+
async runWithRetry(operation) {
|
|
5330
|
+
let attempt = 0;
|
|
5331
|
+
while (true) {
|
|
5332
|
+
try {
|
|
5333
|
+
return operation();
|
|
5334
|
+
} catch (err) {
|
|
5335
|
+
if (!isRetryableSQLiteError(err) || attempt >= this.maxRetries) {
|
|
5336
|
+
throw err;
|
|
5337
|
+
}
|
|
5338
|
+
attempt++;
|
|
5339
|
+
await delay(this.baseRetryDelayMs * attempt);
|
|
5340
|
+
}
|
|
5341
|
+
}
|
|
5325
5342
|
}
|
|
5326
5343
|
}
|
|
5327
5344
|
function rewritePostgresQuery(text) {
|
|
@@ -5415,6 +5432,29 @@ function isBindParams(value) {
|
|
|
5415
5432
|
const tag = Object.prototype.toString.call(value);
|
|
5416
5433
|
return tag === "[object Object]";
|
|
5417
5434
|
}
|
|
5435
|
+
var sqliteRetryableErrorCodes = new Set(["SQLITE_BUSY", "SQLITE_LOCKED"]);
|
|
5436
|
+
var sqliteRetryableErrnos = new Set([5, 6]);
|
|
5437
|
+
function isRetryableSQLiteError(err) {
|
|
5438
|
+
if (!err || typeof err !== "object") {
|
|
5439
|
+
return false;
|
|
5440
|
+
}
|
|
5441
|
+
const code = err.code;
|
|
5442
|
+
if (typeof code === "string") {
|
|
5443
|
+
for (const retryableCode of sqliteRetryableErrorCodes) {
|
|
5444
|
+
if (code.startsWith(retryableCode)) {
|
|
5445
|
+
return true;
|
|
5446
|
+
}
|
|
5447
|
+
}
|
|
5448
|
+
}
|
|
5449
|
+
const errno = err.errno;
|
|
5450
|
+
if (typeof errno === "number" && sqliteRetryableErrnos.has(errno)) {
|
|
5451
|
+
return true;
|
|
5452
|
+
}
|
|
5453
|
+
return false;
|
|
5454
|
+
}
|
|
5455
|
+
function delay(ms) {
|
|
5456
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
5457
|
+
}
|
|
5418
5458
|
|
|
5419
5459
|
// src/index.ts
|
|
5420
5460
|
async function run(setupFunction) {
|
package/dist/sqlite.d.ts
CHANGED
|
@@ -3,10 +3,13 @@ import type { Queryable } from "@absurd-sqlite/sdk";
|
|
|
3
3
|
import type { SQLiteRestBindParams } from "@absurd-sqlite/sdk";
|
|
4
4
|
export declare class BunSqliteConnection implements Queryable {
|
|
5
5
|
private readonly db;
|
|
6
|
+
private readonly maxRetries;
|
|
7
|
+
private readonly baseRetryDelayMs;
|
|
6
8
|
constructor(db: Database);
|
|
7
9
|
query<R extends object = Record<string, any>>(sql: string, params?: SQLiteRestBindParams): Promise<{
|
|
8
10
|
rows: R[];
|
|
9
11
|
}>;
|
|
10
12
|
exec(sql: string, params?: SQLiteRestBindParams): Promise<void>;
|
|
13
|
+
private runWithRetry;
|
|
11
14
|
}
|
|
12
15
|
//# sourceMappingURL=sqlite.d.ts.map
|
package/dist/sqlite.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sqlite.d.ts","sourceRoot":"","sources":["../src/sqlite.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAEtC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AACpD,OAAO,KAAK,EAGV,oBAAoB,EACrB,MAAM,oBAAoB,CAAC;AAE5B,qBAAa,mBAAoB,YAAW,SAAS;IACnD,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAW;
|
|
1
|
+
{"version":3,"file":"sqlite.d.ts","sourceRoot":"","sources":["../src/sqlite.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAEtC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AACpD,OAAO,KAAK,EAGV,oBAAoB,EACrB,MAAM,oBAAoB,CAAC;AAE5B,qBAAa,mBAAoB,YAAW,SAAS;IACnD,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAW;IAC9B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAK;IAChC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAM;gBAE3B,EAAE,EAAE,QAAQ;IAIlB,KAAK,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAChD,GAAG,EAAE,MAAM,EACX,MAAM,CAAC,EAAE,oBAAoB,GAC5B,OAAO,CAAC;QAAE,IAAI,EAAE,CAAC,EAAE,CAAA;KAAE,CAAC;IAiBnB,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,oBAAoB,GAAG,OAAO,CAAC,IAAI,CAAC;YAWvD,YAAY;CAc3B"}
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { Absurd } from "absurd-sdk";
|
|
2
2
|
import { Database } from "bun:sqlite";
|
|
3
|
+
import type { AbsurdClient } from "@absurd-sqlite/sdk";
|
|
3
4
|
|
|
4
5
|
import { BunSqliteConnection } from "./sqlite";
|
|
5
6
|
|
|
7
|
+
export type { AbsurdClient } from "@absurd-sqlite/sdk";
|
|
8
|
+
|
|
6
9
|
/**
|
|
7
10
|
* Register tasks and perform any one-time setup before the worker starts.
|
|
8
11
|
*/
|
|
9
|
-
export type SetupFunction = (absurd:
|
|
12
|
+
export type SetupFunction = (absurd: AbsurdClient) => void | Promise<void>;
|
|
10
13
|
|
|
11
14
|
/**
|
|
12
15
|
* Boots a worker using Bun's SQLite driver and Absurd's task engine.
|
|
@@ -15,9 +18,7 @@ export type SetupFunction = (absurd: Absurd) => void | Promise<void>;
|
|
|
15
18
|
* - ABSURD_DATABASE_PATH: SQLite database file path.
|
|
16
19
|
* - ABSURD_DATABASE_EXTENSION_PATH: Absurd-SQLite extension path (libabsurd.*).
|
|
17
20
|
*/
|
|
18
|
-
export default async function run(
|
|
19
|
-
setupFunction: SetupFunction
|
|
20
|
-
): Promise<void> {
|
|
21
|
+
export default async function run(setupFunction: SetupFunction): Promise<void> {
|
|
21
22
|
const dbPath = process.env.ABSURD_DATABASE_PATH;
|
|
22
23
|
const extensionPath = process.env.ABSURD_DATABASE_EXTENSION_PATH;
|
|
23
24
|
|
package/src/sqlite.ts
CHANGED
|
@@ -9,6 +9,8 @@ import type {
|
|
|
9
9
|
|
|
10
10
|
export class BunSqliteConnection implements Queryable {
|
|
11
11
|
private readonly db: Database;
|
|
12
|
+
private readonly maxRetries = 5;
|
|
13
|
+
private readonly baseRetryDelayMs = 50;
|
|
12
14
|
|
|
13
15
|
constructor(db: Database) {
|
|
14
16
|
this.db = db;
|
|
@@ -25,8 +27,10 @@ export class BunSqliteConnection implements Queryable {
|
|
|
25
27
|
);
|
|
26
28
|
|
|
27
29
|
const statement = this.db.query(sqliteQuery);
|
|
28
|
-
const rows =
|
|
29
|
-
|
|
30
|
+
const rows = await this.runWithRetry(() =>
|
|
31
|
+
statement.all(...sqliteParams).map((row) =>
|
|
32
|
+
decodeRowValues(row as Record<string, unknown>)
|
|
33
|
+
)
|
|
30
34
|
);
|
|
31
35
|
|
|
32
36
|
return { rows: rows as R[] };
|
|
@@ -39,7 +43,23 @@ export class BunSqliteConnection implements Queryable {
|
|
|
39
43
|
paramOrder
|
|
40
44
|
);
|
|
41
45
|
|
|
42
|
-
this.db.query(sqliteQuery)
|
|
46
|
+
const statement = this.db.query(sqliteQuery);
|
|
47
|
+
await this.runWithRetry(() => statement.run(...sqliteParams));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private async runWithRetry<T>(operation: () => T): Promise<T> {
|
|
51
|
+
let attempt = 0;
|
|
52
|
+
while (true) {
|
|
53
|
+
try {
|
|
54
|
+
return operation();
|
|
55
|
+
} catch (err) {
|
|
56
|
+
if (!isRetryableSQLiteError(err) || attempt >= this.maxRetries) {
|
|
57
|
+
throw err;
|
|
58
|
+
}
|
|
59
|
+
attempt++;
|
|
60
|
+
await delay(this.baseRetryDelayMs * attempt);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
43
63
|
}
|
|
44
64
|
}
|
|
45
65
|
|
|
@@ -167,3 +187,32 @@ function isBindParams(value: unknown): value is SQLiteBindParams {
|
|
|
167
187
|
const tag = Object.prototype.toString.call(value);
|
|
168
188
|
return tag === "[object Object]";
|
|
169
189
|
}
|
|
190
|
+
|
|
191
|
+
const sqliteRetryableErrorCodes = new Set(["SQLITE_BUSY", "SQLITE_LOCKED"]);
|
|
192
|
+
const sqliteRetryableErrnos = new Set([5, 6]);
|
|
193
|
+
|
|
194
|
+
function isRetryableSQLiteError(err: unknown): boolean {
|
|
195
|
+
if (!err || typeof err !== "object") {
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const code = (err as any).code;
|
|
200
|
+
if (typeof code === "string") {
|
|
201
|
+
for (const retryableCode of sqliteRetryableErrorCodes) {
|
|
202
|
+
if (code.startsWith(retryableCode)) {
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const errno = (err as any).errno;
|
|
209
|
+
if (typeof errno === "number" && sqliteRetryableErrnos.has(errno)) {
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function delay(ms: number): Promise<void> {
|
|
217
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
218
|
+
}
|
package/test/run.test.ts
CHANGED
|
@@ -57,10 +57,13 @@ afterEach(() => {
|
|
|
57
57
|
|
|
58
58
|
describe("run", () => {
|
|
59
59
|
it("requires ABSURD_DATABASE_PATH", async () => {
|
|
60
|
+
process.env.ABSURD_DATABASE_PATH = "";
|
|
60
61
|
process.env.ABSURD_DATABASE_EXTENSION_PATH = extensionPath;
|
|
61
62
|
const { default: run } = await import("../src/index");
|
|
62
63
|
|
|
63
|
-
await expect(run(() => {})).rejects.toThrow(
|
|
64
|
+
await expect(run(() => {})).rejects.toThrow(
|
|
65
|
+
"ABSURD_DATABASE_PATH is required"
|
|
66
|
+
);
|
|
64
67
|
});
|
|
65
68
|
|
|
66
69
|
it("requires ABSURD_DATABASE_EXTENSION_PATH", async () => {
|
package/test/sqlite.test.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { Database } from "bun:sqlite";
|
|
2
|
-
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { describe, expect, it, jest } from "bun:test";
|
|
3
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
3
6
|
|
|
4
7
|
import { BunSqliteConnection } from "../src/sqlite";
|
|
5
8
|
|
|
@@ -86,4 +89,71 @@ describe("BunSqliteConnection", () => {
|
|
|
86
89
|
expect(rows[0]?.created_at.getTime()).toBe(now);
|
|
87
90
|
db.close();
|
|
88
91
|
});
|
|
92
|
+
|
|
93
|
+
it("retries when SQLite reports the database is busy", async () => {
|
|
94
|
+
const tempDir = mkdtempSync(join(tmpdir(), "absurd-sqlite-busy-"));
|
|
95
|
+
const dbPath = join(tempDir, "busy.db");
|
|
96
|
+
const primary = new Database(dbPath);
|
|
97
|
+
primary.run("PRAGMA busy_timeout = 1");
|
|
98
|
+
const conn = new BunSqliteConnection(primary);
|
|
99
|
+
await conn.exec("CREATE TABLE t_busy (id INTEGER PRIMARY KEY, value TEXT)");
|
|
100
|
+
|
|
101
|
+
const blocker = new Database(dbPath);
|
|
102
|
+
blocker.run("PRAGMA busy_timeout = 1");
|
|
103
|
+
blocker.run("BEGIN EXCLUSIVE");
|
|
104
|
+
|
|
105
|
+
let released = false;
|
|
106
|
+
const releaseLock = () => {
|
|
107
|
+
if (released) return;
|
|
108
|
+
released = true;
|
|
109
|
+
try {
|
|
110
|
+
blocker.run("COMMIT");
|
|
111
|
+
} catch {
|
|
112
|
+
// ignore if already closed
|
|
113
|
+
}
|
|
114
|
+
blocker.close();
|
|
115
|
+
};
|
|
116
|
+
const timer = setTimeout(releaseLock, 20);
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
await conn.exec("INSERT INTO t_busy (value) VALUES ($1)", ["alpha"]);
|
|
120
|
+
const { rows } = await conn.query<{ value: string }>(
|
|
121
|
+
"SELECT value FROM t_busy"
|
|
122
|
+
);
|
|
123
|
+
expect(rows[0]?.value).toBe("alpha");
|
|
124
|
+
} finally {
|
|
125
|
+
clearTimeout(timer);
|
|
126
|
+
releaseLock();
|
|
127
|
+
primary.close();
|
|
128
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("retries on locked error codes from SQLite", async () => {
|
|
133
|
+
const lockedError = new Error("SQLITE_LOCKED: mock lock") as any;
|
|
134
|
+
lockedError.code = "SQLITE_LOCKED_SHAREDCACHE";
|
|
135
|
+
lockedError.errno = 6;
|
|
136
|
+
|
|
137
|
+
let attempts = 0;
|
|
138
|
+
const statement = {
|
|
139
|
+
all: jest.fn(),
|
|
140
|
+
run: jest.fn(() => {
|
|
141
|
+
attempts++;
|
|
142
|
+
if (attempts === 1) {
|
|
143
|
+
throw lockedError;
|
|
144
|
+
}
|
|
145
|
+
return 1;
|
|
146
|
+
}),
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const querySpy = jest.fn().mockReturnValue(statement as any);
|
|
150
|
+
const db = { query: querySpy } as unknown as Database;
|
|
151
|
+
const conn = new BunSqliteConnection(db);
|
|
152
|
+
|
|
153
|
+
await expect(
|
|
154
|
+
conn.exec("UPDATE locked_table SET value = $1 WHERE id = $2", [1, 1])
|
|
155
|
+
).resolves.toBeUndefined();
|
|
156
|
+
expect(statement.run).toHaveBeenCalledTimes(2);
|
|
157
|
+
expect(querySpy).toHaveBeenCalledTimes(1);
|
|
158
|
+
});
|
|
89
159
|
});
|