pit-manager
Version:
Centralized prompt management system for Human Behavior AI agents
339 lines (338 loc) • 12.1 kB
JavaScript
"use strict";
/**
* Core storage engine for PIT TypeScript - Git-like content-addressable storage.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.StorageEngine = void 0;
const fs = require("fs/promises");
const path = require("path");
const better_sqlite3_1 = require("better-sqlite3");
const exceptions_1 = require("../core/exceptions");
const objects_1 = require("./objects");
class StorageEngine {
constructor(repoPath) {
this.db = null;
// Per-hash locks for concurrent operations (simplified for TypeScript)
this.objectLocks = new Map();
this.objectsPath = path.join(repoPath, 'objects');
this.refsPath = path.join(repoPath, 'refs');
this.metadataDbPath = path.join(repoPath, 'metadata.db');
// Ensure the repo directory exists before creating database
try {
require('fs').mkdirSync(repoPath, { recursive: true });
}
catch (error) {
// Directory might already exist, ignore
}
// Initialize synchronously to avoid async constructor issues
this.initMetadataDb();
// Other directories will be created as needed during operations
}
async ensureDirectories() {
// Ensure directories exist - called as needed during operations
await fs.mkdir(this.objectsPath, { recursive: true });
await fs.mkdir(this.refsPath, { recursive: true });
await fs.mkdir(path.join(this.refsPath, 'heads'), { recursive: true });
await fs.mkdir(path.join(this.refsPath, 'tags'), { recursive: true });
}
initMetadataDb() {
this.db = new better_sqlite3_1.default(this.metadataDbPath);
// Initialize metadata database
this.db.exec(`
CREATE TABLE IF NOT EXISTS objects (
hash TEXT PRIMARY KEY,
type TEXT NOT NULL,
size INTEGER NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
ref_count INTEGER DEFAULT 0
)
`);
this.db.exec(`
CREATE TABLE IF NOT EXISTS refs (
name TEXT PRIMARY KEY,
hash TEXT NOT NULL,
type TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
// Create indexes for performance
this.db.exec('CREATE INDEX IF NOT EXISTS idx_objects_type ON objects(type)');
this.db.exec('CREATE INDEX IF NOT EXISTS idx_objects_created_at ON objects(created_at)');
this.db.exec('CREATE INDEX IF NOT EXISTS idx_refs_type ON refs(type)');
}
async getObjectLock(hash) {
const existingLock = this.objectLocks.get(hash);
if (existingLock) {
await existingLock;
}
let resolveLock;
const lock = new Promise((resolve) => {
resolveLock = resolve;
});
this.objectLocks.set(hash, lock);
// Auto-cleanup after operation
setTimeout(() => {
this.objectLocks.delete(hash);
resolveLock();
}, 0);
}
/**
* Store an object and return its hash.
*/
async storeObject(obj) {
const hash = obj.hash();
// Use per-hash lock to prevent race conditions
await this.getObjectLock(hash);
// Check if object already exists
const objectPath = this.getObjectPath(hash);
try {
await fs.access(objectPath);
return hash; // Object already exists
}
catch {
// Object doesn't exist, continue with storage
}
try {
// Ensure directories exist
await this.ensureDirectories();
// Get compressed content
const content = await obj.getRawContent();
// Create directory for object (first 2 chars of hash)
const objectDir = path.dirname(objectPath);
await fs.mkdir(objectDir, { recursive: true });
// Write object to file
await fs.writeFile(objectPath, content);
// Update metadata database
if (this.db) {
const stmt = this.db.prepare('INSERT OR REPLACE INTO objects (hash, type, size, created_at, ref_count) VALUES (?, ?, ?, ?, ?)');
stmt.run(hash, obj.objectType, content.length, new Date().toISOString(), 0);
}
return hash;
}
catch (error) {
throw new exceptions_1.StorageError(`Failed to store object ${hash}: ${error}`);
}
}
/**
* Load an object by hash.
*/
async loadObject(hash) {
const objectPath = this.getObjectPath(hash);
try {
const content = await fs.readFile(objectPath);
return (0, objects_1.deserializeObject)(content);
}
catch (error) {
if (error.code === 'ENOENT') {
throw new exceptions_1.ObjectNotFoundError(`Object ${hash} not found`);
}
throw new exceptions_1.CorruptedObjectError(`Failed to load object ${hash}: ${error.message}`);
}
}
/**
* Check if an object exists.
*/
async objectExists(hash) {
const objectPath = this.getObjectPath(hash);
try {
await fs.access(objectPath);
return true;
}
catch {
return false;
}
}
/**
* Store a reference (branch, tag, etc.).
*/
async storeRef(name, hash, type = 'branch') {
// Avoid double nesting - if name already starts with 'refs/', don't add another refs/ prefix
let refPath;
if (name.startsWith('refs/') && this.refsPath.endsWith('refs')) {
// Use parent path to avoid refs/refs/
refPath = path.join(path.dirname(this.refsPath), name);
}
else {
refPath = path.join(this.refsPath, name);
}
try {
// Create directory for ref if needed
const refDir = path.dirname(refPath);
await fs.mkdir(refDir, { recursive: true });
// Write ref file
await fs.writeFile(refPath, hash, 'utf8');
// Update metadata database
if (this.db) {
const stmt = this.db.prepare('INSERT OR REPLACE INTO refs (name, hash, type, updated_at) VALUES (?, ?, ?, ?)');
stmt.run(name, hash, type, new Date().toISOString());
}
}
catch (error) {
throw new exceptions_1.StorageError(`Failed to store ref ${name}: ${error}`);
}
}
/**
* Load a reference by name.
*/
async loadRef(name) {
// Handle both relative and absolute ref paths
let refPath;
if (name.startsWith('refs/') && this.refsPath.endsWith('refs')) {
refPath = path.join(path.dirname(this.refsPath), name);
}
else {
refPath = path.join(this.refsPath, name);
}
try {
const hash = await fs.readFile(refPath, 'utf8');
return hash.trim();
}
catch (error) {
if (error.code === 'ENOENT') {
throw new exceptions_1.RefNotFoundError(`Reference ${name} not found`);
}
throw new exceptions_1.StorageError(`Failed to load ref ${name}: ${error.message}`);
}
}
/**
* Check if a reference exists.
*/
async refExists(name) {
// Handle both relative and absolute ref paths
let refPath;
if (name.startsWith('refs/') && this.refsPath.endsWith('refs')) {
refPath = path.join(path.dirname(this.refsPath), name);
}
else {
refPath = path.join(this.refsPath, name);
}
try {
await fs.access(refPath);
return true;
}
catch {
return false;
}
}
/**
* List all references of a given type.
*/
async listRefs(type) {
if (!this.db) {
throw new exceptions_1.StorageError('Database not initialized');
}
let query = 'SELECT name, hash, type FROM refs';
const params = [];
if (type) {
query += ' WHERE type = ?';
params.push(type);
}
query += ' ORDER BY name';
const stmt = this.db.prepare(query);
const rows = stmt.all(...params);
return rows.map((row) => ({
name: row.name,
hash: row.hash,
type: row.type
}));
}
/**
* Delete a reference.
*/
async deleteRef(name) {
// Handle both relative and absolute ref paths
let refPath;
if (name.startsWith('refs/') && this.refsPath.endsWith('refs')) {
refPath = path.join(path.dirname(this.refsPath), name);
}
else {
refPath = path.join(this.refsPath, name);
}
try {
await fs.unlink(refPath);
// Remove from metadata database
if (this.db) {
const stmt = this.db.prepare('DELETE FROM refs WHERE name = ?');
stmt.run(name);
}
}
catch (error) {
if (error.code === 'ENOENT') {
throw new exceptions_1.RefNotFoundError(`Reference ${name} not found`);
}
throw new exceptions_1.StorageError(`Failed to delete ref ${name}: ${error.message}`);
}
}
/**
* Get storage statistics.
*/
async getStats() {
if (!this.db) {
throw new exceptions_1.StorageError('Database not initialized');
}
const stats = {
objectCount: 0,
refCount: 0,
totalSize: 0,
objectsByType: {}
};
const stmt1 = this.db.prepare('SELECT COUNT(*) as count, SUM(size) as total FROM objects');
const stmt2 = this.db.prepare('SELECT COUNT(*) as count FROM refs');
const stmt3 = this.db.prepare('SELECT type, COUNT(*) as count FROM objects GROUP BY type');
const row1 = stmt1.get();
stats.objectCount = row1?.count || 0;
stats.totalSize = row1?.total || 0;
const row2 = stmt2.get();
stats.refCount = row2?.count || 0;
const rows3 = stmt3.all();
for (const row of rows3) {
stats.objectsByType[row.type] = row.count;
}
return stats;
}
/**
* Cleanup old objects (garbage collection).
*/
async cleanup(maxAge = 30 * 24 * 60 * 60 * 1000) {
if (!this.db) {
throw new exceptions_1.StorageError('Database not initialized');
}
const cutoffDate = new Date(Date.now() - maxAge).toISOString();
// Find old objects - note: no accessed_at column in our schema
const stmt = this.db.prepare('SELECT hash FROM objects WHERE created_at < ?');
const rows = stmt.all(cutoffDate);
let cleanedCount = 0;
for (const row of rows) {
try {
const objectPath = this.getObjectPath(row.hash);
await fs.unlink(objectPath);
const deleteStmt = this.db.prepare('DELETE FROM objects WHERE hash = ?');
deleteStmt.run(row.hash);
cleanedCount++;
}
catch (error) {
// Continue cleaning other objects
console.warn(`Failed to clean object ${row.hash}: ${error}`);
}
}
return cleanedCount;
}
/**
* Close the storage engine and database connections.
*/
async close() {
if (this.db) {
this.db.close();
this.db = null;
}
}
getObjectPath(hash) {
// Store objects in subdirectories based on first 2 characters of hash
// This prevents too many files in a single directory
const subdir = hash.substring(0, 2);
const filename = hash.substring(2);
return path.join(this.objectsPath, subdir, filename);
}
}
exports.StorageEngine = StorageEngine;