Merge pull request #230 from PR0M3TH3AN/codex/add-unsupported-relays-handling-in-nostrclient

Handle unsupported relays for Nostr COUNT requests
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2025-10-01 19:57:30 -04:00
commit 1c99d4c2f3
2 files changed
+127 -7

No files matched your search

+63 -7
View File
@@ -1509,6 +1509,21 @@ class NostrClient {
this.watchHistoryRefreshPromises = new Map();
this.watchHistoryCacheTtlMs = WATCH_HISTORY_CACHE_TTL_MS;
this.countRequestCounter = 0;
this.countUnsupportedRelays = new Set();
}
makeCountUnsupportedError(relayUrl) {
const normalizedUrl =
typeof relayUrl === "string" && relayUrl.trim()
? relayUrl.trim()
: "";
const error = new Error(
`[nostr] Relay ${normalizedUrl} does not support COUNT frames.`
);
error.code = "count-unsupported";
error.relay = normalizedUrl;
error.unsupported = true;
return error;
}
restoreLocalData() {
@@ -5757,6 +5772,10 @@ class NostrClient {
throw new Error("Invalid relay URL for COUNT request.");
}
if (this.countUnsupportedRelays.has(normalizedUrl)) {
throw this.makeCountUnsupportedError(normalizedUrl);
}
const normalizedFilters = this.normalizeCountFilters(filters);
if (!normalizedFilters.length) {
throw new Error("At least one filter is required for a COUNT request.");
@@ -5824,9 +5843,8 @@ class NostrClient {
} else if (typeof relay.count === "function") {
countPromise = relay.count(normalizedFilters, { id: requestId });
} else {
throw new Error(
`[nostr] Relay ${normalizedUrl} does not support COUNT frames.`
);
this.countUnsupportedRelays.add(normalizedUrl);
throw this.makeCountUnsupportedError(normalizedUrl);
}
const timeoutMs = this.getRequestTimeoutMs(options.timeoutMs);
@@ -5858,8 +5876,29 @@ class NostrClient {
? this.relays
: RELAY_URLS;
const perRelayResults = await Promise.all(
relayList.map(async (url) => {
const normalizedRelayList = relayList
.map((url) => (typeof url === "string" ? url.trim() : ""))
.filter(Boolean);
const activeRelays = [];
const precomputedEntries = [];
for (const url of normalizedRelayList) {
if (this.countUnsupportedRelays.has(url)) {
const error = this.makeCountUnsupportedError(url);
precomputedEntries.push({
url,
ok: false,
error,
unsupported: true,
});
continue;
}
activeRelays.push(url);
}
const activeResults = await Promise.all(
activeRelays.map(async (url) => {
try {
const frame = await this.sendRawCountFrame(url, normalizedFilters, {
timeoutMs: options.timeoutMs,
@@ -5867,14 +5906,31 @@ class NostrClient {
const count = this.extractCountValue(frame?.[2]);
return { url, ok: true, frame, count };
} catch (error) {
if (isDevMode) {
const isUnsupported = error?.code === "count-unsupported";
if (isUnsupported) {
this.countUnsupportedRelays.add(url);
} else if (isDevMode) {
console.warn(`[nostr] COUNT request failed on ${url}:`, error);
}
return { url, ok: false, error };
return { url, ok: false, error, unsupported: isUnsupported };
}
})
);
const resultsByUrl = new Map();
for (const entry of [...precomputedEntries, ...activeResults]) {
if (entry && typeof entry.url === "string") {
resultsByUrl.set(entry.url, entry);
}
}
const perRelayResults = normalizedRelayList.map((url) => {
if (resultsByUrl.has(url)) {
return resultsByUrl.get(url);
}
return { url, ok: false };
});
let bestEstimate = null;
const perRelay = perRelayResults.map((entry) => {
if (!entry || !entry.ok) {
+64
View File
@@ -144,8 +144,72 @@ async function testFallbackAfterCountFailureUsesDefaultWindow() {
}
}
async function testUnsupportedRelaySkipsWarningsAndFallsBack() {
const client = new NostrClient();
const unsupportedRelay = "wss://unsupported.example";
client.relays = [unsupportedRelay];
let ensureRelayCalls = 0;
client.pool = {
ensureRelay: async (url) => {
ensureRelayCalls += 1;
assert.equal(
url,
unsupportedRelay,
"COUNT helper should target the configured relay"
);
return {};
},
};
let listCalls = 0;
client.listVideoViewEvents = async () => {
listCalls += 1;
return [];
};
const originalWarn = console.warn;
const warnCalls = [];
console.warn = (...args) => {
warnCalls.push(args);
};
try {
const pointer = { type: "e", value: "unsupported-relay" };
const result = await client.countVideoViewEvents(pointer, {});
assert.equal(listCalls, 1, "fallback should query list once when COUNT fails");
assert.equal(result?.fallback, true, "unsupported relay should trigger fallback");
assert.ok(
client.countUnsupportedRelays.has(unsupportedRelay),
"unsupported relay should be remembered for future requests"
);
assert.equal(
warnCalls.length,
0,
"unsupported relay error should not emit console warnings"
);
// Subsequent calls should skip the unsupported relay entirely.
await client.countVideoViewEvents(pointer, {});
assert.equal(
ensureRelayCalls,
1,
"unsupported relays should be skipped after the first failure"
);
assert.equal(
warnCalls.length,
0,
"no warnings should be emitted for cached unsupported relays"
);
} finally {
console.warn = originalWarn;
}
}
await testFallbackAddsDefaultSinceWhenPoolMissing();
await testFallbackHonorsCallerOverrides();
await testFallbackAfterCountFailureUsesDefaultWindow();
await testUnsupportedRelaySkipsWarningsAndFallsBack();
console.log("nostr COUNT fallback tests completed successfully.");