@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 CHANGED
@@ -1,8 +1,9 @@
1
- import { Absurd } from "absurd-sdk";
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: Absurd) => void | Promise<void>;
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
  *
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAKpC;;GAEG;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAErE;;;;;;GAMG;AACH,wBAA8B,GAAG,CAC/B,aAAa,EAAE,aAAa,GAC3B,OAAO,CAAC,IAAI,CAAC,CA2Cf"}
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).run(...sqliteParams);
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
@@ -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;gBAElB,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;IAenB,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,oBAAoB,GAAG,OAAO,CAAC,IAAI,CAAC;CAStE"}
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absurd-sqlite/bun-worker",
3
- "version": "0.2.0",
3
+ "version": "0.2.2-alpha.0",
4
4
  "description": "Bun worker utilities for Absurd-SQLite",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
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: Absurd) => void | Promise<void>;
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 = statement.all(...sqliteParams).map((row) =>
29
- decodeRowValues(row as Record<string, unknown>)
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).run(...sqliteParams);
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("ABSURD_DATABASE_PATH is required");
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 () => {
@@ -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
  });