UNPKG

temporal-db

Version:

Git-like versioning for application data

259 lines (217 loc) 7.64 kB
const MerkleTree = require('./merkle'); const Diff = require('./diff'); /** * Manages branches and commits */ class Branch { /** * Creates a new Branch manager * @param {Object} storage - Storage instance */ constructor(storage) { this.storage = storage; } /** * Initialize with default main branch if it doesn't exist * @returns {Promise<boolean>} True if initialization was needed */ async init() { const mainExists = await this.storage.getRef('branch/main'); if (!mainExists) { const emptyTree = MerkleTree.fromObject({}); const rootHash = await MerkleTree.storeTree(this.storage, emptyTree); const commit = { hash: rootHash, parent: null, branch: 'main', message: 'Initial commit', timestamp: Date.now(), rootHash }; await this.storage.saveCommit(commit); await this.storage.saveRef('branch/main', rootHash); await this.storage.saveRef('HEAD', 'branch/main'); return true; } return false; } /** * Get the current branch name * @returns {Promise<string>} Current branch name */ async getCurrentBranch() { const headRef = await this.storage.getRef('HEAD'); if (!headRef) { throw new Error('HEAD reference not found'); } if (headRef.startsWith('branch/')) { return headRef.substring(7); // Remove 'branch/' prefix } throw new Error('HEAD is in detached state'); } /** * Get the latest commit hash for a branch * @param {string} branchName - Branch name * @returns {Promise<string>} Latest commit hash or null */ async getBranchHead(branchName) { return this.storage.getRef(`branch/${branchName}`); } /** * List all branches * @returns {Promise<Array<string>>} Array of branch names */ async listBranches() { const refs = await this.storage.listRefs('branch/'); return Object.keys(refs).map(ref => ref.substring(7)); // Remove 'branch/' prefix } /** * Create a new branch from a source branch * @param {string} newBranchName - Name for the new branch * @param {string} sourceBranchName - Source branch name * @returns {Promise<string>} New branch head commit hash */ async createBranch(newBranchName, sourceBranchName) { if (!newBranchName) { throw new Error('Branch name is required'); } const exists = await this.storage.getRef(`branch/${newBranchName}`); if (exists) { throw new Error(`Branch '${newBranchName}' already exists`); } // Get source branch head const sourceHead = await this.getBranchHead(sourceBranchName); if (!sourceHead) { throw new Error(`Source branch '${sourceBranchName}' does not exist`); } // Create new branch pointing to the same commit await this.storage.saveRef(`branch/${newBranchName}`, sourceHead); return sourceHead; } /** * Switch to a different branch * @param {string} branchName - Branch to switch to * @returns {Promise<string>} New HEAD commit hash */ async checkout(branchName) { const branchHead = await this.getBranchHead(branchName); if (!branchHead) { throw new Error(`Branch '${branchName}' does not exist`); } await this.storage.saveRef('HEAD', `branch/${branchName}`); return branchHead; } /** * Create and store a new commit * @param {string} branchName - Branch to commit to * @param {Object} data - Data to commit * @param {string} message - Commit message * @returns {Promise<Object>} Commit object */ async commit(branchName, data, message) { const branchRef = `branch/${branchName}`; // Get the current branch head const parentHash = await this.storage.getRef(branchRef); // Create Merkle tree from new data const tree = MerkleTree.fromObject(data); const rootHash = await MerkleTree.storeTree(this.storage, tree); // Create commit object const timestamp = Date.now(); const commit = { hash: rootHash, parent: parentHash, branch: branchName, message: message || 'Update', timestamp, rootHash }; // Store commit metadata await this.storage.saveCommit(commit); // Update branch reference await this.storage.saveRef(branchRef, rootHash); return commit; } /** * Get data at a specific commit * @param {string} commitHash - Commit hash * @returns {Promise<Object>} Data at that commit */ async getDataAtCommit(commitHash) { const commit = await this.storage.getCommit(commitHash); if (!commit) { throw new Error(`Commit '${commitHash}' not found`); } const tree = await MerkleTree.retrieveTree(this.storage, commit.rootHash); return MerkleTree.toObject(tree); } /** * Get data for a branch at the latest commit * @param {string} branchName - Branch name * @returns {Promise<Object>} Latest data for the branch */ async getBranchData(branchName) { const headHash = await this.getBranchHead(branchName); if (!headHash) { throw new Error(`Branch '${branchName}' not found`); } return this.getDataAtCommit(headHash); } /** * Get data at a specific point in time * @param {string} branchName - Branch name * @param {Date|string|number} timestamp - Point in time * @returns {Promise<Object>} Data at that time */ async getDataAtTime(branchName, timestamp) { // Convert timestamp to Date object if needed const time = typeof timestamp === 'string' ? new Date(timestamp) : (timestamp instanceof Date ? timestamp : new Date(timestamp)); // Get all commits for the branch const commits = await this.storage.getCommitsForBranch(branchName); // Find the most recent commit before or at the specified time let targetCommit = null; for (const commit of commits) { if (commit.timestamp <= time.getTime()) { targetCommit = commit; break; // commits are ordered most recent first } } if (!targetCommit) { throw new Error(`No commit found on branch '${branchName}' before ${time.toISOString()}`); } return this.getDataAtCommit(targetCommit.hash); } /** * Get commit history for a branch * @param {string} branchName - Branch name * @returns {Promise<Array<Object>>} Array of commit objects */ async getHistory(branchName) { return this.storage.getCommitsForBranch(branchName); } /** * Delete a branch * @param {string} branchName - Branch to delete * @returns {Promise<void>} */ async deleteBranch(branchName) { // Cannot delete the main branch if (branchName === 'main') { throw new Error('Cannot delete the main branch'); } // Check if we're on this branch const currentBranch = await this.getCurrentBranch(); if (currentBranch === branchName) { throw new Error(`Cannot delete the currently checked out branch '${branchName}'`); } // Check if branch exists const exists = await this.getBranchHead(branchName); if (!exists) { throw new Error(`Branch '${branchName}' not found`); } // Delete the branch reference await this.storage.deleteRef(`branch/${branchName}`); } } module.exports = Branch;