@absurd-sqlite/sdk 0.2.0-alpha.3 → 0.2.1-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/cjs/sqlite.js +44 -3
- package/dist/sqlite.d.ts +3 -0
- package/dist/sqlite.d.ts.map +1 -1
- package/dist/sqlite.js +44 -3
- package/dist/sqlite.js.map +1 -1
- package/package.json +2 -2
- package/src/sqlite.ts +53 -4
- package/test/sqlite.test.ts +77 -1
package/dist/cjs/sqlite.js
CHANGED
|
@@ -3,6 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.SqliteConnection = void 0;
|
|
4
4
|
class SqliteConnection {
|
|
5
5
|
db;
|
|
6
|
+
maxRetries = 5;
|
|
7
|
+
baseRetryDelayMs = 50;
|
|
6
8
|
constructor(db) {
|
|
7
9
|
this.db = db;
|
|
8
10
|
// TODO: verbose logging
|
|
@@ -16,15 +18,31 @@ class SqliteConnection {
|
|
|
16
18
|
// https://github.com/WiseLibs/better-sqlite3/blob/6209be238d6a1b181f516e4e636986604b0f62e1/src/objects/statement.cpp#L134C83-L134C95
|
|
17
19
|
throw new Error("The query() method is only statements that return data");
|
|
18
20
|
}
|
|
19
|
-
const rowsDecoded = statement
|
|
21
|
+
const rowsDecoded = await this.runWithRetry(() => statement
|
|
20
22
|
.all(sqliteParams)
|
|
21
|
-
.map((row) => decodeRowValues(statement, row));
|
|
23
|
+
.map((row) => decodeRowValues(statement, row)));
|
|
22
24
|
return { rows: rowsDecoded };
|
|
23
25
|
}
|
|
24
26
|
async exec(sql, params) {
|
|
25
27
|
const sqliteQuery = rewritePostgresQuery(sql);
|
|
26
28
|
const sqliteParams = rewritePostgresParams(params);
|
|
27
|
-
this.db.prepare(sqliteQuery)
|
|
29
|
+
const statement = this.db.prepare(sqliteQuery);
|
|
30
|
+
await this.runWithRetry(() => statement.run(sqliteParams));
|
|
31
|
+
}
|
|
32
|
+
async runWithRetry(operation) {
|
|
33
|
+
let attempt = 0;
|
|
34
|
+
while (true) {
|
|
35
|
+
try {
|
|
36
|
+
return operation();
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
if (!isRetryableSQLiteError(err) || attempt >= this.maxRetries) {
|
|
40
|
+
throw err;
|
|
41
|
+
}
|
|
42
|
+
attempt++;
|
|
43
|
+
await delay(this.baseRetryDelayMs * attempt);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
28
46
|
}
|
|
29
47
|
}
|
|
30
48
|
exports.SqliteConnection = SqliteConnection;
|
|
@@ -115,3 +133,26 @@ function encodeColumnValue(value) {
|
|
|
115
133
|
}
|
|
116
134
|
return value;
|
|
117
135
|
}
|
|
136
|
+
const sqliteRetryableErrorCodes = new Set(["SQLITE_BUSY", "SQLITE_LOCKED"]);
|
|
137
|
+
const sqliteRetryableErrnos = new Set([5, 6]);
|
|
138
|
+
function isRetryableSQLiteError(err) {
|
|
139
|
+
if (!err || typeof err !== "object") {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
const code = err.code;
|
|
143
|
+
if (typeof code === "string") {
|
|
144
|
+
for (const retryableCode of sqliteRetryableErrorCodes) {
|
|
145
|
+
if (code.startsWith(retryableCode)) {
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const errno = err.errno;
|
|
151
|
+
if (typeof errno === "number" && sqliteRetryableErrnos.has(errno)) {
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
function delay(ms) {
|
|
157
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
158
|
+
}
|
package/dist/sqlite.d.ts
CHANGED
|
@@ -2,10 +2,13 @@ import type { Queryable } from "./absurd-types";
|
|
|
2
2
|
import type { SQLiteRestBindParams, SQLiteDatabase } from "./sqlite-types";
|
|
3
3
|
export declare class SqliteConnection implements Queryable {
|
|
4
4
|
private readonly db;
|
|
5
|
+
private readonly maxRetries;
|
|
6
|
+
private readonly baseRetryDelayMs;
|
|
5
7
|
constructor(db: SQLiteDatabase);
|
|
6
8
|
query<R extends object = Record<string, any>>(sql: string, params?: SQLiteRestBindParams): Promise<{
|
|
7
9
|
rows: R[];
|
|
8
10
|
}>;
|
|
9
11
|
exec(sql: string, params?: SQLiteRestBindParams): Promise<void>;
|
|
12
|
+
private runWithRetry;
|
|
10
13
|
}
|
|
11
14
|
//# 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,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,KAAK,EACV,oBAAoB,EACpB,cAAc,EAGf,MAAM,gBAAgB,CAAC;AAExB,qBAAa,gBAAiB,YAAW,SAAS;IAChD,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAiB;
|
|
1
|
+
{"version":3,"file":"sqlite.d.ts","sourceRoot":"","sources":["../src/sqlite.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,KAAK,EACV,oBAAoB,EACpB,cAAc,EAGf,MAAM,gBAAgB,CAAC;AAExB,qBAAa,gBAAiB,YAAW,SAAS;IAChD,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAiB;IACpC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAK;IAChC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAM;gBAE3B,EAAE,EAAE,cAAc;IAKxB,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;IAoBnB,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,oBAAoB,GAAG,OAAO,CAAC,IAAI,CAAC;YAQvD,YAAY;CAc3B"}
|
package/dist/sqlite.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export class SqliteConnection {
|
|
2
2
|
db;
|
|
3
|
+
maxRetries = 5;
|
|
4
|
+
baseRetryDelayMs = 50;
|
|
3
5
|
constructor(db) {
|
|
4
6
|
this.db = db;
|
|
5
7
|
// TODO: verbose logging
|
|
@@ -13,15 +15,31 @@ export class SqliteConnection {
|
|
|
13
15
|
// https://github.com/WiseLibs/better-sqlite3/blob/6209be238d6a1b181f516e4e636986604b0f62e1/src/objects/statement.cpp#L134C83-L134C95
|
|
14
16
|
throw new Error("The query() method is only statements that return data");
|
|
15
17
|
}
|
|
16
|
-
const rowsDecoded = statement
|
|
18
|
+
const rowsDecoded = await this.runWithRetry(() => statement
|
|
17
19
|
.all(sqliteParams)
|
|
18
|
-
.map((row) => decodeRowValues(statement, row));
|
|
20
|
+
.map((row) => decodeRowValues(statement, row)));
|
|
19
21
|
return { rows: rowsDecoded };
|
|
20
22
|
}
|
|
21
23
|
async exec(sql, params) {
|
|
22
24
|
const sqliteQuery = rewritePostgresQuery(sql);
|
|
23
25
|
const sqliteParams = rewritePostgresParams(params);
|
|
24
|
-
this.db.prepare(sqliteQuery)
|
|
26
|
+
const statement = this.db.prepare(sqliteQuery);
|
|
27
|
+
await this.runWithRetry(() => statement.run(sqliteParams));
|
|
28
|
+
}
|
|
29
|
+
async runWithRetry(operation) {
|
|
30
|
+
let attempt = 0;
|
|
31
|
+
while (true) {
|
|
32
|
+
try {
|
|
33
|
+
return operation();
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
if (!isRetryableSQLiteError(err) || attempt >= this.maxRetries) {
|
|
37
|
+
throw err;
|
|
38
|
+
}
|
|
39
|
+
attempt++;
|
|
40
|
+
await delay(this.baseRetryDelayMs * attempt);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
25
43
|
}
|
|
26
44
|
}
|
|
27
45
|
const namedParamPrefix = "p";
|
|
@@ -111,4 +129,27 @@ function encodeColumnValue(value) {
|
|
|
111
129
|
}
|
|
112
130
|
return value;
|
|
113
131
|
}
|
|
132
|
+
const sqliteRetryableErrorCodes = new Set(["SQLITE_BUSY", "SQLITE_LOCKED"]);
|
|
133
|
+
const sqliteRetryableErrnos = new Set([5, 6]);
|
|
134
|
+
function isRetryableSQLiteError(err) {
|
|
135
|
+
if (!err || typeof err !== "object") {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
const code = err.code;
|
|
139
|
+
if (typeof code === "string") {
|
|
140
|
+
for (const retryableCode of sqliteRetryableErrorCodes) {
|
|
141
|
+
if (code.startsWith(retryableCode)) {
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const errno = err.errno;
|
|
147
|
+
if (typeof errno === "number" && sqliteRetryableErrnos.has(errno)) {
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
function delay(ms) {
|
|
153
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
154
|
+
}
|
|
114
155
|
//# sourceMappingURL=sqlite.js.map
|
package/dist/sqlite.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sqlite.js","sourceRoot":"","sources":["../src/sqlite.ts"],"names":[],"mappings":"AAQA,MAAM,OAAO,gBAAgB;IACV,EAAE,CAAiB;
|
|
1
|
+
{"version":3,"file":"sqlite.js","sourceRoot":"","sources":["../src/sqlite.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@absurd-sqlite/sdk",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1-alpha.0",
|
|
4
4
|
"description": "TypeScript SDK for Absurd-SQLite - SQLite-based durable task execution",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -48,4 +48,4 @@
|
|
|
48
48
|
"engines": {
|
|
49
49
|
"node": ">=18.0.0"
|
|
50
50
|
}
|
|
51
|
-
}
|
|
51
|
+
}
|
package/src/sqlite.ts
CHANGED
|
@@ -8,6 +8,8 @@ import type {
|
|
|
8
8
|
|
|
9
9
|
export class SqliteConnection implements Queryable {
|
|
10
10
|
private readonly db: SQLiteDatabase;
|
|
11
|
+
private readonly maxRetries = 5;
|
|
12
|
+
private readonly baseRetryDelayMs = 50;
|
|
11
13
|
|
|
12
14
|
constructor(db: SQLiteDatabase) {
|
|
13
15
|
this.db = db;
|
|
@@ -28,9 +30,11 @@ export class SqliteConnection implements Queryable {
|
|
|
28
30
|
throw new Error("The query() method is only statements that return data");
|
|
29
31
|
}
|
|
30
32
|
|
|
31
|
-
const rowsDecoded =
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
const rowsDecoded = await this.runWithRetry(() =>
|
|
34
|
+
statement
|
|
35
|
+
.all(sqliteParams)
|
|
36
|
+
.map((row) => decodeRowValues(statement, row))
|
|
37
|
+
);
|
|
34
38
|
|
|
35
39
|
return { rows: rowsDecoded };
|
|
36
40
|
}
|
|
@@ -39,7 +43,23 @@ export class SqliteConnection implements Queryable {
|
|
|
39
43
|
const sqliteQuery = rewritePostgresQuery(sql);
|
|
40
44
|
const sqliteParams = rewritePostgresParams(params);
|
|
41
45
|
|
|
42
|
-
this.db.prepare(sqliteQuery)
|
|
46
|
+
const statement = this.db.prepare(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
|
|
|
@@ -160,3 +180,32 @@ function encodeColumnValue(value: any): any {
|
|
|
160
180
|
}
|
|
161
181
|
return value;
|
|
162
182
|
}
|
|
183
|
+
|
|
184
|
+
const sqliteRetryableErrorCodes = new Set(["SQLITE_BUSY", "SQLITE_LOCKED"]);
|
|
185
|
+
const sqliteRetryableErrnos = new Set([5, 6]);
|
|
186
|
+
|
|
187
|
+
function isRetryableSQLiteError(err: unknown): boolean {
|
|
188
|
+
if (!err || typeof err !== "object") {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const code = (err as any).code;
|
|
193
|
+
if (typeof code === "string") {
|
|
194
|
+
for (const retryableCode of sqliteRetryableErrorCodes) {
|
|
195
|
+
if (code.startsWith(retryableCode)) {
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const errno = (err as any).errno;
|
|
202
|
+
if (typeof errno === "number" && sqliteRetryableErrnos.has(errno)) {
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function delay(ms: number): Promise<void> {
|
|
210
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
211
|
+
}
|
package/test/sqlite.test.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import sqlite from "better-sqlite3";
|
|
2
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
3
6
|
|
|
4
7
|
import { SqliteConnection } from "../src/sqlite";
|
|
5
8
|
import type { SQLiteDatabase } from "../src/sqlite-types";
|
|
@@ -82,4 +85,77 @@ describe("SqliteConnection", () => {
|
|
|
82
85
|
expect(rows[0]?.created_at.getTime()).toBe(now);
|
|
83
86
|
db.close();
|
|
84
87
|
});
|
|
88
|
+
|
|
89
|
+
it("retries when SQLite reports the database is busy", async () => {
|
|
90
|
+
const tempDir = mkdtempSync(join(tmpdir(), "absurd-sqlite-busy-"));
|
|
91
|
+
const dbPath = join(tempDir, "busy.db");
|
|
92
|
+
const primary = new sqlite(dbPath) as unknown as SQLiteDatabase;
|
|
93
|
+
(primary as any).pragma("busy_timeout = 1");
|
|
94
|
+
const conn = new SqliteConnection(primary);
|
|
95
|
+
await conn.exec("CREATE TABLE t_busy (id INTEGER PRIMARY KEY, value TEXT)");
|
|
96
|
+
|
|
97
|
+
const blocker = new sqlite(dbPath);
|
|
98
|
+
blocker.pragma("busy_timeout = 1");
|
|
99
|
+
blocker.exec("BEGIN EXCLUSIVE");
|
|
100
|
+
|
|
101
|
+
let released = false;
|
|
102
|
+
const releaseLock = () => {
|
|
103
|
+
if (released) return;
|
|
104
|
+
released = true;
|
|
105
|
+
try {
|
|
106
|
+
blocker.exec("COMMIT");
|
|
107
|
+
} catch (err) {
|
|
108
|
+
// Ignore if the transaction was already closed.
|
|
109
|
+
}
|
|
110
|
+
blocker.close();
|
|
111
|
+
};
|
|
112
|
+
const timer = setTimeout(releaseLock, 50);
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
await conn.exec("INSERT INTO t_busy (value) VALUES ($1)", ["alpha"]);
|
|
116
|
+
const { rows } = await conn.query<{ value: string }>(
|
|
117
|
+
"SELECT value FROM t_busy"
|
|
118
|
+
);
|
|
119
|
+
expect(rows[0]?.value).toBe("alpha");
|
|
120
|
+
} finally {
|
|
121
|
+
clearTimeout(timer);
|
|
122
|
+
releaseLock();
|
|
123
|
+
primary.close();
|
|
124
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("retries on locked error codes from SQLite", async () => {
|
|
129
|
+
const lockedError = new Error("SQLITE_LOCKED: mock lock") as any;
|
|
130
|
+
lockedError.code = "SQLITE_LOCKED_SHAREDCACHE";
|
|
131
|
+
lockedError.errno = 6;
|
|
132
|
+
|
|
133
|
+
let attempts = 0;
|
|
134
|
+
const statement = {
|
|
135
|
+
readonly: false,
|
|
136
|
+
columns: vi.fn().mockReturnValue([]),
|
|
137
|
+
all: vi.fn(),
|
|
138
|
+
run: vi.fn(() => {
|
|
139
|
+
attempts++;
|
|
140
|
+
if (attempts === 1) {
|
|
141
|
+
throw lockedError;
|
|
142
|
+
}
|
|
143
|
+
return 1;
|
|
144
|
+
}),
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const prepareSpy = vi.fn().mockReturnValue(statement as any);
|
|
148
|
+
const db: SQLiteDatabase = {
|
|
149
|
+
prepare: prepareSpy as any,
|
|
150
|
+
close: vi.fn(),
|
|
151
|
+
loadExtension: vi.fn(),
|
|
152
|
+
};
|
|
153
|
+
const conn = new SqliteConnection(db);
|
|
154
|
+
|
|
155
|
+
await expect(
|
|
156
|
+
conn.exec("UPDATE locked_table SET value = $1 WHERE id = $2", [1, 1])
|
|
157
|
+
).resolves.toBeUndefined();
|
|
158
|
+
expect(statement.run).toHaveBeenCalledTimes(2);
|
|
159
|
+
expect(prepareSpy).toHaveBeenCalledTimes(1);
|
|
160
|
+
});
|
|
85
161
|
});
|