Refactor loadOwnProfile to use Promise.race and clean up relay fetching logic.

Extracts `fetchProfileFromRelay` helper method in `AuthService` to encapsulate timeout logic and event validation. This simplifies `loadOwnProfile` and makes it more readable and maintainable.

Also adds comprehensive tests for `loadOwnProfile` in `tests/auth-service.test.mjs`.

Co-authored-by: PR0M3TH3AN <[email protected]>
This commit is contained in:
google-labs-jules[bot]andPR0M3TH3AN committed 2026-02-08 03:42:50 +00:00
1 parent 1fbee4219b
commit dcb8424535
2 files changed
+177 -61

No files matched your search

+82 -61
View File
@@ -1030,8 +1030,63 @@ export default class AuthService {
return { switched: true, detail };
}
async fetchProfileFromRelay(
relayUrl,
filter,
normalizedPubkey,
timeoutMs,
requireEvent,
) {
const fetchPromise = this.nostrClient.pool.list([relayUrl], filter);
let timeoutId;
const timeoutPromise = new Promise((_, reject) => {
timeoutId = setTimeout(() => {
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);
});
try {
const result = await Promise.race([fetchPromise, timeoutPromise]);
clearTimeout(timeoutId);
const events = Array.isArray(result)
? result.filter((event) => event && event.pubkey === normalizedPubkey)
: [];
if (requireEvent && !events.length) {
const emptyError = new Error(
`No profile events returned from ${relayUrl}`,
);
emptyError.code = "empty";
emptyError.relay = relayUrl;
throw emptyError;
}
return { relayUrl, events };
} catch (error) {
clearTimeout(timeoutId);
// Suppress unhandled rejection from fetchPromise if it's still running
fetchPromise.catch(() => {});
if (error.relay) {
throw error;
}
const wrapped = error instanceof Error ? error : new Error(String(error));
wrapped.relay = relayUrl;
throw wrapped;
}
}
async loadOwnProfile(pubkey) {
const normalized = this.normalizeHexPubkey(pubkey) ||
const normalized =
this.normalizeHexPubkey(pubkey) ||
(typeof pubkey === "string" ? pubkey.trim() : "");
if (!normalized) {
return FALLBACK_PROFILE;
@@ -1074,13 +1129,20 @@ export default class AuthService {
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,
about:
typeof data.about === "string" ? data.about : FALLBACK_PROFILE.about,
website:
typeof data.website === "string" ? data.website : FALLBACK_PROFILE.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,
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,
};
} catch (error) {
this.log("[AuthService] Failed to parse profile metadata", error);
@@ -1088,67 +1150,26 @@ export default class AuthService {
}
};
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)
this.fetchProfileFromRelay(
relayUrl,
filter,
normalized,
FAST_PROFILE_TIMEOUT_MS,
true,
),
);
const backgroundPromises = backgroundRelays.map((relayUrl) =>
fetchFromRelay(relayUrl, BACKGROUND_PROFILE_TIMEOUT_MS, false)
this.fetchProfileFromRelay(
relayUrl,
filter,
normalized,
BACKGROUND_PROFILE_TIMEOUT_MS,
false,
),
);
const background = Promise.allSettled([
+95
View File
@@ -305,3 +305,98 @@ await (async () => {
assert.ok(detail);
assert.equal(detail.pubkey, SAMPLE_PUBKEY);
})();
// === Tests for loadOwnProfile logic ===
await (async () => {
resetState();
const PUBKEY = SAMPLE_PUBKEY;
// Test Fast Relay Success
{
const mockNostrClient = {
relays: ["wss://fast1.com", "wss://fast2.com", "wss://slow.com"],
pool: {
list: async (relays) => {
const url = relays[0];
if (url === "wss://fast1.com") {
return [{
pubkey: PUBKEY,
created_at: 100,
content: JSON.stringify({ name: "Fast User" })
}];
}
return new Promise(() => {}); // Hang others
}
},
handleEvent: () => {}
};
const service = new AuthService({ nostrClient: mockNostrClient });
// Override setProfileCacheEntry to avoid side effects or errors from real implementation if not needed,
// but here we just let it run. However, the service needs nostrClient.pool
// We need to make sure setProfileCacheEntry doesn't fail.
// It imports from cache.js which uses profileCache.
// profileCache is imported from state/profileCache.js.
// The previous tests didn't seem to mock setProfileCacheEntry.
const profile = await service.loadOwnProfile(PUBKEY);
assert.equal(profile.name, "Fast User");
}
// Test All Relays Fail
{
const mockNostrClient = {
relays: ["wss://fail1.com", "wss://fail2.com"],
pool: {
list: async () => {
throw new Error("Failed");
}
},
handleEvent: () => {}
};
const service = new AuthService({ nostrClient: mockNostrClient });
const profile = await service.loadOwnProfile(PUBKEY);
assert.equal(profile.name, "Unknown");
}
// Test Timeout Fallback
{
let backgroundResolve;
const backgroundPromise = new Promise(r => backgroundResolve = r);
const mockNostrClient = {
relays: ["wss://fast-fail.com", "wss://fast-fail2.com", "wss://background-ok.com", "wss://background-ok-2.com"],
pool: {
list: async (relays) => {
const url = relays[0];
if (url.includes("fast")) {
// Simulate timeout or error
throw new Error("Fast fail");
}
if (url.includes("background")) {
await backgroundPromise;
return [{
pubkey: PUBKEY,
created_at: 200,
content: JSON.stringify({ name: "Background User" })
}];
}
return [];
}
},
handleEvent: () => {}
};
const service = new AuthService({ nostrClient: mockNostrClient });
const profile = await service.loadOwnProfile(PUBKEY);
assert.equal(profile.name, "Unknown");
// We resolve background promise to let background logic finish eventually (though test finishes here)
backgroundResolve();
}
})();