mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 16:02:26 +00:00
Cold-start fired one kind-30000 REQ per community-blacklist curator (loadCommunityBlacklistEntries did Promise.all over references, each a fetchLatestListEvent) — ~28-32 parallel REQs that dominated the login relay-REQ spike and starved the feed. A Nostr filter ORs `authors` and `#d`, so all curators collapse into ONE batched REQ. New js/adminListBatch.js (pure, mutation-tested): buildBatchedReferenceFilter + selectNewestEventsForReferences (newest per author:d-tag, discards author×d-tag cross-products) + fetchBatchedReferenceEvents (routes through the subscription manager's list — health-gated/timeout-bounded, not raw pool). Helpers extracted to their own module so adminListStore stays under its size cap. Best-effort: on batch failure we return nothing and the cached admin state carries over (readCachedAdminState). Addresses pre-launch TODO #9 (cold-login REQ storm). Tests + mutation- verified (newest selection, cross-product discard). 1249/1250 unit (known UploadModal flake). Co-Authored-By: Claude Opus 4.8 <[email protected]>
89 lines
3.2 KiB
JavaScript
89 lines
3.2 KiB
JavaScript
// Pure helpers for batching the community-blacklist curator fetch into a single
|
||
// multi-author REQ instead of one kind-30000 REQ per curator (the cold-start
|
||
// relay storm). Kept dependency-free so they're cheat-resistant to test.
|
||
|
||
// Build a single batched filter for many (authorHex, dTag) references. A Nostr
|
||
// filter ORs `authors` and ORs `#d`, so one REQ replaces N per-curator REQs.
|
||
// Over-fetches author×dtag cross-products, which selectNewestEventsForReferences
|
||
// discards when matching back to exact references.
|
||
export function buildBatchedReferenceFilter(references, { kind = 30000 } = {}) {
|
||
const authors = new Set();
|
||
const dTags = new Set();
|
||
for (const ref of Array.isArray(references) ? references : []) {
|
||
const author =
|
||
typeof ref?.authorHex === "string" ? ref.authorHex.trim().toLowerCase() : "";
|
||
const dTag = typeof ref?.dTag === "string" ? ref.dTag.trim() : "";
|
||
if (author) authors.add(author);
|
||
if (dTag) dTags.add(dTag);
|
||
}
|
||
if (!authors.size || !dTags.size) {
|
||
return null;
|
||
}
|
||
return {
|
||
kinds: [kind],
|
||
authors: Array.from(authors),
|
||
"#d": Array.from(dTags),
|
||
};
|
||
}
|
||
|
||
function dTagOf(event) {
|
||
const tags = Array.isArray(event?.tags) ? event.tags : [];
|
||
for (const tag of tags) {
|
||
if (Array.isArray(tag) && tag[0] === "d") {
|
||
return typeof tag[1] === "string" ? tag[1] : "";
|
||
}
|
||
}
|
||
return "";
|
||
}
|
||
|
||
// Group batched events to the newest per (author, dTag), then return the events
|
||
// matching the requested references (in reference order). Cross-product events
|
||
// (author×dtag combinations that weren't requested) are discarded.
|
||
export function selectNewestEventsForReferences(events, references) {
|
||
const newest = new Map(); // key `${pubkey}:${dtag}` -> event
|
||
for (const event of Array.isArray(events) ? events : []) {
|
||
const pubkey = typeof event?.pubkey === "string" ? event.pubkey.toLowerCase() : "";
|
||
const dTag = dTagOf(event);
|
||
if (!pubkey || !dTag) {
|
||
continue;
|
||
}
|
||
const key = `${pubkey}:${dTag}`;
|
||
const prev = newest.get(key);
|
||
if (
|
||
!prev ||
|
||
event.created_at > prev.created_at ||
|
||
(event.created_at === prev.created_at && event.id > prev.id)
|
||
) {
|
||
newest.set(key, event);
|
||
}
|
||
}
|
||
const matched = [];
|
||
for (const ref of Array.isArray(references) ? references : []) {
|
||
const author = typeof ref?.authorHex === "string" ? ref.authorHex.toLowerCase() : "";
|
||
const dTag = typeof ref?.dTag === "string" ? ref.dTag : "";
|
||
const event = newest.get(`${author}:${dTag}`);
|
||
if (event) {
|
||
matched.push(event);
|
||
}
|
||
}
|
||
return matched;
|
||
}
|
||
|
||
// Fetch all curator blacklists in ONE batched REQ and return the newest event
|
||
// matched to each reference. `listEvents` is injected (the subscription
|
||
// manager's health-gated, timeout-bounded list) so this stays off the raw pool.
|
||
// Returns [] when there's nothing to batch; rejects on fetch failure.
|
||
export async function fetchBatchedReferenceEvents({
|
||
references,
|
||
listEvents,
|
||
relays,
|
||
timeoutMs,
|
||
}) {
|
||
const filter = buildBatchedReferenceFilter(references);
|
||
if (!filter || typeof listEvents !== "function") {
|
||
return [];
|
||
}
|
||
const events = await listEvents({ filters: [filter], relays, timeoutMs });
|
||
return selectNewestEventsForReferences(events, references);
|
||
}
|