Merge pull request #1751 from PR0M3TH3AN/fix-hashtag-preferences-reliability-11590046793499190431

Fix Hashtag Preferences Reliability
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2026-01-26 14:31:54 -05:00
commit 4a9d771689
3 files changed
+73 -8

No files matched your search

+25 -2
View File
@@ -3482,7 +3482,14 @@ export class NostrClient {
* @param {function} [params.fetchFn] - Custom fetch function (mocks or specialized logic). Defaults to `pool.list`.
* @returns {Promise<import("nostr-tools").Event[]>} Deduplicated list of events found across all relays.
*/
async fetchListIncrementally({ kind, pubkey, dTag, relayUrls, fetchFn } = {}) {
async fetchListIncrementally({
kind,
pubkey,
dTag,
relayUrls,
fetchFn,
since,
} = {}) {
if (!kind || !pubkey) {
throw new Error("fetchListIncrementally requires kind and pubkey");
}
@@ -3514,7 +3521,23 @@ export class NostrClient {
for (const chunk of chunks) {
const promises = chunk.map(async (relayUrl) => {
const lastSeen = this.getSyncLastSeen(kind, normalizedPubkey, dTag, relayUrl);
let lastSeen = this.getSyncLastSeen(
kind,
normalizedPubkey,
dTag,
relayUrl,
);
// If an explicit 'since' override is provided, use it instead of storage.
// This allows services to anchor fetching to their own known state.
if (since !== undefined) {
const overrideSince = Number(since);
lastSeen =
Number.isFinite(overrideSince) && overrideSince >= 0
? Math.floor(overrideSince)
: 0;
}
const filter = {
kinds: [kind],
authors: [normalizedPubkey],
+14 -6
View File
@@ -477,12 +477,20 @@ class HashtagPreferencesService {
// fetchListIncrementally takes a single kind.
// kinds is array of [canonicalKind, legacyKind] if different.
const promises = kinds.map(kind => nostrClient.fetchListIncrementally({
kind,
pubkey: normalized,
dTag: HASHTAG_IDENTIFIER,
relayUrls: relays
}));
// Anchor the fetch to the service's current state to prevent desync.
// If we have cached data, we ask for updates since that timestamp.
// If we have no data, we force a full fetch (since=0).
const since = wasLoadedForUser ? Number(this.eventCreatedAt) || 0 : 0;
const promises = kinds.map((kind) =>
nostrClient.fetchListIncrementally({
kind,
pubkey: normalized,
dTag: HASHTAG_IDENTIFIER,
relayUrls: relays,
since,
}),
);
const results = await Promise.all(promises);
events = results.flat();
+34
View File
@@ -85,6 +85,40 @@ describe("NostrClient", () => {
// pool.list is called with ([url], [filter]), so arguments[1] is an array
assert.equal(filter[0].since, 101);
});
it("should override lastSeen with explicit since parameter", async () => {
// Mock syncMetadataStore to return something high
client.syncMetadataStore.getLastSeen = () => 500;
await client.fetchListIncrementally({
kind: 1,
pubkey: "abc",
relayUrls: ["wss://relay.example.com"],
since: 100 // Explicit override, should be used instead of 500
});
// Verify filter used 'since + 1'
const call = mockPool.list.mock.calls[0];
const filter = call.arguments[1];
assert.equal(filter[0].since, 101);
});
it("should force full fetch if since is 0", async () => {
// Mock syncMetadataStore to return something
client.syncMetadataStore.getLastSeen = () => 500;
await client.fetchListIncrementally({
kind: 1,
pubkey: "abc",
relayUrls: ["wss://relay.example.com"],
since: 0 // Force full fetch
});
const call = mockPool.list.mock.calls[0];
const filter = call.arguments[1];
// Should NOT have 'since' property
assert.equal(filter[0].since, undefined);
});
});
describe("subscribeVideos", () => {