Files
eden/src/lib/server/reference/repository.ts
T
2026-09-18 23:15:05 -04:00

91 lines
5.6 KiB
TypeScript

import { Database } from 'bun:sqlite';
import { existsSync, readFileSync } from 'node:fs';
import { basename, extname, isAbsolute, join, relative, resolve } from 'node:path';
import type { InventoryType, ReferenceRepository } from '../../domain/model';
import type { BuildProfile, ReferenceManifest, ReferenceSnapshot } from './types';
import { MANIFEST_FILE, DATABASE_DIRECTORY } from './compiler';
const MAX_IN_BATCH = 500;
type TypeRow = { type_id: number; name_json: string; category_id: number; volume: number | null };
function assertArtifactPath(root: string, database: string): string {
if (!database || isAbsolute(database) || database.includes('\\') || database.split('/').includes('..')) throw new Error('active reference database path is not a safe relative artifact path');
const artifactRoot = resolve(root, DATABASE_DIRECTORY);
const candidate = resolve(root, database);
const remainder = relative(artifactRoot, candidate);
if (!remainder || remainder.includes('/') || remainder.startsWith('..') || isAbsolute(remainder) || extname(candidate) !== '.sqlite' || !/^[a-f0-9]{64}\.sqlite$/.test(basename(candidate))) throw new Error('active reference database is outside the artifact directory');
return candidate;
}
/** Runtime reader: a read-only handle pinned to one immutable manifest artifact. */
export class SqliteReferenceRepository implements ReferenceRepository, ReferenceSnapshot {
private readonly db: Database;
private closed = false;
readonly pinnedVersion: string;
readonly databasePath: string;
constructor(directory: string, expectedProfile?: BuildProfile) {
const root = resolve(directory);
const manifestPath = join(root, MANIFEST_FILE);
if (!existsSync(manifestPath)) throw new Error(`reference manifest not found: ${manifestPath}`);
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) as ReferenceManifest;
if (manifest.format !== 1 || !manifest.active) throw new Error(`reference manifest has no active artifact: ${manifestPath}`);
if (expectedProfile && manifest.active.profile !== expectedProfile) throw new Error(`reference artifact must use the ${expectedProfile} profile`);
this.pinnedVersion = manifest.active.version;
this.databasePath = assertArtifactPath(root, manifest.active.database);
if (basename(this.databasePath) !== `${this.pinnedVersion}.sqlite`) throw new Error('active reference database filename does not match its manifest version');
if (!existsSync(this.databasePath)) throw new Error(`active reference database not found: ${this.databasePath}`);
this.db = new Database(this.databasePath, { readonly: true, strict: true });
assertReaderSchema(this.db);
}
/** Open another independent pinned handle. The caller owns and closes it. */
pin(): SqliteReferenceSnapshot { this.assertOpen(); return new SqliteReferenceSnapshot(this.databasePath, this.pinnedVersion); }
snapshot(): SqliteReferenceSnapshot { return this.pin(); }
async types(ids: number[]): Promise<InventoryType[]> { this.assertOpen(); return queryTypes(this.db, ids); }
async version(): Promise<string> { this.assertOpen(); return this.pinnedVersion; }
close(): void { if (!this.closed) { this.closed = true; this.db.close(); } }
[Symbol.dispose](): void { this.close(); }
private assertOpen(): void { if (this.closed) throw new Error('reference repository is closed'); }
}
export class SqliteReferenceSnapshot implements ReferenceSnapshot {
private readonly db: Database;
private closed = false;
constructor(readonly databasePath: string, readonly pinnedVersion: string) {
this.db = new Database(databasePath, { readonly: true, strict: true });
assertReaderSchema(this.db);
}
async types(ids: number[]): Promise<InventoryType[]> { this.assertOpen(); return queryTypes(this.db, ids); }
async version(): Promise<string> { this.assertOpen(); return this.pinnedVersion; }
close(): void { if (!this.closed) { this.closed = true; this.db.close(); } }
[Symbol.dispose](): void { this.close(); }
private assertOpen(): void { if (this.closed) throw new Error('reference snapshot is closed'); }
}
/** Cheap metadata check only; full integrity_check belongs to the compiler. */
function assertReaderSchema(db: Database): void {
const row = db.query<{ present: number }, []>("SELECT COUNT(*) AS present FROM sqlite_master WHERE type='table' AND name IN ('types','categories')").get();
if (!row || Number(row.present) !== 2) { db.close(); throw new Error('active reference database has an unexpected schema'); }
}
async function queryTypes(db: Database, requested: number[]): Promise<InventoryType[]> {
const unique = [...new Set(requested.filter((id) => Number.isSafeInteger(id) && id >= 0))];
if (!unique.length) return [];
const rows: TypeRow[] = [];
// SQLite has a finite host-parameter limit. Chunking also bounds preparation
// cost for callers that pass a complete asset snapshot in one request.
for (let offset = 0; offset < unique.length; offset += MAX_IN_BATCH) {
const batch = unique.slice(offset, offset + MAX_IN_BATCH);
const placeholders = batch.map(() => '?').join(',');
rows.push(...db.query<TypeRow, number[]>(`SELECT type_id,name_json,category_id,volume FROM types WHERE type_id IN (${placeholders})`).all(...batch));
}
const byId = new Map(rows.map((row) => [row.type_id, row]));
return unique.flatMap((id) => {
const row = byId.get(id); if (!row) return [];
const names = JSON.parse(row.name_json) as Record<string, string>;
return [{ id: row.type_id, name: names.en ?? Object.values(names)[0] ?? String(row.type_id), categoryId: row.category_id, ...(row.volume === null ? {} : { volume: row.volume }) }];
});
}