@bsv/sdk
Version:
BSV Blockchain Software Development Kit
213 lines • 7.67 kB
JavaScript
const DEFAULT_LATENCY_MS = 1500;
const LATENCY_SMOOTHING_FACTOR = 0.25;
const BASE_BACKOFF_MS = 1000;
const MAX_BACKOFF_MS = 60_000;
const FAILURE_PENALTY_MS = 400;
const SUCCESS_BONUS_MS = 30;
const FAILURE_BACKOFF_GRACE = 2;
const STORAGE_KEY = 'bsvsdk_overlay_host_reputation_v1';
export class HostReputationTracker {
stats;
store;
constructor(store) {
this.stats = new Map();
this.store = store ?? this.getLocalStorageAdapter();
this.loadFromStorage();
}
reset() {
this.stats.clear();
}
recordSuccess(host, latencyMs) {
const entry = this.getOrCreate(host);
const now = Date.now();
const safeLatency = Number.isFinite(latencyMs) && latencyMs >= 0 ? latencyMs : DEFAULT_LATENCY_MS;
if (entry.avgLatencyMs === null) {
entry.avgLatencyMs = safeLatency;
}
else {
entry.avgLatencyMs =
(1 - LATENCY_SMOOTHING_FACTOR) * entry.avgLatencyMs +
LATENCY_SMOOTHING_FACTOR * safeLatency;
}
entry.lastLatencyMs = safeLatency;
entry.totalSuccesses += 1;
entry.consecutiveFailures = 0;
entry.backoffUntil = 0;
entry.lastUpdatedAt = now;
entry.lastError = undefined;
this.saveToStorage();
}
recordFailure(host, reason) {
const entry = this.getOrCreate(host);
const now = Date.now();
entry.totalFailures += 1;
entry.consecutiveFailures += 1;
const msg = typeof reason === 'string'
? reason
: reason instanceof Error
? reason.message
: undefined;
const immediate = typeof msg === 'string' &&
(msg.includes('ERR_NAME_NOT_RESOLVED') ||
msg.includes('ENOTFOUND') ||
msg.includes('getaddrinfo') ||
msg.includes('Failed to fetch'));
if (immediate && entry.consecutiveFailures < FAILURE_BACKOFF_GRACE + 1) {
entry.consecutiveFailures = FAILURE_BACKOFF_GRACE + 1;
}
const penaltyLevel = Math.max(entry.consecutiveFailures - FAILURE_BACKOFF_GRACE, 0);
if (penaltyLevel === 0) {
entry.backoffUntil = 0;
}
else {
const backoffDuration = Math.min(MAX_BACKOFF_MS, BASE_BACKOFF_MS * Math.pow(2, penaltyLevel - 1));
entry.backoffUntil = now + backoffDuration;
}
entry.lastUpdatedAt = now;
entry.lastError =
typeof reason === 'string'
? reason
: reason instanceof Error
? reason.message
: undefined;
this.saveToStorage();
}
rankHosts(hosts, now = Date.now()) {
const seen = new Map();
hosts.forEach((host, idx) => {
if (typeof host !== 'string' || host.length === 0)
return;
if (!seen.has(host))
seen.set(host, idx);
});
const orderedHosts = Array.from(seen.keys());
const ranked = orderedHosts.map((host) => {
const entry = this.getOrCreate(host);
return {
...entry,
score: this.computeScore(entry, now),
originalOrder: seen.get(host) ?? 0
};
});
ranked.sort((a, b) => {
const aInBackoff = a.backoffUntil > now;
const bInBackoff = b.backoffUntil > now;
if (aInBackoff !== bInBackoff)
return aInBackoff ? 1 : -1;
if (a.score !== b.score)
return a.score - b.score;
if (a.totalSuccesses !== b.totalSuccesses)
return b.totalSuccesses - a.totalSuccesses;
return a.originalOrder - b.originalOrder;
});
return ranked.map(({ originalOrder, ...rest }) => rest);
}
snapshot(host) {
const entry = this.stats.get(host);
return entry != null ? { ...entry } : undefined;
}
getStorage() {
try {
const g = typeof globalThis === 'object' ? globalThis : undefined;
if (g == null || g.localStorage == null)
return undefined;
return g.localStorage;
}
catch {
return undefined;
}
}
getLocalStorageAdapter() {
const s = this.getStorage();
if (s == null)
return undefined;
return {
get: (key) => {
try {
return s.getItem(key);
}
catch {
return null;
}
},
set: (key, value) => {
try {
s.setItem(key, value);
}
catch { }
}
};
}
loadFromStorage() {
const s = this.store;
if (s == null)
return;
try {
const raw = s.get(STORAGE_KEY);
if (typeof raw !== 'string' || raw.length === 0)
return;
const data = JSON.parse(raw);
if (typeof data !== 'object' || data === null)
return;
this.stats.clear();
for (const k of Object.keys(data)) {
const v = (data)[k];
if (v != null && typeof v === 'object') {
const entry = {
host: String(v.host ?? k),
totalSuccesses: Number(v.totalSuccesses ?? 0),
totalFailures: Number(v.totalFailures ?? 0),
consecutiveFailures: Number(v.consecutiveFailures ?? 0),
avgLatencyMs: v.avgLatencyMs == null ? null : Number(v.avgLatencyMs),
lastLatencyMs: v.lastLatencyMs == null ? null : Number(v.lastLatencyMs),
backoffUntil: Number(v.backoffUntil ?? 0),
lastUpdatedAt: Number(v.lastUpdatedAt ?? 0),
lastError: typeof v.lastError === 'string' ? v.lastError : undefined
};
this.stats.set(entry.host, entry);
}
}
}
catch { }
}
saveToStorage() {
const s = this.store;
if (s == null)
return;
try {
const obj = {};
for (const [host, entry] of this.stats.entries()) {
obj[host] = entry;
}
s.set(STORAGE_KEY, JSON.stringify(obj));
}
catch { }
}
computeScore(entry, now) {
const latency = entry.avgLatencyMs ?? DEFAULT_LATENCY_MS;
const failurePenalty = entry.consecutiveFailures * FAILURE_PENALTY_MS;
const successBonus = Math.min(entry.totalSuccesses * SUCCESS_BONUS_MS, latency / 2);
const backoffPenalty = entry.backoffUntil > now ? entry.backoffUntil - now : 0;
return latency + failurePenalty + backoffPenalty - successBonus;
}
getOrCreate(host) {
let entry = this.stats.get(host);
if (entry == null) {
entry = {
host,
totalSuccesses: 0,
totalFailures: 0,
consecutiveFailures: 0,
avgLatencyMs: null,
lastLatencyMs: null,
backoffUntil: 0,
lastUpdatedAt: 0
};
this.stats.set(host, entry);
}
return entry;
}
}
const globalTracker = new HostReputationTracker();
export const getOverlayHostReputationTracker = () => globalTracker;
//# sourceMappingURL=HostReputationTracker.js.map