Files
bitvid/js/services/relayHealthService.js
google-labs-jules[bot]andPR0M3TH3AN c47392aba6 Refactor numeric constants to centralized module
- Introduce `js/constants.js` with semantic time constants (e.g., `ONE_MINUTE_MS`, `STANDARD_TIMEOUT_MS`) and feature-specific TTLs.
- Export `KIND_MUTE_LIST` from `js/nostrEventSchemas.js` to standardize NIP-51 mute list kind usage.
- Refactor `js/state/cache.js`, `js/nostr/cachePolicies.js`, `js/channelProfile.js`, and various services to use centralized constants instead of hardcoded magic numbers.
- Update `js/nostr/relayConstants.js` to derive values from the shared time constants.
- Replace duplicate definitions of timeouts and retry delays across the codebase.

Co-authored-by: PR0M3TH3AN <[email protected]>
2026-02-11 03:27:26 +00:00

277 lines
7.2 KiB
JavaScript

import { devLogger, userLogger } from "../utils/logger.js";
import { FIVE_MINUTES_MS, SHORT_TIMEOUT_MS, LONG_TIMEOUT_MS } from "../constants.js";
const TELEMETRY_STORAGE_KEY = "bitvid:relay-health-telemetry-opt-in";
const PERSISTENT_FAILURE_THRESHOLD = 3;
const USER_LOG_COOLDOWN_MS = FIVE_MINUTES_MS;
const DEFAULT_TIMEOUT_MS = SHORT_TIMEOUT_MS;
function resolveLogger(logger) {
if (logger && logger.dev && logger.user) {
return logger;
}
return { dev: devLogger, user: userLogger };
}
function nowMs() {
return typeof performance !== "undefined" && performance.now
? performance.now()
: Date.now();
}
function readTelemetryOptIn() {
if (typeof window === "undefined") {
return false;
}
try {
const stored = window.localStorage.getItem(TELEMETRY_STORAGE_KEY);
return stored === "true";
} catch (error) {
userLogger.warn(
"[relayHealth] Failed to read telemetry preference from storage.",
error,
);
return false;
}
}
function writeTelemetryOptIn(nextValue) {
if (typeof window === "undefined") {
return;
}
try {
window.localStorage.setItem(
TELEMETRY_STORAGE_KEY,
nextValue ? "true" : "false",
);
} catch (error) {
userLogger.warn(
"[relayHealth] Failed to persist telemetry preference.",
error,
);
}
}
class RelayHealthService {
constructor({ relayManager, nostrClient, logger, telemetryEmitter } = {}) {
this.relayManager = relayManager || null;
this.nostrClient = nostrClient || null;
this.logger = resolveLogger(logger);
this.telemetryEmitter =
typeof telemetryEmitter === "function" ? telemetryEmitter : null;
this.relayStates = new Map();
this.attachedRelays = new Set();
this.cachedTelemetryOptIn = readTelemetryOptIn();
}
getTelemetryOptIn() {
return this.cachedTelemetryOptIn;
}
setTelemetryOptIn(value) {
const normalized = Boolean(value);
this.cachedTelemetryOptIn = normalized;
writeTelemetryOptIn(normalized);
return normalized;
}
getRelayUrls() {
if (!this.relayManager || typeof this.relayManager.getEntries !== "function") {
return [];
}
const entries = this.relayManager.getEntries();
return entries
.map((entry) => (typeof entry?.url === "string" ? entry.url.trim() : ""))
.filter(Boolean);
}
ensureRelayState(relayUrl) {
if (!this.relayStates.has(relayUrl)) {
this.relayStates.set(relayUrl, {
url: relayUrl,
connected: false,
lastLatencyMs: null,
errorCount: 0,
consecutiveFailures: 0,
lastCheckedAt: null,
lastErrorAt: null,
lastUserLogAt: null,
});
}
return this.relayStates.get(relayUrl);
}
attachRelayListeners(relayUrl, relay) {
if (!relay || typeof relay.on !== "function") {
return;
}
if (this.attachedRelays.has(relayUrl)) {
return;
}
try {
relay.on("connect", () => {
this.recordRelaySuccess(relayUrl, null);
});
relay.on("disconnect", () => {
this.recordRelayFailure(relayUrl, new Error("relay-disconnect"));
});
relay.on("error", (error) => {
this.recordRelayFailure(relayUrl, error);
});
this.attachedRelays.add(relayUrl);
} catch (error) {
this.logger.dev.warn(
"[relayHealth] Failed to attach relay listeners.",
error,
);
}
}
recordRelaySuccess(relayUrl, latencyMs) {
const state = this.ensureRelayState(relayUrl);
state.connected = true;
state.lastCheckedAt = Date.now();
state.consecutiveFailures = 0;
if (Number.isFinite(latencyMs)) {
state.lastLatencyMs = Math.max(0, Math.round(latencyMs));
}
if (this.nostrClient && typeof this.nostrClient.clearRelayBackoff === "function") {
this.nostrClient.clearRelayBackoff(relayUrl);
}
}
recordRelayFailure(relayUrl, error) {
const state = this.ensureRelayState(relayUrl);
const now = Date.now();
state.connected = false;
state.errorCount += 1;
state.consecutiveFailures += 1;
state.lastCheckedAt = now;
state.lastErrorAt = now;
if (this.nostrClient && typeof this.nostrClient.markRelayUnreachable === "function") {
this.nostrClient.markRelayUnreachable(relayUrl, LONG_TIMEOUT_MS, {
reason: "relay-health-failed",
});
}
this.logger.dev.warn("[relayHealth] Relay check failed.", {
relayUrl,
error,
});
if (
state.consecutiveFailures >= PERSISTENT_FAILURE_THRESHOLD &&
(!state.lastUserLogAt || now - state.lastUserLogAt >= USER_LOG_COOLDOWN_MS)
) {
state.lastUserLogAt = now;
this.logger.user.warn("[relayHealth] Relay unreachable after repeated failures.", {
relayUrl,
errorCount: state.consecutiveFailures,
lastError: error,
});
}
}
async checkRelay(relayUrl) {
if (!relayUrl) {
return;
}
if (!this.nostrClient) {
this.recordRelayFailure(relayUrl, new Error("nostr-client-unavailable"));
return;
}
try {
if (typeof this.nostrClient.ensurePool === "function") {
await this.nostrClient.ensurePool();
}
} catch (error) {
this.recordRelayFailure(relayUrl, error);
return;
}
if (!this.nostrClient.pool || typeof this.nostrClient.pool.ensureRelay !== "function") {
this.recordRelayFailure(relayUrl, new Error("relay-pool-unavailable"));
return;
}
const start = nowMs();
try {
const relay = await Promise.race([
this.nostrClient.pool.ensureRelay(relayUrl),
new Promise((_, reject) =>
setTimeout(
() => reject(new Error("relay-connection-timeout")),
DEFAULT_TIMEOUT_MS,
),
),
]);
if (!relay) {
throw new Error("relay-unavailable");
}
this.attachRelayListeners(relayUrl, relay);
this.recordRelaySuccess(relayUrl, nowMs() - start);
} catch (error) {
this.recordRelayFailure(relayUrl, error);
}
}
getSnapshot(relayUrls = null) {
const urls = Array.isArray(relayUrls) ? relayUrls : this.getRelayUrls();
return urls.map((url) => ({ ...this.ensureRelayState(url) }));
}
async refresh(relayUrls = null) {
const urls = Array.isArray(relayUrls) ? relayUrls : this.getRelayUrls();
if (!urls.length) {
return [];
}
await Promise.allSettled(urls.map((url) => this.checkRelay(url)));
const snapshot = this.getSnapshot(urls);
this.emitTelemetry(snapshot);
return snapshot;
}
emitTelemetry(snapshot) {
if (!this.telemetryEmitter || !this.getTelemetryOptIn()) {
return;
}
const payload = {
sampledAt: Date.now(),
relays: Array.isArray(snapshot)
? snapshot.map((entry) => ({
url: entry.url,
connected: Boolean(entry.connected),
latencyMs:
Number.isFinite(entry.lastLatencyMs) ? entry.lastLatencyMs : null,
errorCount: Number.isFinite(entry.errorCount)
? entry.errorCount
: 0,
}))
: [],
};
try {
this.telemetryEmitter("relay_health_snapshot", payload);
} catch (error) {
this.logger.user.warn(
"[relayHealth] Failed to emit relay health telemetry.",
error,
);
}
}
}
export default RelayHealthService;