@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,289 @@
1
+ // packages/sqlite/src/json-cache.ts
2
+ // levelsio-style JSON file caching for expensive queries
3
+ import { randomBytes } from 'node:crypto';
4
+ import { promises as fs } from 'node:fs';
5
+ import { dirname, join } from 'node:path';
6
+ const isDevelopment = process.env.NODE_ENV === 'development';
7
+ const isCacheEntry = (data) => {
8
+ return (data != null &&
9
+ typeof data === 'object' &&
10
+ 'value' in data &&
11
+ 'metadata' in data &&
12
+ typeof data.metadata?.createdTime === 'number');
13
+ };
14
+ /**
15
+ * Simple, fast JSON file cache inspired by levelsio's approach
16
+ * - Each cache key is a separate JSON file
17
+ * - Atomic writes using temp files
18
+ * - TTL-based expiration
19
+ * - Stale-while-revalidate support
20
+ * - No external dependencies
21
+ */
22
+ export class JsonCache {
23
+ cacheDir;
24
+ defaultTtl;
25
+ pendingWrites = new Map();
26
+ constructor(config) {
27
+ this.cacheDir = config.cacheDir;
28
+ this.defaultTtl = config.defaultTtl ?? 300000; // 5 minutes default
29
+ }
30
+ /**
31
+ * Initialize cache directory
32
+ */
33
+ async init() {
34
+ try {
35
+ await fs.mkdir(this.cacheDir, { recursive: true });
36
+ if (isDevelopment) {
37
+ }
38
+ }
39
+ catch (error) {
40
+ console.error('[json-cache] Failed to initialize:', error);
41
+ throw error;
42
+ }
43
+ }
44
+ /**
45
+ * Get cache file path for a key
46
+ */
47
+ getFilePath(key) {
48
+ // Sanitize key for filesystem
49
+ const sanitized = key.replace(/[^a-z0-9-_.]/gi, '_');
50
+ return join(this.cacheDir, `${sanitized}.json`);
51
+ }
52
+ /**
53
+ * Get value from cache
54
+ */
55
+ async get(key) {
56
+ const filePath = this.getFilePath(key);
57
+ try {
58
+ const data = await fs.readFile(filePath, 'utf-8');
59
+ const parsed = JSON.parse(data);
60
+ if (!isCacheEntry(parsed)) {
61
+ await this.delete(key);
62
+ return null;
63
+ }
64
+ const entry = parsed;
65
+ const now = Date.now();
66
+ const age = now - entry.metadata.createdTime;
67
+ // Check if expired
68
+ if (entry.metadata.ttl && age > entry.metadata.ttl) {
69
+ // Check stale-while-revalidate
70
+ if (entry.metadata.swr &&
71
+ age <= entry.metadata.ttl + entry.metadata.swr) {
72
+ // Return stale value but trigger background revalidation
73
+ if (isDevelopment) {
74
+ }
75
+ return entry.value;
76
+ }
77
+ // Expired and past SWR window
78
+ await this.delete(key);
79
+ return null;
80
+ }
81
+ return entry.value;
82
+ }
83
+ catch (error) {
84
+ if (error instanceof Error &&
85
+ error.code === 'ENOENT') {
86
+ // File doesn't exist - cache miss
87
+ return null;
88
+ }
89
+ console.error(`[json-cache] Error reading ${key}:`, error);
90
+ return null;
91
+ }
92
+ }
93
+ /**
94
+ * Set value in cache with atomic write
95
+ */
96
+ async set(key, value, options) {
97
+ const filePath = this.getFilePath(key);
98
+ // If there's already a pending write for this key, wait for it
99
+ const pendingWrite = this.pendingWrites.get(key);
100
+ if (pendingWrite) {
101
+ await pendingWrite;
102
+ }
103
+ // Create new write promise
104
+ const writePromise = this._atomicWrite(filePath, value, options);
105
+ this.pendingWrites.set(key, writePromise);
106
+ try {
107
+ await writePromise;
108
+ }
109
+ finally {
110
+ this.pendingWrites.delete(key);
111
+ }
112
+ }
113
+ /**
114
+ * Atomic write using temp file
115
+ */
116
+ async _atomicWrite(filePath, value, options) {
117
+ const entry = {
118
+ value,
119
+ metadata: {
120
+ createdTime: Date.now(),
121
+ ttl: options?.ttl ?? this.defaultTtl,
122
+ swr: options?.swr ?? null
123
+ }
124
+ };
125
+ // Ensure directory exists
126
+ const dir = dirname(filePath);
127
+ await fs.mkdir(dir, { recursive: true });
128
+ // Write to temp file first (atomic operation)
129
+ const tempPath = `${filePath}.${randomBytes(8).toString('hex')}.tmp`;
130
+ try {
131
+ await fs.writeFile(tempPath, JSON.stringify(entry, null, isDevelopment ? 2 : 0), 'utf-8');
132
+ // Atomic rename
133
+ await fs.rename(tempPath, filePath);
134
+ if (isDevelopment) {
135
+ }
136
+ }
137
+ catch (error) {
138
+ // Clean up temp file if it exists
139
+ try {
140
+ await fs.unlink(tempPath);
141
+ }
142
+ catch {
143
+ // Ignore cleanup errors
144
+ }
145
+ throw error;
146
+ }
147
+ }
148
+ /**
149
+ * Delete cache entry
150
+ */
151
+ async delete(key) {
152
+ const filePath = this.getFilePath(key);
153
+ try {
154
+ await fs.unlink(filePath);
155
+ if (isDevelopment) {
156
+ }
157
+ }
158
+ catch (error) {
159
+ if (!(error instanceof Error) ||
160
+ error.code !== 'ENOENT') {
161
+ console.error(`[json-cache] Error deleting ${key}:`, error);
162
+ }
163
+ }
164
+ }
165
+ /**
166
+ * Check if cache entry exists
167
+ */
168
+ async has(key) {
169
+ const filePath = this.getFilePath(key);
170
+ try {
171
+ await fs.access(filePath);
172
+ return true;
173
+ }
174
+ catch {
175
+ return false;
176
+ }
177
+ }
178
+ /**
179
+ * Clear all cache entries
180
+ */
181
+ async clear() {
182
+ try {
183
+ const files = await fs.readdir(this.cacheDir);
184
+ await Promise.all(files
185
+ .filter(file => file.endsWith('.json'))
186
+ .map(file => fs.unlink(join(this.cacheDir, file))));
187
+ if (isDevelopment) {
188
+ }
189
+ }
190
+ catch (error) {
191
+ console.error('[json-cache] Error clearing cache:', error);
192
+ }
193
+ }
194
+ /**
195
+ * Get cache statistics
196
+ */
197
+ async stats() {
198
+ try {
199
+ const files = await fs.readdir(this.cacheDir);
200
+ const jsonFiles = files.filter(file => file.endsWith('.json'));
201
+ return {
202
+ size: jsonFiles.length,
203
+ entries: jsonFiles.map(file => file.replace('.json', ''))
204
+ };
205
+ }
206
+ catch {
207
+ return { size: 0, entries: [] };
208
+ }
209
+ }
210
+ /**
211
+ * Clean up expired entries
212
+ */
213
+ async cleanup() {
214
+ try {
215
+ const files = await fs.readdir(this.cacheDir);
216
+ const jsonFiles = files.filter(file => file.endsWith('.json'));
217
+ let cleaned = 0;
218
+ for (const file of jsonFiles) {
219
+ const filePath = join(this.cacheDir, file);
220
+ try {
221
+ const data = await fs.readFile(filePath, 'utf-8');
222
+ const parsed = JSON.parse(data);
223
+ if (!isCacheEntry(parsed)) {
224
+ await fs.unlink(filePath);
225
+ cleaned++;
226
+ continue;
227
+ }
228
+ const entry = parsed;
229
+ const now = Date.now();
230
+ const age = now - entry.metadata.createdTime;
231
+ // Check if expired (including SWR window)
232
+ const totalTtl = entry.metadata.ttl ?? this.defaultTtl;
233
+ const swrWindow = entry.metadata.swr ?? 0;
234
+ if (age > totalTtl + swrWindow) {
235
+ await fs.unlink(filePath);
236
+ cleaned++;
237
+ }
238
+ }
239
+ catch {
240
+ // If we can't read/parse the file, delete it
241
+ await fs.unlink(filePath);
242
+ cleaned++;
243
+ }
244
+ }
245
+ if (isDevelopment && cleaned > 0) {
246
+ }
247
+ return cleaned;
248
+ }
249
+ catch (error) {
250
+ console.error('[json-cache] Error during cleanup:', error);
251
+ return 0;
252
+ }
253
+ }
254
+ }
255
+ /**
256
+ * Factory function to create and initialize a JSON cache instance.
257
+ * Awaits directory creation so the cache is ready to use immediately.
258
+ */
259
+ export const createJsonCache = async (config) => {
260
+ const cache = new JsonCache(config);
261
+ await cache.init();
262
+ // Set up periodic cleanup (every hour)
263
+ if (process.env.NODE_ENV !== 'test') {
264
+ setInterval(() => {
265
+ cache.cleanup().catch(error => {
266
+ console.error('[json-cache] Cleanup failed:', error);
267
+ });
268
+ }, 3600000); // 1 hour
269
+ }
270
+ return cache;
271
+ };
272
+ /**
273
+ * Helper function for cachified pattern
274
+ */
275
+ export const cachified = async (options) => {
276
+ const { cache, key, getFreshValue, ttl, swr } = options;
277
+ // Try to get from cache
278
+ const cached = await cache.get(key);
279
+ if (cached !== null) {
280
+ return cached;
281
+ }
282
+ // Cache miss - get fresh value
283
+ const freshValue = await getFreshValue();
284
+ // Store in cache (don't await - fire and forget)
285
+ cache.set(key, freshValue, { ttl, swr }).catch(error => {
286
+ console.error(`[json-cache] Failed to cache ${key}:`, error);
287
+ });
288
+ return freshValue;
289
+ };
@@ -0,0 +1,98 @@
1
+ import { EventEmitter } from 'node:events';
2
+ export type PoolMetrics = {
3
+ totalQueries: number;
4
+ activeQueries: number;
5
+ totalDuration: number;
6
+ errors: number;
7
+ workerStats: {
8
+ [workerId: string]: {
9
+ queries: number;
10
+ errors: number;
11
+ totalDuration: number;
12
+ };
13
+ };
14
+ };
15
+ export type PoolConfig = {
16
+ databasePath?: string;
17
+ poolSize?: number;
18
+ timeout?: number;
19
+ };
20
+ declare class SQLiteConnectionPool extends EventEmitter {
21
+ private workers;
22
+ private maxWorkers;
23
+ private workerTimeout;
24
+ private databasePath;
25
+ private pendingOperations;
26
+ private nextWorkerIndex;
27
+ private isShuttingDown;
28
+ private metrics;
29
+ constructor(config?: PoolConfig);
30
+ private initializePool;
31
+ private handleWorkerMessage;
32
+ private handleWorkerError;
33
+ private handleWorkerExit;
34
+ /**
35
+ * Execute SQL query with parameters
36
+ */
37
+ exec<T = unknown>(sql: string, params?: unknown[]): Promise<T>;
38
+ /**
39
+ * Execute operations on a specific worker (for transactions)
40
+ */
41
+ execOnWorker<T = unknown>(workerIndex: number, sql: string, params?: unknown[]): Promise<T>;
42
+ /**
43
+ * Get current pool metrics
44
+ */
45
+ getMetrics(): PoolMetrics;
46
+ /**
47
+ * Get pool status for health checks
48
+ */
49
+ getStatus(): {
50
+ workers: number;
51
+ activeQueries: number;
52
+ isShuttingDown: boolean;
53
+ totalQueries: number;
54
+ errorRate: number;
55
+ avgDuration: number;
56
+ };
57
+ /**
58
+ * Graceful shutdown of the connection pool
59
+ */
60
+ shutdown(timeoutMs?: number): Promise<void>;
61
+ }
62
+ /**
63
+ * Create a new connection pool instance
64
+ */
65
+ export declare function createPool(config?: PoolConfig): SQLiteConnectionPool;
66
+ /**
67
+ * Get the singleton connection pool instance
68
+ */
69
+ export declare function getPool(): SQLiteConnectionPool;
70
+ /**
71
+ * Execute SQL query using the connection pool
72
+ */
73
+ export declare function exec<T = unknown>(sql: string, params?: unknown[]): Promise<T>;
74
+ /**
75
+ * Get pool metrics for monitoring
76
+ */
77
+ export declare function getPoolMetrics(): PoolMetrics;
78
+ /**
79
+ * Get pool status for health checks
80
+ */
81
+ export declare function getPoolStatus(): {
82
+ workers: number;
83
+ activeQueries: number;
84
+ isShuttingDown: boolean;
85
+ totalQueries: number;
86
+ errorRate: number;
87
+ avgDuration: number;
88
+ };
89
+ /**
90
+ * Shutdown the connection pool
91
+ */
92
+ export declare function shutdownPool(timeoutMs?: number): Promise<void>;
93
+ /**
94
+ * Execute multiple operations atomically on a single worker
95
+ */
96
+ export declare function withTransaction<T>(operation: (execute: (sql: string, params?: unknown[]) => Promise<unknown>) => Promise<T>): Promise<T>;
97
+ export {};
98
+ //# sourceMappingURL=pool.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pool.d.ts","sourceRoot":"","sources":["../../src/pool.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AA0B3C,MAAM,MAAM,WAAW,GAAG;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE;QACX,CAAC,QAAQ,EAAE,MAAM,GAAG;YAClB,OAAO,EAAE,MAAM,CAAC;YAChB,MAAM,EAAE,MAAM,CAAC;YACf,aAAa,EAAE,MAAM,CAAC;SACvB,CAAC;KACH,CAAC;CACH,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACvB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,cAAM,oBAAqB,SAAQ,YAAY;IAC7C,OAAO,CAAC,OAAO,CAAgB;IAC/B,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,YAAY,CAAqB;IAEzC,OAAO,CAAC,iBAAiB,CASrB;IAEJ,OAAO,CAAC,eAAe,CAAK;IAC5B,OAAO,CAAC,cAAc,CAAS;IAE/B,OAAO,CAAC,OAAO,CAMb;gBAEU,MAAM,CAAC,EAAE,UAAU;IAgB/B,OAAO,CAAC,cAAc;IAsBtB,OAAO,CAAC,mBAAmB;IAyD3B,OAAO,CAAC,iBAAiB;IAMzB,OAAO,CAAC,gBAAgB;IAMxB;;OAEG;IACG,IAAI,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,GAAE,OAAO,EAAO,GAAG,OAAO,CAAC,CAAC,CAAC;IAyCxE;;OAEG;IACG,YAAY,CAAC,CAAC,GAAG,OAAO,EAC5B,WAAW,EAAE,MAAM,EACnB,GAAG,EAAE,MAAM,EACX,MAAM,GAAE,OAAO,EAAO,GACrB,OAAO,CAAC,CAAC,CAAC;IAyCb;;OAEG;IACH,UAAU,IAAI,WAAW;IAOzB;;OAEG;IACH,SAAS;;;;;;;;IAiBT;;OAEG;IACG,QAAQ,CAAC,SAAS,SAAO,GAAG,OAAO,CAAC,IAAI,CAAC;CAqChD;AAKD;;GAEG;AACH,wBAAgB,UAAU,CAAC,MAAM,CAAC,EAAE,UAAU,GAAG,oBAAoB,CAQpE;AAED;;GAEG;AACH,wBAAgB,OAAO,IAAI,oBAAoB,CAK9C;AAED;;GAEG;AACH,wBAAsB,IAAI,CAAC,CAAC,GAAG,OAAO,EACpC,GAAG,EAAE,MAAM,EACX,MAAM,GAAE,OAAO,EAAO,GACrB,OAAO,CAAC,CAAC,CAAC,CAEZ;AAED;;GAEG;AACH,wBAAgB,cAAc,IAAI,WAAW,CAE5C;AAED;;GAEG;AACH,wBAAgB,aAAa;;;;;;;EAE5B;AAED;;GAEG;AACH,wBAAsB,YAAY,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAcpE;AAED;;GAEG;AACH,wBAAsB,eAAe,CAAC,CAAC,EACrC,SAAS,EAAE,CACT,OAAO,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,OAAO,CAAC,KAC3D,OAAO,CAAC,CAAC,CAAC,GACd,OAAO,CAAC,CAAC,CAAC,CA4BZ"}