UNPKG

temporal-db

Version:

Git-like versioning for application data

277 lines (234 loc) 8.09 kB
const CryptoJS = require('crypto-js'); const _ = require('lodash'); /** * MerkleTree implementation for content-addressable storage * and efficient structural comparison of objects */ class MerkleTree { /** * Create a Merkle tree from an object * @param {Object} data - Source object to create tree from * @returns {Object} Merkle tree representation */ static fromObject(data) { if (data === null || data === undefined) { return { hash: MerkleTree.hashData(null), value: null, type: 'null' }; } const type = Array.isArray(data) ? 'array' : typeof data; // Handle primitive types directly if (type !== 'object' && type !== 'array') { return { hash: MerkleTree.hashData(data), value: data, type }; } // Handle objects and arrays const children = {}; const keys = Object.keys(data).sort(); // Sort keys for consistent hashing for (const key of keys) { children[key] = MerkleTree.fromObject(data[key]); } // Calculate hash based on children's hashes const childrenHashes = {}; for (const key of keys) { childrenHashes[key] = children[key].hash; } const hash = MerkleTree.hashData(childrenHashes); return { hash, type, children }; } /** * Convert a Merkle tree back to its original object * @param {Object} tree - Merkle tree to convert * @returns {*} Original object */ static toObject(tree) { if (!tree) return null; // Handle primitive types if (tree.type !== 'object' && tree.type !== 'array') { return tree.value; } // Handle objects and arrays const result = tree.type === 'array' ? [] : {}; if (tree.children) { for (const key of Object.keys(tree.children)) { result[key] = MerkleTree.toObject(tree.children[key]); } } return result; } /** * Hash data consistently * @param {*} data - Data to hash * @returns {string} Hash of the data */ static hashData(data) { const json = JSON.stringify(data); return CryptoJS.SHA256(json).toString(); } /** * Compare two trees and return paths to differences * @param {Object} oldTree - Previous Merkle tree * @param {Object} newTree - New Merkle tree * @param {string} [basePath=''] - Current path for recursion * @returns {Object} Object with added, modified, and deleted paths */ static diff(oldTree, newTree, basePath = '') { const result = { added: [], modified: [], deleted: [] }; // Both trees are null or exactly the same if (!oldTree && !newTree) { return result; } // If either tree is null (but not both), the entire subtree is added/deleted if (!oldTree) { result.added.push(basePath || '.'); // Use '.' for root return result; } if (!newTree) { result.deleted.push(basePath || '.'); return result; } // If hashes are the same, trees are identical - quick exit if (oldTree.hash === newTree.hash) { return result; } // Handle primitive values if (oldTree.type !== 'object' && oldTree.type !== 'array' && newTree.type !== 'object' && newTree.type !== 'array') { result.modified.push(basePath || '.'); return result; } // Type changed (e.g., object to array) if (oldTree.type !== newTree.type) { result.modified.push(basePath || '.'); return result; } // Both are objects or arrays, compare children recursively const oldKeys = oldTree.children ? Object.keys(oldTree.children) : []; const newKeys = newTree.children ? Object.keys(newTree.children) : []; // Find deleted keys for (const key of oldKeys) { if (!newKeys.includes(key)) { const path = basePath ? `${basePath}.${key}` : key; result.deleted.push(path); } } // Find added and modified keys for (const key of newKeys) { const path = basePath ? `${basePath}.${key}` : key; if (!oldKeys.includes(key)) { result.added.push(path); } else if (oldTree.children[key].hash !== newTree.children[key].hash) { // Key exists in both but content differs, recurse const childDiff = MerkleTree.diff(oldTree.children[key], newTree.children[key], path); // If the child diff contains an exact modification of this path, // it means the whole subtree is different const hasDirectModification = childDiff.modified.includes(path); if (hasDirectModification) { result.modified.push(path); } else { // Otherwise, merge the child differences result.added = result.added.concat(childDiff.added); result.modified = result.modified.concat(childDiff.modified); result.deleted = result.deleted.concat(childDiff.deleted); } } } return result; } /** * Find the lowest common ancestor of multiple paths * @param {Array<string>} paths - Array of paths * @returns {string} Lowest common ancestor path */ static findLowestCommonAncestor(paths) { if (!paths || paths.length === 0) return ''; if (paths.length === 1) return paths[0]; // Split paths into segments const segments = paths.map(path => path.split('.')); // Find the minimum length const minLength = Math.min(...segments.map(s => s.length)); // Find common prefix let commonPrefix = []; for (let i = 0; i < minLength; i++) { const segment = segments[0][i]; if (segments.every(s => s[i] === segment)) { commonPrefix.push(segment); } else { break; } } return commonPrefix.join('.'); } /** * Store a Merkle tree in the storage layer * @param {Object} storage - Storage instance * @param {Object} tree - Merkle tree to store * @returns {Promise<string>} Root hash of the stored tree */ static async storeTree(storage, tree) { // For primitive types, store directly if (tree.type !== 'object' && tree.type !== 'array') { await storage.put({ type: tree.type, value: tree.value }, tree.hash); return tree.hash; } // For objects and arrays, store children recursively first const storedChildren = {}; for (const key of Object.keys(tree.children || {})) { storedChildren[key] = await MerkleTree.storeTree(storage, tree.children[key]); } // Store this node with references to children const node = { type: tree.type, children: storedChildren }; // Store directly with the hash as the key await storage.put(node, tree.hash); return tree.hash; } /** * Retrieve a Merkle tree from storage * @param {Object} storage - Storage instance * @param {string} hash - Root hash of the tree to retrieve * @returns {Promise<Object>} Retrieved Merkle tree */ static async retrieveTree(storage, hash) { if (!hash) return null; const node = await storage.get(hash); if (!node) { throw new Error(`Node with hash ${hash} not found in storage`); } // Leaf node with primitive value if (node.type !== 'object' && node.type !== 'array') { return { hash, type: node.type, value: node.value }; } // Internal node with children const children = {}; for (const key of Object.keys(node.children || {})) { const childHash = node.children[key]; children[key] = await MerkleTree.retrieveTree(storage, childHash); } return { hash, type: node.type, children }; } } module.exports = MerkleTree;