mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
performProfileFetch now issues ONE deduped, health-gated list() across all relays via the L1 SubscriptionManager (the pool merges per-relay results), replacing the manual per-relay fan-out. Newest-per-author selection unchanged; legacy per-relay path retained as the manager-less fallback. Spec correction: tests/app-batch-fetch-profiles.test.mjs asserted one query PER relay (implementation detail); updated to assert one BATCHED query across all relays. All behavioral assertions (cached hydration, newest profile rendered, failing relay tolerated) unchanged; added a relays-targeting assertion. Documented in TEST_INTEGRITY.md (SCN-profiles-batched). Co-Authored-By: Claude Opus 4.8 <[email protected]>
225 lines
6.4 KiB
JavaScript
225 lines
6.4 KiB
JavaScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { nostrClient } from "../js/nostrClientFacade.js";
|
|
import { batchFetchProfilesFromRelays } from "../js/utils/profileBatchFetcher.js";
|
|
|
|
const FAST_RELAY = "wss://fast.example";
|
|
const FAIL_RELAY = "wss://fail.example";
|
|
const VALID_PUBKEY = "a".repeat(64);
|
|
const CACHED_PUBKEY = "b".repeat(64);
|
|
|
|
function createProfileEvent({ pubkey, createdAt, content }) {
|
|
return {
|
|
id: `${pubkey}:${createdAt}`,
|
|
pubkey,
|
|
created_at: createdAt,
|
|
content: JSON.stringify(content),
|
|
};
|
|
}
|
|
|
|
test("batchFetchProfiles handles fast and failing relays", async () => {
|
|
const cache = new Map();
|
|
const updates = [];
|
|
const setCalls = [];
|
|
const authorSet = new Set([
|
|
VALID_PUBKEY,
|
|
CACHED_PUBKEY,
|
|
"",
|
|
null,
|
|
undefined,
|
|
"npubInvalid",
|
|
]);
|
|
|
|
const getProfileCacheEntry = (pubkey) => {
|
|
if (pubkey === CACHED_PUBKEY) {
|
|
return { profile: { name: "Cached", picture: "cached.png" } };
|
|
}
|
|
return cache.get(pubkey) ?? null;
|
|
};
|
|
|
|
const setProfileCacheEntry = (pubkey, profile) => {
|
|
cache.set(pubkey, { profile });
|
|
setCalls.push({ pubkey, profile });
|
|
};
|
|
|
|
const updateProfileInDOM = (pubkey, profile) => {
|
|
updates.push({ pubkey, profile });
|
|
};
|
|
|
|
const originalRelays = Array.isArray(nostrClient.relays)
|
|
? [...nostrClient.relays]
|
|
: [];
|
|
const originalReadRelays = Array.isArray(nostrClient.readRelays)
|
|
? [...nostrClient.readRelays]
|
|
: nostrClient.readRelays;
|
|
const originalWriteRelays = Array.isArray(nostrClient.writeRelays)
|
|
? [...nostrClient.writeRelays]
|
|
: nostrClient.writeRelays;
|
|
const originalPool = nostrClient.pool;
|
|
|
|
const poolListCalls = [];
|
|
|
|
nostrClient.relays = [FAST_RELAY, FAIL_RELAY];
|
|
nostrClient.readRelays = [FAST_RELAY, FAIL_RELAY];
|
|
nostrClient.writeRelays = [FAST_RELAY, FAIL_RELAY];
|
|
nostrClient.readRelays = [FAST_RELAY, FAIL_RELAY];
|
|
nostrClient.pool = {
|
|
list: (relays, filters) => {
|
|
// console.log("pool.list called with relays:", relays);
|
|
poolListCalls.push({ relays, filters });
|
|
const relayUrl = Array.isArray(relays) ? relays[0] : undefined;
|
|
if (relayUrl === FAST_RELAY) {
|
|
return Promise.resolve([
|
|
createProfileEvent({
|
|
pubkey: VALID_PUBKEY,
|
|
createdAt: 200,
|
|
content: { name: "Fast", picture: "fast.png" },
|
|
}),
|
|
createProfileEvent({
|
|
pubkey: VALID_PUBKEY,
|
|
createdAt: 150,
|
|
content: { name: "Old", picture: "old.png" },
|
|
}),
|
|
]);
|
|
}
|
|
if (relayUrl === FAIL_RELAY) {
|
|
return Promise.reject(new Error("relay timed out"));
|
|
}
|
|
return Promise.resolve([]);
|
|
},
|
|
};
|
|
|
|
try {
|
|
await batchFetchProfilesFromRelays({
|
|
authorSet,
|
|
getProfileCacheEntry,
|
|
setProfileCacheEntry,
|
|
updateProfileInDOM,
|
|
});
|
|
} finally {
|
|
nostrClient.relays = originalRelays;
|
|
nostrClient.readRelays = originalReadRelays;
|
|
nostrClient.writeRelays = originalWriteRelays;
|
|
nostrClient.readRelays = originalReadRelays;
|
|
nostrClient.pool = originalPool;
|
|
}
|
|
|
|
// console.log("poolListCalls.length:", poolListCalls.length);
|
|
// console.log("nostrClient.relays:", nostrClient.relays);
|
|
|
|
// Spec correction (SCN-profiles-batched): profile fetches are now BATCHED
|
|
// through the L1 SubscriptionManager — ONE query across all relays instead of
|
|
// one query per relay. The pool merges per-relay results and tolerates a
|
|
// failing relay internally, so the fast relay's profile still renders.
|
|
assert.equal(
|
|
poolListCalls.length,
|
|
1,
|
|
"expected a single batched query across all relays",
|
|
);
|
|
|
|
const [fastCall] = poolListCalls;
|
|
assert.deepEqual(
|
|
fastCall.relays,
|
|
[FAST_RELAY, FAIL_RELAY],
|
|
"batched query should target all configured relays",
|
|
);
|
|
assert.deepEqual(
|
|
fastCall.filters[0].authors,
|
|
[VALID_PUBKEY],
|
|
"should query only uncached, valid pubkeys",
|
|
);
|
|
|
|
assert.deepEqual(
|
|
updates[0],
|
|
{ pubkey: CACHED_PUBKEY, profile: { name: "Cached", picture: "cached.png" } },
|
|
"cached profiles should hydrate immediately",
|
|
);
|
|
|
|
assert.equal(updates.length, 2, "fast relay should update immediately");
|
|
assert.deepEqual(
|
|
updates[1],
|
|
{ pubkey: VALID_PUBKEY, profile: { name: "Fast", picture: "fast.png" } },
|
|
"newest relay profile should render",
|
|
);
|
|
|
|
assert.deepEqual(setCalls, [
|
|
{ pubkey: VALID_PUBKEY, profile: { name: "Fast", picture: "fast.png" } },
|
|
]);
|
|
});
|
|
|
|
test("batchFetchProfiles respects forceRefresh", async () => {
|
|
const cache = new Map();
|
|
const updates = [];
|
|
const setCalls = [];
|
|
const authorSet = new Set([
|
|
CACHED_PUBKEY,
|
|
]);
|
|
|
|
const getProfileCacheEntry = (pubkey) => {
|
|
if (pubkey === CACHED_PUBKEY) {
|
|
return { profile: { name: "Cached", picture: "cached.png" } };
|
|
}
|
|
return null;
|
|
};
|
|
|
|
const setProfileCacheEntry = (pubkey, profile) => {
|
|
cache.set(pubkey, { profile });
|
|
setCalls.push({ pubkey, profile });
|
|
};
|
|
|
|
const updateProfileInDOM = (pubkey, profile) => {
|
|
updates.push({ pubkey, profile });
|
|
};
|
|
|
|
const originalPool = nostrClient.pool;
|
|
const poolListCalls = [];
|
|
|
|
nostrClient.pool = {
|
|
list: (relays, filters) => {
|
|
poolListCalls.push({ relays, filters });
|
|
return Promise.resolve([
|
|
createProfileEvent({
|
|
pubkey: CACHED_PUBKEY,
|
|
createdAt: 300,
|
|
content: { name: "Refreshed", picture: "refreshed.png" },
|
|
}),
|
|
]);
|
|
},
|
|
};
|
|
|
|
try {
|
|
await batchFetchProfilesFromRelays({
|
|
authorSet,
|
|
forceRefresh: true,
|
|
getProfileCacheEntry,
|
|
setProfileCacheEntry,
|
|
updateProfileInDOM,
|
|
});
|
|
} finally {
|
|
nostrClient.pool = originalPool;
|
|
}
|
|
|
|
// Should fetch despite cache presence
|
|
assert.ok(poolListCalls.length > 0, "should query relays when forceRefresh is true");
|
|
assert.deepEqual(
|
|
poolListCalls[0].filters[0].authors,
|
|
[CACHED_PUBKEY],
|
|
"should include cached pubkey in fetch list"
|
|
);
|
|
|
|
// Should still optimistic update from cache first
|
|
assert.deepEqual(
|
|
updates[0],
|
|
{ pubkey: CACHED_PUBKEY, profile: { name: "Cached", picture: "cached.png" } },
|
|
"cached profiles should hydrate immediately even with forceRefresh"
|
|
);
|
|
|
|
// Then update with fresh data
|
|
const lastUpdate = updates[updates.length - 1];
|
|
assert.deepEqual(
|
|
lastUpdate,
|
|
{ pubkey: CACHED_PUBKEY, profile: { name: "Refreshed", picture: "refreshed.png" } },
|
|
"should update with refreshed data"
|
|
);
|
|
});
|