refactor(profiles): batch profile fetch through SubscriptionManager (P4)

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]>
This commit is contained in:
thePR0M3TH3ANandClaude Opus 4.8 committed 2026-06-14 16:15:06 -04:00
1 parent dd3fbfdae6
commit d693bf55aa
3 files changed
+79 -24

No files matched your search

+39
View File
@@ -0,0 +1,39 @@
# Test Integrity Notes
Machine-readable record of test-expectation changes made under the Spec
Correction Protocol (see CLAUDE.md → Dark Factory rules). Each entry justifies
why a previous expectation was wrong relative to intended behavior and confirms
the replacement is equally strict (or stricter).
---
```yaml
test_integrity_note:
date: 2026-06-14
file: tests/app-batch-fetch-profiles.test.mjs
change_type: ["spec_correction"]
scenarios:
- id: SCN-profiles-batched
given: "Multiple relays configured (one fast, one failing) and uncached, valid pubkeys to fetch"
when: "batchFetchProfiles runs after profile fetches were migrated to the L1 SubscriptionManager"
then: "Profiles are fetched in ONE batched query across all relays (not one query per relay); the fast relay's newest profile still renders, cached profiles hydrate immediately, and a failing relay does not break the result"
observable_outcomes:
- "exactly one pool.list call, targeting all configured relays"
- "query authors = only uncached/valid pubkeys"
- "DOM updates: cached profile first, then newest fast-relay profile"
- "cache set with the newest (Fast) profile only"
determinism_controls:
- "mock pool.list with deterministic per-relay responses; fixed createdAt ordering"
anti_cheat_rationale:
prevents:
- "over-mocking internal logic"
- "hard-coded return value"
note: "Only the per-relay CALL-COUNT assertion changed (2 -> 1), because the
refactor intentionally batches the fetch. All behavioral assertions
(correct/newest profile rendered, cached hydration, failing relay
tolerated) are unchanged, and a new assertion verifies the single batched
query targets all relays — so coverage is equal or stronger."
relaxation:
did_relax_any_assertion: false
if_true_explain_spec_basis: ""
```
+26 -23
View File
@@ -106,33 +106,43 @@ async function performProfileFetch({
const newestProfiles = new Map();
// Prefer the L1 SubscriptionManager: ONE deduped, health-gated list() across
// all relays (the pool merges per-relay results) instead of a manual
// per-relay fan-out. Falls back to the legacy per-relay path in manager-less
// environments (tests / mock clients).
const manager =
typeof nostr.getSubscriptionManager === "function"
? nostr.getSubscriptionManager()
: null;
let events = [];
if (manager) {
try {
const result = await manager.list({ filters: [filter], relays: relayUrls });
events = Array.isArray(result) ? result : [];
} catch (error) {
logger.warn("[profileMetadata] Batched profile fetch failed:", error);
}
} else {
const results = await Promise.allSettled(
relayUrls.map((relayUrl) =>
nostr.pool
.list([relayUrl], [filter])
.then((events) => ({ relayUrl, events }))
.then((relayEvents) => ({ relayUrl, relayEvents }))
)
);
results.forEach((result, index) => {
const relayUrl =
result.status === "fulfilled"
? result.value.relayUrl
: relayUrls[index];
if (result.status !== "fulfilled") {
logger.warn(
`[profileMetadata] Failed to fetch profiles from relay ${relayUrl}:`,
`[profileMetadata] Failed to fetch profiles from relay ${relayUrls[index]}:`,
result.reason,
);
return;
}
const events = Array.isArray(result.value.events)
? result.value.events
: [];
if (!events.length) {
return;
if (Array.isArray(result.value.relayEvents)) {
events.push(...result.value.relayEvents);
}
});
}
for (const event of events) {
@@ -141,9 +151,7 @@ async function performProfileFetch({
continue;
}
const createdAt = Number.isFinite(event?.created_at)
? event.created_at
: 0;
const createdAt = Number.isFinite(event?.created_at) ? event.created_at : 0;
const previous = newestProfiles.get(pubkey);
if (previous && previous.createdAt >= createdAt) {
continue;
@@ -154,13 +162,8 @@ async function performProfileFetch({
continue;
}
newestProfiles.set(pubkey, {
createdAt,
profile,
event,
});
newestProfiles.set(pubkey, { createdAt, profile, event });
}
});
return newestProfiles;
}
+14 -1
View File
@@ -107,9 +107,22 @@ test("batchFetchProfiles handles fast and failing relays", async () => {
// console.log("poolListCalls.length:", poolListCalls.length);
// console.log("nostrClient.relays:", nostrClient.relays);
assert.equal(poolListCalls.length, 2, "expected a query per relay");
// 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],