@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.
- package/LICENSE +21 -0
- package/README.md +251 -0
- package/dist/scripts/benchmark.d.ts +3 -0
- package/dist/scripts/benchmark.d.ts.map +1 -0
- package/dist/scripts/benchmark.js +286 -0
- package/dist/scripts/load-test-utils.d.ts +77 -0
- package/dist/scripts/load-test-utils.d.ts.map +1 -0
- package/dist/scripts/load-test-utils.js +235 -0
- package/dist/src/cache-lock.d.ts +25 -0
- package/dist/src/cache-lock.d.ts.map +1 -0
- package/dist/src/cache-lock.js +95 -0
- package/dist/src/connection.d.ts +26 -0
- package/dist/src/connection.d.ts.map +1 -0
- package/dist/src/connection.js +132 -0
- package/dist/src/json-cache.d.ts +89 -0
- package/dist/src/json-cache.d.ts.map +1 -0
- package/dist/src/json-cache.js +289 -0
- package/dist/src/pool.d.ts +98 -0
- package/dist/src/pool.d.ts.map +1 -0
- package/dist/src/pool.js +331 -0
- package/dist/src/prisma-immediate-tx.d.ts +23 -0
- package/dist/src/prisma-immediate-tx.d.ts.map +1 -0
- package/dist/src/prisma-immediate-tx.js +42 -0
- package/dist/src/query-logger.d.ts +21 -0
- package/dist/src/query-logger.d.ts.map +1 -0
- package/dist/src/query-logger.js +60 -0
- package/dist/src/retry.d.ts +14 -0
- package/dist/src/retry.d.ts.map +1 -0
- package/dist/src/retry.js +49 -0
- package/dist/src/ttl-cache.d.ts +57 -0
- package/dist/src/ttl-cache.d.ts.map +1 -0
- package/dist/src/ttl-cache.js +92 -0
- package/dist/src/worker.d.ts +38 -0
- package/dist/src/worker.d.ts.map +1 -0
- package/dist/src/worker.js +294 -0
- package/dist/src/write-mutex.d.ts +33 -0
- package/dist/src/write-mutex.d.ts.map +1 -0
- package/dist/src/write-mutex.js +60 -0
- package/package.json +48 -0
- package/scripts/benchmark.ts +373 -0
- package/scripts/load-test-utils.ts +370 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// packages/sqlite/src/ttl-cache.ts
|
|
2
|
+
// In-memory TTL cache for server-side deduplication across HTTP requests.
|
|
3
|
+
// Complements json-cache (persistent, file-based) with a lightweight
|
|
4
|
+
// in-memory cache that auto-expires and auto-prunes.
|
|
5
|
+
/**
|
|
6
|
+
* In-memory TTL cache backed by a Map.
|
|
7
|
+
*
|
|
8
|
+
* Designed for server-side deduplication of expensive lookups (auth sessions,
|
|
9
|
+
* membership checks, bootstrap data) across HTTP requests. Entries expire
|
|
10
|
+
* after `ttl` milliseconds and are automatically pruned when the map exceeds
|
|
11
|
+
* `maxSize`.
|
|
12
|
+
*
|
|
13
|
+
* Unlike WeakMap per-request caching (which deduplicates within a single
|
|
14
|
+
* request), TTLCache deduplicates across requests — e.g. when TanStack
|
|
15
|
+
* Router replays `beforeLoad` on client hydration, the server returns the
|
|
16
|
+
* cached result instantly (0 DB queries).
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```typescript
|
|
20
|
+
* import { TTLCache } from '@bitclaw/sqlite/ttl-cache';
|
|
21
|
+
*
|
|
22
|
+
* type BootstrapData = { user: User; workspaces: Workspace[] };
|
|
23
|
+
*
|
|
24
|
+
* const bootstrapCache = new TTLCache<BootstrapData>({ ttl: 30_000 });
|
|
25
|
+
*
|
|
26
|
+
* // In your server function:
|
|
27
|
+
* const cached = bootstrapCache.get(sessionId);
|
|
28
|
+
* if (cached) return cached;
|
|
29
|
+
*
|
|
30
|
+
* const data = await expensiveQuery();
|
|
31
|
+
* bootstrapCache.set(sessionId, data);
|
|
32
|
+
* return data;
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export class TTLCache {
|
|
36
|
+
cache = new Map();
|
|
37
|
+
ttl;
|
|
38
|
+
maxSize;
|
|
39
|
+
constructor(options = {}) {
|
|
40
|
+
this.ttl = options.ttl ?? 30_000;
|
|
41
|
+
this.maxSize = options.maxSize ?? 100;
|
|
42
|
+
}
|
|
43
|
+
/** Get a cached value if it exists and hasn't expired. */
|
|
44
|
+
get(key) {
|
|
45
|
+
const entry = this.cache.get(key);
|
|
46
|
+
if (!entry)
|
|
47
|
+
return undefined;
|
|
48
|
+
if (entry.expiresAt <= Date.now()) {
|
|
49
|
+
this.cache.delete(key);
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
return entry.value;
|
|
53
|
+
}
|
|
54
|
+
/** Check if a non-expired entry exists for the given key. */
|
|
55
|
+
has(key) {
|
|
56
|
+
return this.get(key) !== undefined;
|
|
57
|
+
}
|
|
58
|
+
/** Store a value with the configured TTL. Auto-prunes if maxSize exceeded. */
|
|
59
|
+
set(key, value) {
|
|
60
|
+
this.cache.set(key, {
|
|
61
|
+
value,
|
|
62
|
+
expiresAt: Date.now() + this.ttl
|
|
63
|
+
});
|
|
64
|
+
if (this.cache.size > this.maxSize) {
|
|
65
|
+
this.prune();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/** Remove a specific entry. */
|
|
69
|
+
delete(key) {
|
|
70
|
+
return this.cache.delete(key);
|
|
71
|
+
}
|
|
72
|
+
/** Remove all entries. */
|
|
73
|
+
clear() {
|
|
74
|
+
this.cache.clear();
|
|
75
|
+
}
|
|
76
|
+
/** Number of entries (including potentially expired ones). */
|
|
77
|
+
get size() {
|
|
78
|
+
return this.cache.size;
|
|
79
|
+
}
|
|
80
|
+
/** Remove all expired entries. */
|
|
81
|
+
prune() {
|
|
82
|
+
const now = Date.now();
|
|
83
|
+
let pruned = 0;
|
|
84
|
+
for (const [key, entry] of this.cache) {
|
|
85
|
+
if (entry.expiresAt <= now) {
|
|
86
|
+
this.cache.delete(key);
|
|
87
|
+
pruned++;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return pruned;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
type DatabaseConfig = {
|
|
2
|
+
path: string;
|
|
3
|
+
options: {
|
|
4
|
+
verbose?: ((message?: unknown, ...additionalArgs: unknown[]) => void) | undefined;
|
|
5
|
+
fileMustExist: boolean;
|
|
6
|
+
};
|
|
7
|
+
};
|
|
8
|
+
type WorkerMessage = {
|
|
9
|
+
id: string;
|
|
10
|
+
sql: string;
|
|
11
|
+
params?: unknown[];
|
|
12
|
+
type?: string;
|
|
13
|
+
};
|
|
14
|
+
type WorkerResponse = {
|
|
15
|
+
id: string;
|
|
16
|
+
result?: unknown;
|
|
17
|
+
error?: {
|
|
18
|
+
message: string;
|
|
19
|
+
code?: string;
|
|
20
|
+
errno?: number;
|
|
21
|
+
};
|
|
22
|
+
workerId: string;
|
|
23
|
+
durationMs: number;
|
|
24
|
+
success: boolean;
|
|
25
|
+
};
|
|
26
|
+
export declare class SQLiteWorker {
|
|
27
|
+
private db;
|
|
28
|
+
private config;
|
|
29
|
+
private workerId;
|
|
30
|
+
constructor(config?: DatabaseConfig);
|
|
31
|
+
private initializeDatabase;
|
|
32
|
+
handleMessage(message: WorkerMessage): Promise<WorkerResponse>;
|
|
33
|
+
private executeQuery;
|
|
34
|
+
shutdown(): void;
|
|
35
|
+
getWorkerId(): string;
|
|
36
|
+
}
|
|
37
|
+
export {};
|
|
38
|
+
//# sourceMappingURL=worker.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
// packages/sqlite/src/worker.ts
|
|
2
|
+
// SQLite Worker Thread - Optimized for Hetzner VPS deployment
|
|
3
|
+
// Uses bun:sqlite for 3-6x faster reads compared to better-sqlite3
|
|
4
|
+
import { Database } from 'bun:sqlite';
|
|
5
|
+
import { parentPort, workerData } from 'node:worker_threads';
|
|
6
|
+
const isDevelopment = process.env.NODE_ENV === 'development';
|
|
7
|
+
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
|
+
const stmtCache = new StatementCache();
|
|
54
|
+
// Worker-specific database configuration
|
|
55
|
+
async function getDbConfig() {
|
|
56
|
+
const envPath = workerData?.databasePath ??
|
|
57
|
+
process.env.DATABASE_PATH;
|
|
58
|
+
if (envPath) {
|
|
59
|
+
return {
|
|
60
|
+
path: envPath,
|
|
61
|
+
options: {
|
|
62
|
+
verbose: undefined,
|
|
63
|
+
fileMustExist: false
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
if (isTest) {
|
|
68
|
+
return {
|
|
69
|
+
path: ':memory:',
|
|
70
|
+
options: {
|
|
71
|
+
verbose: undefined,
|
|
72
|
+
fileMustExist: false
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
if (isDevelopment) {
|
|
77
|
+
return {
|
|
78
|
+
path: './data/app.db',
|
|
79
|
+
options: {
|
|
80
|
+
verbose: undefined,
|
|
81
|
+
fileMustExist: false
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
// Production
|
|
86
|
+
return {
|
|
87
|
+
path: '/data/app.db',
|
|
88
|
+
options: { verbose: undefined, fileMustExist: false }
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
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;
|
|
102
|
+
}
|
|
103
|
+
// SQLite Worker class
|
|
104
|
+
export class SQLiteWorker {
|
|
105
|
+
db = null;
|
|
106
|
+
config;
|
|
107
|
+
workerId;
|
|
108
|
+
constructor(config) {
|
|
109
|
+
this.config = config;
|
|
110
|
+
this.workerId = `worker-${Date.now()}-${Math.random().toString(36).substring(2, 7)}`;
|
|
111
|
+
}
|
|
112
|
+
async initializeDatabase() {
|
|
113
|
+
if (this.db)
|
|
114
|
+
return;
|
|
115
|
+
// Get config if not provided
|
|
116
|
+
if (!this.config) {
|
|
117
|
+
this.config = await getDbConfig();
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
// bun:sqlite constructor options differ from better-sqlite3
|
|
121
|
+
this.db = new Database(this.config.path, {
|
|
122
|
+
create: !this.config.options.fileMustExist,
|
|
123
|
+
readonly: false
|
|
124
|
+
});
|
|
125
|
+
// ✅ OPTIMIZED: Enhanced multi-worker SQLite configuration
|
|
126
|
+
// bun:sqlite uses run() for PRAGMA statements instead of pragma()
|
|
127
|
+
if (this.config.path !== ':memory:') {
|
|
128
|
+
// WAL mode with optimized settings
|
|
129
|
+
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
|
|
136
|
+
}
|
|
137
|
+
// ✅ PERFORMANCE: Optimized PRAGMA settings for workers
|
|
138
|
+
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');
|
|
142
|
+
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
|
|
146
|
+
this.db.run('PRAGMA optimize');
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
console.error(`[${this.workerId}] Failed to initialize database:`, error);
|
|
150
|
+
throw error;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
async handleMessage(message) {
|
|
154
|
+
const start = performance.now();
|
|
155
|
+
let success = false;
|
|
156
|
+
let result;
|
|
157
|
+
let error;
|
|
158
|
+
try {
|
|
159
|
+
if (message.sql === '__SHUTDOWN__') {
|
|
160
|
+
this.shutdown();
|
|
161
|
+
return {
|
|
162
|
+
id: message.id,
|
|
163
|
+
result: { shutdown: true },
|
|
164
|
+
workerId: this.workerId,
|
|
165
|
+
durationMs: performance.now() - start,
|
|
166
|
+
success: true
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
// Lazy initialization
|
|
170
|
+
if (!this.db) {
|
|
171
|
+
await this.initializeDatabase();
|
|
172
|
+
}
|
|
173
|
+
result = this.executeQuery(message.sql, message.params || []);
|
|
174
|
+
success = true;
|
|
175
|
+
}
|
|
176
|
+
catch (err) {
|
|
177
|
+
// err (not error) to avoid shadowing the outer `error` return variable
|
|
178
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
179
|
+
error = {
|
|
180
|
+
message: e.message,
|
|
181
|
+
code: e.code,
|
|
182
|
+
errno: e.errno
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
id: message.id,
|
|
187
|
+
result,
|
|
188
|
+
error,
|
|
189
|
+
workerId: this.workerId,
|
|
190
|
+
durationMs: performance.now() - start,
|
|
191
|
+
success
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
executeQuery(sql, params) {
|
|
195
|
+
if (!this.db) {
|
|
196
|
+
throw new Error('Database not initialized');
|
|
197
|
+
}
|
|
198
|
+
const sqlUpper = sql.trim().toUpperCase();
|
|
199
|
+
try {
|
|
200
|
+
const stmt = getOrCreateStatement(this.db, sql);
|
|
201
|
+
let result;
|
|
202
|
+
if (sqlUpper.startsWith('SELECT')) {
|
|
203
|
+
if (sqlUpper.includes('LIMIT 1') || sqlUpper.includes('COUNT(*)')) {
|
|
204
|
+
// bun:sqlite uses .get() with params directly
|
|
205
|
+
result = stmt.get(...params);
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
result = stmt.all(...params);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
else if (sqlUpper.startsWith('INSERT') ||
|
|
212
|
+
sqlUpper.startsWith('UPDATE') ||
|
|
213
|
+
sqlUpper.startsWith('DELETE')) {
|
|
214
|
+
// bun:sqlite .run() returns { changes, lastInsertRowid }
|
|
215
|
+
const runResult = stmt.run(...params);
|
|
216
|
+
result = {
|
|
217
|
+
changes: runResult.changes,
|
|
218
|
+
lastInsertRowid: runResult.lastInsertRowid
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
this.db.run(sql);
|
|
223
|
+
result = { success: true };
|
|
224
|
+
}
|
|
225
|
+
return result;
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
const e = error instanceof Error ? error : new Error(String(error));
|
|
229
|
+
console.error(`[${this.workerId}] SQL FAILED:`, {
|
|
230
|
+
sql: `${sql.substring(0, 60)}...`,
|
|
231
|
+
error: e.message,
|
|
232
|
+
code: e.code,
|
|
233
|
+
errno: e.errno,
|
|
234
|
+
params: params?.length || 0
|
|
235
|
+
});
|
|
236
|
+
throw error;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
shutdown() {
|
|
240
|
+
if (this.db) {
|
|
241
|
+
try {
|
|
242
|
+
// Clear prepared statement cache
|
|
243
|
+
stmtCache.clear();
|
|
244
|
+
this.db.close();
|
|
245
|
+
this.db = null;
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
console.error(`[${this.workerId}] Error during shutdown:`, error);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
getWorkerId() {
|
|
253
|
+
return this.workerId;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
// Worker thread main execution
|
|
257
|
+
if (parentPort) {
|
|
258
|
+
const worker = new SQLiteWorker();
|
|
259
|
+
parentPort.on('message', async (message) => {
|
|
260
|
+
try {
|
|
261
|
+
if (!message.id) {
|
|
262
|
+
console.error(`[${worker.getWorkerId()}] CRITICAL: Message missing ID:`, message);
|
|
263
|
+
parentPort?.postMessage({
|
|
264
|
+
id: 'unknown',
|
|
265
|
+
error: { message: 'Message missing ID' },
|
|
266
|
+
workerId: worker.getWorkerId(),
|
|
267
|
+
durationMs: 0,
|
|
268
|
+
success: false
|
|
269
|
+
});
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
const response = await worker.handleMessage(message);
|
|
273
|
+
if (!response.id) {
|
|
274
|
+
console.error(`[${worker.getWorkerId()}] CRITICAL: Response missing ID:`, response);
|
|
275
|
+
response.id = message.id;
|
|
276
|
+
}
|
|
277
|
+
parentPort?.postMessage(response);
|
|
278
|
+
}
|
|
279
|
+
catch (error) {
|
|
280
|
+
const e = error instanceof Error ? error : new Error(String(error));
|
|
281
|
+
parentPort?.postMessage({
|
|
282
|
+
id: message.id || 'unknown',
|
|
283
|
+
error: {
|
|
284
|
+
message: e.message,
|
|
285
|
+
code: e.code,
|
|
286
|
+
errno: e.errno
|
|
287
|
+
},
|
|
288
|
+
workerId: worker.getWorkerId(),
|
|
289
|
+
durationMs: 0,
|
|
290
|
+
success: false
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
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 —
|
|
4
|
+
* no atomic operations needed.
|
|
5
|
+
*/
|
|
6
|
+
export declare class WriteMutex {
|
|
7
|
+
private queue;
|
|
8
|
+
/**
|
|
9
|
+
* Acquire the mutex, execute the function, then release.
|
|
10
|
+
* Only one function runs at a time per mutex instance.
|
|
11
|
+
*/
|
|
12
|
+
acquire<T>(fn: () => T | Promise<T>): Promise<T>;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* A map of named mutexes for per-resource write serialization.
|
|
16
|
+
* Useful for per-workspace or per-database write locking.
|
|
17
|
+
*/
|
|
18
|
+
export declare class WriteMutexMap {
|
|
19
|
+
private mutexes;
|
|
20
|
+
/**
|
|
21
|
+
* Acquire the mutex for a given key, execute the function, then release.
|
|
22
|
+
*/
|
|
23
|
+
withLock<T>(key: string, fn: () => T | Promise<T>): Promise<T>;
|
|
24
|
+
/**
|
|
25
|
+
* Remove a mutex for a key (e.g., when evicting a connection).
|
|
26
|
+
*/
|
|
27
|
+
delete(key: string): void;
|
|
28
|
+
/**
|
|
29
|
+
* Get the number of tracked mutexes.
|
|
30
|
+
*/
|
|
31
|
+
get size(): number;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=write-mutex.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"write-mutex.d.ts","sourceRoot":"","sources":["../../src/write-mutex.ts"],"names":[],"mappings":"AAGA;;;;GAIG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,KAAK,CAAoC;IAEjD;;;OAGG;IACG,OAAO,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;CAkBvD;AAED;;;GAGG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,OAAO,CAAiC;IAEhD;;OAEG;IACG,QAAQ,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IASpE;;OAEG;IACH,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAIzB;;OAEG;IACH,IAAI,IAAI,IAAI,MAAM,CAEjB;CACF"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// packages/sqlite/src/write-mutex.ts
|
|
2
|
+
// Promise-based per-resource write mutex for SQLite concurrency
|
|
3
|
+
/**
|
|
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 —
|
|
6
|
+
* no atomic operations needed.
|
|
7
|
+
*/
|
|
8
|
+
export class WriteMutex {
|
|
9
|
+
queue = Promise.resolve();
|
|
10
|
+
/**
|
|
11
|
+
* Acquire the mutex, execute the function, then release.
|
|
12
|
+
* Only one function runs at a time per mutex instance.
|
|
13
|
+
*/
|
|
14
|
+
async acquire(fn) {
|
|
15
|
+
let release;
|
|
16
|
+
const gate = new Promise(resolve => {
|
|
17
|
+
release = resolve;
|
|
18
|
+
});
|
|
19
|
+
// Chain onto the queue so we wait for prior operations
|
|
20
|
+
const prior = this.queue;
|
|
21
|
+
this.queue = gate;
|
|
22
|
+
await prior;
|
|
23
|
+
try {
|
|
24
|
+
return await fn();
|
|
25
|
+
}
|
|
26
|
+
finally {
|
|
27
|
+
release();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* A map of named mutexes for per-resource write serialization.
|
|
33
|
+
* Useful for per-workspace or per-database write locking.
|
|
34
|
+
*/
|
|
35
|
+
export class WriteMutexMap {
|
|
36
|
+
mutexes = new Map();
|
|
37
|
+
/**
|
|
38
|
+
* Acquire the mutex for a given key, execute the function, then release.
|
|
39
|
+
*/
|
|
40
|
+
async withLock(key, fn) {
|
|
41
|
+
let mutex = this.mutexes.get(key);
|
|
42
|
+
if (!mutex) {
|
|
43
|
+
mutex = new WriteMutex();
|
|
44
|
+
this.mutexes.set(key, mutex);
|
|
45
|
+
}
|
|
46
|
+
return mutex.acquire(fn);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Remove a mutex for a key (e.g., when evicting a connection).
|
|
50
|
+
*/
|
|
51
|
+
delete(key) {
|
|
52
|
+
this.mutexes.delete(key);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Get the number of tracked mutexes.
|
|
56
|
+
*/
|
|
57
|
+
get size() {
|
|
58
|
+
return this.mutexes.size;
|
|
59
|
+
}
|
|
60
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bitclaw/sqlite",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "High-performance SQLite worker pool and utilities using bun:sqlite",
|
|
5
|
+
"files": ["dist", "scripts", "LICENSE", "README.md"],
|
|
6
|
+
"type": "module",
|
|
7
|
+
"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" }
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsc -p tsconfig.build.json",
|
|
22
|
+
"test": "bun test",
|
|
23
|
+
"test:watch": "bun test --watch",
|
|
24
|
+
"typecheck": "tsc --noEmit",
|
|
25
|
+
"lint": "biome check",
|
|
26
|
+
"lint:fix": "biome check --write",
|
|
27
|
+
"format": "biome format --write",
|
|
28
|
+
"knip": "knip",
|
|
29
|
+
"benchmark": "bun scripts/benchmark.ts",
|
|
30
|
+
"benchmark:quick": "bun scripts/benchmark.ts --quick",
|
|
31
|
+
"prepublishOnly": "npm run build && npm run test",
|
|
32
|
+
"publish:dev": "npm run build && npm publish --tag dev --access public",
|
|
33
|
+
"publish:patch": "npm whoami && npm version patch && git push --follow-tags && npm publish --access public",
|
|
34
|
+
"publish:minor": "npm whoami && npm version minor && git push --follow-tags && npm publish --access public",
|
|
35
|
+
"publish:major": "npm whoami && npm version major && git push --follow-tags && npm publish --access public"
|
|
36
|
+
},
|
|
37
|
+
"keywords": ["sqlite", "bun", "worker-pool", "json-cache", "ttl-cache"],
|
|
38
|
+
"author": "bitclaw",
|
|
39
|
+
"license": "MIT",
|
|
40
|
+
"engines": { "bun": ">=1.3.0" },
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@biomejs/biome": "^2.4.15",
|
|
43
|
+
"knip": "^6.12.1",
|
|
44
|
+
"@types/bun": "^1.3.9",
|
|
45
|
+
"@types/node": "^22.0.0",
|
|
46
|
+
"typescript": "^5.8.3"
|
|
47
|
+
}
|
|
48
|
+
}
|