Merge pull request #390 from PR0M3TH3AN/codex/new-task

Speed up profile and relay fetches
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2025-10-09 19:05:51 -04:00
commit b5ee063260
3 files changed
+547 -103

No files matched your search

+170 -30
View File
@@ -8,6 +8,10 @@ import {
const MODE_SEQUENCE = ["both", "read", "write"];
const FAST_RELAY_FETCH_LIMIT = 3;
const FAST_RELAY_TIMEOUT_MS = 2500;
const BACKGROUND_RELAY_TIMEOUT_MS = 6000;
function normalizeHexPubkey(pubkey) {
if (typeof pubkey !== "string") {
return null;
@@ -413,44 +417,180 @@ class RelayPreferencesManager {
const filter = { kinds: [10002], authors: [normalizedPubkey], limit: 1 };
const targetRelays = this.getPublishTargets();
const events = [];
await Promise.all(
targetRelays.map(async (relayUrl) => {
try {
const result = await nostrClient.pool.list([relayUrl], [filter]);
if (Array.isArray(result) && result.length) {
events.push(...result);
}
} catch (error) {
if (isDevMode) {
console.warn(`[relayManager] Failed to fetch relay list from ${relayUrl}:`, error);
}
const applyEvents = (events, { skipIfEmpty = false } = {}) => {
if (!Array.isArray(events) || !events.length) {
if (skipIfEmpty) {
return null;
}
})
);
this.lastEvent = null;
this.lastLoadSource = "default";
this.setEntries(this.defaultEntries, { allowEmpty: false });
return { ok: true, source: "default", events: [] };
}
const sorted = events
.filter((event) => event && event.pubkey === normalizedPubkey)
.sort((a, b) => (b?.created_at || 0) - (a?.created_at || 0));
if (!sorted.length) {
if (skipIfEmpty) {
return null;
}
this.lastEvent = null;
this.lastLoadSource = "default";
this.setEntries(this.defaultEntries, { allowEmpty: false });
return { ok: true, source: "default", events: [] };
}
const newest = sorted[0];
this.lastEvent = newest;
const parsed = parseRelayTags(newest?.tags);
if (parsed.length) {
this.setEntries(parsed, { allowEmpty: false });
this.lastLoadSource = "event";
return { ok: true, source: "event", event: newest };
}
if (!events.length) {
this.lastEvent = null;
this.lastLoadSource = "default";
this.setEntries(this.defaultEntries, { allowEmpty: false });
return { ok: true, source: "default", events: [] };
return { ok: true, source: "default", event: newest };
};
const fetchFromRelay = (relayUrl, timeoutMs, requireEvent) => {
return new Promise((resolve, reject) => {
let settled = false;
const timer = setTimeout(() => {
if (settled) {
return;
}
settled = true;
const timeoutError = new Error(
`Timed out fetching relay list from ${relayUrl} after ${timeoutMs}ms`
);
timeoutError.code = "timeout";
timeoutError.relay = relayUrl;
timeoutError.timeoutMs = timeoutMs;
reject(timeoutError);
}, timeoutMs);
Promise.resolve()
.then(() => nostrClient.pool.list([relayUrl], [filter]))
.then((result) => {
if (settled) {
return;
}
settled = true;
clearTimeout(timer);
const events = Array.isArray(result)
? result.filter((event) => event && event.pubkey === normalizedPubkey)
: [];
if (requireEvent && !events.length) {
const emptyError = new Error(
`No relay list events returned from ${relayUrl}`
);
emptyError.code = "empty";
emptyError.relay = relayUrl;
reject(emptyError);
return;
}
resolve({ relayUrl, events });
})
.catch((error) => {
if (settled) {
return;
}
settled = true;
clearTimeout(timer);
const wrapped =
error instanceof Error ? error : new Error(String(error));
wrapped.relay = relayUrl;
reject(wrapped);
});
});
};
const fastRelays = targetRelays.slice(0, FAST_RELAY_FETCH_LIMIT);
const backgroundRelays = targetRelays.slice(fastRelays.length);
const fastPromises = fastRelays.map((relayUrl) =>
fetchFromRelay(relayUrl, FAST_RELAY_TIMEOUT_MS, true)
);
const backgroundPromises = backgroundRelays.map((relayUrl) =>
fetchFromRelay(relayUrl, BACKGROUND_RELAY_TIMEOUT_MS, false)
);
const background = Promise.allSettled([
...fastPromises,
...backgroundPromises,
])
.then((outcomes) => {
const aggregated = [];
outcomes.forEach((outcome) => {
if (outcome.status === "fulfilled") {
const events = Array.isArray(outcome.value?.events)
? outcome.value.events
: [];
if (events.length) {
aggregated.push(...events);
}
} else {
const reason = outcome.reason;
if (reason?.code === "timeout") {
if (isDevMode) {
console.warn(
`[relayManager] Relay ${reason.relay} timed out while loading relay list (${reason.timeoutMs}ms)`
);
}
} else if (isDevMode) {
console.warn(
`[relayManager] Relay ${reason?.relay || "unknown"} failed while loading relay list:`,
reason
);
}
}
});
if (!aggregated.length) {
return;
}
applyEvents(aggregated, { skipIfEmpty: true });
})
.catch((error) => {
if (isDevMode) {
console.warn("[relayManager] Background relay refresh failed", error);
}
});
let fastResult = null;
if (fastPromises.length) {
try {
fastResult = await Promise.any(fastPromises);
} catch (error) {
if (error instanceof AggregateError) {
error.errors?.forEach((err) => {
if (err?.code === "timeout" && isDevMode) {
console.warn(
`[relayManager] Relay ${err.relay} timed out while loading relay list (${err.timeoutMs}ms)`
);
}
});
} else if (isDevMode) {
console.warn("[relayManager] Fast relay fetch failed", error);
}
}
}
events.sort((a, b) => (b?.created_at || 0) - (a?.created_at || 0));
const newest = events[0];
this.lastEvent = newest;
const parsed = parseRelayTags(newest?.tags);
if (parsed.length) {
this.setEntries(parsed, { allowEmpty: false });
this.lastLoadSource = "event";
return { ok: true, source: "event", event: newest };
if (fastResult?.events?.length) {
const result = applyEvents(fastResult.events);
background.catch(() => {});
return result;
}
this.lastLoadSource = "default";
this.setEntries(this.defaultEntries, { allowEmpty: false });
return { ok: true, source: "default", event: newest };
const fallback = applyEvents([], { skipIfEmpty: false });
background.catch(() => {});
return fallback;
}
async publishRelayList(pubkey, options = {}) {
+178 -10
View File
@@ -72,6 +72,10 @@ const FALLBACK_PROFILE = {
lud06: "",
};
const FAST_PROFILE_RELAY_LIMIT = 3;
const FAST_PROFILE_TIMEOUT_MS = 2500;
const BACKGROUND_PROFILE_TIMEOUT_MS = 6000;
export default class AuthService {
constructor({ nostrClient, userBlocks, relayManager, logger } = {}) {
this.nostrClient = nostrClient || null;
@@ -655,15 +659,37 @@ export default class AuthService {
return FALLBACK_PROFILE;
}
const events = await this.nostrClient.pool.list(this.nostrClient.relays, [
{ kinds: [0], authors: [normalized], limit: 1 },
]);
const relays = Array.isArray(this.nostrClient.relays)
? this.nostrClient.relays.filter((url) => typeof url === "string" && url)
: [];
let profile = FALLBACK_PROFILE;
if (Array.isArray(events) && events.length && events[0]?.content) {
if (!relays.length) {
this.setProfileCacheEntry(normalized, FALLBACK_PROFILE, {
persist: true,
reason: "load-own-profile:fallback",
});
return FALLBACK_PROFILE;
}
const filter = [{ kinds: [0], authors: [normalized], limit: 1 }];
const selectNewestEvent = (eventList) => {
if (!Array.isArray(eventList) || !eventList.length) {
return null;
}
const sorted = eventList
.filter((event) => event && event.pubkey === normalized)
.sort((a, b) => (b?.created_at || 0) - (a?.created_at || 0));
return sorted.length ? sorted[0] : null;
};
const buildProfileFromEvent = (event) => {
if (!event?.content) {
return FALLBACK_PROFILE;
}
try {
const data = JSON.parse(events[0].content);
profile = {
const data = JSON.parse(event.content);
return {
name: data.display_name || data.name || FALLBACK_PROFILE.name,
picture: data.picture || FALLBACK_PROFILE.picture,
about: typeof data.about === "string" ? data.about : FALLBACK_PROFILE.about,
@@ -676,15 +702,157 @@ export default class AuthService {
};
} catch (error) {
this.log("[AuthService] Failed to parse profile metadata", error);
return FALLBACK_PROFILE;
}
};
const fetchFromRelay = (relayUrl, timeoutMs, requireEvent) => {
return new Promise((resolve, reject) => {
let settled = false;
const timer = setTimeout(() => {
if (settled) {
return;
}
settled = true;
const timeoutError = new Error(
`Timed out fetching profile from ${relayUrl} after ${timeoutMs}ms`
);
timeoutError.code = "timeout";
timeoutError.relay = relayUrl;
timeoutError.timeoutMs = timeoutMs;
reject(timeoutError);
}, timeoutMs);
Promise.resolve()
.then(() => this.nostrClient.pool.list([relayUrl], filter))
.then((result) => {
if (settled) {
return;
}
settled = true;
clearTimeout(timer);
const events = Array.isArray(result)
? result.filter((event) => event && event.pubkey === normalized)
: [];
if (requireEvent && !events.length) {
const emptyError = new Error(
`No profile events returned from ${relayUrl}`
);
emptyError.code = "empty";
emptyError.relay = relayUrl;
reject(emptyError);
return;
}
resolve({ relayUrl, events });
})
.catch((error) => {
if (settled) {
return;
}
settled = true;
clearTimeout(timer);
const wrapped =
error instanceof Error ? error : new Error(String(error));
wrapped.relay = relayUrl;
reject(wrapped);
});
});
};
const fastRelays = relays.slice(0, FAST_PROFILE_RELAY_LIMIT);
const backgroundRelays = relays.slice(fastRelays.length);
const fastPromises = fastRelays.map((relayUrl) =>
fetchFromRelay(relayUrl, FAST_PROFILE_TIMEOUT_MS, true)
);
const backgroundPromises = backgroundRelays.map((relayUrl) =>
fetchFromRelay(relayUrl, BACKGROUND_PROFILE_TIMEOUT_MS, false)
);
const background = Promise.allSettled([
...fastPromises,
...backgroundPromises,
])
.then((outcomes) => {
const aggregated = [];
outcomes.forEach((outcome) => {
if (outcome.status === "fulfilled") {
const events = Array.isArray(outcome.value?.events)
? outcome.value.events
: [];
if (events.length) {
aggregated.push(...events);
}
} else if (outcome.status === "rejected") {
const reason = outcome.reason;
if (reason?.code === "timeout") {
this.log(
`[AuthService] Relay ${reason.relay} timed out loading profile (background)`
);
} else {
this.log(
`[AuthService] Relay ${reason?.relay || "unknown"} failed loading profile (background)`,
reason
);
}
}
});
const newest = selectNewestEvent(aggregated);
if (!newest) {
return;
}
const profile = buildProfileFromEvent(newest);
this.setProfileCacheEntry(normalized, profile, {
persist: true,
reason: "load-own-profile:background",
});
})
.catch((error) => {
this.log("[AuthService] Background profile refresh failed", error);
});
let fastResult = null;
if (fastPromises.length) {
try {
fastResult = await Promise.any(fastPromises);
} catch (error) {
if (error instanceof AggregateError) {
error.errors?.forEach((err) => {
if (err?.code === "timeout") {
this.log(
`[AuthService] Relay ${err.relay} timed out loading profile`
);
}
});
} else {
this.log("[AuthService] Failed to load profile from fast relays", error);
}
}
}
this.setProfileCacheEntry(normalized, profile, {
if (fastResult?.events?.length) {
const newest = selectNewestEvent(fastResult.events);
if (newest) {
const profile = buildProfileFromEvent(newest);
this.setProfileCacheEntry(normalized, profile, {
persist: true,
reason: "load-own-profile:fast",
});
background.catch(() => {});
return profile;
}
}
this.setProfileCacheEntry(normalized, FALLBACK_PROFILE, {
persist: true,
reason: "load-own-profile",
reason: "load-own-profile:fallback",
});
return profile;
background.catch(() => {});
return FALLBACK_PROFILE;
}
async fetchProfile(pubkey, { forceRefresh = false } = {}) {
+199 -63
View File
@@ -6,6 +6,10 @@ import {
assertAnyRelayAccepted,
} from "./nostrPublish.js";
const FAST_BLOCKLIST_RELAY_LIMIT = 3;
const FAST_BLOCKLIST_TIMEOUT_MS = 2500;
const BACKGROUND_BLOCKLIST_TIMEOUT_MS = 6000;
function normalizeHex(pubkey) {
if (typeof pubkey !== "string") {
return null;
@@ -86,82 +90,214 @@ class UserBlockListManager {
limit: 1,
};
const events = [];
const relays = Array.isArray(nostrClient.relays)
? nostrClient.relays
? nostrClient.relays.filter((relay) => typeof relay === "string" && relay)
: [];
const results = await Promise.allSettled(
relays.map((relay) =>
Promise.resolve()
.then(() => nostrClient.pool.list([relay], [filter]))
.then((res) => ({ relay, res }))
.catch((error) => {
throw { relay, error };
})
)
);
for (const outcome of results) {
if (outcome.status === "fulfilled") {
const { res } = outcome.value || {};
if (Array.isArray(res) && res.length) {
events.push(...res);
}
} else {
const { relay, error } = outcome.reason || {};
console.error(
`[UserBlockList] Relay error at ${relay}:`,
error ?? outcome.reason
);
}
}
if (!events.length) {
if (!relays.length) {
this.blockedPubkeys.clear();
this.blockEventId = null;
this.loaded = true;
return;
}
events.sort((a, b) => b.created_at - a.created_at);
const newest = events[0];
this.blockEventId = newest.id;
const fastRelays = relays.slice(0, FAST_BLOCKLIST_RELAY_LIMIT);
const backgroundRelays = relays.slice(fastRelays.length);
let decrypted = "";
try {
decrypted = await window.nostr.nip04.decrypt(
normalized,
newest.content
);
} catch (err) {
console.error("[UserBlockList] Failed to decrypt block list:", err);
this.blockedPubkeys.clear();
this.loaded = true;
const fetchFromRelay = (relayUrl, timeoutMs, requireEvent) =>
new Promise((resolve, reject) => {
let settled = false;
const timer = setTimeout(() => {
if (settled) {
return;
}
settled = true;
const timeoutError = new Error(
`Timed out fetching block list from ${relayUrl} after ${timeoutMs}ms`
);
timeoutError.code = "timeout";
timeoutError.relay = relayUrl;
timeoutError.timeoutMs = timeoutMs;
reject(timeoutError);
}, timeoutMs);
Promise.resolve()
.then(() => nostrClient.pool.list([relayUrl], [filter]))
.then((result) => {
if (settled) {
return;
}
settled = true;
clearTimeout(timer);
const events = Array.isArray(result)
? result.filter((event) => event && event.pubkey === normalized)
: [];
if (requireEvent && !events.length) {
const emptyError = new Error(
`No block list events returned from ${relayUrl}`
);
emptyError.code = "empty";
emptyError.relay = relayUrl;
reject(emptyError);
return;
}
resolve({ relayUrl, events });
})
.catch((error) => {
if (settled) {
return;
}
settled = true;
clearTimeout(timer);
const wrapped =
error instanceof Error ? error : new Error(String(error));
wrapped.relay = relayUrl;
reject(wrapped);
});
});
const fastPromises = fastRelays.map((relayUrl) =>
fetchFromRelay(relayUrl, FAST_BLOCKLIST_TIMEOUT_MS, true)
);
const backgroundPromises = backgroundRelays.map((relayUrl) =>
fetchFromRelay(relayUrl, BACKGROUND_BLOCKLIST_TIMEOUT_MS, false)
);
const applyEvents = async (events, { skipIfEmpty = false } = {}) => {
if (!Array.isArray(events) || !events.length) {
if (skipIfEmpty) {
return;
}
this.blockedPubkeys.clear();
this.blockEventId = null;
return;
}
const sorted = events
.filter((event) => event && event.pubkey === normalized)
.sort((a, b) => (b?.created_at || 0) - (a?.created_at || 0));
if (!sorted.length) {
if (skipIfEmpty) {
return;
}
this.blockedPubkeys.clear();
this.blockEventId = null;
return;
}
const newest = sorted[0];
if (newest?.id && newest.id === this.blockEventId && this.blockedPubkeys.size) {
return;
}
this.blockEventId = newest?.id || null;
if (!newest?.content) {
this.blockedPubkeys.clear();
return;
}
let decrypted = "";
try {
decrypted = await window.nostr.nip04.decrypt(normalized, newest.content);
} catch (err) {
console.error("[UserBlockList] Failed to decrypt block list:", err);
this.blockedPubkeys.clear();
return;
}
try {
const parsed = JSON.parse(decrypted);
const list = Array.isArray(parsed?.blockedPubkeys)
? parsed.blockedPubkeys
: [];
const sanitized = list
.map((entry) => normalizeHex(entry))
.filter((candidate) => {
if (!candidate) {
return false;
}
if (candidate === normalized) {
return false;
}
return true;
});
this.blockedPubkeys = new Set(sanitized);
} catch (err) {
console.error("[UserBlockList] Failed to parse block list:", err);
this.blockedPubkeys.clear();
}
};
const background = Promise.allSettled([
...fastPromises,
...backgroundPromises,
])
.then(async (outcomes) => {
const aggregated = [];
for (const outcome of outcomes) {
if (outcome.status === "fulfilled") {
const events = Array.isArray(outcome.value?.events)
? outcome.value.events
: [];
if (events.length) {
aggregated.push(...events);
}
} else {
const reason = outcome.reason;
if (reason?.code === "timeout") {
console.warn(
`[UserBlockList] Relay ${reason.relay} timed out while loading block list (${reason.timeoutMs}ms)`
);
} else {
const relay = reason?.relay || reason?.relayUrl;
console.error(
`[UserBlockList] Relay error at ${relay}:`,
reason?.error ?? reason
);
}
}
}
if (!aggregated.length) {
return;
}
await applyEvents(aggregated, { skipIfEmpty: true });
})
.catch((error) => {
console.error("[UserBlockList] background block list refresh failed:", error);
});
let fastResult = null;
if (fastPromises.length) {
try {
fastResult = await Promise.any(fastPromises);
} catch (error) {
if (error instanceof AggregateError) {
error.errors?.forEach((err) => {
if (err?.code === "timeout") {
console.warn(
`[UserBlockList] Relay ${err.relay} timed out while loading block list (${err.timeoutMs}ms)`
);
}
});
} else {
console.error("[UserBlockList] Fast block list fetch failed:", error);
}
}
}
if (fastResult?.events?.length) {
await applyEvents(fastResult.events);
background.catch(() => {});
return;
}
try {
const parsed = JSON.parse(decrypted);
const list = Array.isArray(parsed?.blockedPubkeys)
? parsed.blockedPubkeys
: [];
const sanitized = list
.map((entry) => normalizeHex(entry))
.filter((candidate) => {
if (!candidate) {
return false;
}
if (candidate === normalized) {
return false;
}
return true;
});
this.blockedPubkeys = new Set(sanitized);
} catch (err) {
console.error("[UserBlockList] Failed to parse block list:", err);
this.blockedPubkeys.clear();
}
this.blockedPubkeys.clear();
this.blockEventId = null;
background.catch(() => {});
} catch (error) {
console.error("[UserBlockList] loadBlocks failed:", error);
this.blockedPubkeys.clear();