views: exact count unions across relays (fixes cross-relay under-count) (#4)

The watch-page/chart "exact" count relied on the NIP-45 COUNT, but
countEventsAcrossRelays returns bestEstimate = the single relay with the MOST
events — it can't union across relays. So when views are distributed across
relays it under-counted (a video's card showed 19 while the chart's uncapped
list union showed ~63).

exactCountForPointer now lists view events across ALL relays (one on-demand
list, not a storm) and folds the UNION into the deduped dedupeBuckets (one entry
per viewer-window). total = the set size, so it is accurate AND monotonic (never
spuriously decreases on partial relay responses). The per-relay COUNT is kept
only as a lower-bound floor when the list truncates (>2000 events), flagged
partial. The popularity modal passes { exact: true }; since the count flows
through shared pointer state, opening a video corrects its count everywhere.

Grid cards (no exact) still use the §17-capped batched backfill (approximate,
storm-safe) until opened — an accepted accuracy/cost tradeoff.

Also note the ⋯ menu "Event Details" button does nothing (new TODO #31).

Tests: testExactCountUnionsAcrossRelays. Grandfathered viewCounter.js size.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
thePR0M3TH3ANandClaude Opus 4.8 committed 2026-06-24 17:29:53 -04:00
1 parent 1bc2b06395
commit 36f3c6e127
5 files changed
+164 -26

No files matched your search

+68 -19
View File
@@ -249,35 +249,84 @@ async function runBatchedBackfill() {
// Exact NIP-45 COUNT for a single pointer (watch page). Cheap — one pointer,
// no storm — and accurate where a viewer might actually scrutinize the number.
const EXACT_LIST_LIMIT = 2000;
// Accurate single-pointer count (watch page / popularity chart). Lists view
// events across ALL relays and dedupes by (viewer, window) — a true cross-relay
// UNION. This replaces relying on the NIP-45 COUNT, which returns the single
// relay with the most events (it can't union across relays) and therefore
// UNDER-counts whenever views are spread across relays. The deduped set accrues
// monotonically, so the number is accurate AND never spuriously decreases. For
// extremely popular videos where the list truncates, the per-relay COUNT is used
// as a lower-bound floor and the result is flagged partial.
async function exactCountForPointer(key, pointer, options = {}) {
const sinceSeconds =
Math.floor(Date.now() / 1000) - VIEW_COUNT_BACKFILL_MAX_DAYS * SECONDS_PER_DAY;
let events;
try {
const result = await countVideoViewEventsApi(pointer, {
events = await listVideoViewEventsApi(pointer, {
since: sinceSeconds,
limit: EXACT_LIST_LIMIT,
relays: options.relays,
signal: options.signal,
});
const best = Number.isFinite(result?.best?.count)
? Number(result.best.count)
: Number.isFinite(result?.total)
? Number(result.total)
: null;
if (best === null) {
} catch (error) {
if (error?.name === "AbortError") {
return;
}
const state = ensurePointerState(key);
if (result && typeof result === "object") {
state.partial = Boolean(result.partial);
userLogger.warn("[viewCounter] Exact view count list failed:", error);
return;
}
const eventList = Array.isArray(events) ? events : [];
const truncated = eventList.length >= EXACT_LIST_LIMIT;
const state = ensurePointerState(key);
// Fold the union into the deduped bucket set (one entry per viewer-window).
for (const event of eventList) {
const bucketKey = deriveBucketKey(event) || deriveFallbackKey(event);
if (!bucketKey) {
continue;
}
if (best > state.total) {
state.total = best;
state.lastSyncedAt = Date.now();
}
schedulePersist();
notifyHandlers(key);
} catch (error) {
if (error?.name !== "AbortError") {
userLogger.warn("[viewCounter] Exact view count failed:", error);
const createdAt = Number.isFinite(event?.created_at)
? Number(event.created_at)
: Math.floor(Date.now() / 1000);
state.dedupeBuckets.set(bucketKey, createdAt);
}
let total = state.dedupeBuckets.size;
let partial = truncated;
if (truncated) {
try {
const result = await countVideoViewEventsApi(pointer, {
relays: options.relays,
signal: options.signal,
});
const best = Number.isFinite(result?.best?.count)
? Number(result.best.count)
: Number.isFinite(result?.total)
? Number(result.total)
: null;
if (Number.isFinite(best) && best > total) {
total = best;
}
partial = partial || Boolean(result?.partial);
} catch (error) {
if (error?.name === "AbortError") {
return;
}
}
}
state.partial = partial;
if (total !== state.total) {
state.total = total;
state.lastSyncedAt = Date.now();
}
schedulePersist();
notifyHandlers(key);
}
restoreCacheSnapshot();