feat(beadboard-550.1): add DoltClient with connection pooling and metadata config
- src/lib/dolt-client.ts: getDoltConnection(projectRoot) returns cached mysql2 Pool - Reads host/port/database from .beads/metadata.json (no hardcoded values) - DoltConnectionError typed error for missing metadata or unreachable server - Tests connectivity on first connection; caches pool per resolved projectRoot - connectionLimit: 5 for Next.js server-side concurrency Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
bb1231860e
commit
91b4777a7c
1 changed files with 92 additions and 0 deletions
92
src/lib/dolt-client.ts
Normal file
92
src/lib/dolt-client.ts
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
import mysql from 'mysql2/promise';
|
||||
|
||||
export class DoltConnectionError extends Error {
|
||||
constructor(message: string, public readonly cause?: unknown) {
|
||||
super(message);
|
||||
this.name = 'DoltConnectionError';
|
||||
}
|
||||
}
|
||||
|
||||
interface DoltMetadata {
|
||||
dolt_server_port: number;
|
||||
dolt_database: string;
|
||||
dolt_server_host?: string;
|
||||
}
|
||||
|
||||
async function readDoltMetadata(projectRoot: string): Promise<DoltMetadata> {
|
||||
const metadataPath = path.join(projectRoot, '.beads', 'metadata.json');
|
||||
let raw: string;
|
||||
try {
|
||||
raw = await fs.readFile(metadataPath, 'utf-8');
|
||||
} catch (err) {
|
||||
throw new DoltConnectionError(
|
||||
`Cannot read Dolt metadata at ${metadataPath} — is this a bd (beads) project?`,
|
||||
err
|
||||
);
|
||||
}
|
||||
|
||||
let parsed: Record<string, unknown>;
|
||||
try {
|
||||
parsed = JSON.parse(raw) as Record<string, unknown>;
|
||||
} catch (err) {
|
||||
throw new DoltConnectionError(`Invalid JSON in ${metadataPath}`, err);
|
||||
}
|
||||
|
||||
const port = parsed.dolt_server_port;
|
||||
const database = parsed.dolt_database;
|
||||
if (typeof port !== 'number' || typeof database !== 'string') {
|
||||
throw new DoltConnectionError(
|
||||
`${metadataPath} is missing required fields: dolt_server_port (number) and dolt_database (string)`
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
dolt_server_port: port,
|
||||
dolt_database: database,
|
||||
dolt_server_host: typeof parsed.dolt_server_host === 'string' ? parsed.dolt_server_host : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
// Cache pools per resolved project root to avoid reconnecting on every request
|
||||
const poolCache = new Map<string, mysql.Pool>();
|
||||
|
||||
export async function getDoltConnection(projectRoot: string): Promise<mysql.Pool> {
|
||||
const key = path.resolve(projectRoot);
|
||||
|
||||
const existing = poolCache.get(key);
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
const metadata = await readDoltMetadata(projectRoot);
|
||||
const host = metadata.dolt_server_host ?? '127.0.0.1';
|
||||
|
||||
const pool = mysql.createPool({
|
||||
host,
|
||||
port: metadata.dolt_server_port,
|
||||
database: metadata.dolt_database,
|
||||
user: 'root',
|
||||
password: '',
|
||||
connectionLimit: 5,
|
||||
waitForConnections: true,
|
||||
queueLimit: 0,
|
||||
});
|
||||
|
||||
// Verify server is reachable before caching — fail fast with a typed error
|
||||
try {
|
||||
const conn = await pool.getConnection();
|
||||
conn.release();
|
||||
} catch (err) {
|
||||
await pool.end().catch(() => {});
|
||||
throw new DoltConnectionError(
|
||||
`Cannot connect to Dolt SQL server at ${host}:${metadata.dolt_server_port} (database: ${metadata.dolt_database})`,
|
||||
err
|
||||
);
|
||||
}
|
||||
|
||||
poolCache.set(key, pool);
|
||||
return pool;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue