mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
Consolidate profile metadata fetches
This commit is contained in:
1 parent
d698eff9cd
commit
89b8d124fa
6 files changed
+360
-148
No files matched your search
@@ -26,6 +26,7 @@ import { profileCache } from "../state/profileCache.js";
|
||||
import getDefaultAuthProvider, {
|
||||
providers as defaultAuthProviders,
|
||||
} from "./authProviders/index.js";
|
||||
import { fetchProfileMetadata } from "./profileMetadataService.js";
|
||||
|
||||
class SimpleEventEmitter {
|
||||
constructor(logger = null) {
|
||||
@@ -1158,44 +1159,16 @@ export default class AuthService {
|
||||
}
|
||||
|
||||
try {
|
||||
const results = await Promise.all(
|
||||
this.nostrClient.relays.map((relayUrl) =>
|
||||
this.nostrClient.pool.list([relayUrl], [
|
||||
{ kinds: [0], authors: [normalized], limit: 1 },
|
||||
])
|
||||
)
|
||||
);
|
||||
const result = await fetchProfileMetadata(normalized, {
|
||||
nostr: this.nostrClient,
|
||||
});
|
||||
|
||||
const events = results.flat();
|
||||
let newest = null;
|
||||
for (const event of events) {
|
||||
if (!event || event.pubkey !== normalized || !event.content) {
|
||||
continue;
|
||||
}
|
||||
if (!newest || event.created_at > newest.created_at) {
|
||||
newest = event;
|
||||
}
|
||||
if (result?.event && typeof this.nostrClient?.handleEvent === "function") {
|
||||
this.nostrClient.handleEvent(result.event);
|
||||
}
|
||||
|
||||
if (newest) {
|
||||
if (this.nostrClient && typeof this.nostrClient.handleEvent === "function") {
|
||||
this.nostrClient.handleEvent(newest);
|
||||
}
|
||||
if (newest.content) {
|
||||
const data = JSON.parse(newest.content);
|
||||
const profile = {
|
||||
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,
|
||||
website:
|
||||
typeof data.website === "string" ? data.website : FALLBACK_PROFILE.website,
|
||||
banner:
|
||||
typeof data.banner === "string" ? data.banner : FALLBACK_PROFILE.banner,
|
||||
lud16: typeof data.lud16 === "string" ? data.lud16 : FALLBACK_PROFILE.lud16,
|
||||
lud06: typeof data.lud06 === "string" ? data.lud06 : FALLBACK_PROFILE.lud06,
|
||||
};
|
||||
return profile;
|
||||
}
|
||||
if (result?.profile) {
|
||||
return result.profile;
|
||||
}
|
||||
} catch (error) {
|
||||
this.log("[AuthService] Failed to fetch profile", error);
|
||||
|
||||
@@ -241,6 +241,8 @@ class HashtagPreferencesService {
|
||||
this.backgroundLoading = false;
|
||||
this.preferencesVersion = DEFAULT_VERSION;
|
||||
this.decryptRetryTimeoutId = null;
|
||||
this.loadPromise = null;
|
||||
this.loadingPubkey = null;
|
||||
|
||||
profileCache.subscribe((event, detail) => {
|
||||
if (event === "profileChanged") {
|
||||
@@ -273,6 +275,8 @@ class HashtagPreferencesService {
|
||||
clearTimeout(this.decryptRetryTimeoutId);
|
||||
this.decryptRetryTimeoutId = null;
|
||||
}
|
||||
this.loadPromise = null;
|
||||
this.loadingPubkey = null;
|
||||
this.emitChange("reset");
|
||||
}
|
||||
|
||||
@@ -452,6 +456,38 @@ class HashtagPreferencesService {
|
||||
|
||||
async load(pubkey, options = {}) {
|
||||
const normalized = normalizeHexPubkey(pubkey);
|
||||
|
||||
if (this.loadPromise && this.loadingPubkey === normalized) {
|
||||
devLogger.log(`${LOG_PREFIX} Reusing in-flight preferences load.`, {
|
||||
pubkey: normalized,
|
||||
});
|
||||
return this.loadPromise;
|
||||
}
|
||||
|
||||
if (this.loadPromise && this.loadingPubkey && this.loadingPubkey !== normalized) {
|
||||
devLogger.log(`${LOG_PREFIX} Waiting for existing preferences load to settle.`, {
|
||||
previous: this.loadingPubkey,
|
||||
next: normalized,
|
||||
});
|
||||
await this.loadPromise.catch(() => {});
|
||||
}
|
||||
|
||||
const loadPromise = this.loadInternal(normalized, options);
|
||||
this.loadPromise = loadPromise;
|
||||
this.loadingPubkey = normalized;
|
||||
|
||||
try {
|
||||
return await loadPromise;
|
||||
} finally {
|
||||
if (this.loadPromise === loadPromise) {
|
||||
this.loadPromise = null;
|
||||
this.loadingPubkey = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async loadInternal(pubkey, options = {}) {
|
||||
const normalized = normalizeHexPubkey(pubkey);
|
||||
const allowPermissionPrompt = options?.allowPermissionPrompt !== false;
|
||||
let wasLoadedForUser =
|
||||
this.activePubkey &&
|
||||
|
||||
@@ -1032,16 +1032,27 @@ export class NostrService {
|
||||
}
|
||||
|
||||
async ensureDirectMessageSubscription({ actorPubkey, relays, ...handlers } = {}) {
|
||||
if (this.dmSubscription) {
|
||||
return this.dmSubscription;
|
||||
}
|
||||
|
||||
const activeActor = actorPubkey || this.resolveActiveDmActor();
|
||||
const normalizedActor = normalizeHexPubkey(activeActor);
|
||||
if (!normalizedActor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this.dmSubscription) {
|
||||
if (this.dmActorPubkey === normalizedActor) {
|
||||
devLogger.log(
|
||||
"[nostrService] Reusing existing DM subscription for active actor.",
|
||||
{ actorPubkey: normalizedActor },
|
||||
);
|
||||
return this.dmSubscription;
|
||||
}
|
||||
devLogger.log(
|
||||
"[nostrService] Replacing existing DM subscription for new actor.",
|
||||
{ previous: this.dmActorPubkey, next: normalizedActor },
|
||||
);
|
||||
this.stopDirectMessageSubscription();
|
||||
}
|
||||
|
||||
let resolvedRelays = Array.isArray(relays) ? relays : null;
|
||||
let relaySelection = null;
|
||||
|
||||
@@ -1142,6 +1153,10 @@ export class NostrService {
|
||||
|
||||
this.dmSubscription = subscription;
|
||||
this.dmActorPubkey = normalizedActor;
|
||||
devLogger.log("[nostrService] DM subscription activated.", {
|
||||
actorPubkey: normalizedActor,
|
||||
relays: resolvedRelays,
|
||||
});
|
||||
this.emit("directMessages:subscribed", { subscription });
|
||||
if (relaySelection?.warning === DM_RELAY_WARNING_FALLBACK) {
|
||||
this.emit("directMessages:relayWarning", {
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
import { nostrClient } from "../nostrClientFacade.js";
|
||||
import { devLogger } from "../utils/logger.js";
|
||||
|
||||
const DEFAULT_PROFILE_IMAGE = "assets/svg/default-profile.svg";
|
||||
const HEX64_REGEX = /^[0-9a-f]{64}$/i;
|
||||
const inFlight = new Map();
|
||||
|
||||
function normalizePubkey(pubkey) {
|
||||
if (typeof pubkey !== "string") {
|
||||
return "";
|
||||
}
|
||||
|
||||
const trimmed = pubkey.trim();
|
||||
if (!trimmed || !HEX64_REGEX.test(trimmed)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return trimmed.toLowerCase();
|
||||
}
|
||||
|
||||
function resolveRelays(relays, nostr) {
|
||||
if (Array.isArray(relays) && relays.length) {
|
||||
return relays.filter((relay) => typeof relay === "string" && relay.trim());
|
||||
}
|
||||
|
||||
if (Array.isArray(nostr?.readRelays) && nostr.readRelays.length) {
|
||||
return nostr.readRelays.filter((relay) => typeof relay === "string" && relay.trim());
|
||||
}
|
||||
|
||||
if (Array.isArray(nostr?.relays) && nostr.relays.length) {
|
||||
return nostr.relays.filter((relay) => typeof relay === "string" && relay.trim());
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
function parseProfileEvent(event, defaultProfileImage) {
|
||||
if (!event || typeof event !== "object" || !event.content) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const data = JSON.parse(event.content);
|
||||
const profile = {
|
||||
name: data?.display_name || data?.name || "Unknown",
|
||||
picture: data?.picture || defaultProfileImage,
|
||||
};
|
||||
|
||||
const about = typeof data?.about === "string" ? data.about : "";
|
||||
if (about) {
|
||||
profile.about = about;
|
||||
}
|
||||
|
||||
const website = typeof data?.website === "string" ? data.website : "";
|
||||
if (website) {
|
||||
profile.website = website;
|
||||
}
|
||||
|
||||
const banner = typeof data?.banner === "string" ? data.banner : "";
|
||||
if (banner) {
|
||||
profile.banner = banner;
|
||||
}
|
||||
|
||||
const lud16 = typeof data?.lud16 === "string" ? data.lud16 : "";
|
||||
if (lud16) {
|
||||
profile.lud16 = lud16;
|
||||
}
|
||||
|
||||
const lud06 = typeof data?.lud06 === "string" ? data.lud06 : "";
|
||||
if (lud06) {
|
||||
profile.lud06 = lud06;
|
||||
}
|
||||
|
||||
return profile;
|
||||
} catch (error) {
|
||||
devLogger.warn("[profileMetadata] Failed to parse profile event", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function performProfileFetch({
|
||||
pubkeys,
|
||||
nostr,
|
||||
relays,
|
||||
logger,
|
||||
defaultProfileImage,
|
||||
} = {}) {
|
||||
if (!nostr?.pool || typeof nostr.pool.list !== "function") {
|
||||
logger.warn("[profileMetadata] Relay pool is not ready; skipping profile fetch.");
|
||||
return new Map();
|
||||
}
|
||||
|
||||
const relayUrls = resolveRelays(relays, nostr);
|
||||
if (!relayUrls.length) {
|
||||
logger.warn("[profileMetadata] No relays configured; skipping profile fetch.");
|
||||
return new Map();
|
||||
}
|
||||
|
||||
const filter = {
|
||||
kinds: [0],
|
||||
authors: pubkeys,
|
||||
limit: pubkeys.length,
|
||||
};
|
||||
|
||||
const newestProfiles = new Map();
|
||||
|
||||
const results = await Promise.allSettled(
|
||||
relayUrls.map((relayUrl) =>
|
||||
nostr.pool
|
||||
.list([relayUrl], [filter])
|
||||
.then((events) => ({ relayUrl, events }))
|
||||
)
|
||||
);
|
||||
|
||||
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}:`,
|
||||
result.reason,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const events = Array.isArray(result.value.events)
|
||||
? result.value.events
|
||||
: [];
|
||||
if (!events.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const event of events) {
|
||||
const pubkey = normalizePubkey(event?.pubkey);
|
||||
if (!pubkey) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const createdAt = Number.isFinite(event?.created_at)
|
||||
? event.created_at
|
||||
: 0;
|
||||
const previous = newestProfiles.get(pubkey);
|
||||
if (previous && previous.createdAt >= createdAt) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const profile = parseProfileEvent(event, defaultProfileImage);
|
||||
if (!profile) {
|
||||
continue;
|
||||
}
|
||||
|
||||
newestProfiles.set(pubkey, {
|
||||
createdAt,
|
||||
profile,
|
||||
event,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return newestProfiles;
|
||||
}
|
||||
|
||||
export async function fetchProfileMetadataBatch({
|
||||
pubkeys,
|
||||
nostr = nostrClient,
|
||||
relays = null,
|
||||
logger = devLogger,
|
||||
defaultProfileImage = DEFAULT_PROFILE_IMAGE,
|
||||
} = {}) {
|
||||
const normalizedPubkeys = Array.isArray(pubkeys)
|
||||
? pubkeys.map(normalizePubkey).filter(Boolean)
|
||||
: [];
|
||||
|
||||
if (!normalizedPubkeys.length) {
|
||||
return new Map();
|
||||
}
|
||||
|
||||
const uniquePubkeys = Array.from(new Set(normalizedPubkeys));
|
||||
const results = new Map();
|
||||
const waiters = [];
|
||||
const toFetch = [];
|
||||
|
||||
uniquePubkeys.forEach((pubkey) => {
|
||||
const existing = inFlight.get(pubkey);
|
||||
if (existing) {
|
||||
logger.debug?.("[profileMetadata] Reusing in-flight profile fetch", {
|
||||
pubkey,
|
||||
});
|
||||
waiters.push(
|
||||
existing.then((entry) => {
|
||||
if (entry) {
|
||||
results.set(pubkey, entry);
|
||||
}
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
toFetch.push(pubkey);
|
||||
});
|
||||
|
||||
let batchPromise = null;
|
||||
if (toFetch.length) {
|
||||
logger.debug?.("[profileMetadata] Fetching profile metadata", {
|
||||
count: toFetch.length,
|
||||
});
|
||||
const fetchPromise = performProfileFetch({
|
||||
pubkeys: toFetch,
|
||||
nostr,
|
||||
relays,
|
||||
logger,
|
||||
defaultProfileImage,
|
||||
});
|
||||
|
||||
toFetch.forEach((pubkey) => {
|
||||
const perPubkeyPromise = fetchPromise
|
||||
.then((map) => map.get(pubkey) || null)
|
||||
.finally(() => {
|
||||
if (inFlight.get(pubkey) === perPubkeyPromise) {
|
||||
inFlight.delete(pubkey);
|
||||
}
|
||||
});
|
||||
inFlight.set(pubkey, perPubkeyPromise);
|
||||
});
|
||||
|
||||
batchPromise = fetchPromise.then((map) => {
|
||||
map.forEach((value, pubkey) => results.set(pubkey, value));
|
||||
});
|
||||
}
|
||||
|
||||
await Promise.all([...waiters, batchPromise].filter(Boolean));
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
export async function fetchProfileMetadata(pubkey, options = {}) {
|
||||
const normalized = normalizePubkey(pubkey);
|
||||
if (!normalized) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const results = await fetchProfileMetadataBatch({
|
||||
pubkeys: [normalized],
|
||||
...options,
|
||||
});
|
||||
|
||||
return results.get(normalized) || null;
|
||||
}
|
||||
@@ -680,6 +680,8 @@ class UserBlockListManager {
|
||||
this.emitter = new TinyEventEmitter();
|
||||
this.seedStateCache = new Map();
|
||||
this.decryptRetryTimeoutId = null;
|
||||
this.loadPromise = null;
|
||||
this.loadingPubkey = null;
|
||||
|
||||
profileCache.subscribe((event, detail) => {
|
||||
if (event === "profileChanged") {
|
||||
@@ -705,6 +707,8 @@ class UserBlockListManager {
|
||||
clearTimeout(this.decryptRetryTimeoutId);
|
||||
this.decryptRetryTimeoutId = null;
|
||||
}
|
||||
this.loadPromise = null;
|
||||
this.loadingPubkey = null;
|
||||
}
|
||||
|
||||
on(eventName, handler) {
|
||||
@@ -767,6 +771,38 @@ class UserBlockListManager {
|
||||
|
||||
async loadBlocks(userPubkey, options = {}) {
|
||||
const normalized = normalizeHex(userPubkey);
|
||||
|
||||
if (this.loadPromise && this.loadingPubkey === normalized) {
|
||||
devLogger.log("[UserBlockList] Reusing in-flight block list load.", {
|
||||
pubkey: normalized,
|
||||
});
|
||||
return this.loadPromise;
|
||||
}
|
||||
|
||||
if (this.loadPromise && this.loadingPubkey && this.loadingPubkey !== normalized) {
|
||||
devLogger.log("[UserBlockList] Waiting for existing block list load to settle.", {
|
||||
previous: this.loadingPubkey,
|
||||
next: normalized,
|
||||
});
|
||||
await this.loadPromise.catch(() => {});
|
||||
}
|
||||
|
||||
const loadPromise = this.loadBlocksInternal(normalized, options);
|
||||
this.loadPromise = loadPromise;
|
||||
this.loadingPubkey = normalized;
|
||||
|
||||
try {
|
||||
return await loadPromise;
|
||||
} finally {
|
||||
if (this.loadPromise === loadPromise) {
|
||||
this.loadPromise = null;
|
||||
this.loadingPubkey = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async loadBlocksInternal(userPubkey, options = {}) {
|
||||
const normalized = normalizeHex(userPubkey);
|
||||
this.activePubkey = normalized;
|
||||
|
||||
if (!normalized) {
|
||||
|
||||
+11
-109
@@ -2,6 +2,7 @@
|
||||
|
||||
import { nostrClient } from "../nostrClientFacade.js";
|
||||
import { devLogger } from "./logger.js";
|
||||
import { fetchProfileMetadataBatch } from "../services/profileMetadataService.js";
|
||||
|
||||
const DEFAULT_PROFILE_IMAGE = "assets/svg/default-profile.svg";
|
||||
const HEX64_REGEX = /^[0-9a-f]{64}$/i;
|
||||
@@ -57,117 +58,18 @@ export async function batchFetchProfilesFromRelays({
|
||||
return;
|
||||
}
|
||||
|
||||
if (!nostr?.pool || typeof nostr.pool.list !== "function") {
|
||||
logger.warn(
|
||||
"[batchFetchProfiles] Relay pool is not ready; skipping profile fetch.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
const profileResults = await fetchProfileMetadataBatch({
|
||||
pubkeys: toFetch,
|
||||
nostr,
|
||||
logger,
|
||||
defaultProfileImage,
|
||||
});
|
||||
|
||||
const filter = {
|
||||
kinds: [0],
|
||||
authors: toFetch,
|
||||
limit: toFetch.length,
|
||||
};
|
||||
|
||||
const newestProfiles = new Map();
|
||||
|
||||
const applyProfileEvent = (evt, relayUrl) => {
|
||||
if (!evt || typeof evt !== "object") {
|
||||
profileResults.forEach((entry, pubkey) => {
|
||||
if (!entry?.profile) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pubkey =
|
||||
typeof evt.pubkey === "string" && hex64Regex.test(evt.pubkey)
|
||||
? evt.pubkey
|
||||
: null;
|
||||
if (!pubkey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const createdAt = Number.isFinite(evt.created_at) ? evt.created_at : 0;
|
||||
const previous = newestProfiles.get(pubkey);
|
||||
if (previous && previous.createdAt >= createdAt) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const data = JSON.parse(evt.content);
|
||||
const profile = {
|
||||
name: data?.name || data?.display_name || "Unknown",
|
||||
picture: data?.picture || defaultProfileImage,
|
||||
};
|
||||
|
||||
newestProfiles.set(pubkey, { createdAt, profile });
|
||||
setProfileCacheEntry(pubkey, profile);
|
||||
updateProfileInDOM(pubkey, profile);
|
||||
} catch (error) {
|
||||
logger.warn(
|
||||
`[batchFetchProfiles] Profile parse error for ${pubkey} from ${relayUrl}:`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const relayPromises = (Array.isArray(nostr.relays) ? nostr.relays : []).map(
|
||||
(relayUrl) =>
|
||||
nostr.pool
|
||||
.list([relayUrl], [filter])
|
||||
.then((events) => ({ relayUrl, events })),
|
||||
);
|
||||
|
||||
if (!relayPromises.length) {
|
||||
logger.warn(
|
||||
"[batchFetchProfiles] No relays configured; skipping profile fetch.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const results = await Promise.allSettled(relayPromises);
|
||||
|
||||
results.forEach((result, index) => {
|
||||
const relayUrl =
|
||||
result.status === "fulfilled"
|
||||
? result.value.relayUrl
|
||||
: nostr.relays[index];
|
||||
|
||||
if (result.status === "fulfilled") {
|
||||
const events = Array.isArray(result.value.events)
|
||||
? result.value.events
|
||||
: [];
|
||||
if (!events.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newestByPubkey = new Map();
|
||||
for (const evt of events) {
|
||||
if (!evt || typeof evt !== "object") {
|
||||
continue;
|
||||
}
|
||||
const pubkey =
|
||||
typeof evt.pubkey === "string" && hex64Regex.test(evt.pubkey)
|
||||
? evt.pubkey
|
||||
: null;
|
||||
if (!pubkey) {
|
||||
continue;
|
||||
}
|
||||
const createdAt = Number.isFinite(evt.created_at)
|
||||
? evt.created_at
|
||||
: 0;
|
||||
const prior = newestByPubkey.get(pubkey);
|
||||
if (!prior || createdAt > prior.createdAt) {
|
||||
newestByPubkey.set(pubkey, { createdAt, event: evt });
|
||||
}
|
||||
}
|
||||
|
||||
newestByPubkey.forEach(({ event }) => {
|
||||
applyProfileEvent(event, relayUrl);
|
||||
});
|
||||
} else if (relayUrl) {
|
||||
logger.warn(
|
||||
`[batchFetchProfiles] Failed to fetch profiles from relay ${relayUrl}:`,
|
||||
result.reason,
|
||||
);
|
||||
}
|
||||
setProfileCacheEntry(pubkey, entry.profile);
|
||||
updateProfileInDOM(pubkey, entry.profile);
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user