prompt-version-manager
Version:
Centralized prompt management system for Human Behavior AI agents
438 lines • 16.2 kB
JavaScript
;
/**
* Core storage engine for PVM TypeScript - Git-like content-addressable storage.
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.StorageEngine = void 0;
const fs = __importStar(require("fs/promises"));
const path = __importStar(require("path"));
const sqlite3 = __importStar(require("sqlite3"));
const exceptions_1 = require("../core/exceptions");
const objects_1 = require("./objects");
class StorageEngine {
objectsPath;
refsPath;
metadataDbPath;
db = null;
// Per-hash locks for concurrent operations (simplified for TypeScript)
objectLocks = new Map();
constructor(repoPath) {
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 sqlite3.Database(this.metadataDbPath);
// Initialize metadata database
this.db.serialize(() => {
this.db.run(`
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.run(`
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.run('CREATE INDEX IF NOT EXISTS idx_objects_type ON objects(type)');
this.db.run('CREATE INDEX IF NOT EXISTS idx_objects_created_at ON objects(created_at)');
this.db.run('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) {
await new Promise((resolve, reject) => {
this.db.run('INSERT OR REPLACE INTO objects (hash, type, size, created_at, ref_count) VALUES (?, ?, ?, ?, ?)', [hash, obj.objectType, content.length, new Date().toISOString(), 0], (err) => {
if (err)
reject(err);
else
resolve();
});
});
}
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) {
await new Promise((resolve, reject) => {
this.db.run('INSERT OR REPLACE INTO refs (name, hash, type, updated_at) VALUES (?, ?, ?, ?)', [name, hash, type, new Date().toISOString()], (err) => {
if (err)
reject(err);
else
resolve();
});
});
}
}
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');
}
return new Promise((resolve, reject) => {
let query = 'SELECT name, hash, type FROM refs';
const params = [];
if (type) {
query += ' WHERE type = ?';
params.push(type);
}
query += ' ORDER BY name';
this.db.all(query, params, (err, rows) => {
if (err) {
reject(new exceptions_1.StorageError(`Failed to list refs: ${err}`));
}
else {
resolve(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) {
await new Promise((resolve, reject) => {
this.db.run('DELETE FROM refs WHERE name = ?', [name], (err) => {
if (err)
reject(err);
else
resolve();
});
});
}
}
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');
}
return new Promise((resolve, reject) => {
const stats = {
objectCount: 0,
refCount: 0,
totalSize: 0,
objectsByType: {}
};
// Get object count and total size
this.db.get('SELECT COUNT(*) as count, SUM(size) as total FROM objects', (err, row) => {
if (err) {
reject(new exceptions_1.StorageError(`Failed to get stats: ${err}`));
return;
}
stats.objectCount = row.count || 0;
stats.totalSize = row.total || 0;
// Get ref count
this.db.get('SELECT COUNT(*) as count FROM refs', (err, row) => {
if (err) {
reject(new exceptions_1.StorageError(`Failed to get stats: ${err}`));
return;
}
stats.refCount = row.count || 0;
// Get objects by type
this.db.all('SELECT type, COUNT(*) as count FROM objects GROUP BY type', (err, rows) => {
if (err) {
reject(new exceptions_1.StorageError(`Failed to get stats: ${err}`));
return;
}
for (const row of rows) {
stats.objectsByType[row.type] = row.count;
}
resolve(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();
return new Promise((resolve, reject) => {
// Find old objects
this.db.all('SELECT hash FROM objects WHERE accessed_at < ? AND created_at < ?', [cutoffDate, cutoffDate], async (err, rows) => {
if (err) {
reject(new exceptions_1.StorageError(`Failed to cleanup: ${err}`));
return;
}
let cleanedCount = 0;
for (const row of rows) {
try {
const objectPath = this.getObjectPath(row.hash);
await fs.unlink(objectPath);
await new Promise((resolve, reject) => {
this.db.run('DELETE FROM objects WHERE hash = ?', [row.hash], (err) => {
if (err)
reject(err);
else
resolve();
});
});
cleanedCount++;
}
catch (error) {
// Continue cleaning other objects
console.warn(`Failed to clean object ${row.hash}: ${error}`);
}
}
resolve(cleanedCount);
});
});
}
/**
* Close the storage engine and database connections.
*/
async close() {
if (this.db) {
await new Promise((resolve, reject) => {
this.db.close((err) => {
if (err)
reject(err);
else
resolve();
});
});
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;
//# sourceMappingURL=engine.js.map