@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.
@@ -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).run(sqliteParams);
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
@@ -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;gBAExB,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;IAkBnB,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,oBAAoB,GAAG,OAAO,CAAC,IAAI,CAAC;CAMtE"}
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).run(sqliteParams);
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
@@ -1 +1 @@
1
- {"version":3,"file":"sqlite.js","sourceRoot":"","sources":["../src/sqlite.ts"],"names":[],"mappings":"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"}
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.0-alpha.3",
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 = statement
32
- .all(sqliteParams)
33
- .map((row) => decodeRowValues(statement, row));
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).run(sqliteParams);
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
+ }
@@ -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
  });