Adjust relay count aggregation for view totals

This commit is contained in:
thePR0M3TH3AN
2025-10-01 10:16:22 -04:00
parent 87ab3f6435
commit a20dc838dc
3 changed files with 133 additions and 19 deletions

View File

@@ -2465,6 +2465,7 @@ class NostrClient {
return {
total: Array.isArray(events) ? events.length : 0,
perRelay: [],
best: null,
fallback: true,
};
}
@@ -2539,6 +2540,7 @@ class NostrClient {
return {
total: Array.isArray(events) ? events.length : 0,
perRelay: [],
best: null,
fallback: true,
};
}
@@ -4766,7 +4768,7 @@ class NostrClient {
async countEventsAcrossRelays(filters, options = {}) {
const normalizedFilters = this.normalizeCountFilters(filters);
if (!normalizedFilters.length) {
return { total: 0, perRelay: [] };
return { total: 0, best: null, perRelay: [] };
}
const relayList =
@@ -4776,7 +4778,7 @@ class NostrClient {
? this.relays
: RELAY_URLS;
const perRelay = await Promise.all(
const perRelayResults = await Promise.all(
relayList.map(async (url) => {
try {
const frame = await this.sendRawCountFrame(url, normalizedFilters, {
@@ -4793,15 +4795,47 @@ class NostrClient {
})
);
const total = perRelay.reduce((sum, entry) => {
let bestEstimate = null;
const perRelay = perRelayResults.map((entry) => {
if (!entry || !entry.ok) {
return sum;
return entry;
}
const value = Number(entry.count);
return Number.isFinite(value) && value > 0 ? sum + value : sum;
}, 0);
return { total, perRelay };
const numericValue = Number(entry.count);
const normalizedCount =
Number.isFinite(numericValue) && numericValue >= 0 ? numericValue : 0;
const normalizedEntry = {
...entry,
count: normalizedCount,
};
if (!Number.isFinite(numericValue) || numericValue < 0) {
normalizedEntry.rawCount = entry.count;
}
if (
!bestEstimate ||
normalizedCount > bestEstimate.count ||
(bestEstimate && normalizedCount === bestEstimate.count && !bestEstimate.frame)
) {
bestEstimate = {
relay: normalizedEntry.url,
count: normalizedCount,
frame: normalizedEntry.frame,
};
}
return normalizedEntry;
});
const total = bestEstimate ? bestEstimate.count : 0;
return {
total,
best: bestEstimate,
perRelay,
};
}
/**

View File

@@ -485,10 +485,18 @@ async function hydratePointer(key, listeners) {
mutated = applyEventToState(key, event) || mutated;
}
}
if (countResult && Number.isFinite(countResult.total)) {
const bestCount = Number.isFinite(countResult?.best?.count)
? Number(countResult.best.count)
: Number.isFinite(countResult?.total)
? Number(countResult.total)
: null;
if (bestCount !== null) {
const state = ensurePointerState(key);
if (countResult.total > state.total) {
state.total = Number(countResult.total);
const shouldUpdate =
!countResult?.fallback || bestCount >= state.total || state.total === 0;
if (shouldUpdate && state.total !== bestCount) {
state.total = bestCount;
state.lastSyncedAt = Date.now();
mutated = true;
}

View File

@@ -46,7 +46,7 @@ const { nostrClient } = await import("../js/nostr.js");
function createMockNostrHarness() {
const storedEvents = new Map();
const customTotals = new Map();
const customCountResults = new Map();
const subscribers = new Map();
const metrics = { list: 0, count: 0 };
@@ -108,11 +108,37 @@ function createMockNostrHarness() {
const countVideoViewEvents = async (pointer) => {
metrics.count += 1;
const key = pointerKeyFromInput(pointer);
if (customTotals.has(key)) {
return { total: customTotals.get(key) };
if (customCountResults.has(key)) {
const stored = customCountResults.get(key);
if (stored && typeof stored === "object" && !Array.isArray(stored)) {
const totalValue = Number(stored.total);
const normalizedTotal =
Number.isFinite(totalValue) && totalValue >= 0 ? totalValue : 0;
const perRelay = Array.isArray(stored.perRelay)
? stored.perRelay.map((entry) =>
entry && typeof entry === "object" ? { ...entry } : entry
)
: [];
const result = {
total: normalizedTotal,
perRelay,
best:
stored.best && typeof stored.best === "object"
? { ...stored.best }
: null,
};
if (stored.fallback) {
result.fallback = true;
}
return result;
}
const numericTotal = Number(stored);
const normalizedTotal =
Number.isFinite(numericTotal) && numericTotal >= 0 ? numericTotal : 0;
return { total: normalizedTotal, perRelay: [], best: null };
}
const events = storedEvents.get(key) || [];
return { total: events.length };
return { total: events.length, perRelay: [], best: null };
};
const subscribeVideoViewEvents = (pointer, options = {}) => {
@@ -152,16 +178,22 @@ function createMockNostrHarness() {
};
const setCountTotal = (key, total) => {
if (Number.isFinite(total)) {
customTotals.set(key, Number(total));
if (
typeof total === "object" &&
total !== null &&
!Array.isArray(total)
) {
customCountResults.set(key, JSON.parse(JSON.stringify(total)));
} else if (Number.isFinite(total)) {
customCountResults.set(key, Number(total));
} else {
customTotals.delete(key);
customCountResults.delete(key);
}
};
const reset = () => {
storedEvents.clear();
customTotals.clear();
customCountResults.clear();
subscribers.clear();
resetMetrics();
};
@@ -319,6 +351,45 @@ async function testHydrationSkipsStaleEventsAndRollsOff() {
}
}
async function testRelayCountAggregationUsesBestEstimate() {
localStorage.clear();
harness.reset();
harness.resetMetrics();
const pointer = { type: "e", value: "view-counter-multi-relay" };
const pointerKey = harness.pointerKeyFromInput(pointer);
harness.setEvents(pointerKey, []);
harness.setCountTotal(pointerKey, {
total: 5,
best: { relay: "wss://relay.alpha", count: 5 },
perRelay: [
{ url: "wss://relay.alpha", ok: true, count: 5 },
{ url: "wss://relay.beta", ok: true, count: 5 },
],
});
const updates = [];
const token = subscribeToVideoViewCount(pointer, (state) => {
updates.push({ ...state });
});
try {
await flushPromises();
await flushPromises();
const final = updates.at(-1);
assert.ok(final, "expected hydration update for aggregated relay counts");
assert.equal(
final.total,
5,
"identical relay COUNT responses should not be double-counted"
);
} finally {
unsubscribeFromVideoViewCount(pointer, token);
}
}
async function testLocalIngestNotifiesImmediately() {
localStorage.clear();
harness.reset();
@@ -458,5 +529,6 @@ await testDedupesWithinWindow();
await testHydrationSkipsStaleEventsAndRollsOff();
await testLocalIngestNotifiesImmediately();
await testUnsubscribeStopsCallbacks();
await testRelayCountAggregationUsesBestEstimate();
console.log("View counter tests completed successfully.");