@bitclaw/sqlite 1.1.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.
Files changed (41) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +251 -0
  3. package/dist/scripts/benchmark.d.ts +3 -0
  4. package/dist/scripts/benchmark.d.ts.map +1 -0
  5. package/dist/scripts/benchmark.js +286 -0
  6. package/dist/scripts/load-test-utils.d.ts +77 -0
  7. package/dist/scripts/load-test-utils.d.ts.map +1 -0
  8. package/dist/scripts/load-test-utils.js +235 -0
  9. package/dist/src/cache-lock.d.ts +25 -0
  10. package/dist/src/cache-lock.d.ts.map +1 -0
  11. package/dist/src/cache-lock.js +95 -0
  12. package/dist/src/connection.d.ts +26 -0
  13. package/dist/src/connection.d.ts.map +1 -0
  14. package/dist/src/connection.js +132 -0
  15. package/dist/src/json-cache.d.ts +89 -0
  16. package/dist/src/json-cache.d.ts.map +1 -0
  17. package/dist/src/json-cache.js +289 -0
  18. package/dist/src/pool.d.ts +98 -0
  19. package/dist/src/pool.d.ts.map +1 -0
  20. package/dist/src/pool.js +331 -0
  21. package/dist/src/prisma-immediate-tx.d.ts +23 -0
  22. package/dist/src/prisma-immediate-tx.d.ts.map +1 -0
  23. package/dist/src/prisma-immediate-tx.js +42 -0
  24. package/dist/src/query-logger.d.ts +21 -0
  25. package/dist/src/query-logger.d.ts.map +1 -0
  26. package/dist/src/query-logger.js +60 -0
  27. package/dist/src/retry.d.ts +14 -0
  28. package/dist/src/retry.d.ts.map +1 -0
  29. package/dist/src/retry.js +49 -0
  30. package/dist/src/ttl-cache.d.ts +57 -0
  31. package/dist/src/ttl-cache.d.ts.map +1 -0
  32. package/dist/src/ttl-cache.js +92 -0
  33. package/dist/src/worker.d.ts +38 -0
  34. package/dist/src/worker.d.ts.map +1 -0
  35. package/dist/src/worker.js +294 -0
  36. package/dist/src/write-mutex.d.ts +33 -0
  37. package/dist/src/write-mutex.d.ts.map +1 -0
  38. package/dist/src/write-mutex.js +60 -0
  39. package/package.json +48 -0
  40. package/scripts/benchmark.ts +373 -0
  41. package/scripts/load-test-utils.ts +370 -0
@@ -0,0 +1,235 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Shared load test utilities for application-level HTTP throughput testing.
4
+ *
5
+ * Unlike benchmark.ts (which tests raw SQLite pool.exec() calls), these utilities
6
+ * measure end-to-end HTTP performance through the full stack: HTTP server, middleware,
7
+ * ORM (Prisma), SSR rendering, etc.
8
+ *
9
+ * Usage:
10
+ * Import into app-specific load tests:
11
+ * import { runLoadTest, formatResults } from '@bitclaw/sqlite/load-test-utils'
12
+ *
13
+ * Or run directly for a quick test:
14
+ * bun run packages/sqlite/scripts/load-test-utils.ts --url http://localhost:3001
15
+ */
16
+ /* ------------------------------------------------------------------
17
+ * Percentile calculation (reused from benchmark.ts)
18
+ * ------------------------------------------------------------------ */
19
+ export function percentile(sorted, p) {
20
+ if (!sorted.length)
21
+ return 0;
22
+ const idx = (p / 100) * (sorted.length - 1);
23
+ const lo = Math.floor(idx);
24
+ const hi = Math.ceil(idx);
25
+ const loVal = sorted[lo] ?? 0;
26
+ const hiVal = sorted[hi] ?? 0;
27
+ if (lo === hi)
28
+ return loVal;
29
+ return loVal + (hiVal - loVal) * (idx - lo);
30
+ }
31
+ /* ------------------------------------------------------------------
32
+ * Single request measurement
33
+ * ------------------------------------------------------------------ */
34
+ export async function measureResponseTime(url, method = 'GET', body, headers) {
35
+ const start = performance.now();
36
+ try {
37
+ const response = await fetch(url, {
38
+ method,
39
+ body: method !== 'GET' ? body : undefined,
40
+ headers: {
41
+ Accept: 'text/html,application/json',
42
+ 'User-Agent': 'sqlite-saas-load-test/1.0',
43
+ ...headers
44
+ }
45
+ });
46
+ const responseBody = await response.text();
47
+ const latencyMs = performance.now() - start;
48
+ return {
49
+ statusCode: response.status,
50
+ latencyMs,
51
+ success: response.status >= 200 && response.status < 400,
52
+ bodySize: responseBody.length
53
+ };
54
+ }
55
+ catch {
56
+ return {
57
+ statusCode: 0,
58
+ latencyMs: performance.now() - start,
59
+ success: false,
60
+ bodySize: 0
61
+ };
62
+ }
63
+ }
64
+ /* ------------------------------------------------------------------
65
+ * Worker loop for sustained load
66
+ * ------------------------------------------------------------------ */
67
+ async function workerLoop(url, method, body, headers, endTs, results) {
68
+ while (performance.now() < endTs) {
69
+ const result = await measureResponseTime(url, method, body, headers);
70
+ results.push(result);
71
+ }
72
+ }
73
+ /* ------------------------------------------------------------------
74
+ * Run a single scenario (one endpoint at one concurrency level)
75
+ * ------------------------------------------------------------------ */
76
+ async function runScenario(baseUrl, endpoint, concurrency, durationSec, warmupRequests) {
77
+ const url = `${baseUrl}${endpoint.path}`;
78
+ const method = endpoint.method ?? 'GET';
79
+ const label = endpoint.label ?? endpoint.path;
80
+ // Warm-up phase
81
+ if (warmupRequests > 0) {
82
+ const warmups = Array.from({ length: Math.min(warmupRequests, concurrency) }, () => measureResponseTime(url, method, endpoint.body, endpoint.headers));
83
+ await Promise.allSettled(warmups);
84
+ }
85
+ // Measurement phase
86
+ const results = [];
87
+ const endTs = performance.now() + durationSec * 1000;
88
+ const workers = Array.from({ length: concurrency }, () => workerLoop(url, method, endpoint.body, endpoint.headers, endTs, results));
89
+ await Promise.allSettled(workers);
90
+ // Calculate metrics
91
+ const latencies = results.map(r => r.latencyMs).sort((a, b) => a - b);
92
+ const successCount = results.filter(r => r.success).length;
93
+ const failCount = results.length - successCount;
94
+ const totalBodySize = results.reduce((sum, r) => sum + r.bodySize, 0);
95
+ const statusCodes = {};
96
+ for (const r of results) {
97
+ statusCodes[r.statusCode] = (statusCodes[r.statusCode] ?? 0) + 1;
98
+ }
99
+ return {
100
+ endpoint: endpoint.path,
101
+ label,
102
+ method,
103
+ concurrency,
104
+ durationSec,
105
+ totalRequests: results.length,
106
+ successCount,
107
+ failCount,
108
+ successRate: results.length > 0 ? (successCount / results.length) * 100 : 0,
109
+ throughput: results.length / durationSec,
110
+ p50: percentile(latencies, 50),
111
+ p95: percentile(latencies, 95),
112
+ p99: percentile(latencies, 99),
113
+ min: latencies[0] ?? 0,
114
+ max: latencies.at(-1) ?? 0,
115
+ avg: latencies.length > 0
116
+ ? latencies.reduce((a, b) => a + b, 0) / latencies.length
117
+ : 0,
118
+ statusCodes,
119
+ avgBodySize: results.length > 0 ? totalBodySize / results.length : 0
120
+ };
121
+ }
122
+ /* ------------------------------------------------------------------
123
+ * Main load test runner
124
+ * ------------------------------------------------------------------ */
125
+ export async function runLoadTest(config) {
126
+ const startedAt = new Date().toISOString();
127
+ const scenarios = [];
128
+ const warmup = config.warmupRequests ?? 5;
129
+ for (const endpoint of config.endpoints) {
130
+ for (const concurrency of config.concurrencyLevels) {
131
+ const _label = endpoint.label ?? endpoint.path;
132
+ const result = await runScenario(config.baseUrl, endpoint, concurrency, config.durationSec, warmup);
133
+ scenarios.push(result);
134
+ }
135
+ }
136
+ return {
137
+ baseUrl: config.baseUrl,
138
+ startedAt,
139
+ completedAt: new Date().toISOString(),
140
+ scenarios
141
+ };
142
+ }
143
+ /* ------------------------------------------------------------------
144
+ * Format results as a table
145
+ * ------------------------------------------------------------------ */
146
+ export function formatResults(results) {
147
+ const lines = [];
148
+ lines.push('');
149
+ lines.push('='.repeat(100));
150
+ lines.push(' APPLICATION-LEVEL LOAD TEST RESULTS');
151
+ lines.push(` Target: ${results.baseUrl}`);
152
+ lines.push(` Started: ${results.startedAt}`);
153
+ lines.push(` Completed: ${results.completedAt}`);
154
+ lines.push('='.repeat(100));
155
+ lines.push('');
156
+ // Summary table header
157
+ const header = [
158
+ 'Endpoint'.padEnd(25),
159
+ 'Conc'.padStart(5),
160
+ 'Req/s'.padStart(8),
161
+ 'Total'.padStart(7),
162
+ 'P50ms'.padStart(8),
163
+ 'P95ms'.padStart(8),
164
+ 'P99ms'.padStart(8),
165
+ 'Success'.padStart(8),
166
+ 'AvgBody'.padStart(8)
167
+ ].join(' | ');
168
+ lines.push(header);
169
+ lines.push('-'.repeat(header.length));
170
+ for (const s of results.scenarios) {
171
+ const row = [
172
+ s.label.padEnd(25).slice(0, 25),
173
+ String(s.concurrency).padStart(5),
174
+ s.throughput.toFixed(0).padStart(8),
175
+ String(s.totalRequests).padStart(7),
176
+ s.p50.toFixed(1).padStart(8),
177
+ s.p95.toFixed(1).padStart(8),
178
+ s.p99.toFixed(1).padStart(8),
179
+ `${s.successRate.toFixed(1)}%`.padStart(8),
180
+ formatBytes(s.avgBodySize).padStart(8)
181
+ ].join(' | ');
182
+ lines.push(row);
183
+ }
184
+ lines.push('');
185
+ // Pool-level comparison note
186
+ lines.push('-'.repeat(100));
187
+ lines.push(' NOTE: Pool-level benchmarks (raw pool.exec) show 6,102-13,781 req/s.');
188
+ lines.push(' Application-level throughput is lower due to HTTP overhead, middleware,');
189
+ lines.push(' Prisma ORM, SSR rendering, and serialization.');
190
+ lines.push('-'.repeat(100));
191
+ lines.push('');
192
+ // Status code breakdown
193
+ lines.push(' Status Code Breakdown:');
194
+ for (const s of results.scenarios) {
195
+ const codes = Object.entries(s.statusCodes)
196
+ .map(([code, count]) => `${code}:${count}`)
197
+ .join(', ');
198
+ lines.push(` ${s.label} @${s.concurrency}: ${codes}`);
199
+ }
200
+ lines.push('');
201
+ return lines.join('\n');
202
+ }
203
+ function formatBytes(bytes) {
204
+ if (bytes < 1024)
205
+ return `${Math.round(bytes)}B`;
206
+ if (bytes < 1024 * 1024)
207
+ return `${(bytes / 1024).toFixed(1)}KB`;
208
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
209
+ }
210
+ /* ------------------------------------------------------------------
211
+ * CLI: run directly for a quick smoke test
212
+ * ------------------------------------------------------------------ */
213
+ async function main() {
214
+ const args = process.argv.slice(2);
215
+ const urlIdx = args.indexOf('--url');
216
+ const baseUrl = (urlIdx !== -1 ? args[urlIdx + 1] : undefined) ?? 'http://localhost:3001';
217
+ if (args.includes('--help')) {
218
+ process.exit(0);
219
+ }
220
+ // Quick smoke test against the provided URL
221
+ const _results = await runLoadTest({
222
+ baseUrl,
223
+ endpoints: [{ path: '/', label: 'Homepage' }],
224
+ concurrencyLevels: [1, 10],
225
+ durationSec: 5,
226
+ warmupRequests: 3
227
+ });
228
+ }
229
+ // Run if executed directly
230
+ if (import.meta.path === Bun.main) {
231
+ main().catch(err => {
232
+ console.error('Load test failed:', err);
233
+ process.exit(1);
234
+ });
235
+ }
@@ -0,0 +1,25 @@
1
+ import type { Database } from 'bun:sqlite';
2
+ export type LockResult = {
3
+ acquired: boolean;
4
+ owner: string;
5
+ };
6
+ export declare class CacheLock {
7
+ private readonly db;
8
+ private readonly acquireStmt;
9
+ private readonly checkOwnerStmt;
10
+ private readonly releaseStmt;
11
+ private readonly forceReleaseStmt;
12
+ private readonly refreshStmt;
13
+ private readonly isLockedStmt;
14
+ private readonly pruneExpiredStmt;
15
+ private readonly deleteExpiredStmt;
16
+ constructor(db: Database);
17
+ acquire(key: string, ttlMs: number, owner?: string): LockResult;
18
+ release(key: string, owner: string): boolean;
19
+ forceRelease(key: string): boolean;
20
+ refresh(key: string, ttlMs: number, owner: string): boolean;
21
+ isLocked(key: string): boolean;
22
+ pruneExpired(): number;
23
+ withLock<T>(key: string, ttlMs: number, fn: () => T | Promise<T>): Promise<T>;
24
+ }
25
+ //# sourceMappingURL=cache-lock.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache-lock.d.ts","sourceRoot":"","sources":["../../src/cache-lock.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAG3C,MAAM,MAAM,UAAU,GAAG;IACvB,QAAQ,EAAE,OAAO,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,qBAAa,SAAS;IACpB,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAW;IAE9B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC;IAC7B,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAC;IAChC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC;IAC7B,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAC;IAClC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC;IAC7B,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAC;IAC9B,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAC;IAClC,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAC;gBAEvB,EAAE,EAAE,QAAQ;IAyCxB,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,UAAU;IA+B/D,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO;IAK5C,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAKlC,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO;IAU3D,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAK9B,YAAY,IAAI,MAAM;IAKhB,QAAQ,CAAC,CAAC,EACd,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,MAAM,EACb,EAAE,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,GACvB,OAAO,CAAC,CAAC,CAAC;CAYd"}
@@ -0,0 +1,95 @@
1
+ // packages/sqlite/src/cache-lock.ts
2
+ // DB-backed distributed locks for mutual exclusion using bun:sqlite
3
+ import { randomUUID } from 'node:crypto';
4
+ export class CacheLock {
5
+ db;
6
+ acquireStmt;
7
+ checkOwnerStmt;
8
+ releaseStmt;
9
+ forceReleaseStmt;
10
+ refreshStmt;
11
+ isLockedStmt;
12
+ pruneExpiredStmt;
13
+ deleteExpiredStmt;
14
+ constructor(db) {
15
+ this.db = db;
16
+ db.run(`
17
+ CREATE TABLE IF NOT EXISTS cache_locks (
18
+ key TEXT PRIMARY KEY,
19
+ owner TEXT NOT NULL,
20
+ expiration INTEGER NOT NULL
21
+ )
22
+ `);
23
+ db.run(`
24
+ CREATE INDEX IF NOT EXISTS idx_cache_locks_expiration
25
+ ON cache_locks (expiration)
26
+ `);
27
+ this.deleteExpiredStmt = db.query('DELETE FROM cache_locks WHERE key = $key AND expiration <= $now');
28
+ this.acquireStmt = db.query('INSERT OR IGNORE INTO cache_locks (key, owner, expiration) VALUES ($key, $owner, $expiration)');
29
+ this.checkOwnerStmt = db.query('SELECT owner FROM cache_locks WHERE key = $key');
30
+ this.releaseStmt = db.query('DELETE FROM cache_locks WHERE key = $key AND owner = $owner');
31
+ this.forceReleaseStmt = db.query('DELETE FROM cache_locks WHERE key = $key');
32
+ this.refreshStmt = db.query('UPDATE cache_locks SET expiration = $expiration WHERE key = $key AND owner = $owner');
33
+ this.isLockedStmt = db.query('SELECT 1 FROM cache_locks WHERE key = $key AND expiration > $now LIMIT 1');
34
+ this.pruneExpiredStmt = db.query('DELETE FROM cache_locks WHERE expiration <= $now');
35
+ }
36
+ acquire(key, ttlMs, owner) {
37
+ const actualOwner = owner ?? randomUUID();
38
+ const now = Date.now();
39
+ const expiration = now + ttlMs;
40
+ const result = this.db.transaction(() => {
41
+ // Remove expired lock for this key first
42
+ this.deleteExpiredStmt.run({ $key: key, $now: now });
43
+ // Try to insert — OR IGNORE means it fails silently if key exists
44
+ this.acquireStmt.run({
45
+ $key: key,
46
+ $owner: actualOwner,
47
+ $expiration: expiration
48
+ });
49
+ // Check who owns the lock
50
+ const row = this.checkOwnerStmt.get({ $key: key });
51
+ if (row && row.owner === actualOwner) {
52
+ return { acquired: true, owner: actualOwner };
53
+ }
54
+ return { acquired: false, owner: row?.owner ?? '' };
55
+ });
56
+ return result.immediate();
57
+ }
58
+ release(key, owner) {
59
+ const result = this.releaseStmt.run({ $key: key, $owner: owner });
60
+ return result.changes > 0;
61
+ }
62
+ forceRelease(key) {
63
+ const result = this.forceReleaseStmt.run({ $key: key });
64
+ return result.changes > 0;
65
+ }
66
+ refresh(key, ttlMs, owner) {
67
+ const expiration = Date.now() + ttlMs;
68
+ const result = this.refreshStmt.run({
69
+ $key: key,
70
+ $owner: owner,
71
+ $expiration: expiration
72
+ });
73
+ return result.changes > 0;
74
+ }
75
+ isLocked(key) {
76
+ const row = this.isLockedStmt.get({ $key: key, $now: Date.now() });
77
+ return row != null;
78
+ }
79
+ pruneExpired() {
80
+ const result = this.pruneExpiredStmt.run({ $now: Date.now() });
81
+ return result.changes;
82
+ }
83
+ async withLock(key, ttlMs, fn) {
84
+ const result = this.acquire(key, ttlMs);
85
+ if (!result.acquired) {
86
+ throw new Error(`Failed to acquire lock: ${key}`);
87
+ }
88
+ try {
89
+ return await fn();
90
+ }
91
+ finally {
92
+ this.release(key, result.owner);
93
+ }
94
+ }
95
+ }
@@ -0,0 +1,26 @@
1
+ import { Database } from 'bun:sqlite';
2
+ export type ConnectionConfig = {
3
+ path: string;
4
+ readonly?: boolean;
5
+ verbose?: boolean;
6
+ };
7
+ /**
8
+ * Initialize a SQLite database with optimal settings
9
+ */
10
+ export declare function initializeConnection(config: ConnectionConfig): Database;
11
+ /**
12
+ * Close database connection gracefully
13
+ */
14
+ export declare function closeConnection(db: Database): void;
15
+ /**
16
+ * Check database health
17
+ */
18
+ export declare function checkHealth(db: Database): {
19
+ healthy: boolean;
20
+ details: Record<string, unknown>;
21
+ };
22
+ /**
23
+ * Get database statistics
24
+ */
25
+ export declare function getStats(db: Database): Record<string, unknown>;
26
+ //# sourceMappingURL=connection.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,132 @@
1
+ // packages/sqlite/src/connection.ts
2
+ // Database connection initialization and configuration
3
+ // Uses bun:sqlite for 3-6x faster reads compared to better-sqlite3
4
+ import { Database } from 'bun:sqlite';
5
+ /**
6
+ * Initialize a SQLite database with optimal settings
7
+ */
8
+ export function initializeConnection(config) {
9
+ const db = new Database(config.path, {
10
+ readonly: config.readonly ?? false,
11
+ create: true
12
+ });
13
+ // Apply optimal PRAGMA settings
14
+ if (config.path !== ':memory:') {
15
+ // WAL mode for better concurrency
16
+ 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
22
+ }
23
+ // Performance optimizations
24
+ db.run('PRAGMA foreign_keys = ON');
25
+ db.run('PRAGMA synchronous = NORMAL'); // Safe with WAL
26
+ db.run('PRAGMA cache_size = -4000'); // 4MB cache
27
+ db.run('PRAGMA temp_store = MEMORY');
28
+ db.run('PRAGMA mmap_size = 67108864'); // 64MB mmap
29
+ // Query optimizer
30
+ db.run('PRAGMA optimize');
31
+ return db;
32
+ }
33
+ /**
34
+ * Close database connection gracefully
35
+ */
36
+ export function closeConnection(db) {
37
+ try {
38
+ // Checkpoint WAL before closing
39
+ try {
40
+ db.run('PRAGMA wal_checkpoint(TRUNCATE)');
41
+ }
42
+ catch (error) {
43
+ console.warn('[db] WAL checkpoint failed:', error);
44
+ }
45
+ db.close();
46
+ }
47
+ catch (error) {
48
+ console.error('[db] Error closing connection:', error);
49
+ }
50
+ }
51
+ /**
52
+ * Whitelist of allowed PRAGMA names to prevent SQL injection
53
+ * via string interpolation in getPragmaValue.
54
+ */
55
+ const ALLOWED_PRAGMAS = new Set([
56
+ 'journal_mode',
57
+ 'wal_autocheckpoint',
58
+ 'busy_timeout',
59
+ 'cache_size',
60
+ 'foreign_keys',
61
+ 'synchronous',
62
+ 'temp_store',
63
+ 'mmap_size',
64
+ 'page_size',
65
+ 'locking_mode',
66
+ 'page_count',
67
+ 'wal_checkpoint'
68
+ ]);
69
+ /**
70
+ * Helper to get a PRAGMA value
71
+ */
72
+ function getPragmaValue(db, pragma) {
73
+ if (!ALLOWED_PRAGMAS.has(pragma)) {
74
+ throw new Error(`PRAGMA "${pragma}" is not allowed. Allowed PRAGMAs: ${[...ALLOWED_PRAGMAS].join(', ')}`);
75
+ }
76
+ const result = db.query(`PRAGMA ${pragma}`).get();
77
+ return result ? Object.values(result)[0] : null;
78
+ }
79
+ /**
80
+ * Check database health
81
+ */
82
+ export function checkHealth(db) {
83
+ try {
84
+ // Simple query to verify database is accessible
85
+ const result = db.query('SELECT 1 as health').get();
86
+ const walInfo = db.query('PRAGMA wal_checkpoint').all();
87
+ const pageCount = getPragmaValue(db, 'page_count');
88
+ const pageSize = getPragmaValue(db, 'page_size');
89
+ return {
90
+ healthy: result.health === 1,
91
+ details: {
92
+ accessible: true,
93
+ journalMode: getPragmaValue(db, 'journal_mode'),
94
+ pageCount,
95
+ pageSize,
96
+ databaseSize: pageCount * pageSize,
97
+ walInfo
98
+ }
99
+ };
100
+ }
101
+ catch (error) {
102
+ return {
103
+ healthy: false,
104
+ details: {
105
+ error: error instanceof Error ? error.message : String(error),
106
+ accessible: false
107
+ }
108
+ };
109
+ }
110
+ }
111
+ /**
112
+ * Get database statistics
113
+ */
114
+ export function getStats(db) {
115
+ try {
116
+ return {
117
+ journalMode: getPragmaValue(db, 'journal_mode'),
118
+ pageCount: getPragmaValue(db, 'page_count'),
119
+ pageSize: getPragmaValue(db, 'page_size'),
120
+ cacheSize: getPragmaValue(db, 'cache_size'),
121
+ mmapSize: getPragmaValue(db, 'mmap_size'),
122
+ walAutocheckpoint: getPragmaValue(db, 'wal_autocheckpoint'),
123
+ synchronous: getPragmaValue(db, 'synchronous'),
124
+ foreignKeys: getPragmaValue(db, 'foreign_keys')
125
+ };
126
+ }
127
+ catch (error) {
128
+ return {
129
+ error: error instanceof Error ? error.message : String(error)
130
+ };
131
+ }
132
+ }
@@ -0,0 +1,89 @@
1
+ export type CacheEntry<T> = {
2
+ value: T;
3
+ metadata: {
4
+ createdTime: number;
5
+ ttl: number | null;
6
+ swr?: number | null;
7
+ };
8
+ };
9
+ export type CacheOptions = {
10
+ ttl?: number;
11
+ swr?: number;
12
+ };
13
+ export type JsonCacheConfig = {
14
+ cacheDir: string;
15
+ defaultTtl?: number;
16
+ };
17
+ /**
18
+ * Simple, fast JSON file cache inspired by levelsio's approach
19
+ * - Each cache key is a separate JSON file
20
+ * - Atomic writes using temp files
21
+ * - TTL-based expiration
22
+ * - Stale-while-revalidate support
23
+ * - No external dependencies
24
+ */
25
+ export declare class JsonCache {
26
+ private cacheDir;
27
+ private defaultTtl;
28
+ private pendingWrites;
29
+ constructor(config: JsonCacheConfig);
30
+ /**
31
+ * Initialize cache directory
32
+ */
33
+ init(): Promise<void>;
34
+ /**
35
+ * Get cache file path for a key
36
+ */
37
+ private getFilePath;
38
+ /**
39
+ * Get value from cache
40
+ */
41
+ get<T = unknown>(key: string): Promise<T | null>;
42
+ /**
43
+ * Set value in cache with atomic write
44
+ */
45
+ set<T = unknown>(key: string, value: T, options?: CacheOptions): Promise<void>;
46
+ /**
47
+ * Atomic write using temp file
48
+ */
49
+ private _atomicWrite;
50
+ /**
51
+ * Delete cache entry
52
+ */
53
+ delete(key: string): Promise<void>;
54
+ /**
55
+ * Check if cache entry exists
56
+ */
57
+ has(key: string): Promise<boolean>;
58
+ /**
59
+ * Clear all cache entries
60
+ */
61
+ clear(): Promise<void>;
62
+ /**
63
+ * Get cache statistics
64
+ */
65
+ stats(): Promise<{
66
+ size: number;
67
+ entries: string[];
68
+ }>;
69
+ /**
70
+ * Clean up expired entries
71
+ */
72
+ cleanup(): Promise<number>;
73
+ }
74
+ /**
75
+ * Factory function to create and initialize a JSON cache instance.
76
+ * Awaits directory creation so the cache is ready to use immediately.
77
+ */
78
+ export declare const createJsonCache: (config: JsonCacheConfig) => Promise<JsonCache>;
79
+ /**
80
+ * Helper function for cachified pattern
81
+ */
82
+ export declare const cachified: <T>(options: {
83
+ cache: JsonCache;
84
+ key: string;
85
+ getFreshValue: () => Promise<T>;
86
+ ttl?: number;
87
+ swr?: number;
88
+ }) => Promise<T>;
89
+ //# sourceMappingURL=json-cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"json-cache.d.ts","sourceRoot":"","sources":["../../src/json-cache.ts"],"names":[],"mappings":"AAmBA,MAAM,MAAM,UAAU,CAAC,CAAC,IAAI;IAC1B,KAAK,EAAE,CAAC,CAAC;IACT,QAAQ,EAAE;QACR,WAAW,EAAE,MAAM,CAAC;QACpB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;QACnB,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;KACrB,CAAC;CACH,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF;;;;;;;GAOG;AACH,qBAAa,SAAS;IACpB,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,aAAa,CAAoC;gBAE7C,MAAM,EAAE,eAAe;IAKnC;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAW3B;;OAEG;IACH,OAAO,CAAC,WAAW;IAMnB;;OAEG;IACG,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;IA+CtD;;OAEG;IACG,GAAG,CAAC,CAAC,GAAG,OAAO,EACnB,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,CAAC,EACR,OAAO,CAAC,EAAE,YAAY,GACrB,OAAO,CAAC,IAAI,CAAC;IAoBhB;;OAEG;YACW,YAAY;IA4C1B;;OAEG;IACG,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAiBxC;;OAEG;IACG,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAWxC;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAiB5B;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IAc3D;;OAEG;IACG,OAAO,IAAI,OAAO,CAAC,MAAM,CAAC;CA+CjC;AAED;;;GAGG;AACH,eAAO,MAAM,eAAe,GAC1B,QAAQ,eAAe,KACtB,OAAO,CAAC,SAAS,CAenB,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,SAAS,GAAU,CAAC,EAAE,SAAS;IAC1C,KAAK,EAAE,SAAS,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,aAAa,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,CAAC;IAChC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,KAAG,OAAO,CAAC,CAAC,CAkBZ,CAAC"}