@bsv/sdk
Version:
BSV Blockchain Software Development Kit
155 lines • 7.21 kB
JavaScript
/**
* 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