Add fast relay fallback for list fetches

This commit is contained in:
thePR0M3TH3AN committed 2026-01-28 15:28:58 -05:00
1 parent bcb9f13751
commit e4ad07b2cf
3 files changed
+149 -61

No files matched your search

+41 -14
View File
@@ -27,6 +27,9 @@ const HEX64_REGEX = /^[0-9a-f]{64}$/i;
const DEFAULT_VERSION = 1;
const DECRYPT_TIMEOUT_MS = 90000;
const DECRYPT_RETRY_DELAY_MS = 10000;
const FAST_RELAY_LIMIT = 3;
const FAST_RELAY_TIMEOUT_MS = 4000;
const FULL_RELAY_TIMEOUT_MS = 12000;
class TinyEventEmitter {
constructor() {
@@ -448,7 +451,8 @@ class HashtagPreferencesService {
);
}
async load(pubkey) {
async load(pubkey, options = {}) {
const { skipFastRelays = false, isBackground = false, skipCache = false } = options;
const normalized = normalizeHexPubkey(pubkey);
let wasLoadedForUser =
this.activePubkey &&
@@ -464,7 +468,7 @@ class HashtagPreferencesService {
}
// Try cache first
if (this.loadFromCache(normalized)) {
if (!skipCache && this.loadFromCache(normalized)) {
wasLoadedForUser = true;
}
@@ -523,20 +527,43 @@ class HashtagPreferencesService {
// 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 fastRelays = relays.slice(0, FAST_RELAY_LIMIT);
const shouldUseFastRelays =
!skipFastRelays && fastRelays.length > 0 && fastRelays.length < relays.length;
const initialRelays = shouldUseFastRelays ? fastRelays : relays;
const initialTimeoutMs = shouldUseFastRelays
? FAST_RELAY_TIMEOUT_MS
: FULL_RELAY_TIMEOUT_MS;
const promises = kinds.map((kind) =>
nostrClient.fetchListIncrementally({
kind,
pubkey: normalized,
dTag: HASHTAG_IDENTIFIER,
relayUrls: relays,
since,
timeoutMs: 12000,
}),
);
const fetchFromRelays = async (relayList, timeoutMs) => {
const promises = kinds.map((kind) =>
nostrClient.fetchListIncrementally({
kind,
pubkey: normalized,
dTag: HASHTAG_IDENTIFIER,
relayUrls: relayList,
since,
timeoutMs,
}),
);
const results = await Promise.all(promises);
events = results.flat();
const results = await Promise.all(promises);
return results.flat();
};
events = await fetchFromRelays(initialRelays, initialTimeoutMs);
if (!events.length && shouldUseFastRelays && !isBackground) {
devLogger.log(
`${LOG_PREFIX} Fast relay fetch returned no events; continuing in background.`,
);
this.load(normalized, {
skipFastRelays: true,
isBackground: true,
skipCache: true,
}).catch((error) => {
devLogger.warn(`${LOG_PREFIX} Background refresh failed`, error);
});
}
} catch (error) {
fetchError = error;
+61 -26
View File
@@ -38,6 +38,9 @@ const SUBSCRIPTION_SET_KIND =
getNostrEventSchema(NOTE_TYPES.SUBSCRIPTION_LIST)?.kind ?? 30000;
const DECRYPT_TIMEOUT_MS = 90000;
const DECRYPT_RETRY_DELAY_MS = 10000;
const FAST_RELAY_LIMIT = 3;
const FAST_RELAY_TIMEOUT_MS = 4000;
const FULL_RELAY_TIMEOUT_MS = 12000;
function normalizeHexPubkey(value) {
if (typeof value !== "string") {
@@ -448,14 +451,16 @@ class SubscriptionsManager {
);
}
async updateFromRelays(userPubkey) {
async updateFromRelays(userPubkey, options = {}) {
if (!userPubkey) return;
try {
const normalizedUserPubkey = normalizeHexPubkey(userPubkey) || userPubkey;
const { relayUrlsOverride, skipFastRelays = false, isBackground = false } = options;
const readRelays = relayManager.getReadRelayUrls();
const relayUrls = readRelays.length > 0 ? readRelays : Array.from(DEFAULT_RELAY_URLS);
const fallbackRelayUrls = readRelays.length > 0 ? readRelays : Array.from(DEFAULT_RELAY_URLS);
const relayUrls = relayUrlsOverride?.length ? relayUrlsOverride : fallbackRelayUrls;
if (!relayUrls.length) {
devLogger.warn(
"[SubscriptionsManager] No relay URLs available while loading subscriptions.",
@@ -463,45 +468,75 @@ class SubscriptionsManager {
return;
}
const fastRelayUrls = relayUrls.slice(0, FAST_RELAY_LIMIT);
const shouldUseFastRelays =
!skipFastRelays && fastRelayUrls.length > 0 && fastRelayUrls.length < relayUrls.length;
const initialRelayUrls = shouldUseFastRelays ? fastRelayUrls : relayUrls;
const initialTimeoutMs = shouldUseFastRelays
? FAST_RELAY_TIMEOUT_MS
: FULL_RELAY_TIMEOUT_MS;
// Use incremental fetch helper
const cachedSnapshot = parseCachedSubscriptionSnapshot(
profileCache.getProfileData(normalizedUserPubkey, "subscriptions"),
);
const shouldForceFullFetch = !cachedSnapshot.hasSnapshot;
let events = await nostrClient.fetchListIncrementally({
kind: SUBSCRIPTION_SET_KIND,
pubkey: normalizedUserPubkey,
dTag: SUBSCRIPTION_LIST_IDENTIFIER,
relayUrls,
since: shouldForceFullFetch ? 0 : undefined,
timeoutMs: 12000,
});
const normalizedSessionActorPubkey = normalizeNostrPubkey(
nostrClient?.sessionActor?.pubkey,
);
const shouldFetchSessionActor =
normalizedSessionActorPubkey &&
normalizedSessionActorPubkey !== normalizedUserPubkey;
if (shouldFetchSessionActor) {
const sessionCachedSnapshot = parseCachedSubscriptionSnapshot(
profileCache.getProfileData(
normalizedSessionActorPubkey,
"subscriptions",
),
);
const shouldForceSessionFetch = !sessionCachedSnapshot.hasSnapshot;
const sessionEvents = await nostrClient.fetchListIncrementally({
const fetchFromRelays = async (relayList, timeoutMs) => {
let events = await nostrClient.fetchListIncrementally({
kind: SUBSCRIPTION_SET_KIND,
pubkey: normalizedSessionActorPubkey,
pubkey: normalizedUserPubkey,
dTag: SUBSCRIPTION_LIST_IDENTIFIER,
relayUrls,
since: shouldForceSessionFetch ? 0 : undefined,
timeoutMs: 12000,
relayUrls: relayList,
since: shouldForceFullFetch ? 0 : undefined,
timeoutMs,
});
if (sessionEvents.length) {
events = events.concat(sessionEvents);
if (shouldFetchSessionActor) {
const sessionCachedSnapshot = parseCachedSubscriptionSnapshot(
profileCache.getProfileData(
normalizedSessionActorPubkey,
"subscriptions",
),
);
const shouldForceSessionFetch = !sessionCachedSnapshot.hasSnapshot;
const sessionEvents = await nostrClient.fetchListIncrementally({
kind: SUBSCRIPTION_SET_KIND,
pubkey: normalizedSessionActorPubkey,
dTag: SUBSCRIPTION_LIST_IDENTIFIER,
relayUrls: relayList,
since: shouldForceSessionFetch ? 0 : undefined,
timeoutMs,
});
if (sessionEvents.length) {
events = events.concat(sessionEvents);
}
}
return events;
};
let events = await fetchFromRelays(initialRelayUrls, initialTimeoutMs);
if (!events.length && shouldUseFastRelays && !isBackground) {
devLogger.log(
"[SubscriptionsManager] Fast relay fetch returned no events; continuing in background.",
);
this.updateFromRelays(normalizedUserPubkey, {
relayUrlsOverride: relayUrls,
skipFastRelays: true,
isBackground: true,
}).catch((error) => {
devLogger.warn(
"[SubscriptionsManager] Background relay refresh failed:",
error,
);
});
}
const mergedEvents = [];
+47 -21
View File
@@ -73,7 +73,7 @@ export const USER_BLOCK_EVENTS = Object.freeze({
const FAST_BLOCKLIST_RELAY_LIMIT = 3;
const FAST_BLOCKLIST_TIMEOUT_MS = 2500;
const BACKGROUND_BLOCKLIST_TIMEOUT_MS = 6000;
const FULL_BLOCKLIST_TIMEOUT_MS = 12000;
const DECRYPT_TIMEOUT_MS = 90000;
const DECRYPT_RETRY_DELAY_MS = 10000;
@@ -751,28 +751,39 @@ class UserBlockListManager {
return;
}
try {
statusCallback?.(detail);
} catch (callbackError) {
userLogger.warn(
"[UserBlockList] statusCallback threw while emitting status",
callbackError,
);
}
if (!options.isBackground) {
try {
statusCallback?.(detail);
} catch (callbackError) {
userLogger.warn(
"[UserBlockList] statusCallback threw while emitting status",
callbackError,
);
}
try {
this.emitter.emit(USER_BLOCK_EVENTS.STATUS, detail);
} catch (emitterError) {
userLogger.warn(
"[UserBlockList] Failed to dispatch status event",
emitterError,
);
try {
this.emitter.emit(USER_BLOCK_EVENTS.STATUS, detail);
} catch (emitterError) {
userLogger.warn(
"[UserBlockList] Failed to dispatch status event",
emitterError,
);
}
}
};
const readRelays = relayManager.getReadRelayUrls();
const relays = readRelays.length > 0 ? readRelays : Array.from(DEFAULT_RELAY_URLS);
emitStatus({ status: "loading", relays });
const fastRelays = relays.slice(0, FAST_BLOCKLIST_RELAY_LIMIT);
const shouldUseFastRelays =
!options.skipFastRelays &&
fastRelays.length > 0 &&
fastRelays.length < relays.length;
const initialRelays = shouldUseFastRelays ? fastRelays : relays;
const initialTimeoutMs = shouldUseFastRelays
? FAST_BLOCKLIST_TIMEOUT_MS
: FULL_BLOCKLIST_TIMEOUT_MS;
emitStatus({ status: "loading", relays: initialRelays });
const localBlocks = this._loadLocal(normalized);
const hasLocalData = !!localBlocks;
@@ -1206,17 +1217,17 @@ class UserBlockListManager {
nostrClient.fetchListIncrementally({
kind: 10000,
pubkey: normalized,
relayUrls: relays,
relayUrls: initialRelays,
since: fetchSince,
timeoutMs: 12000,
timeoutMs: initialTimeoutMs,
}),
nostrClient.fetchListIncrementally({
kind: 10000,
pubkey: normalized,
dTag: BLOCK_LIST_IDENTIFIER,
relayUrls: relays,
relayUrls: initialRelays,
since: fetchSince,
timeoutMs: 12000,
timeoutMs: initialTimeoutMs,
}),
]);
@@ -1224,6 +1235,21 @@ class UserBlockListManager {
// If combined is empty, check if we have loaded state
if (!combined.length) {
if (shouldUseFastRelays && !options.isBackground) {
devLogger.log(
"[UserBlockList] Fast relay fetch returned no events; continuing in background.",
);
this.loadBlocks(normalized, {
...options,
skipFastRelays: true,
isBackground: true,
}).catch((error) => {
devLogger.warn(
"[UserBlockList] Background relay refresh failed:",
error,
);
});
}
if (!this.loaded) {
// Assuming empty if we have no prior state
await applyEvents([], { source: "incremental" });