mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
fix(core): optimize moderation subscriptions and reduce log noise (agent)
- Refactor `ModerationService` to use batched subscriptions for trusted mute lists, reducing relay load. - Fix `WebTorrent` memory leak by setting `setMaxListeners(0)` on client instances. - Suppress "Failed to connect to relay" warnings in `countDiagnostics.js`. - Degrade benign DM sync errors (e.g. "no-decryptors") to info/debug logs in `profileModalController.js`. Co-authored-by: PR0M3TH3AN <[email protected]>
This commit is contained in:
1 parent
b38517b4f2
commit
fd4318c2f9
4 files changed
+129
-149
No files matched your search
@@ -43,6 +43,13 @@ export function logCountTimeoutCleanupFailure(error) {
|
||||
export function logRelayCountFailure(url, error) {
|
||||
const normalizedUrl =
|
||||
typeof url === "string" && url.trim() ? url.trim() : "(unknown relay)";
|
||||
|
||||
const errorMessage = error && typeof error.message === "string" ? error.message : String(error);
|
||||
if (errorMessage.includes("Failed to connect to relay")) {
|
||||
// Suppress connection failures from warning logs to reduce noise
|
||||
return;
|
||||
}
|
||||
|
||||
logCountWarning(`[nostr] COUNT request failed on ${normalizedUrl}:`, [error], {
|
||||
key: `relay:${normalizedUrl}`,
|
||||
});
|
||||
|
||||
+100
-145
@@ -537,9 +537,12 @@ export class ModerationService {
|
||||
|
||||
this.trustedMuteLists = new Map();
|
||||
this.trustedMutedAuthors = new Map();
|
||||
this.trustedMuteSubscriptions = new Map();
|
||||
this.trustedMuteSubscriptions = new Map(); // Kept for legacy/individual manual subs if needed, but primary logic is now batched
|
||||
this.trustedSeedOnly = false;
|
||||
|
||||
this.batchedMuteSubscriptions = [];
|
||||
this.muteRefreshTimer = null;
|
||||
|
||||
this.emitter = new SimpleEventEmitter((message, error) => {
|
||||
try {
|
||||
this.log(message, error);
|
||||
@@ -1011,6 +1014,7 @@ export class ModerationService {
|
||||
clearTrustedMuteTracking({ previousContacts = null } = {}) {
|
||||
void previousContacts;
|
||||
|
||||
// Clear legacy individual subscriptions
|
||||
for (const [pubkey, entry] of this.trustedMuteSubscriptions.entries()) {
|
||||
if (entry && typeof entry.unsub === "function") {
|
||||
try {
|
||||
@@ -1020,9 +1024,16 @@ export class ModerationService {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.trustedMuteSubscriptions.clear();
|
||||
|
||||
// Clear batched subscriptions
|
||||
this.teardownBatchedMuteSubscriptions();
|
||||
|
||||
if (this.muteRefreshTimer) {
|
||||
clearTimeout(this.muteRefreshTimer);
|
||||
this.muteRefreshTimer = null;
|
||||
}
|
||||
|
||||
for (const [author, aggregate] of this.trustedMutedAuthors.entries()) {
|
||||
if (aggregate && aggregate.muters instanceof Map) {
|
||||
aggregate.muters.clear();
|
||||
@@ -1041,6 +1052,21 @@ export class ModerationService {
|
||||
this.rebuildTrustedContacts(new Set(), { previous: emptyPrevious });
|
||||
}
|
||||
|
||||
teardownBatchedMuteSubscriptions() {
|
||||
if (Array.isArray(this.batchedMuteSubscriptions)) {
|
||||
for (const sub of this.batchedMuteSubscriptions) {
|
||||
if (sub && typeof sub.unsub === "function") {
|
||||
try {
|
||||
sub.unsub();
|
||||
} catch (error) {
|
||||
this.log("[moderationService] failed to unsub batched subscription", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this.batchedMuteSubscriptions = [];
|
||||
}
|
||||
|
||||
isTrustedMuteOwner(pubkey) {
|
||||
const normalized = normalizeHex(pubkey);
|
||||
if (!normalized) {
|
||||
@@ -1053,37 +1079,78 @@ export class ModerationService {
|
||||
}
|
||||
|
||||
reconcileTrustedMuteSubscriptions(previousSet, nextSet) {
|
||||
const previous = previousSet instanceof Set ? new Set(previousSet) : new Set();
|
||||
const next = nextSet instanceof Set ? new Set(nextSet) : new Set();
|
||||
this.scheduleTrustedMuteSubscriptionRefresh();
|
||||
}
|
||||
|
||||
this.log("[moderationService] reconcileTrustedMuteSubscriptions", {
|
||||
previous: previous.size,
|
||||
next: next.size,
|
||||
});
|
||||
|
||||
if (this.viewerPubkey) {
|
||||
if (this.trustedMuteSubscriptions.has(this.viewerPubkey)) {
|
||||
previous.add(this.viewerPubkey);
|
||||
}
|
||||
next.add(this.viewerPubkey);
|
||||
scheduleTrustedMuteSubscriptionRefresh() {
|
||||
if (this.muteRefreshTimer) {
|
||||
clearTimeout(this.muteRefreshTimer);
|
||||
}
|
||||
|
||||
for (const value of previous) {
|
||||
if (!next.has(value)) {
|
||||
this.teardownTrustedMuteSubscription(value);
|
||||
}
|
||||
}
|
||||
|
||||
for (const value of next) {
|
||||
if (previous.has(value)) {
|
||||
continue;
|
||||
}
|
||||
this.subscribeToTrustedMuteList(value).catch((error) => {
|
||||
this.log(
|
||||
`(moderationService) failed to subscribe to trusted mute list for ${value}`,
|
||||
error,
|
||||
);
|
||||
this.muteRefreshTimer = setTimeout(() => {
|
||||
this.refreshTrustedMuteSubscriptions().catch((error) => {
|
||||
this.log("[moderationService] refreshTrustedMuteSubscriptions failed", error);
|
||||
});
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
async refreshTrustedMuteSubscriptions() {
|
||||
this.muteRefreshTimer = null;
|
||||
|
||||
const targets = new Set();
|
||||
if (this.trustedContacts instanceof Set) {
|
||||
for (const contact of this.trustedContacts) {
|
||||
targets.add(contact);
|
||||
}
|
||||
}
|
||||
if (this.viewerPubkey) {
|
||||
targets.add(this.viewerPubkey);
|
||||
}
|
||||
|
||||
if (targets.size === 0) {
|
||||
this.teardownBatchedMuteSubscriptions();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.ensurePool();
|
||||
} catch (error) {
|
||||
this.log("[moderationService] ensurePool failed during mute refresh", error);
|
||||
return;
|
||||
}
|
||||
|
||||
const relays = resolveRelayList(this.nostrClient);
|
||||
if (!relays.length) {
|
||||
this.log("[moderationService] no relays available for mute refresh");
|
||||
return;
|
||||
}
|
||||
|
||||
this.teardownBatchedMuteSubscriptions();
|
||||
|
||||
const authors = Array.from(targets);
|
||||
const BATCH_SIZE = 400;
|
||||
const chunks = [];
|
||||
|
||||
for (let i = 0; i < authors.length; i += BATCH_SIZE) {
|
||||
chunks.push(authors.slice(i, i + BATCH_SIZE));
|
||||
}
|
||||
|
||||
this.log(`[moderationService] Refreshing mute subscriptions for ${authors.length} authors in ${chunks.length} batches`);
|
||||
|
||||
for (const chunk of chunks) {
|
||||
const filter = { kinds: [10000], authors: chunk };
|
||||
try {
|
||||
const sub = this.nostrClient.pool.sub(relays, [filter]);
|
||||
sub.on("event", (event) => {
|
||||
this.ingestTrustedMuteEvent(event);
|
||||
});
|
||||
sub.on("eose", () => {
|
||||
// Optional: handle EOSE if needed, e.g., for loading indicators
|
||||
});
|
||||
this.batchedMuteSubscriptions.push(sub);
|
||||
} catch (error) {
|
||||
this.log("[moderationService] failed to create batched subscription", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1105,125 +1172,13 @@ export class ModerationService {
|
||||
}
|
||||
|
||||
async subscribeToTrustedMuteList(pubkey) {
|
||||
const normalized = normalizeHex(pubkey);
|
||||
if (!normalized || !this.isTrustedMuteOwner(normalized)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let record = this.trustedMuteSubscriptions.get(normalized);
|
||||
if (!record) {
|
||||
record = { unsub: null, promise: null };
|
||||
this.trustedMuteSubscriptions.set(normalized, record);
|
||||
} else if (record.promise) {
|
||||
try {
|
||||
await record.promise;
|
||||
} catch {
|
||||
/* noop */
|
||||
}
|
||||
return;
|
||||
} else if (typeof record.unsub === "function") {
|
||||
this.log(`[moderationService] already subscribed to ${normalized}`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.log(`[moderationService] subscribing to trusted mute list for ${normalized}`);
|
||||
|
||||
record.promise = (async () => {
|
||||
try {
|
||||
await this.ensurePool();
|
||||
} catch (error) {
|
||||
this.log(
|
||||
`(moderationService) ensurePool failed while subscribing to trusted mute list for ${normalized}`,
|
||||
error,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.isTrustedMuteOwner(normalized)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const relays = resolveRelayList(this.nostrClient);
|
||||
if (!relays.length) {
|
||||
this.log(`[moderationService] no relays available for ${normalized}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const filter = { kinds: [10000], authors: [normalized], limit: 1 };
|
||||
|
||||
let events = [];
|
||||
try {
|
||||
events = await this.nostrClient.pool.list(relays, [filter]);
|
||||
} catch (error) {
|
||||
this.log(
|
||||
`(moderationService) failed to backfill trusted mute list for ${normalized}`,
|
||||
error,
|
||||
);
|
||||
events = [];
|
||||
}
|
||||
|
||||
if (Array.isArray(events) && events.length) {
|
||||
let latest = null;
|
||||
for (const event of events) {
|
||||
if (!event || ensureNumber(event.created_at) <= 0) {
|
||||
continue;
|
||||
}
|
||||
if (!latest || ensureNumber(event.created_at) > ensureNumber(latest.created_at)) {
|
||||
latest = event;
|
||||
}
|
||||
}
|
||||
if (latest) {
|
||||
this.ingestTrustedMuteEvent(latest);
|
||||
}
|
||||
} else {
|
||||
this.replaceTrustedMuteList(normalized, new Set(), { createdAt: 0, eventId: "" });
|
||||
}
|
||||
|
||||
if (!this.isTrustedMuteOwner(normalized)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const sub = this.nostrClient.pool.sub(relays, [filter]);
|
||||
sub.on("event", (event) => {
|
||||
this.ingestTrustedMuteEvent(event);
|
||||
});
|
||||
sub.on("eose", () => {});
|
||||
record.unsub = typeof sub.unsub === "function" ? () => sub.unsub() : null;
|
||||
this.log(`[moderationService] subscribed to trusted mute list for ${normalized}`);
|
||||
} catch (error) {
|
||||
this.log(
|
||||
`(moderationService) failed to subscribe to trusted mute list for ${normalized}`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
})();
|
||||
|
||||
try {
|
||||
await record.promise;
|
||||
} catch (error) {
|
||||
this.log("[moderationService] trusted mute subscription promise rejected", error);
|
||||
} finally {
|
||||
record.promise = null;
|
||||
}
|
||||
// Deprecated: Logic moved to batched subscription refresh
|
||||
return this.scheduleTrustedMuteSubscriptionRefresh();
|
||||
}
|
||||
|
||||
teardownTrustedMuteSubscription(pubkey) {
|
||||
const normalized = normalizeHex(pubkey);
|
||||
if (!normalized) {
|
||||
return;
|
||||
}
|
||||
|
||||
const record = this.trustedMuteSubscriptions.get(normalized);
|
||||
if (record && typeof record.unsub === "function") {
|
||||
try {
|
||||
record.unsub();
|
||||
} catch (error) {
|
||||
this.log("[moderationService] failed to teardown trusted mute subscription", error);
|
||||
}
|
||||
}
|
||||
this.trustedMuteSubscriptions.delete(normalized);
|
||||
this.replaceTrustedMuteList(normalized, new Set(), { createdAt: 0, eventId: "" });
|
||||
// Deprecated: Logic moved to batched subscription refresh
|
||||
return this.scheduleTrustedMuteSubscriptionRefresh();
|
||||
}
|
||||
|
||||
replaceTrustedMuteList(ownerPubkey, mutedAuthors, { createdAt = 0, eventId = "" } = {}) {
|
||||
|
||||
@@ -5044,10 +5044,22 @@ export class ProfileModalController {
|
||||
|
||||
handleDirectMessagesError(detail = {}) {
|
||||
const error = detail?.error || detail?.failure || detail;
|
||||
userLogger.warn(
|
||||
"[profileModal] Direct message sync issue detected:",
|
||||
error,
|
||||
);
|
||||
const reason = detail?.context?.reason || "";
|
||||
const errorCode = error?.code || "";
|
||||
|
||||
const isBenign =
|
||||
reason === "no-decryptors" ||
|
||||
errorCode === "decryption-failed" ||
|
||||
(typeof error === "string" && error.includes("no-decryptors"));
|
||||
|
||||
if (isBenign) {
|
||||
devLogger.info("[profileModal] Direct message sync info:", error);
|
||||
} else {
|
||||
userLogger.warn(
|
||||
"[profileModal] Direct message sync issue detected:",
|
||||
error,
|
||||
);
|
||||
}
|
||||
|
||||
if (this.activeMessagesRequest) {
|
||||
return;
|
||||
|
||||
@@ -166,6 +166,9 @@ export class TorrentClient {
|
||||
ensureClientForProbe() {
|
||||
if (!this.probeClient) {
|
||||
this.probeClient = new this.WebTorrentClass();
|
||||
if (typeof this.probeClient.setMaxListeners === "function") {
|
||||
this.probeClient.setMaxListeners(0);
|
||||
}
|
||||
}
|
||||
return this.probeClient;
|
||||
}
|
||||
@@ -366,6 +369,9 @@ export class TorrentClient {
|
||||
async init() {
|
||||
if (!this.client) {
|
||||
this.client = new this.WebTorrentClass();
|
||||
if (typeof this.client.setMaxListeners === "function") {
|
||||
this.client.setMaxListeners(0);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.serviceWorkerDisabled) {
|
||||
|
||||
Reference in new issue
Block a user