mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-31 18:02:27 +00:00
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:
1 parent
1fbee4219b
commit
dcb8424535
2 files changed
+177
-61
No files matched your search
+82
-61
@@ -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([
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
})();
|
||||
Reference in new issue
Block a user