@vibesdotdev/localdb 0.0.1
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 +121 -0
- package/SPEC.md +119 -0
- package/dist/cli/commands/_shared.consumer.d.ts +5 -0
- package/dist/cli/commands/_shared.consumer.d.ts.map +1 -0
- package/dist/cli/commands/_shared.consumer.js +17 -0
- package/dist/cli/commands/_shared.consumer.js.map +1 -0
- package/dist/cli/commands/migrate/dev.localdb.migrate.cli-command.descriptor.d.ts +19 -0
- package/dist/cli/commands/migrate/dev.localdb.migrate.cli-command.descriptor.d.ts.map +1 -0
- package/dist/cli/commands/migrate/dev.localdb.migrate.cli-command.descriptor.js +19 -0
- package/dist/cli/commands/migrate/dev.localdb.migrate.cli-command.descriptor.js.map +1 -0
- package/dist/cli/commands/migrate/dev.localdb.migrate.cli-command.impl.consumer.d.ts +5 -0
- package/dist/cli/commands/migrate/dev.localdb.migrate.cli-command.impl.consumer.d.ts.map +1 -0
- package/dist/cli/commands/migrate/dev.localdb.migrate.cli-command.impl.consumer.js +80 -0
- package/dist/cli/commands/migrate/dev.localdb.migrate.cli-command.impl.consumer.js.map +1 -0
- package/dist/cli/commands/namespaces/dev.localdb.namespaces.cli-command.descriptor.d.ts +19 -0
- package/dist/cli/commands/namespaces/dev.localdb.namespaces.cli-command.descriptor.d.ts.map +1 -0
- package/dist/cli/commands/namespaces/dev.localdb.namespaces.cli-command.descriptor.js +16 -0
- package/dist/cli/commands/namespaces/dev.localdb.namespaces.cli-command.descriptor.js.map +1 -0
- package/dist/cli/commands/namespaces/dev.localdb.namespaces.cli-command.impl.d.ts +5 -0
- package/dist/cli/commands/namespaces/dev.localdb.namespaces.cli-command.impl.d.ts.map +1 -0
- package/dist/cli/commands/namespaces/dev.localdb.namespaces.cli-command.impl.js +33 -0
- package/dist/cli/commands/namespaces/dev.localdb.namespaces.cli-command.impl.js.map +1 -0
- package/dist/cli/commands/pools/dev.localdb.pools.cli-command.descriptor.d.ts +15 -0
- package/dist/cli/commands/pools/dev.localdb.pools.cli-command.descriptor.d.ts.map +1 -0
- package/dist/cli/commands/pools/dev.localdb.pools.cli-command.descriptor.js +15 -0
- package/dist/cli/commands/pools/dev.localdb.pools.cli-command.descriptor.js.map +1 -0
- package/dist/cli/commands/pools/dev.localdb.pools.cli-command.impl.d.ts +5 -0
- package/dist/cli/commands/pools/dev.localdb.pools.cli-command.impl.d.ts.map +1 -0
- package/dist/cli/commands/pools/dev.localdb.pools.cli-command.impl.js +28 -0
- package/dist/cli/commands/pools/dev.localdb.pools.cli-command.impl.js.map +1 -0
- package/dist/cli/commands/query/dev.localdb.query.cli-command.descriptor.d.ts +24 -0
- package/dist/cli/commands/query/dev.localdb.query.cli-command.descriptor.d.ts.map +1 -0
- package/dist/cli/commands/query/dev.localdb.query.cli-command.descriptor.js +20 -0
- package/dist/cli/commands/query/dev.localdb.query.cli-command.descriptor.js.map +1 -0
- package/dist/cli/commands/query/dev.localdb.query.cli-command.impl.consumer.d.ts +5 -0
- package/dist/cli/commands/query/dev.localdb.query.cli-command.impl.consumer.d.ts.map +1 -0
- package/dist/cli/commands/query/dev.localdb.query.cli-command.impl.consumer.js +63 -0
- package/dist/cli/commands/query/dev.localdb.query.cli-command.impl.consumer.js.map +1 -0
- package/dist/cli/commands/status/dev.localdb.status.cli-command.descriptor.d.ts +19 -0
- package/dist/cli/commands/status/dev.localdb.status.cli-command.descriptor.d.ts.map +1 -0
- package/dist/cli/commands/status/dev.localdb.status.cli-command.descriptor.js +19 -0
- package/dist/cli/commands/status/dev.localdb.status.cli-command.descriptor.js.map +1 -0
- package/dist/cli/commands/status/dev.localdb.status.cli-command.impl.consumer.d.ts +5 -0
- package/dist/cli/commands/status/dev.localdb.status.cli-command.impl.consumer.d.ts.map +1 -0
- package/dist/cli/commands/status/dev.localdb.status.cli-command.impl.consumer.js +73 -0
- package/dist/cli/commands/status/dev.localdb.status.cli-command.impl.consumer.js.map +1 -0
- package/dist/core/connection-pool.d.ts +84 -0
- package/dist/core/connection-pool.d.ts.map +1 -0
- package/dist/core/connection-pool.js +191 -0
- package/dist/core/connection-pool.js.map +1 -0
- package/dist/core/database.d.ts +137 -0
- package/dist/core/database.d.ts.map +1 -0
- package/dist/core/database.js +347 -0
- package/dist/core/database.js.map +1 -0
- package/dist/core/error-context.d.ts +2 -0
- package/dist/core/error-context.d.ts.map +1 -0
- package/dist/core/error-context.js +17 -0
- package/dist/core/error-context.js.map +1 -0
- package/dist/core/migration-registry.d.ts +89 -0
- package/dist/core/migration-registry.d.ts.map +1 -0
- package/dist/core/migration-registry.js +226 -0
- package/dist/core/migration-registry.js.map +1 -0
- package/dist/core/runtime.d.ts +3 -0
- package/dist/core/runtime.d.ts.map +1 -0
- package/dist/core/runtime.js +17 -0
- package/dist/core/runtime.js.map +1 -0
- package/dist/dev.localdb.cli-group.descriptor.d.ts +9 -0
- package/dist/dev.localdb.cli-group.descriptor.d.ts.map +1 -0
- package/dist/dev.localdb.cli-group.descriptor.js +17 -0
- package/dist/dev.localdb.cli-group.descriptor.js.map +1 -0
- package/dist/dev.localdb.context.descriptor.d.ts +21 -0
- package/dist/dev.localdb.context.descriptor.d.ts.map +1 -0
- package/dist/dev.localdb.context.descriptor.js +12 -0
- package/dist/dev.localdb.context.descriptor.js.map +1 -0
- package/dist/dev.localdb.context.impl.consumer.d.ts +9 -0
- package/dist/dev.localdb.context.impl.consumer.d.ts.map +1 -0
- package/dist/dev.localdb.context.impl.consumer.js +10 -0
- package/dist/dev.localdb.context.impl.consumer.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/localdb.cloud.plugin.d.ts +17 -0
- package/dist/localdb.cloud.plugin.d.ts.map +1 -0
- package/dist/localdb.cloud.plugin.js +23 -0
- package/dist/localdb.cloud.plugin.js.map +1 -0
- package/dist/localdb.plugin.d.ts +8 -0
- package/dist/localdb.plugin.d.ts.map +1 -0
- package/dist/localdb.plugin.js +38 -0
- package/dist/localdb.plugin.js.map +1 -0
- package/dist/migrations/load-sql.d.ts +45 -0
- package/dist/migrations/load-sql.d.ts.map +1 -0
- package/dist/migrations/load-sql.js +117 -0
- package/dist/migrations/load-sql.js.map +1 -0
- package/dist/schemas/api.d.ts +82 -0
- package/dist/schemas/api.d.ts.map +1 -0
- package/dist/schemas/api.js +2 -0
- package/dist/schemas/api.js.map +1 -0
- package/package.json +146 -0
- package/src/cli/commands/_shared.consumer.ts +20 -0
- package/src/cli/commands/migrate/dev.localdb.migrate.cli-command.descriptor.ts +20 -0
- package/src/cli/commands/migrate/dev.localdb.migrate.cli-command.impl.consumer.ts +97 -0
- package/src/cli/commands/namespaces/dev.localdb.namespaces.cli-command.descriptor.ts +17 -0
- package/src/cli/commands/namespaces/dev.localdb.namespaces.cli-command.impl.ts +46 -0
- package/src/cli/commands/pools/dev.localdb.pools.cli-command.descriptor.ts +16 -0
- package/src/cli/commands/pools/dev.localdb.pools.cli-command.impl.ts +37 -0
- package/src/cli/commands/query/dev.localdb.query.cli-command.descriptor.ts +21 -0
- package/src/cli/commands/query/dev.localdb.query.cli-command.impl.consumer.ts +69 -0
- package/src/cli/commands/status/dev.localdb.status.cli-command.descriptor.ts +20 -0
- package/src/cli/commands/status/dev.localdb.status.cli-command.impl.consumer.ts +93 -0
- package/src/core/connection-pool.ts +240 -0
- package/src/core/database.ts +419 -0
- package/src/core/error-context.ts +19 -0
- package/src/core/migration-registry.ts +321 -0
- package/src/core/runtime.ts +17 -0
- package/src/dev.localdb.cli-group.descriptor.ts +20 -0
- package/src/dev.localdb.context.descriptor.ts +13 -0
- package/src/dev.localdb.context.impl.consumer.ts +12 -0
- package/src/index.ts +28 -0
- package/src/localdb.cloud.plugin.ts +24 -0
- package/src/localdb.plugin.ts +43 -0
- package/src/migrations/atlas/001-initial-schema.sql +173 -0
- package/src/migrations/atlas/002-lang-server-fields.sql +12 -0
- package/src/migrations/atlas/003-config-id-dedup.sql +31 -0
- package/src/migrations/atlas/004-fix-on-conflict-constraints.sql +25 -0
- package/src/migrations/atlas/005-diagnostics.sql +66 -0
- package/src/migrations/atlas/006-diagnostic-summaries.sql +65 -0
- package/src/migrations/atlas/007-diagnostic-slice.sql +83 -0
- package/src/migrations/load-sql.ts +133 -0
- package/src/schemas/api.ts +92 -0
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local Database Manager
|
|
3
|
+
*
|
|
4
|
+
* High-level API for managing local SQLite databases in vibes dev.
|
|
5
|
+
* Provides connection pooling, migration management, and consistent configuration.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* const db = localdb.getDatabase('atlas', '.vibes/atlas/project.db');
|
|
9
|
+
* // db is ready to use with WAL mode, busy timeout, and migrations applied
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { Database } from 'bun:sqlite';
|
|
13
|
+
import { getLogger } from '@vibesdotdev/logging';
|
|
14
|
+
import { connectionPool } from './connection-pool.ts';
|
|
15
|
+
import { migrationRegistry, type Migration } from './migration-registry.ts';
|
|
16
|
+
import { toErrorContext } from './error-context';
|
|
17
|
+
import { isLocaldbServerRuntime } from './runtime.ts';
|
|
18
|
+
|
|
19
|
+
const logger = getLogger('localdb:database');
|
|
20
|
+
|
|
21
|
+
let mkdir: typeof import('node:fs/promises').mkdir;
|
|
22
|
+
let copyFile: typeof import('node:fs/promises').copyFile;
|
|
23
|
+
let unlink: typeof import('node:fs/promises').unlink;
|
|
24
|
+
let readdir: typeof import('node:fs/promises').readdir;
|
|
25
|
+
let dirname: typeof import('node:path').dirname;
|
|
26
|
+
let basename: typeof import('node:path').basename;
|
|
27
|
+
|
|
28
|
+
const BACKUP_RETENTION_COUNT = 12;
|
|
29
|
+
const isServerRuntime = isLocaldbServerRuntime();
|
|
30
|
+
|
|
31
|
+
if (isServerRuntime) {
|
|
32
|
+
({ mkdir, copyFile, unlink, readdir } = await import('node:fs/promises'));
|
|
33
|
+
({ dirname, basename } = await import('node:path'));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface DatabaseOptions {
|
|
37
|
+
/** Namespace for migrations (e.g., 'atlas', 'session') */
|
|
38
|
+
namespace: string;
|
|
39
|
+
|
|
40
|
+
/** Path to database file */
|
|
41
|
+
dbPath: string;
|
|
42
|
+
|
|
43
|
+
/** Consumer identifier for debugging */
|
|
44
|
+
consumer?: string;
|
|
45
|
+
|
|
46
|
+
/** Skip migration check/run on initialization */
|
|
47
|
+
skipMigrations?: boolean;
|
|
48
|
+
|
|
49
|
+
/** Use connection pool (default: true) */
|
|
50
|
+
usePool?: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface NodeLikeError {
|
|
54
|
+
code?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isFileMissingError(error: unknown): boolean {
|
|
58
|
+
if (!error || typeof error !== 'object') return false;
|
|
59
|
+
return (error as NodeLikeError).code === 'ENOENT';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export class LocalDatabase {
|
|
63
|
+
private db!: Database;
|
|
64
|
+
private options: Required<DatabaseOptions>;
|
|
65
|
+
private initialized = false;
|
|
66
|
+
|
|
67
|
+
constructor(options: DatabaseOptions) {
|
|
68
|
+
this.options = {
|
|
69
|
+
consumer: 'unknown',
|
|
70
|
+
skipMigrations: false,
|
|
71
|
+
usePool: true,
|
|
72
|
+
...options
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Initialize the database connection
|
|
78
|
+
* - Creates directory if needed
|
|
79
|
+
* - Gets/creates pooled connection
|
|
80
|
+
* - Runs pending migrations
|
|
81
|
+
*/
|
|
82
|
+
async initialize(): Promise<void> {
|
|
83
|
+
if (this.initialized) return;
|
|
84
|
+
|
|
85
|
+
// Ensure directory exists
|
|
86
|
+
if (!isServerRuntime) {
|
|
87
|
+
throw new Error('LocalDatabase is not available in browser builds');
|
|
88
|
+
}
|
|
89
|
+
await mkdir(dirname(this.options.dbPath), { recursive: true });
|
|
90
|
+
|
|
91
|
+
// Get connection (pooled or direct)
|
|
92
|
+
this.establishConnection();
|
|
93
|
+
|
|
94
|
+
// Run migrations if needed
|
|
95
|
+
if (!this.options.skipMigrations) {
|
|
96
|
+
try {
|
|
97
|
+
// Check for incompatible database from old migration system
|
|
98
|
+
if (migrationRegistry.isIncompatibleDatabase(this.db, this.options.namespace)) {
|
|
99
|
+
logger.error(
|
|
100
|
+
`[LocalDB] Detected incompatible database for ${this.options.namespace} (from legacy migration system). Initiating recovery...`
|
|
101
|
+
);
|
|
102
|
+
await this.performRecovery();
|
|
103
|
+
} else if (migrationRegistry.needsMigration(this.db, this.options.namespace)) {
|
|
104
|
+
await migrationRegistry.runMigrations(this.db, this.options.namespace);
|
|
105
|
+
}
|
|
106
|
+
} catch (error) {
|
|
107
|
+
logger.error(
|
|
108
|
+
`[LocalDB] Migration failed for ${this.options.namespace}. Initiating automatic recovery/trapdoor...`,
|
|
109
|
+
toErrorContext(error)
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
await this.performRecovery();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
this.initialized = true;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Establish initial connection
|
|
121
|
+
*/
|
|
122
|
+
private establishConnection(): void {
|
|
123
|
+
if (this.options.usePool) {
|
|
124
|
+
this.db = connectionPool.getConnection(this.options.dbPath, this.options.consumer);
|
|
125
|
+
} else {
|
|
126
|
+
// Direct connection (for special cases)
|
|
127
|
+
this.db = new Database(this.options.dbPath);
|
|
128
|
+
this.configureDatabase(this.db);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Perform recovery from migration failure
|
|
134
|
+
* 1. Close connection
|
|
135
|
+
* 2. Backup corrupted DB
|
|
136
|
+
* 3. Delete corrupted DB
|
|
137
|
+
* 4. Re-initialize fresh DB
|
|
138
|
+
* 5. Re-run migrations
|
|
139
|
+
*/
|
|
140
|
+
private async performRecovery(): Promise<void> {
|
|
141
|
+
// 1. Close the broken database
|
|
142
|
+
if (this.options.usePool) {
|
|
143
|
+
connectionPool.closeConnection(this.options.dbPath, true);
|
|
144
|
+
} else {
|
|
145
|
+
this.db.close();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// 2. Backup the broken file
|
|
149
|
+
const backupPath = `${this.options.dbPath}.bak-${Date.now()}`;
|
|
150
|
+
try {
|
|
151
|
+
await copyFile(this.options.dbPath, backupPath);
|
|
152
|
+
logger.info(`[LocalDB] Backed up corrupted database to: ${backupPath}`);
|
|
153
|
+
await this.pruneBackupFiles();
|
|
154
|
+
} catch (backupError) {
|
|
155
|
+
logger.error(
|
|
156
|
+
'[LocalDB] Failed to backup corrupted database:',
|
|
157
|
+
toErrorContext(backupError)
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// 3. Delete the broken file and WAL artifacts
|
|
162
|
+
try {
|
|
163
|
+
await unlink(this.options.dbPath);
|
|
164
|
+
await unlink(`${this.options.dbPath}-wal`).catch(() => {});
|
|
165
|
+
await unlink(`${this.options.dbPath}-shm`).catch(() => {});
|
|
166
|
+
logger.info('[LocalDB] Deleted corrupted database file.');
|
|
167
|
+
} catch (deleteError) {
|
|
168
|
+
if (isFileMissingError(deleteError)) {
|
|
169
|
+
logger.info(
|
|
170
|
+
'[LocalDB] Corrupted database file already removed during recovery. Continuing...'
|
|
171
|
+
);
|
|
172
|
+
} else {
|
|
173
|
+
logger.error(
|
|
174
|
+
'[LocalDB] Failed to delete corrupted database:',
|
|
175
|
+
toErrorContext(deleteError)
|
|
176
|
+
);
|
|
177
|
+
throw deleteError; // If we can't delete, we can't recover.
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// 4. Re-establish connection (creates new DB file)
|
|
182
|
+
logger.info('[LocalDB] Re-initializing fresh database...');
|
|
183
|
+
this.establishConnection();
|
|
184
|
+
|
|
185
|
+
// 5. Re-run migrations on fresh DB
|
|
186
|
+
try {
|
|
187
|
+
if (migrationRegistry.needsMigration(this.db, this.options.namespace)) {
|
|
188
|
+
await migrationRegistry.runMigrations(this.db, this.options.namespace);
|
|
189
|
+
}
|
|
190
|
+
logger.info('[LocalDB] Recovery successful. Database reset.');
|
|
191
|
+
} catch (retryError) {
|
|
192
|
+
logger.error(
|
|
193
|
+
'[LocalDB] Recovery failed. Fresh database migration failed:',
|
|
194
|
+
toErrorContext(retryError)
|
|
195
|
+
);
|
|
196
|
+
throw retryError;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private async pruneBackupFiles(): Promise<void> {
|
|
201
|
+
const directory = dirname(this.options.dbPath);
|
|
202
|
+
const filename = basename(this.options.dbPath);
|
|
203
|
+
const backupPrefix = `${filename}.bak-`;
|
|
204
|
+
const files = await readdir(directory).catch(() => []);
|
|
205
|
+
const backups = files
|
|
206
|
+
.filter((entry) => entry.startsWith(backupPrefix))
|
|
207
|
+
.map((entry) => ({ entry, timestamp: Number(entry.slice(backupPrefix.length)) }))
|
|
208
|
+
.sort((a, b) => {
|
|
209
|
+
if (Number.isNaN(a.timestamp) || Number.isNaN(b.timestamp)) {
|
|
210
|
+
return b.entry.localeCompare(a.entry);
|
|
211
|
+
}
|
|
212
|
+
return b.timestamp - a.timestamp;
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
for (const backup of backups.slice(BACKUP_RETENTION_COUNT)) {
|
|
216
|
+
await unlink(`${directory}/${backup.entry}`).catch(() => {});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Configure database with optimal settings (for non-pooled connections)
|
|
222
|
+
*/
|
|
223
|
+
private configureDatabase(db: Database): void {
|
|
224
|
+
db.exec('PRAGMA journal_mode = WAL;');
|
|
225
|
+
db.exec('PRAGMA busy_timeout = 5000;');
|
|
226
|
+
db.exec('PRAGMA foreign_keys = ON;');
|
|
227
|
+
db.exec('PRAGMA synchronous = NORMAL;');
|
|
228
|
+
db.exec('PRAGMA temp_store = MEMORY;');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Get the underlying Database instance
|
|
233
|
+
*/
|
|
234
|
+
getDatabase(): Database {
|
|
235
|
+
if (!this.initialized) {
|
|
236
|
+
throw new Error('Database not initialized. Call initialize() first.');
|
|
237
|
+
}
|
|
238
|
+
return this.db;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Close the database connection
|
|
243
|
+
*/
|
|
244
|
+
close(): void {
|
|
245
|
+
if (!this.initialized) return;
|
|
246
|
+
|
|
247
|
+
if (this.options.usePool) {
|
|
248
|
+
// Release back to pool
|
|
249
|
+
connectionPool.releaseConnection(this.options.dbPath);
|
|
250
|
+
} else {
|
|
251
|
+
// Direct close
|
|
252
|
+
this.db.close();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
this.initialized = false;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Check if database is healthy
|
|
260
|
+
*/
|
|
261
|
+
healthCheck(): boolean {
|
|
262
|
+
if (!this.initialized) return false;
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
const result = this.db.query('SELECT 1').get();
|
|
266
|
+
return result !== null;
|
|
267
|
+
} catch {
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Get migration status for this database
|
|
274
|
+
*/
|
|
275
|
+
getMigrationStatus() {
|
|
276
|
+
if (!this.initialized) {
|
|
277
|
+
throw new Error('Database not initialized');
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return migrationRegistry.getDatabaseStatus(this.db, this.options.namespace);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* LocalDB Manager
|
|
286
|
+
* Main entry point for accessing local databases
|
|
287
|
+
*/
|
|
288
|
+
class LocalDBManager {
|
|
289
|
+
private databases = new Map<
|
|
290
|
+
string,
|
|
291
|
+
{
|
|
292
|
+
database: LocalDatabase;
|
|
293
|
+
references: number;
|
|
294
|
+
}
|
|
295
|
+
>();
|
|
296
|
+
private dbPathLocks = new Map<string, Promise<void>>();
|
|
297
|
+
|
|
298
|
+
private async withDbPathLock<T>(dbPath: string, task: () => Promise<T>): Promise<T> {
|
|
299
|
+
const prior = this.dbPathLocks.get(dbPath) ?? Promise.resolve();
|
|
300
|
+
let release: (() => void) | undefined;
|
|
301
|
+
const current = new Promise<void>((resolve) => {
|
|
302
|
+
release = resolve;
|
|
303
|
+
});
|
|
304
|
+
const queued = prior.then(() => current);
|
|
305
|
+
this.dbPathLocks.set(dbPath, queued);
|
|
306
|
+
await prior;
|
|
307
|
+
try {
|
|
308
|
+
return await task();
|
|
309
|
+
} finally {
|
|
310
|
+
release?.();
|
|
311
|
+
if (this.dbPathLocks.get(dbPath) === queued) {
|
|
312
|
+
this.dbPathLocks.delete(dbPath);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Register migrations for a namespace
|
|
319
|
+
* Must be called before getDatabase() for that namespace
|
|
320
|
+
*/
|
|
321
|
+
registerMigrations(namespace: string, migrations: Migration[]): void {
|
|
322
|
+
migrationRegistry.register(namespace, migrations);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Get or create a database instance
|
|
327
|
+
*
|
|
328
|
+
* @param namespace Migration namespace (e.g., 'atlas', 'session')
|
|
329
|
+
* @param dbPath Path to database file
|
|
330
|
+
* @param consumer Consumer identifier for debugging
|
|
331
|
+
* @returns Initialized database instance
|
|
332
|
+
*/
|
|
333
|
+
getDatabase(namespace: string, dbPath: string, consumer?: string): Promise<LocalDatabase> {
|
|
334
|
+
const key = `${namespace}:${dbPath}`;
|
|
335
|
+
return this.withDbPathLock(dbPath, async () => {
|
|
336
|
+
const existing = this.databases.get(key);
|
|
337
|
+
if (existing) {
|
|
338
|
+
existing.references += 1;
|
|
339
|
+
return existing.database;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (!this.databases.has(key)) {
|
|
343
|
+
const db = new LocalDatabase({
|
|
344
|
+
namespace,
|
|
345
|
+
dbPath,
|
|
346
|
+
consumer
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
await db.initialize();
|
|
350
|
+
this.databases.set(key, {
|
|
351
|
+
database: db,
|
|
352
|
+
references: 1
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return this.databases.get(key)!.database;
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Get a raw Database instance (auto-configured with WAL, etc.)
|
|
362
|
+
* For when you don't need migration management
|
|
363
|
+
*/
|
|
364
|
+
getRawDatabase(dbPath: string, consumer = 'raw'): Database {
|
|
365
|
+
return connectionPool.getConnection(dbPath, consumer);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Release a database instance
|
|
370
|
+
*/
|
|
371
|
+
async releaseDatabase(namespace: string, dbPath: string): Promise<void> {
|
|
372
|
+
const key = `${namespace}:${dbPath}`;
|
|
373
|
+
await this.withDbPathLock(dbPath, async () => {
|
|
374
|
+
const entry = this.databases.get(key);
|
|
375
|
+
if (!entry) return;
|
|
376
|
+
|
|
377
|
+
entry.references = Math.max(0, entry.references - 1);
|
|
378
|
+
if (entry.references === 0) {
|
|
379
|
+
await entry.database.close();
|
|
380
|
+
this.databases.delete(key);
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Get connection pool statistics
|
|
387
|
+
*/
|
|
388
|
+
getPoolStats() {
|
|
389
|
+
return connectionPool.getStats();
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Get migration registry status
|
|
394
|
+
*/
|
|
395
|
+
getMigrationStatus() {
|
|
396
|
+
return migrationRegistry.getStatus();
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* List all registered migration namespaces
|
|
401
|
+
*/
|
|
402
|
+
listNamespaces(): string[] {
|
|
403
|
+
return migrationRegistry.listNamespaces();
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Close all database connections
|
|
408
|
+
*/
|
|
409
|
+
async closeAll(): Promise<void> {
|
|
410
|
+
for (const entry of this.databases.values()) {
|
|
411
|
+
await entry.database.close();
|
|
412
|
+
}
|
|
413
|
+
this.databases.clear();
|
|
414
|
+
connectionPool.closeAll();
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Singleton instance
|
|
419
|
+
export const localdb = new LocalDBManager();
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function toErrorContext(error: unknown): Record<string, unknown> | undefined {
|
|
2
|
+
if (error instanceof Error) {
|
|
3
|
+
return {
|
|
4
|
+
error: error.message,
|
|
5
|
+
name: error.name,
|
|
6
|
+
stack: error.stack
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
if (typeof error === 'object' && error !== null && !Array.isArray(error)) {
|
|
11
|
+
return error as Record<string, unknown>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (error === undefined) {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return { error: String(error) };
|
|
19
|
+
}
|