@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 +3 -3
- package/dist/src/cache-lock.js +1 -1
- package/dist/src/connection.d.ts.map +1 -1
- package/dist/src/connection.js +4 -10
- package/dist/src/prisma-immediate-tx.js +1 -1
- package/dist/src/query-logger.d.ts +1 -1
- package/dist/src/query-logger.js +3 -3
- package/dist/src/statement-cache.d.ts +23 -0
- package/dist/src/statement-cache.d.ts.map +1 -0
- package/dist/src/statement-cache.js +52 -0
- package/dist/src/tenant-db.d.ts +21 -0
- package/dist/src/tenant-db.d.ts.map +1 -0
- package/dist/src/tenant-db.js +143 -0
- package/dist/src/ttl-cache.d.ts +1 -1
- package/dist/src/ttl-cache.js +1 -1
- package/dist/src/worker.d.ts.map +1 -1
- package/dist/src/worker.js +6 -71
- package/dist/src/write-mutex.d.ts +1 -1
- package/dist/src/write-mutex.js +1 -1
- package/package.json +69 -15
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**
|
|
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
|
|
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
|
|
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
|
|
package/dist/src/cache-lock.js
CHANGED
|
@@ -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
|
|
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,
|
|
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"}
|
package/dist/src/connection.js
CHANGED
|
@@ -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
|
-
|
|
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');
|
|
26
|
-
db.run('PRAGMA cache_size = -
|
|
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 =
|
|
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*
|
|
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
|
|
16
|
+
* Usage - always wrap, logging auto-enables in dev:
|
|
17
17
|
*
|
|
18
18
|
* const db = wrapWithQueryLogging(new Database(path), { label: 'ws:abc123' });
|
|
19
19
|
*/
|
package/dist/src/query-logger.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
// packages/sqlite/src/query-logger.ts
|
|
2
|
-
// Dev-mode query logging for bun:sqlite
|
|
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
|
|
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
|
|
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
|
+
};
|
package/dist/src/ttl-cache.d.ts
CHANGED
|
@@ -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
|
|
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
|
*
|
package/dist/src/ttl-cache.js
CHANGED
|
@@ -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
|
|
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
|
*
|
package/dist/src/worker.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../../src/worker.ts"],"names":[],"mappings":"
|
|
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"}
|
package/dist/src/worker.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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');
|
|
140
|
-
|
|
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
|
-
|
|
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 {
|
package/dist/src/write-mutex.js
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "High-performance SQLite worker pool and utilities using bun:sqlite",
|
|
5
|
-
"files": [
|
|
5
|
+
"files": [
|
|
6
|
+
"dist",
|
|
7
|
+
"scripts",
|
|
8
|
+
"LICENSE",
|
|
9
|
+
"README.md"
|
|
10
|
+
],
|
|
6
11
|
"type": "module",
|
|
7
12
|
"exports": {
|
|
8
|
-
"./pool": {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
"./
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
"./
|
|
17
|
-
|
|
18
|
-
|
|
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": [
|
|
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": {
|
|
92
|
+
"engines": {
|
|
93
|
+
"bun": ">=1.3.0"
|
|
94
|
+
},
|
|
41
95
|
"devDependencies": {
|
|
42
96
|
"@biomejs/biome": "^2.4.15",
|
|
43
97
|
"knip": "^6.12.1",
|