@stalkchain/grpc-pool
Version:
High-availability gRPC connection pooling module with active-active configuration, deduplication, and stale connection detection
165 lines • 5.86 kB
JavaScript
"use strict";
/**
* lib/deduplication.ts - Transaction signature deduplication service
*
* Implements LRU cache with time-based expiration for detecting duplicate
* transactions across multiple gRPC endpoints. Uses signature buffers as
* unique identifiers with automatic cleanup. Uses binary encoding for
* optimal performance and memory usage.
*
* @module lib/deduplication
* @author StalkChain Team
* @version 1.1.0
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.DeduplicationService = void 0;
const constants_1 = require("../constants");
/**
* Transaction deduplication service using LRU cache with TTL
*
* Efficiently tracks seen transaction signatures to prevent duplicate
* emissions when same transaction comes from multiple endpoints.
* Uses binary encoding for optimal performance and memory efficiency.
*/
class DeduplicationService {
constructor(options) {
this.cache = new Map();
this.cleanupInterval = null;
this.config = {
ttlMs: options?.deduplicationTtlMs ?? constants_1.DEFAULT_CONFIG.DEDUP_TTL_MS,
maxSignatures: options?.maxCacheSize ?? constants_1.DEFAULT_CONFIG.MAX_DEDUP_SIGNATURES
};
this.startCleanupInterval();
}
/**
* Check if signature has been seen before (is duplicate)
*
* Uses Buffer.toString('binary') for optimal performance and memory usage.
*
* ⚠️ IMPORTANT: toString('binary') produces strings with characters that have
* code points 0-255, which can include non-printable characters and high
* Unicode code points. This would be problematic for:
* - JSON serialization/deserialization
* - Network transmission
* - Database storage
* - Logging/debugging (hard to read)
* - Any external system interaction
*
* ✅ SAFE FOR OUR USE CASE because:
* - Only used internally as Map keys
* - Never serialized to JSON
* - Never sent over network
* - Never stored in database
* - Never logged (we use base58 for logging)
* - 34% faster than base64 (18.6M vs 13.9M ops/sec)
* - 27% less memory usage (64 vs 88 characters)
* - Perfect 1:1 mapping, no data loss
*
* @param signatureBuffer - Transaction signature as Buffer
* @returns true if duplicate, false if new/unique
*/
isDuplicate(signatureBuffer) {
if (!Buffer.isBuffer(signatureBuffer)) {
return false;
}
// Use binary encoding for optimal performance - see method documentation
// for why this is safe for internal Map keys but would be problematic elsewhere
const signatureBinary = signatureBuffer.toString('binary');
const now = Date.now();
// Check if signature exists and is still valid (within TTL)
const entry = this.cache.get(signatureBinary);
if (entry) {
const age = now - entry.timestamp;
if (age <= this.config.ttlMs) {
// Signature found and still valid - it's a duplicate
return true;
}
else {
// Signature expired - remove it and treat as new
this.cache.delete(signatureBinary);
}
}
// New signature - add to cache
this.addSignature(signatureBinary, now);
return false;
}
/**
* Add signature to cache with current timestamp
*/
addSignature(signatureBinary, timestamp) {
// Enforce size limit - remove oldest entries if at capacity
if (this.cache.size >= this.config.maxSignatures) {
this.removeOldestEntries(Math.floor(this.config.maxSignatures * 0.1)); // Remove 10%
}
// Add new entry
this.cache.set(signatureBinary, {
timestamp,
signatureBinary
});
}
/**
* Remove oldest entries from cache (LRU behavior)
*/
removeOldestEntries(count) {
const entries = Array.from(this.cache.entries());
// Sort by timestamp (oldest first)
entries.sort((a, b) => a[1].timestamp - b[1].timestamp);
// Remove oldest entries
for (let i = 0; i < Math.min(count, entries.length); i++) {
const entry = entries[i];
if (entry) {
this.cache.delete(entry[0]);
}
}
}
/**
* Start automatic cleanup interval to remove expired entries
*/
startCleanupInterval() {
// Clean up every 1 second
this.cleanupInterval = setInterval(() => {
this.cleanupExpiredEntries();
}, 1000);
}
/**
* Remove all expired entries from cache
*/
cleanupExpiredEntries() {
const now = Date.now();
const expiredKeys = [];
// Find expired entries
for (const [key, entry] of this.cache.entries()) {
if (entry && entry.timestamp) {
const age = now - entry.timestamp;
if (age > this.config.ttlMs) {
expiredKeys.push(key);
}
}
}
// Remove expired entries
expiredKeys.forEach(key => this.cache.delete(key));
// Silent cleanup - no logging needed for normal operation
}
/**
* Get current cache statistics for monitoring
*/
getStats() {
return {
size: this.cache.size,
maxSize: this.config.maxSignatures,
ttlMs: this.config.ttlMs
};
}
/**
* Clear all entries and stop cleanup interval
*/
destroy() {
if (this.cleanupInterval) {
clearInterval(this.cleanupInterval);
this.cleanupInterval = null;
}
this.cache.clear();
}
}
exports.DeduplicationService = DeduplicationService;
//# sourceMappingURL=deduplication.js.map