mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 16:02:26 +00:00
Replaces the per-card view-count transport (each card: list + NIP-45 COUNT +
live sub, × relays — the ~880 REQ/s storm) with ONE shared batched kind-30079
subscription + ONE backfill list() over all visible cards, bucketed back to
per-pointer state by #a. Re-enables the VideoListView circuit-breaker.
Grid counts are convergent/approximate (cheap, persists via the existing view
cache); the watch page opts into an exact NIP-45 COUNT for its single pointer
(modalCoordinator passes { exact: true }) — accurate where it matters, one
request, no storm. Legacy per-pointer path retained for SubscriptionManager-less
environments (tests/mocks).
Verified: nip07-heat steady-idle with views on; flaky multi-relay harness still
all-PASS; new cheat-resistant batching+bucketing test. (Pre-existing
view-counter publish-session-tag failure is unrelated — fails at baseline.)
Co-Authored-By: Claude Opus 4.8 <[email protected]>
82 lines
2.7 KiB
JavaScript
82 lines
2.7 KiB
JavaScript
// Scenario (SCN-view-counts-batched):
|
|
// Given multiple grid cards subscribe to view counts,
|
|
// When a SubscriptionManager is available,
|
|
// Then they share ONE batched kind-30079 subscription whose filter covers all
|
|
// their addresses, and incoming view events are bucketed back to the correct
|
|
// per-pointer count (one video's views never leak into another's).
|
|
//
|
|
// P3 fix for the per-card view-count storm. See docs/architecture-refactor.md.
|
|
|
|
import "./test-helpers/setup-localstorage.mjs";
|
|
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
const { nostrClient } = await import("../js/nostrClientFacade.js");
|
|
const vc = await import("../js/viewCounter.js");
|
|
|
|
const tick = (ms = 20) => new Promise((r) => setTimeout(r, ms));
|
|
|
|
test("grid view counts share one batched sub and bucket by #a", async (t) => {
|
|
const original = nostrClient.getSubscriptionManager;
|
|
let subscribeCalls = 0;
|
|
let listCalls = 0;
|
|
let lastFilters = null;
|
|
let onEvent = null;
|
|
const handle = {
|
|
update({ filters }) {
|
|
lastFilters = filters;
|
|
},
|
|
close() {},
|
|
};
|
|
nostrClient.getSubscriptionManager = () => ({
|
|
subscribe(opts) {
|
|
subscribeCalls += 1;
|
|
lastFilters = opts.filters;
|
|
onEvent = opts.onEvent;
|
|
return handle;
|
|
},
|
|
list() {
|
|
listCalls += 1;
|
|
return Promise.resolve([]);
|
|
},
|
|
});
|
|
t.after(() => {
|
|
nostrClient.getSubscriptionManager = original;
|
|
});
|
|
|
|
const pk = "a".repeat(64);
|
|
const A1 = `30078:${pk}:vid-batch-1`;
|
|
const A2 = `30078:${pk}:vid-batch-2`;
|
|
let total1 = null;
|
|
let total2 = null;
|
|
const tok1 = vc.subscribeToVideoViewCount({ type: "a", value: A1 }, ({ total }) => {
|
|
total1 = total;
|
|
});
|
|
const tok2 = vc.subscribeToVideoViewCount({ type: "a", value: A2 }, ({ total }) => {
|
|
total2 = total;
|
|
});
|
|
|
|
await tick(); // let the coalesced live refresh + backfill run
|
|
|
|
assert.equal(subscribeCalls, 1, "exactly one batched subscription for both cards");
|
|
const aVals = (lastFilters || []).flatMap((f) => f["#a"] || []);
|
|
assert.ok(
|
|
aVals.includes(A1) && aVals.includes(A2),
|
|
"the single batched filter covers both video addresses",
|
|
);
|
|
assert.ok(typeof onEvent === "function", "batched sub wired an onEvent handler");
|
|
|
|
// Deliver a view event for A1 only — only A1's count should move.
|
|
onEvent({
|
|
id: "view-1",
|
|
pubkey: "b".repeat(64),
|
|
created_at: Math.floor(Date.now() / 1000),
|
|
tags: [["a", A1], ["t", "view"]],
|
|
});
|
|
assert.equal(total1, 1, "A1 view count incremented");
|
|
assert.equal(total2, 0, "A2 view count unaffected (correct per-#a bucketing)");
|
|
|
|
vc.unsubscribeFromVideoViewCount({ type: "a", value: A1 }, tok1);
|
|
vc.unsubscribeFromVideoViewCount({ type: "a", value: A2 }, tok2);
|
|
});
|