mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-01 10:22:25 +00:00
Merge pull request #1982 from PR0M3TH3AN/codex/implement-relay-sanitization-and-health-filtering
Improve relay sanitization and health filtering
This commit is contained in:
6 files changed
+76
-26
No files matched your search
@@ -275,6 +275,7 @@ class Application {
|
||||
this.dmRelayHints = new Map();
|
||||
this.authLoadingState = { profile: "idle", lists: "idle", dms: "idle" };
|
||||
this.relayUiRefreshTimeout = null;
|
||||
this.lastRelayHealthWarningAt = 0;
|
||||
this.activeIntervals = [];
|
||||
this.torrentStatusIntervalId = null;
|
||||
this.torrentStatusNodes = null;
|
||||
@@ -6567,6 +6568,35 @@ class Application {
|
||||
});
|
||||
}
|
||||
|
||||
checkRelayHealthWarning() {
|
||||
if (!nostrClient || typeof nostrClient.getHealthyRelays !== "function") {
|
||||
return false;
|
||||
}
|
||||
|
||||
const relayCandidates = Array.isArray(nostrClient.relays) ? nostrClient.relays : [];
|
||||
if (!relayCandidates.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const healthyRelays = nostrClient.getHealthyRelays(relayCandidates);
|
||||
if (healthyRelays.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const cooldownMs = 30000;
|
||||
if (now - (this.lastRelayHealthWarningAt || 0) < cooldownMs) {
|
||||
return true;
|
||||
}
|
||||
|
||||
this.lastRelayHealthWarningAt = now;
|
||||
this.showStatus(
|
||||
"All configured relays are unhealthy. Data may be missing until a relay reconnects.",
|
||||
{ autoHideMs: 12000, showSpinner: false },
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to videos (older + new) and render them as they come in.
|
||||
*/
|
||||
@@ -6586,6 +6616,7 @@ class Application {
|
||||
this.loadVideosPromise = (async () => {
|
||||
devLogger.log("Starting loadVideos... (forceFetch =", forceFetch, ")");
|
||||
this.setFeedTelemetryContext("recent");
|
||||
this.checkRelayHealthWarning();
|
||||
|
||||
const container = this.mountVideoListView();
|
||||
const hasCachedVideos =
|
||||
@@ -6641,6 +6672,7 @@ class Application {
|
||||
async loadForYouVideos(forceFetch = false) {
|
||||
devLogger.log("Starting loadForYouVideos... (forceFetch =", forceFetch, ")");
|
||||
this.setFeedTelemetryContext("for-you");
|
||||
this.checkRelayHealthWarning();
|
||||
|
||||
const container = this.mountVideoListView({ includeTags: false });
|
||||
const hasCachedVideos =
|
||||
|
||||
+2
-7
@@ -1,6 +1,7 @@
|
||||
import Application from "./app.js";
|
||||
import { nostrClient } from "./nostrClientFacade.js";
|
||||
import { convertEventToVideo, getDTagValueFromTags } from "./nostr/index.js";
|
||||
import { sanitizeRelayList } from "./nostr/nip46Client.js";
|
||||
import { resolveVideoPointer } from "./utils/videoPointer.js";
|
||||
import { devLogger, userLogger } from "./utils/logger.js";
|
||||
import { nostrToolsReady } from "./nostrToolsBootstrap.js";
|
||||
@@ -180,13 +181,7 @@ const buildRelayList = (relays) => {
|
||||
...(Array.isArray(nostrClient.relays) ? nostrClient.relays : []),
|
||||
];
|
||||
|
||||
const result = Array.from(
|
||||
new Set(
|
||||
combined
|
||||
.map((relay) => (typeof relay === "string" ? relay.trim() : ""))
|
||||
.filter(Boolean)
|
||||
)
|
||||
);
|
||||
const result = sanitizeRelayList(combined);
|
||||
diag?.emit("relay-list", { relays: result });
|
||||
return result;
|
||||
};
|
||||
|
||||
+30
-3
@@ -415,6 +415,8 @@ const RELAY_RECONNECT_MAX_DELAY_MS = 60000;
|
||||
const RELAY_RECONNECT_MAX_ATTEMPTS = 5;
|
||||
const RELAY_CIRCUIT_BREAKER_THRESHOLD = 3;
|
||||
const RELAY_CIRCUIT_BREAKER_COOLDOWN_MS = 10 * 60 * 1000;
|
||||
const RELAY_FAILURE_WINDOW_MS = 5 * 60 * 1000;
|
||||
const RELAY_FAILURE_WINDOW_THRESHOLD = 3;
|
||||
const EVENTS_CACHE_STORAGE_KEY = "bitvid:eventsCache:v1";
|
||||
// We use the policy TTL, but currently the storage backend is hardcoded to IDB (with localStorage fallback).
|
||||
// Future refactors should make EventsCacheStore dynamic based on CACHE_POLICIES[NOTE_TYPES.VIDEO_POST].storage.
|
||||
@@ -1294,6 +1296,7 @@ export class NostrClient {
|
||||
this.unreachableRelays = new Set();
|
||||
this.relayBackoff = new Map();
|
||||
this.relayFailureCounts = new Map();
|
||||
this.relayFailureWindows = new Map();
|
||||
this.relayCircuitBreakers = new Map();
|
||||
this.relayReconnectTimer = null;
|
||||
this.relayReconnectAttempt = 0;
|
||||
@@ -4578,10 +4581,30 @@ export class NostrClient {
|
||||
}
|
||||
this.relayBackoff.delete(normalized);
|
||||
this.relayFailureCounts.delete(normalized);
|
||||
this.relayFailureWindows.delete(normalized);
|
||||
this.unreachableRelays.delete(normalized);
|
||||
this.relayCircuitBreakers.delete(normalized);
|
||||
}
|
||||
|
||||
recordRelayFailureWindow(url) {
|
||||
if (!url || typeof url !== "string") {
|
||||
return 0;
|
||||
}
|
||||
const normalized = url.trim();
|
||||
if (!normalized) {
|
||||
return 0;
|
||||
}
|
||||
const now = Date.now();
|
||||
const cutoff = now - RELAY_FAILURE_WINDOW_MS;
|
||||
const history = this.relayFailureWindows.get(normalized) || [];
|
||||
const filtered = history.filter(
|
||||
(timestamp) => Number.isFinite(timestamp) && timestamp >= cutoff,
|
||||
);
|
||||
filtered.push(now);
|
||||
this.relayFailureWindows.set(normalized, filtered);
|
||||
return filtered.length;
|
||||
}
|
||||
|
||||
markRelayUnreachable(url, ttlMs = 60000, { reason = null } = {}) {
|
||||
if (!url || typeof url !== "string") {
|
||||
return;
|
||||
@@ -4590,6 +4613,7 @@ export class NostrClient {
|
||||
if (!normalized) {
|
||||
return;
|
||||
}
|
||||
const windowFailureCount = this.recordRelayFailureWindow(normalized);
|
||||
const nextFailureCount = (this.relayFailureCounts.get(normalized) || 0) + 1;
|
||||
this.relayFailureCounts.set(normalized, nextFailureCount);
|
||||
const backoffMs = this.resolveRelayBackoffMs(nextFailureCount, ttlMs);
|
||||
@@ -4601,7 +4625,7 @@ export class NostrClient {
|
||||
reason,
|
||||
});
|
||||
this.unreachableRelays.add(normalized);
|
||||
if (nextFailureCount >= RELAY_CIRCUIT_BREAKER_THRESHOLD) {
|
||||
if (windowFailureCount >= RELAY_FAILURE_WINDOW_THRESHOLD) {
|
||||
const now = Date.now();
|
||||
const openUntil = now + RELAY_CIRCUIT_BREAKER_COOLDOWN_MS;
|
||||
const existing = this.relayCircuitBreakers.get(normalized);
|
||||
@@ -4609,17 +4633,18 @@ export class NostrClient {
|
||||
existing && Number.isFinite(existing.openUntil)
|
||||
? Math.max(existing.openUntil, openUntil)
|
||||
: openUntil;
|
||||
const circuitReason = reason || "windowed-failures";
|
||||
this.relayCircuitBreakers.set(normalized, {
|
||||
openUntil: nextOpenUntil,
|
||||
failureCount: nextFailureCount,
|
||||
reason,
|
||||
reason: circuitReason,
|
||||
});
|
||||
if (isDevMode) {
|
||||
devLogger.debug("[nostr] Circuit breaker opened for relay.", {
|
||||
relay: normalized,
|
||||
openUntil: nextOpenUntil,
|
||||
failureCount: nextFailureCount,
|
||||
reason,
|
||||
reason: circuitReason,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -4644,6 +4669,7 @@ export class NostrClient {
|
||||
}
|
||||
this.relayCircuitBreakers.delete(url);
|
||||
this.relayFailureCounts.delete(url);
|
||||
this.relayFailureWindows.delete(url);
|
||||
this.unreachableRelays.delete(url);
|
||||
this.relayBackoff.delete(url);
|
||||
if (isDevMode) {
|
||||
@@ -4662,6 +4688,7 @@ export class NostrClient {
|
||||
}
|
||||
this.relayBackoff.delete(url);
|
||||
this.relayFailureCounts.delete(url);
|
||||
this.relayFailureWindows.delete(url);
|
||||
this.unreachableRelays.delete(url);
|
||||
if (isDevMode) {
|
||||
devLogger.debug(`[nostr] Relay backoff expired for ${url}.`);
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
assertAnyRelayAccepted,
|
||||
} from "../nostrPublish.js";
|
||||
import { userLogger, devLogger } from "../utils/logger.js";
|
||||
import { sanitizeRelayList } from "../nostr/nip46Client.js";
|
||||
import { normalizeHashtag } from "../utils/hashtagNormalization.js";
|
||||
import { profileCache } from "../state/profileCache.js";
|
||||
import { DEFAULT_RELAY_URLS } from "../nostr/toolkit.js";
|
||||
@@ -98,14 +99,6 @@ function normalizeHexPubkey(pubkey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function sanitizeRelayList(candidate) {
|
||||
return Array.isArray(candidate)
|
||||
? candidate
|
||||
.map((url) => (typeof url === "string" ? url.trim() : ""))
|
||||
.filter(Boolean)
|
||||
: [];
|
||||
}
|
||||
|
||||
function normalizeEncryptionToken(value) {
|
||||
if (typeof value !== "string") {
|
||||
return "";
|
||||
|
||||
@@ -132,6 +132,10 @@ class RelayHealthService {
|
||||
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) {
|
||||
@@ -141,6 +145,12 @@ class RelayHealthService {
|
||||
state.lastCheckedAt = Date.now();
|
||||
state.lastErrorAt = Date.now();
|
||||
|
||||
if (this.nostrClient && typeof this.nostrClient.markRelayUnreachable === "function") {
|
||||
this.nostrClient.markRelayUnreachable(relayUrl, 60000, {
|
||||
reason: "relay-health-failed",
|
||||
});
|
||||
}
|
||||
|
||||
this.logger.dev.warn("[relayHealth] Relay check failed.", {
|
||||
relayUrl,
|
||||
error,
|
||||
|
||||
+1
-8
@@ -5,7 +5,7 @@ import {
|
||||
requestDefaultExtensionPermissions,
|
||||
} from "./nostrClientFacade.js";
|
||||
import { convertEventToVideo as sharedConvertEventToVideo } from "./nostr/index.js";
|
||||
import { normalizeNostrPubkey } from "./nostr/nip46Client.js";
|
||||
import { normalizeNostrPubkey, sanitizeRelayList } from "./nostr/nip46Client.js";
|
||||
import {
|
||||
listVideoViewEvents,
|
||||
subscribeVideoViewEvents,
|
||||
@@ -1229,13 +1229,6 @@ class SubscriptionsManager {
|
||||
throw signErr;
|
||||
}
|
||||
|
||||
const sanitizeRelayList = (candidate) =>
|
||||
Array.isArray(candidate)
|
||||
? candidate
|
||||
.map((url) => (typeof url === "string" ? url.trim() : ""))
|
||||
.filter(Boolean)
|
||||
: [];
|
||||
|
||||
const writeRelays = sanitizeRelayList(nostrClient.writeRelays);
|
||||
const fallbackRelays = writeRelays.length
|
||||
? writeRelays
|
||||
|
||||
Reference in new issue
Block a user