mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-06 12:52:26 +00:00
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:
1 parent
1bc2b06395
commit
36f3c6e127
5 files changed
+164
-26
No files matched your search
+68
-19
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user