@bitclaw/sqlite 1.1.0 → 1.2.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 CHANGED
@@ -36,7 +36,7 @@ Based on benchmarks from SecureLogin project:
36
36
  - **100% success rate** under load
37
37
  - **Prepared statement cache hit rate**: 100%
38
38
 
39
- These numbers reflect **direct SQLite pool operations** no HTTP server, no ORM, no middleware. Application-level throughput (through TanStack Start + Prisma + SSR) will be lower. Use `bun run test:load` in each app for end-to-end numbers.
39
+ These numbers reflect **direct SQLite pool operations** - no HTTP server, no ORM, no middleware. Application-level throughput (through TanStack Start + Prisma + SSR) will be lower. Use `bun run test:load` in each app for end-to-end numbers.
40
40
 
41
41
  ## Benchmarking Methodology
42
42
 
@@ -155,7 +155,7 @@ await cache.delete('user:123');
155
155
 
156
156
  ### Query Logger
157
157
 
158
- Dev-mode SQL logging for bun:sqlite mirrors Prisma's `prisma:query` output. Zero overhead in production.
158
+ Dev-mode SQL logging for bun:sqlite - mirrors Prisma's `prisma:query` output. Zero overhead in production.
159
159
 
160
160
  ```typescript
161
161
  import { Database } from 'bun:sqlite';
@@ -194,7 +194,7 @@ bootstrapCache.set(sessionId, data);
194
194
  return data;
195
195
  ```
196
196
 
197
- Unlike `WeakMap` per-request caching (which deduplicates within a single SSR request), `TTLCache` deduplicates **across** HTTP requests e.g. when TanStack Router replays `beforeLoad` on client hydration, the server returns the cached result instantly (0 DB queries).
197
+ Unlike `WeakMap` per-request caching (which deduplicates within a single SSR request), `TTLCache` deduplicates **across** HTTP requests - e.g. when TanStack Router replays `beforeLoad` on client hydration, the server returns the cached result instantly (0 DB queries).
198
198
 
199
199
  ## Configuration
200
200
 
@@ -40,7 +40,7 @@ export class CacheLock {
40
40
  const result = this.db.transaction(() => {
41
41
  // Remove expired lock for this key first
42
42
  this.deleteExpiredStmt.run({ $key: key, $now: now });
43
- // Try to insert OR IGNORE means it fails silently if key exists
43
+ // Try to insert - OR IGNORE means it fails silently if key exists
44
44
  this.acquireStmt.run({
45
45
  $key: key,
46
46
  $owner: actualOwner,
@@ -1 +1 @@
1
- {"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../../src/connection.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAEtC,MAAM,MAAM,gBAAgB,GAAG;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC;AAEF;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,gBAAgB,GAAG,QAAQ,CA+BvE;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,EAAE,EAAE,QAAQ,GAAG,IAAI,CAalD;AAkCD;;GAEG;AACH,wBAAgB,WAAW,CAAC,EAAE,EAAE,QAAQ,GAAG;IACzC,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC,CA6BA;AAED;;GAEG;AACH,wBAAgB,QAAQ,CAAC,EAAE,EAAE,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAiB9D"}
1
+ {"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../../src/connection.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAEtC,MAAM,MAAM,gBAAgB,GAAG;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC;AAEF;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,gBAAgB,GAAG,QAAQ,CAsBvE;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,EAAE,EAAE,QAAQ,GAAG,IAAI,CAalD;AAkCD;;GAEG;AACH,wBAAgB,WAAW,CAAC,EAAE,EAAE,QAAQ,GAAG;IACzC,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC,CA6BA;AAED;;GAEG;AACH,wBAAgB,QAAQ,CAAC,EAAE,EAAE,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAiB9D"}
@@ -12,20 +12,14 @@ export function initializeConnection(config) {
12
12
  });
13
13
  // Apply optimal PRAGMA settings
14
14
  if (config.path !== ':memory:') {
15
- // WAL mode for better concurrency
16
15
  db.run('PRAGMA journal_mode = WAL');
17
- // Reduced busy timeout for worker coordination
18
- db.run('PRAGMA busy_timeout = 5000');
19
- // More frequent checkpoints
20
- db.run('PRAGMA wal_autocheckpoint = 100');
21
- // Note: cache_shared is not available in bun:sqlite
16
+ db.run('PRAGMA busy_timeout = 10000');
22
17
  }
23
- // Performance optimizations
24
18
  db.run('PRAGMA foreign_keys = ON');
25
- db.run('PRAGMA synchronous = NORMAL'); // Safe with WAL
26
- db.run('PRAGMA cache_size = -4000'); // 4MB cache
19
+ db.run('PRAGMA synchronous = NORMAL');
20
+ db.run('PRAGMA cache_size = -20000'); // 20MB cache
27
21
  db.run('PRAGMA temp_store = MEMORY');
28
- db.run('PRAGMA mmap_size = 67108864'); // 64MB mmap
22
+ db.run('PRAGMA mmap_size = 268435456'); // 256MB mmap
29
23
  // Query optimizer
30
24
  db.run('PRAGMA optimize');
31
25
  return db;
@@ -3,7 +3,7 @@
3
3
  //
4
4
  // Prisma's $transaction() uses BEGIN DEFERRED by default. When a deferred
5
5
  // transaction tries to upgrade from read to write lock, SQLite returns
6
- // SQLITE_BUSY *immediately* bypassing busy_timeout entirely.
6
+ // SQLITE_BUSY *immediately* - bypassing busy_timeout entirely.
7
7
  //
8
8
  // This helper wraps writes in BEGIN IMMEDIATE via $executeRawUnsafe,
9
9
  // which acquires the write lock upfront and respects busy_timeout.
@@ -13,7 +13,7 @@ export type QueryLoggerOptions = {
13
13
  *
14
14
  * In production, returns the database unchanged (zero overhead).
15
15
  *
16
- * Usage always wrap, logging auto-enables in dev:
16
+ * Usage - always wrap, logging auto-enables in dev:
17
17
  *
18
18
  * const db = wrapWithQueryLogging(new Database(path), { label: 'ws:abc123' });
19
19
  */
@@ -1,11 +1,11 @@
1
1
  // packages/sqlite/src/query-logger.ts
2
- // Dev-mode query logging for bun:sqlite mirrors Prisma's `prisma:query` output.
2
+ // Dev-mode query logging for bun:sqlite - mirrors Prisma's `prisma:query` output.
3
3
  // Wraps a Database with a transparent Proxy that logs SQL on query/run/exec/prepare.
4
4
  // Zero overhead in production: returns the database as-is when NODE_ENV !== 'development'.
5
5
  // biome-ignore lint/suspicious/noConsole: intentional dev-mode query logging (mirrors Prisma's log: ['query'])
6
6
  const log = console.log;
7
7
  const isDev = process.env.NODE_ENV === 'development';
8
- // ANSI green for the prefix contrasts with Prisma's blue `prisma:query`
8
+ // ANSI green for the prefix - contrasts with Prisma's blue `prisma:query`
9
9
  const GREEN = '\x1b[32m';
10
10
  const RESET = '\x1b[0m';
11
11
  const formatSql = (sql) => sql.replace(/\s+/g, ' ').trim();
@@ -19,7 +19,7 @@ const formatSql = (sql) => sql.replace(/\s+/g, ' ').trim();
19
19
  *
20
20
  * In production, returns the database unchanged (zero overhead).
21
21
  *
22
- * Usage always wrap, logging auto-enables in dev:
22
+ * Usage - always wrap, logging auto-enables in dev:
23
23
  *
24
24
  * const db = wrapWithQueryLogging(new Database(path), { label: 'ws:abc123' });
25
25
  */
@@ -0,0 +1,23 @@
1
+ import type { Database, Statement } from 'bun:sqlite';
2
+ type CachedStatement = Statement<unknown>;
3
+ export declare class StatementCache {
4
+ private cache;
5
+ private maxSize;
6
+ private hits;
7
+ private misses;
8
+ constructor(options?: {
9
+ maxSize?: number;
10
+ });
11
+ getOrPrepare(db: Database, sql: string): CachedStatement;
12
+ get(sql: string): CachedStatement | null;
13
+ set(sql: string, stmt: CachedStatement): void;
14
+ getStats(): {
15
+ hits: number;
16
+ misses: number;
17
+ size: number;
18
+ hitRate: number;
19
+ };
20
+ clear(): void;
21
+ }
22
+ export {};
23
+ //# sourceMappingURL=statement-cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"statement-cache.d.ts","sourceRoot":"","sources":["../../src/statement-cache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAGtD,KAAK,eAAe,GAAG,SAAS,CAAC,OAAO,CAAC,CAAC;AAE1C,qBAAa,cAAc;IACzB,OAAO,CAAC,KAAK,CAAsC;IACnD,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,IAAI,CAAK;IACjB,OAAO,CAAC,MAAM,CAAK;gBAEP,OAAO,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE;IAI1C,YAAY,CAAC,EAAE,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,GAAG,eAAe;IASxD,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,eAAe,GAAG,IAAI;IAWxC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,eAAe,GAAG,IAAI;IAW7C,QAAQ,IAAI;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE;IAU3E,KAAK,IAAI,IAAI;CAKd"}
@@ -0,0 +1,52 @@
1
+ export class StatementCache {
2
+ cache = new Map();
3
+ maxSize;
4
+ hits = 0;
5
+ misses = 0;
6
+ constructor(options) {
7
+ this.maxSize = options?.maxSize ?? 256;
8
+ }
9
+ getOrPrepare(db, sql) {
10
+ const normalized = sql.trim().replace(/\s+/g, ' ');
11
+ const cached = this.get(normalized);
12
+ if (cached)
13
+ return cached;
14
+ const stmt = db.query(normalized);
15
+ this.set(normalized, stmt);
16
+ return stmt;
17
+ }
18
+ get(sql) {
19
+ const stmt = this.cache.get(sql);
20
+ if (stmt) {
21
+ this.hits += 1;
22
+ this.cache.delete(sql);
23
+ this.cache.set(sql, stmt);
24
+ return stmt;
25
+ }
26
+ return null;
27
+ }
28
+ set(sql, stmt) {
29
+ this.misses += 1;
30
+ if (this.cache.size >= this.maxSize) {
31
+ const firstKey = this.cache.keys().next().value;
32
+ if (firstKey) {
33
+ this.cache.delete(firstKey);
34
+ }
35
+ }
36
+ this.cache.set(sql, stmt);
37
+ }
38
+ getStats() {
39
+ const total = this.hits + this.misses;
40
+ return {
41
+ hits: this.hits,
42
+ misses: this.misses,
43
+ size: this.cache.size,
44
+ hitRate: total > 0 ? (this.hits / total) * 100 : 0
45
+ };
46
+ }
47
+ clear() {
48
+ this.cache.clear();
49
+ this.hits = 0;
50
+ this.misses = 0;
51
+ }
52
+ }
@@ -0,0 +1,21 @@
1
+ import { Database } from 'bun:sqlite';
2
+ export type TenantDbConfig = {
3
+ maxConnections?: number;
4
+ idleTimeoutMs?: number;
5
+ cleanupIntervalMs?: number;
6
+ onOpen?: (db: Database, tenantId: string) => void;
7
+ wrapDb?: (raw: Database, tenantId: string) => Database;
8
+ };
9
+ export type TenantDbManager = {
10
+ getDb: (tenantId: string, dbPath: string) => Database;
11
+ withWriteLock: <T>(tenantId: string, fn: () => T | Promise<T>) => Promise<T>;
12
+ evict: (tenantId: string) => void;
13
+ evictIdle: (maxIdleMs?: number) => number;
14
+ closeAll: () => void;
15
+ getStats: () => {
16
+ activeConnections: number;
17
+ oldestAccess: number | null;
18
+ };
19
+ };
20
+ export declare const createTenantDbManager: (config?: TenantDbConfig) => TenantDbManager;
21
+ //# sourceMappingURL=tenant-db.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tenant-db.d.ts","sourceRoot":"","sources":["../../src/tenant-db.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAKtC,MAAM,MAAM,cAAc,GAAG;IAC3B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,MAAM,CAAC,EAAE,CAAC,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAClD,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,KAAK,QAAQ,CAAC;CACxD,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,QAAQ,CAAC;IACtD,aAAa,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IAC7E,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,SAAS,EAAE,CAAC,SAAS,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IAC1C,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,QAAQ,EAAE,MAAM;QAAE,iBAAiB,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;CAC5E,CAAC;AAmBF,eAAO,MAAM,qBAAqB,GAChC,SAAS,cAAc,KACtB,eA8IF,CAAC"}
@@ -0,0 +1,143 @@
1
+ import { Database } from 'bun:sqlite';
2
+ import * as fs from 'node:fs';
3
+ import * as path from 'node:path';
4
+ import { WriteMutexMap } from './write-mutex';
5
+ const applyPragmas = (db, dbPath) => {
6
+ if (dbPath !== ':memory:') {
7
+ db.run('PRAGMA journal_mode = WAL');
8
+ db.run('PRAGMA busy_timeout = 10000');
9
+ }
10
+ db.run('PRAGMA synchronous = NORMAL');
11
+ db.run('PRAGMA cache_size = -20000'); // 20MB
12
+ db.run('PRAGMA temp_store = MEMORY');
13
+ db.run('PRAGMA mmap_size = 268435456'); // 256MB
14
+ db.run('PRAGMA foreign_keys = ON');
15
+ };
16
+ export const createTenantDbManager = (config) => {
17
+ const maxConnections = config?.maxConnections ?? 200;
18
+ const idleTimeoutMs = config?.idleTimeoutMs ?? 300_000;
19
+ const cleanupIntervalMs = config?.cleanupIntervalMs ?? 60_000;
20
+ const connections = new Map();
21
+ const writeMutexes = new WriteMutexMap();
22
+ let cleanupIntervalId = null;
23
+ const startCleanup = () => {
24
+ if (cleanupIntervalId)
25
+ return;
26
+ cleanupIntervalId = setInterval(() => {
27
+ evictIdle();
28
+ for (const conn of connections.values()) {
29
+ try {
30
+ conn.db.run('PRAGMA wal_checkpoint(PASSIVE)');
31
+ conn.db.run('PRAGMA optimize');
32
+ }
33
+ catch {
34
+ // Non-critical
35
+ }
36
+ }
37
+ }, cleanupIntervalMs);
38
+ if (cleanupIntervalId.unref) {
39
+ cleanupIntervalId.unref();
40
+ }
41
+ };
42
+ const evict = (tenantId) => {
43
+ const entry = connections.get(tenantId);
44
+ if (entry) {
45
+ try {
46
+ entry.db.close();
47
+ }
48
+ catch {
49
+ // Ignore
50
+ }
51
+ connections.delete(tenantId);
52
+ writeMutexes.delete(tenantId);
53
+ }
54
+ };
55
+ const evictIdle = (maxIdleMs = idleTimeoutMs) => {
56
+ const now = Date.now();
57
+ let evicted = 0;
58
+ for (const [id, conn] of connections) {
59
+ if (now - conn.lastAccessed > maxIdleMs) {
60
+ try {
61
+ conn.db.close();
62
+ }
63
+ catch {
64
+ // Ignore
65
+ }
66
+ connections.delete(id);
67
+ writeMutexes.delete(id);
68
+ evicted++;
69
+ }
70
+ }
71
+ return evicted;
72
+ };
73
+ const getDb = (tenantId, dbPath) => {
74
+ const cached = connections.get(tenantId);
75
+ if (cached) {
76
+ cached.lastAccessed = Date.now();
77
+ return cached.db;
78
+ }
79
+ const dir = path.dirname(dbPath);
80
+ if (!fs.existsSync(dir)) {
81
+ fs.mkdirSync(dir, { recursive: true });
82
+ }
83
+ const raw = new Database(dbPath);
84
+ applyPragmas(raw, dbPath);
85
+ try {
86
+ config?.onOpen?.(raw, tenantId);
87
+ }
88
+ catch (err) {
89
+ raw.close();
90
+ throw err;
91
+ }
92
+ const db = config?.wrapDb ? config.wrapDb(raw, tenantId) : raw;
93
+ connections.set(tenantId, { db, lastAccessed: Date.now() });
94
+ if (connections.size > maxConnections) {
95
+ let lruId = null;
96
+ let lruAccessed = Number.MAX_SAFE_INTEGER;
97
+ for (const [id, conn] of connections) {
98
+ if (conn.lastAccessed < lruAccessed) {
99
+ lruAccessed = conn.lastAccessed;
100
+ lruId = id;
101
+ }
102
+ }
103
+ if (lruId) {
104
+ try {
105
+ connections.get(lruId)?.db.close();
106
+ }
107
+ catch {
108
+ // Ignore
109
+ }
110
+ connections.delete(lruId);
111
+ writeMutexes.delete(lruId);
112
+ }
113
+ }
114
+ startCleanup();
115
+ return db;
116
+ };
117
+ const withWriteLock = (tenantId, fn) => writeMutexes.withLock(tenantId, fn);
118
+ const closeAll = () => {
119
+ for (const [, conn] of connections) {
120
+ try {
121
+ conn.db.close();
122
+ }
123
+ catch {
124
+ // Ignore
125
+ }
126
+ }
127
+ connections.clear();
128
+ if (cleanupIntervalId) {
129
+ clearInterval(cleanupIntervalId);
130
+ cleanupIntervalId = null;
131
+ }
132
+ };
133
+ const getStats = () => {
134
+ let oldestAccess = null;
135
+ for (const conn of connections.values()) {
136
+ if (oldestAccess === null || conn.lastAccessed < oldestAccess) {
137
+ oldestAccess = conn.lastAccessed;
138
+ }
139
+ }
140
+ return { activeConnections: connections.size, oldestAccess };
141
+ };
142
+ return { getDb, withWriteLock, evict, evictIdle, closeAll, getStats };
143
+ };
@@ -13,7 +13,7 @@ export type TTLCacheOptions = {
13
13
  * `maxSize`.
14
14
  *
15
15
  * Unlike WeakMap per-request caching (which deduplicates within a single
16
- * request), TTLCache deduplicates across requests e.g. when TanStack
16
+ * request), TTLCache deduplicates across requests - e.g. when TanStack
17
17
  * Router replays `beforeLoad` on client hydration, the server returns the
18
18
  * cached result instantly (0 DB queries).
19
19
  *
@@ -11,7 +11,7 @@
11
11
  * `maxSize`.
12
12
  *
13
13
  * Unlike WeakMap per-request caching (which deduplicates within a single
14
- * request), TTLCache deduplicates across requests e.g. when TanStack
14
+ * request), TTLCache deduplicates across requests - e.g. when TanStack
15
15
  * Router replays `beforeLoad` on client hydration, the server returns the
16
16
  * cached result instantly (0 DB queries).
17
17
  *
@@ -1 +1 @@
1
- {"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../../src/worker.ts"],"names":[],"mappings":"AAoEA,KAAK,cAAc,GAAG;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE;QACP,OAAO,CAAC,EACJ,CAAC,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,GAAG,cAAc,EAAE,OAAO,EAAE,KAAK,IAAI,CAAC,GAC3D,SAAS,CAAC;QACd,aAAa,EAAE,OAAO,CAAC;KACxB,CAAC;CACH,CAAC;AAEF,KAAK,aAAa,GAAG;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,OAAO,EAAE,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,KAAK,cAAc,GAAG;IACpB,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE;QACN,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,CAAC;IACF,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;CAClB,CAAC;AA6DF,qBAAa,YAAY;IACvB,OAAO,CAAC,EAAE,CAAyB;IACnC,OAAO,CAAC,MAAM,CAA6B;IAC3C,OAAO,CAAC,QAAQ,CAAS;gBAEb,MAAM,CAAC,EAAE,cAAc;YAKrB,kBAAkB;IAkD1B,aAAa,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,cAAc,CAAC;IA6CpE,OAAO,CAAC,YAAY;IAoDpB,QAAQ,IAAI,IAAI;IAchB,WAAW,IAAI,MAAM;CAGtB"}
1
+ {"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../../src/worker.ts"],"names":[],"mappings":"AAcA,KAAK,cAAc,GAAG;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE;QACP,OAAO,CAAC,EACJ,CAAC,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,GAAG,cAAc,EAAE,OAAO,EAAE,KAAK,IAAI,CAAC,GAC3D,SAAS,CAAC;QACd,aAAa,EAAE,OAAO,CAAC;KACxB,CAAC;CACH,CAAC;AAEF,KAAK,aAAa,GAAG;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,OAAO,EAAE,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,KAAK,cAAc,GAAG;IACpB,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE;QACN,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,CAAC;IACF,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;CAClB,CAAC;AAkDF,qBAAa,YAAY;IACvB,OAAO,CAAC,EAAE,CAAyB;IACnC,OAAO,CAAC,MAAM,CAA6B;IAC3C,OAAO,CAAC,QAAQ,CAAS;gBAEb,MAAM,CAAC,EAAE,cAAc;YAKrB,kBAAkB;IAgC1B,aAAa,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,cAAc,CAAC;IA6CpE,OAAO,CAAC,YAAY;IAoDpB,QAAQ,IAAI,IAAI;IAchB,WAAW,IAAI,MAAM;CAGtB"}
@@ -3,53 +3,9 @@
3
3
  // Uses bun:sqlite for 3-6x faster reads compared to better-sqlite3
4
4
  import { Database } from 'bun:sqlite';
5
5
  import { parentPort, workerData } from 'node:worker_threads';
6
+ import { StatementCache } from './statement-cache';
6
7
  const isDevelopment = process.env.NODE_ENV === 'development';
7
8
  const isTest = process.env.NODE_ENV === 'test';
8
- // ---------------------------------------------------------------------------
9
- // Prepared-statement LRU cache for optimal performance
10
- // ---------------------------------------------------------------------------
11
- class StatementCache {
12
- cache = new Map();
13
- maxSize = 256;
14
- hits = 0;
15
- misses = 0;
16
- get(sql) {
17
- const stmt = this.cache.get(sql);
18
- if (stmt) {
19
- this.hits += 1;
20
- // Move to end (LRU behavior)
21
- this.cache.delete(sql);
22
- this.cache.set(sql, stmt);
23
- return stmt;
24
- }
25
- return null;
26
- }
27
- set(sql, stmt) {
28
- this.misses += 1;
29
- // If at capacity, remove oldest
30
- if (this.cache.size >= this.maxSize) {
31
- const firstKey = this.cache.keys().next().value;
32
- if (firstKey) {
33
- this.cache.delete(firstKey);
34
- }
35
- }
36
- this.cache.set(sql, stmt);
37
- }
38
- getStats() {
39
- const total = this.hits + this.misses;
40
- return {
41
- hits: this.hits,
42
- misses: this.misses,
43
- size: this.cache.size,
44
- hitRate: total > 0 ? (this.hits / total) * 100 : 0
45
- };
46
- }
47
- clear() {
48
- this.cache.clear();
49
- this.hits = 0;
50
- this.misses = 0;
51
- }
52
- }
53
9
  const stmtCache = new StatementCache();
54
10
  // Worker-specific database configuration
55
11
  async function getDbConfig() {
@@ -89,16 +45,7 @@ async function getDbConfig() {
89
45
  };
90
46
  }
91
47
  function getOrCreateStatement(db, sql) {
92
- // Normalize SQL for better cache hits
93
- const normalizedSql = sql.trim().replace(/\s+/g, ' ');
94
- let stmt = stmtCache.get(normalizedSql);
95
- if (stmt) {
96
- return stmt;
97
- }
98
- // Cache miss → prepare + insert (bun:sqlite uses .query() instead of .prepare())
99
- stmt = db.query(normalizedSql);
100
- stmtCache.set(normalizedSql, stmt);
101
- return stmt;
48
+ return stmtCache.getOrPrepare(db, sql);
102
49
  }
103
50
  // SQLite Worker class
104
51
  export class SQLiteWorker {
@@ -122,27 +69,15 @@ export class SQLiteWorker {
122
69
  create: !this.config.options.fileMustExist,
123
70
  readonly: false
124
71
  });
125
- // ✅ OPTIMIZED: Enhanced multi-worker SQLite configuration
126
- // bun:sqlite uses run() for PRAGMA statements instead of pragma()
127
72
  if (this.config.path !== ':memory:') {
128
- // WAL mode with optimized settings
129
73
  this.db.run('PRAGMA journal_mode = WAL');
130
- // CRITICAL: Reduced timeout for better worker coordination
131
- this.db.run('PRAGMA busy_timeout = 5000');
132
- // ✅ PERFORMANCE: More frequent checkpoints for multi-worker
133
- this.db.run('PRAGMA wal_autocheckpoint = 100');
134
- // ✅ CONCURRENCY: Enable shared cache for same-process workers
135
- // Note: cache_shared is not available in bun:sqlite, skip it
74
+ this.db.run('PRAGMA busy_timeout = 10000');
136
75
  }
137
- // ✅ PERFORMANCE: Optimized PRAGMA settings for workers
138
76
  this.db.run('PRAGMA foreign_keys = ON');
139
- this.db.run('PRAGMA synchronous = NORMAL'); // Optimal for WAL mode
140
- // MEMORY: 4MB per worker cache
141
- this.db.run('PRAGMA cache_size = -4000');
77
+ this.db.run('PRAGMA synchronous = NORMAL');
78
+ this.db.run('PRAGMA cache_size = -20000'); // 20MB cache
142
79
  this.db.run('PRAGMA temp_store = MEMORY');
143
- // CONCURRENCY: Optimized mmap for multi-worker
144
- this.db.run('PRAGMA mmap_size = 67108864'); // 64MB
145
- // ✅ PERFORMANCE: Enable query planner optimizations
80
+ this.db.run('PRAGMA mmap_size = 268435456'); // 256MB mmap
146
81
  this.db.run('PRAGMA optimize');
147
82
  }
148
83
  catch (error) {
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * A simple async mutex that serializes write access to a resource.
3
- * Bun is single-threaded, so this works as a plain promise queue
3
+ * Bun is single-threaded, so this works as a plain promise queue -
4
4
  * no atomic operations needed.
5
5
  */
6
6
  export declare class WriteMutex {
@@ -2,7 +2,7 @@
2
2
  // Promise-based per-resource write mutex for SQLite concurrency
3
3
  /**
4
4
  * A simple async mutex that serializes write access to a resource.
5
- * Bun is single-threaded, so this works as a plain promise queue
5
+ * Bun is single-threaded, so this works as a plain promise queue -
6
6
  * no atomic operations needed.
7
7
  */
8
8
  export class WriteMutex {
package/package.json CHANGED
@@ -1,21 +1,67 @@
1
1
  {
2
2
  "name": "@bitclaw/sqlite",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "High-performance SQLite worker pool and utilities using bun:sqlite",
5
- "files": ["dist", "scripts", "LICENSE", "README.md"],
5
+ "files": [
6
+ "dist",
7
+ "scripts",
8
+ "LICENSE",
9
+ "README.md"
10
+ ],
6
11
  "type": "module",
7
12
  "exports": {
8
- "./pool": { "types": "./dist/src/pool.d.ts", "default": "./dist/src/pool.js" },
9
- "./worker": { "types": "./dist/src/worker.d.ts", "default": "./dist/src/worker.js" },
10
- "./connection": { "types": "./dist/src/connection.d.ts", "default": "./dist/src/connection.js" },
11
- "./json-cache": { "types": "./dist/src/json-cache.d.ts", "default": "./dist/src/json-cache.js" },
12
- "./retry": { "types": "./dist/src/retry.d.ts", "default": "./dist/src/retry.js" },
13
- "./write-mutex": { "types": "./dist/src/write-mutex.d.ts", "default": "./dist/src/write-mutex.js" },
14
- "./prisma-immediate-tx": { "types": "./dist/src/prisma-immediate-tx.d.ts", "default": "./dist/src/prisma-immediate-tx.js" },
15
- "./query-logger": { "types": "./dist/src/query-logger.d.ts", "default": "./dist/src/query-logger.js" },
16
- "./ttl-cache": { "types": "./dist/src/ttl-cache.d.ts", "default": "./dist/src/ttl-cache.js" },
17
- "./cache-lock": { "types": "./dist/src/cache-lock.d.ts", "default": "./dist/src/cache-lock.js" },
18
- "./load-test-utils": { "types": "./dist/scripts/load-test-utils.d.ts", "default": "./dist/scripts/load-test-utils.js" }
13
+ "./pool": {
14
+ "types": "./dist/src/pool.d.ts",
15
+ "default": "./dist/src/pool.js"
16
+ },
17
+ "./worker": {
18
+ "types": "./dist/src/worker.d.ts",
19
+ "default": "./dist/src/worker.js"
20
+ },
21
+ "./connection": {
22
+ "types": "./dist/src/connection.d.ts",
23
+ "default": "./dist/src/connection.js"
24
+ },
25
+ "./json-cache": {
26
+ "types": "./dist/src/json-cache.d.ts",
27
+ "default": "./dist/src/json-cache.js"
28
+ },
29
+ "./retry": {
30
+ "types": "./dist/src/retry.d.ts",
31
+ "default": "./dist/src/retry.js"
32
+ },
33
+ "./write-mutex": {
34
+ "types": "./dist/src/write-mutex.d.ts",
35
+ "default": "./dist/src/write-mutex.js"
36
+ },
37
+ "./prisma-immediate-tx": {
38
+ "types": "./dist/src/prisma-immediate-tx.d.ts",
39
+ "default": "./dist/src/prisma-immediate-tx.js"
40
+ },
41
+ "./query-logger": {
42
+ "types": "./dist/src/query-logger.d.ts",
43
+ "default": "./dist/src/query-logger.js"
44
+ },
45
+ "./ttl-cache": {
46
+ "types": "./dist/src/ttl-cache.d.ts",
47
+ "default": "./dist/src/ttl-cache.js"
48
+ },
49
+ "./cache-lock": {
50
+ "types": "./dist/src/cache-lock.d.ts",
51
+ "default": "./dist/src/cache-lock.js"
52
+ },
53
+ "./tenant-db": {
54
+ "types": "./dist/src/tenant-db.d.ts",
55
+ "default": "./dist/src/tenant-db.js"
56
+ },
57
+ "./statement-cache": {
58
+ "types": "./dist/src/statement-cache.d.ts",
59
+ "default": "./dist/src/statement-cache.js"
60
+ },
61
+ "./load-test-utils": {
62
+ "types": "./dist/scripts/load-test-utils.d.ts",
63
+ "default": "./dist/scripts/load-test-utils.js"
64
+ }
19
65
  },
20
66
  "scripts": {
21
67
  "build": "tsc -p tsconfig.build.json",
@@ -34,10 +80,18 @@
34
80
  "publish:minor": "npm whoami && npm version minor && git push --follow-tags && npm publish --access public",
35
81
  "publish:major": "npm whoami && npm version major && git push --follow-tags && npm publish --access public"
36
82
  },
37
- "keywords": ["sqlite", "bun", "worker-pool", "json-cache", "ttl-cache"],
83
+ "keywords": [
84
+ "sqlite",
85
+ "bun",
86
+ "worker-pool",
87
+ "json-cache",
88
+ "ttl-cache"
89
+ ],
38
90
  "author": "bitclaw",
39
91
  "license": "MIT",
40
- "engines": { "bun": ">=1.3.0" },
92
+ "engines": {
93
+ "bun": ">=1.3.0"
94
+ },
41
95
  "devDependencies": {
42
96
  "@biomejs/biome": "^2.4.15",
43
97
  "knip": "^6.12.1",