UNPKG

@bsv/sdk

Version:

BSV Blockchain Software Development Kit

155 lines 7.21 kB
/** * Historian builds a chronological history (oldest → newest) of typed values by traversing * a transaction's input ancestry and interpreting each output with a provided interpreter. * * Core ideas: * - You provide an interpreter `(tx, outputIndex) => T | undefined` that decodes one output * into your domain value (e.g., kvstore entries). If it returns `undefined`, that output * contributes nothing to history. * - Traversal follows `Transaction.inputs[].sourceTransaction` recursively, so callers must * supply transactions whose inputs have `sourceTransaction` populated (e.g., via overlay * history reconstruction). * - The traversal visits each transaction once (cycle-safe) and collects interpreted values * in reverse-chronological order, then returns them as chronological (oldest-first). * - Optional caching support: provide a `historyCache` Map to cache complete history results * and avoid re-traversing identical transaction chains with the same context. * * Usage: * - Construct with an interpreter (and optional cache) * - Call historian.buildHistory(tx, context) to get an array of values representing the history of a token over time. * * Example: * const cache = new Map() // Optional: for caching repeated queries * const historian = new Historian(interpreter, { historyCache: cache }) * const history = await historian.buildHistory(tipTransaction, context) * // history: T[] (e.g., prior values for a protected kvstore key) * * Caching: * - Cache keys are generated from `interpreterVersion|txid|contextKey` * - Cached results are immutable snapshots to prevent external mutation * - Bump `interpreterVersion` when interpreter semantics change to invalidate old cache entries */ export class Historian { interpreter; debug; // --- minimal cache support --- historyCache; interpreterVersion; ctxKeyFn; /** * Create a new Historian instance * * @param interpreter - Function to interpret transaction outputs into typed values * @param options - Configuration options * @param options.debug - Enable debug logging (default: false) * @param options.historyCache - Optional external cache for complete history results * @param options.interpreterVersion - Version identifier for cache invalidation (default: 'v1') * @param options.ctxKeyFn - Custom function to serialize context for cache keys (default: JSON.stringify) */ constructor(interpreter, options) { this.interpreter = interpreter; this.debug = options?.debug ?? false; // Configure caching (all optional) this.historyCache = options?.historyCache; this.interpreterVersion = options?.interpreterVersion ?? 'v1'; this.ctxKeyFn = options?.ctxKeyFn ?? ((ctx) => { try { return JSON.stringify(ctx ?? null); } catch { return ''; } }); } historyKey(startTransaction, context) { const txid = startTransaction.id('hex'); const ctxKey = this.ctxKeyFn(context); return `${this.interpreterVersion}|${txid}|${ctxKey}`; } /** * Build history by traversing input chain from a starting transaction * Returns values in chronological order (oldest first) * * If caching is enabled, will first check for cached results matching the * startTransaction and context. On cache miss, performs full traversal and * stores the result for future queries. * * @param startTransaction - The transaction to start traversal from * @param context - The context to pass to the interpreter * @returns Array of interpreted values in chronological order */ async buildHistory(startTransaction, context) { // --- minimal cache fast path --- if (this.historyCache != null) { const cacheKey = this.historyKey(startTransaction, context); if (this.historyCache.has(cacheKey)) { const cached = this.historyCache.get(cacheKey); if (cached != null) { if (this.debug) console.log('[Historian] History cache hit:', cacheKey); // Return a shallow copy to avoid external mutation of the cached array return cached.slice(); } } } const history = []; const visited = new Set(); // Recursively traverse input transactions to build history const traverseHistory = async (transaction) => { const txid = transaction.id('hex'); // Prevent infinite loops if (visited.has(txid)) { if (this.debug) { console.log(`[Historian] Skipping already visited transaction: ${txid}`); } return; } visited.add(txid); if (this.debug) { console.log(`[Historian] Processing transaction: ${txid}`); } // Check all outputs in this transaction for interpretable values for (let outputIndex = 0; outputIndex < transaction.outputs.length; outputIndex++) { try { // Try to interpret this output const interpretedValue = await Promise.resolve(this.interpreter(transaction, outputIndex, context)); if (interpretedValue !== undefined) { history.push(interpretedValue); if (this.debug) { console.log('[Historian] Added value to history:', interpretedValue); } } } catch (error) { if (this.debug) { console.log(`[Historian] Failed to interpret output ${outputIndex}:`, error); } // Skip outputs that can't be interpreted } } // Recursively traverse input transactions for (const input of transaction.inputs) { if (input.sourceTransaction != null) { await traverseHistory(input.sourceTransaction); } else if (this.debug) { console.log('[Historian] Input missing sourceTransaction, skipping'); } } }; // Start traversal from the provided transaction await traverseHistory(startTransaction); // History is built in reverse chronological order during traversal, // so we reverse it to return oldest-first const chronological = history.reverse(); if (this.historyCache != null) { const cacheKey = this.historyKey(startTransaction, context); // Store an immutable snapshot to avoid accidental external mutation this.historyCache.set(cacheKey, Object.freeze(chronological.slice())); if (this.debug) console.log('[Historian] History cached:', cacheKey); } return chronological; } } //# sourceMappingURL=Historian.js.map