mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-01 10:22:25 +00:00
fetchLatestListEvent fetched admin lists via fetchListIncrementally without an explicit `since`, so it relied on the persisted per-relay lastSeen. Admin lists are replaceable events with a single current version whose created_at does not advance, so once lastSeen == that created_at, every later load queries since=lastSeen+1 and the relay returns nothing — perpetually hiding the current list and dropping admin lists whenever the local cache is absent. Pass since: 0 to force a full fetch of the (tiny) current event each time. Other list callers anchor an explicit since to their own cached state; admin lists have no comparable per-list timestamp, so always re-fetching current is both correct and cheap. Adds tests/nostr/relayBatchFetcherReplaceable.test.mjs documenting the replaceable-event since-gate at the fetcher boundary: relying on persisted lastSeen hides the current event; since: 0 returns it. Co-Authored-By: Claude Opus 4.8 <[email protected]>
95 lines
3.3 KiB
JavaScript
95 lines
3.3 KiB
JavaScript
// Regression test documenting why replaceable-list fetches must not be gated by
|
|
// a persisted per-relay lastSeen, and why admin lists pass `since: 0`.
|
|
//
|
|
// Replaceable events (NIP-51/NIP-33 lists: mute 10000, lists 30000/30002,
|
|
// hashtag prefs 30015, admin lists) have exactly ONE current version. The
|
|
// incremental optimization stores lastSeen = current created_at, then queries
|
|
// `since = lastSeen + 1` on the next load. For a replaceable event whose
|
|
// created_at never advances, that filter perpetually excludes the current
|
|
// event, so the fetch returns nothing even though the list exists. Callers that
|
|
// lack a durable content cache then see an empty list.
|
|
//
|
|
// Scenario (SCN-replaceable-since-gate):
|
|
// Given a relay holding one replaceable list event at created_at = T, and a
|
|
// persisted lastSeen = T,
|
|
// When the list is fetched relying on the persisted lastSeen,
|
|
// Then the current event is hidden (returns empty) — the latent bug.
|
|
// When the list is fetched with an explicit since: 0 (full fetch),
|
|
// Then the current event is returned — the fix used by admin lists.
|
|
|
|
import { describe, it } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { RelayBatchFetcher } from "../../js/nostr/relayBatchFetcher.js";
|
|
|
|
const PUBKEY = "a".repeat(64);
|
|
const RELAY = "wss://r1.example.com";
|
|
const T = 1000;
|
|
|
|
// One current replaceable list event living on the relay at created_at = T.
|
|
const currentEvent = {
|
|
id: "current-list-event",
|
|
kind: 30000,
|
|
pubkey: PUBKEY,
|
|
created_at: T,
|
|
tags: [["d", "admin:editors"]],
|
|
content: "",
|
|
};
|
|
|
|
function makeFetcher() {
|
|
const client = {
|
|
relays: [RELAY],
|
|
readRelays: [],
|
|
getHealthyRelays: (list) => list,
|
|
ensurePool: async () => ({ list: async () => [] }),
|
|
// Persisted lastSeen says we already saw the current version at time T.
|
|
getSyncLastSeen: () => T,
|
|
updateSyncLastSeen: () => {},
|
|
markRelayUnreachable: () => {},
|
|
};
|
|
|
|
// Behavioral relay clone: returns the current replaceable event only when the
|
|
// query is not gated past T (i.e. since is absent or <= T).
|
|
const fetchFn = async (_relayUrl, filter) => {
|
|
const since = filter?.since;
|
|
if (since === undefined || since <= T) {
|
|
return [currentEvent];
|
|
}
|
|
return [];
|
|
};
|
|
|
|
return { fetcher: new RelayBatchFetcher(client), fetchFn };
|
|
}
|
|
|
|
describe("fetchListIncrementally + replaceable events", () => {
|
|
it("relying on persisted lastSeen hides the current replaceable event (documents the bug)", async () => {
|
|
const { fetcher, fetchFn } = makeFetcher();
|
|
const events = await fetcher.fetchListIncrementally({
|
|
kind: 30000,
|
|
pubkey: PUBKEY,
|
|
dTag: "admin:editors",
|
|
relayUrls: [RELAY],
|
|
fetchFn,
|
|
// no `since` => uses persisted lastSeen = T => filter.since = T + 1
|
|
});
|
|
assert.equal(
|
|
events.length,
|
|
0,
|
|
"persisted lastSeen gates since=T+1, hiding the current event",
|
|
);
|
|
});
|
|
|
|
it("since: 0 forces a full fetch and returns the current event (the admin-list fix)", async () => {
|
|
const { fetcher, fetchFn } = makeFetcher();
|
|
const events = await fetcher.fetchListIncrementally({
|
|
kind: 30000,
|
|
pubkey: PUBKEY,
|
|
dTag: "admin:editors",
|
|
relayUrls: [RELAY],
|
|
fetchFn,
|
|
since: 0,
|
|
});
|
|
assert.equal(events.length, 1, "full fetch must return the current event");
|
|
assert.equal(events[0].id, currentEvent.id);
|
|
});
|
|
});
|