@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,69 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { getVibesRuntime } from '@vibesdotdev/runtime';
|
|
3
|
+
import type { UIContext } from '@vibesdotdev/cli/providers';
|
|
4
|
+
import type { LocaldbApi } from '../../..';
|
|
5
|
+
import { resolveFromCwd } from '../_shared.consumer';
|
|
6
|
+
|
|
7
|
+
export default {
|
|
8
|
+
async execute(args: Record<string, unknown>, opts: Record<string, unknown>): Promise<void> {
|
|
9
|
+
const runtime = getVibesRuntime();
|
|
10
|
+
const ui = (await runtime.context('cli/ui')) as UIContext;
|
|
11
|
+
const localdb = (await runtime.context('dev/localdb')) as LocaldbApi;
|
|
12
|
+
|
|
13
|
+
const sql = typeof args.sql === 'string' ? args.sql : '';
|
|
14
|
+
const dbOpt = typeof opts.db === 'string' ? opts.db : '';
|
|
15
|
+
|
|
16
|
+
if (!sql.trim()) {
|
|
17
|
+
ui.error('Missing SQL statement');
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
if (!dbOpt.trim()) {
|
|
21
|
+
ui.error('Missing required option: --db <path>');
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const fullPath = resolveFromCwd(dbOpt);
|
|
26
|
+
if (!existsSync(fullPath)) {
|
|
27
|
+
ui.error(`Database not found: ${dbOpt}`);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const db = localdb.getRawDatabase(fullPath, 'localdb-query');
|
|
33
|
+
const isSelect = sql.trim().toLowerCase().startsWith('select');
|
|
34
|
+
|
|
35
|
+
if (isSelect) {
|
|
36
|
+
const results = db.query(sql).all();
|
|
37
|
+
if (opts.json) {
|
|
38
|
+
ui.json(results);
|
|
39
|
+
} else if (results.length === 0) {
|
|
40
|
+
ui.info('No results.');
|
|
41
|
+
} else {
|
|
42
|
+
ui.table(results);
|
|
43
|
+
ui.info(`(${results.length} row${results.length !== 1 ? 's' : ''})`);
|
|
44
|
+
}
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const stmt = db.prepare(sql);
|
|
49
|
+
const result = stmt.run();
|
|
50
|
+
|
|
51
|
+
const payload = {
|
|
52
|
+
changes: result.changes,
|
|
53
|
+
lastInsertRowid: result.lastInsertRowid
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
if (opts.json) {
|
|
57
|
+
ui.json(payload);
|
|
58
|
+
} else {
|
|
59
|
+
ui.success('Query executed successfully.');
|
|
60
|
+
ui.log(`Changes: ${payload.changes}`);
|
|
61
|
+
if (payload.lastInsertRowid)
|
|
62
|
+
ui.log(`Last Insert Row ID: ${String(payload.lastInsertRowid)}`);
|
|
63
|
+
}
|
|
64
|
+
} catch (error) {
|
|
65
|
+
ui.error(`Error executing query: ${String(error)}`);
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dev LocalDB Status CLI Command Descriptor
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { createRuntimeAsset } from '@vibesdotdev/runtime';
|
|
6
|
+
|
|
7
|
+
export default createRuntimeAsset({
|
|
8
|
+
id: 'dev.localdb.status',
|
|
9
|
+
kind: 'cli/command',
|
|
10
|
+
name: 'status',
|
|
11
|
+
description: 'Show database and migration status',
|
|
12
|
+
group: 'dev.localdb',
|
|
13
|
+
surfaces: ['cli'],
|
|
14
|
+
hardware: ['consumer'],
|
|
15
|
+
enabled: true,
|
|
16
|
+
options: [
|
|
17
|
+
{ flags: '--namespace <name>', description: 'Show status for specific namespace' },
|
|
18
|
+
{ flags: '--verbose', description: 'Show detailed migration and connection information' }
|
|
19
|
+
]
|
|
20
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { getVibesRuntime } from '@vibesdotdev/runtime';
|
|
3
|
+
import { requestCLIExit, type UIContext } from '@vibesdotdev/cli/providers';
|
|
4
|
+
import type { LocaldbApi, MigrationRegistryApi } from '../../..';
|
|
5
|
+
import { formatAgeMs, resolveDatabasePathForNamespace, resolveFromCwd } from '../_shared.consumer';
|
|
6
|
+
|
|
7
|
+
export default {
|
|
8
|
+
async execute(_args: Record<string, unknown>, opts: Record<string, unknown>): Promise<void> {
|
|
9
|
+
const runtime = getVibesRuntime();
|
|
10
|
+
const ui = (await runtime.context('cli/ui')) as UIContext;
|
|
11
|
+
const localdb = (await runtime.context('dev/localdb')) as LocaldbApi;
|
|
12
|
+
|
|
13
|
+
const namespaceOpt = typeof opts.namespace === 'string' ? opts.namespace : undefined;
|
|
14
|
+
const verbose = Boolean(opts.verbose);
|
|
15
|
+
|
|
16
|
+
ui.log('\n=== LocalDB Status ===\n');
|
|
17
|
+
|
|
18
|
+
const namespaces = localdb.listNamespaces();
|
|
19
|
+
if (namespaces.length === 0) {
|
|
20
|
+
ui.info('No migration namespaces registered.');
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const namespacesToShow = namespaceOpt
|
|
25
|
+
? namespaces.filter((ns) => ns === namespaceOpt)
|
|
26
|
+
: namespaces;
|
|
27
|
+
|
|
28
|
+
if (namespacesToShow.length === 0) {
|
|
29
|
+
ui.error(`Namespace '${namespaceOpt}' not found.`);
|
|
30
|
+
ui.info(`Available namespaces: ${namespaces.join(', ')}`);
|
|
31
|
+
requestCLIExit(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const { migrationRegistry } =
|
|
35
|
+
(await import('../../../core/migration-registry')) as {
|
|
36
|
+
migrationRegistry: MigrationRegistryApi;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
for (const ns of namespacesToShow) {
|
|
40
|
+
ui.log(`\nNamespace: ${ns}`);
|
|
41
|
+
ui.log('─'.repeat(50));
|
|
42
|
+
|
|
43
|
+
const dbPath = resolveDatabasePathForNamespace(ns);
|
|
44
|
+
if (!dbPath) {
|
|
45
|
+
ui.warn('No known database path for this namespace');
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const fullPath = resolveFromCwd(dbPath);
|
|
50
|
+
const exists = existsSync(fullPath);
|
|
51
|
+
|
|
52
|
+
ui.log(`Database: ${dbPath}`);
|
|
53
|
+
ui.log(`Exists: ${exists ? 'yes' : 'no'}`);
|
|
54
|
+
|
|
55
|
+
if (!exists) continue;
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const db = localdb.getRawDatabase(fullPath, 'localdb-status');
|
|
59
|
+
const status = migrationRegistry.getDatabaseStatus(db, ns);
|
|
60
|
+
|
|
61
|
+
ui.log(`Schema Version: v${status.currentVersion}`);
|
|
62
|
+
ui.log(`Target Version: v${status.targetVersion}`);
|
|
63
|
+
ui.log(`Status: ${status.pendingMigrations > 0 ? 'migrations pending' : 'up to date'}`);
|
|
64
|
+
|
|
65
|
+
if (verbose && status.migrations.length > 0) {
|
|
66
|
+
ui.log('\nMigrations:');
|
|
67
|
+
for (const m of status.migrations) {
|
|
68
|
+
ui.log(` ${m.applied ? '✓' : '○'} v${m.version}: ${m.name}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
} catch (error) {
|
|
72
|
+
ui.error(`Error reading migration status: ${String(error)}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (verbose) {
|
|
77
|
+
ui.log('\n\n=== Connection Pool ===\n');
|
|
78
|
+
const poolStats = localdb.getPoolStats();
|
|
79
|
+
ui.log(`Total Connections: ${poolStats.total}`);
|
|
80
|
+
ui.log(`Active: ${poolStats.active}`);
|
|
81
|
+
ui.log(`Idle: ${poolStats.idle}`);
|
|
82
|
+
|
|
83
|
+
if (poolStats.connections.length > 0) {
|
|
84
|
+
ui.log('\nConnections:');
|
|
85
|
+
for (const conn of poolStats.connections) {
|
|
86
|
+
ui.log(
|
|
87
|
+
` • ${conn.path} (refs: ${conn.refCount}, age: ${formatAgeMs(conn.ageMs)}, consumer: ${conn.consumer})`
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
};
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite Connection Pool
|
|
3
|
+
*
|
|
4
|
+
* Centralized connection management for all SQLite databases in vibes dev.
|
|
5
|
+
* Ensures only one connection per database file to prevent SQLITE_BUSY errors.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Singleton pattern per database path
|
|
9
|
+
* - Automatic WAL mode and busy timeout configuration
|
|
10
|
+
* - Connection reuse across consumers
|
|
11
|
+
* - Safe cleanup on process exit
|
|
12
|
+
* - Connection statistics and monitoring
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { Database } from 'bun:sqlite';
|
|
16
|
+
import { getLogger } from '@vibesdotdev/logging';
|
|
17
|
+
import { toErrorContext } from './error-context';
|
|
18
|
+
|
|
19
|
+
const logger = getLogger('localdb:pool');
|
|
20
|
+
|
|
21
|
+
export interface PooledConnection {
|
|
22
|
+
db: Database;
|
|
23
|
+
refCount: number;
|
|
24
|
+
lastUsed: number;
|
|
25
|
+
createdAt: number;
|
|
26
|
+
consumer: string; // First consumer that opened this connection
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ConnectionStats {
|
|
30
|
+
total: number;
|
|
31
|
+
active: number;
|
|
32
|
+
idle: number;
|
|
33
|
+
connections: Array<{
|
|
34
|
+
path: string;
|
|
35
|
+
refCount: number;
|
|
36
|
+
ageMs: number;
|
|
37
|
+
consumer: string;
|
|
38
|
+
}>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
class SQLiteConnectionPool {
|
|
42
|
+
private connections = new Map<string, PooledConnection>();
|
|
43
|
+
private cleanupInterval?: Timer;
|
|
44
|
+
private readonly STALE_THRESHOLD_MS = 30 * 60 * 1000; // 30 minutes
|
|
45
|
+
private readonly CLEANUP_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
|
|
46
|
+
|
|
47
|
+
constructor() {
|
|
48
|
+
// Periodic cleanup of stale connections
|
|
49
|
+
this.startCleanup();
|
|
50
|
+
|
|
51
|
+
// Ensure proper cleanup on process exit
|
|
52
|
+
this.registerExitHandlers();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Get or create a database connection with optimal configuration
|
|
57
|
+
*/
|
|
58
|
+
getConnection(dbPath: string, consumer = 'unknown'): Database {
|
|
59
|
+
let pooled = this.connections.get(dbPath);
|
|
60
|
+
|
|
61
|
+
if (!pooled) {
|
|
62
|
+
const db = new Database(dbPath);
|
|
63
|
+
|
|
64
|
+
// Configure for optimal concurrent access
|
|
65
|
+
this.configureDatabase(db);
|
|
66
|
+
|
|
67
|
+
pooled = {
|
|
68
|
+
db,
|
|
69
|
+
refCount: 0,
|
|
70
|
+
lastUsed: Date.now(),
|
|
71
|
+
createdAt: Date.now(),
|
|
72
|
+
consumer
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
this.connections.set(dbPath, pooled);
|
|
76
|
+
logger.debug(`Created connection: ${dbPath} (${consumer})`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
pooled.refCount++;
|
|
80
|
+
pooled.lastUsed = Date.now();
|
|
81
|
+
|
|
82
|
+
return pooled.db;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Configure database with optimal settings for concurrent access
|
|
87
|
+
*/
|
|
88
|
+
private configureDatabase(db: Database): void {
|
|
89
|
+
// WAL mode: Readers don't block writers, writers don't block readers
|
|
90
|
+
db.exec('PRAGMA journal_mode = WAL;');
|
|
91
|
+
|
|
92
|
+
// Busy timeout: Wait up to 5 seconds for locks instead of failing immediately
|
|
93
|
+
db.exec('PRAGMA busy_timeout = 5000;');
|
|
94
|
+
|
|
95
|
+
// Foreign keys: Enable referential integrity
|
|
96
|
+
db.exec('PRAGMA foreign_keys = ON;');
|
|
97
|
+
|
|
98
|
+
// Synchronous: NORMAL is safe with WAL and much faster
|
|
99
|
+
db.exec('PRAGMA synchronous = NORMAL;');
|
|
100
|
+
|
|
101
|
+
// Temp store: Use memory for temp tables (faster)
|
|
102
|
+
db.exec('PRAGMA temp_store = MEMORY;');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Release a connection (decrement reference count)
|
|
107
|
+
* Connection remains in pool for reuse
|
|
108
|
+
*/
|
|
109
|
+
releaseConnection(dbPath: string): void {
|
|
110
|
+
const pooled = this.connections.get(dbPath);
|
|
111
|
+
if (pooled) {
|
|
112
|
+
pooled.refCount = Math.max(0, pooled.refCount - 1);
|
|
113
|
+
pooled.lastUsed = Date.now();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Close a specific connection immediately
|
|
119
|
+
* Only closes if reference count is zero (unless forced)
|
|
120
|
+
*/
|
|
121
|
+
closeConnection(dbPath: string, force = false): boolean {
|
|
122
|
+
const pooled = this.connections.get(dbPath);
|
|
123
|
+
if (!pooled) return false;
|
|
124
|
+
|
|
125
|
+
if (force || pooled.refCount === 0) {
|
|
126
|
+
try {
|
|
127
|
+
pooled.db.close();
|
|
128
|
+
this.connections.delete(dbPath);
|
|
129
|
+
// Use stderr to avoid breaking MCP stdio protocol
|
|
130
|
+
logger.debug(`[LocalDB] Closed connection: ${dbPath}`);
|
|
131
|
+
return true;
|
|
132
|
+
} catch (error) {
|
|
133
|
+
logger.error(`[LocalDB] Error closing ${dbPath}:`, toErrorContext(error));
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Clean up stale connections (unused for 30+ minutes with zero references)
|
|
143
|
+
*/
|
|
144
|
+
private cleanupStale(): void {
|
|
145
|
+
const now = Date.now();
|
|
146
|
+
|
|
147
|
+
for (const [dbPath, pooled] of this.connections.entries()) {
|
|
148
|
+
const age = now - pooled.lastUsed;
|
|
149
|
+
if (pooled.refCount === 0 && age > this.STALE_THRESHOLD_MS) {
|
|
150
|
+
this.closeConnection(dbPath, true);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Start periodic cleanup
|
|
157
|
+
*/
|
|
158
|
+
private startCleanup(): void {
|
|
159
|
+
this.cleanupInterval = setInterval(() => this.cleanupStale(), this.CLEANUP_INTERVAL_MS);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Stop periodic cleanup
|
|
164
|
+
*/
|
|
165
|
+
private stopCleanup(): void {
|
|
166
|
+
if (this.cleanupInterval) {
|
|
167
|
+
clearInterval(this.cleanupInterval);
|
|
168
|
+
this.cleanupInterval = undefined;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Close all connections (typically on shutdown)
|
|
174
|
+
*/
|
|
175
|
+
closeAll(): void {
|
|
176
|
+
this.stopCleanup();
|
|
177
|
+
|
|
178
|
+
for (const [dbPath, pooled] of this.connections.entries()) {
|
|
179
|
+
try {
|
|
180
|
+
pooled.db.close();
|
|
181
|
+
// Use stderr to avoid breaking MCP stdio protocol
|
|
182
|
+
logger.debug(`[LocalDB] Closed connection on shutdown: ${dbPath}`);
|
|
183
|
+
} catch (error) {
|
|
184
|
+
logger.error(`[LocalDB] Error closing ${dbPath}:`, toErrorContext(error));
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
this.connections.clear();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Get pool statistics
|
|
193
|
+
*/
|
|
194
|
+
getStats(): ConnectionStats {
|
|
195
|
+
let active = 0;
|
|
196
|
+
let idle = 0;
|
|
197
|
+
const connections: ConnectionStats['connections'] = [];
|
|
198
|
+
const now = Date.now();
|
|
199
|
+
|
|
200
|
+
for (const [path, pooled] of this.connections.entries()) {
|
|
201
|
+
if (pooled.refCount > 0) {
|
|
202
|
+
active++;
|
|
203
|
+
} else {
|
|
204
|
+
idle++;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
connections.push({
|
|
208
|
+
path,
|
|
209
|
+
refCount: pooled.refCount,
|
|
210
|
+
ageMs: now - pooled.createdAt,
|
|
211
|
+
consumer: pooled.consumer
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
total: this.connections.size,
|
|
217
|
+
active,
|
|
218
|
+
idle,
|
|
219
|
+
connections
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Register process exit handlers for cleanup
|
|
225
|
+
*/
|
|
226
|
+
private registerExitHandlers(): void {
|
|
227
|
+
process.on('beforeExit', () => this.closeAll());
|
|
228
|
+
process.on('SIGINT', () => {
|
|
229
|
+
this.closeAll();
|
|
230
|
+
process.exit(0);
|
|
231
|
+
});
|
|
232
|
+
process.on('SIGTERM', () => {
|
|
233
|
+
this.closeAll();
|
|
234
|
+
process.exit(0);
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Singleton instance
|
|
240
|
+
export const connectionPool = new SQLiteConnectionPool();
|