Merge pull request #2139 from PR0M3TH3AN/claude/nostr-relay-sync-login-eezJv

Optimize login flow: parallelize list loading with relay readiness
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2026-02-05 19:00:03 -05:00
commit 930c730557
3 files changed
+88 -38

No files matched your search

+43 -15
View File
@@ -4135,6 +4135,14 @@ class Application {
: Promise.resolve(detail?.postLogin ?? null);
const postLoginResult = detail?.postLogin ?? null;
// relaysReadyPromise resolves as soon as the user's relay list is loaded,
// before profile fetching completes. Lists can start loading immediately
// after relays are available — they don't depend on profile data.
const relaysReadyPromise =
detail && typeof detail.relaysReadyPromise?.then === "function"
? detail.relaysReadyPromise
: postLoginPromise;
if (detail && typeof detail === "object") {
try {
detail.__handled = true;
@@ -4270,8 +4278,10 @@ class Application {
return null;
});
// Chain list loading to profile state to ensure relays are loaded first.
const listStatePromise = profileStatePromise.then(() => {
// Start list loading as soon as relays are ready — lists don't depend on
// profile data, only on relay URLs. This runs in parallel with profile
// fetching instead of sequentially after it.
const listStatePromise = relaysReadyPromise.then(() => {
const tasks = [];
if (activePubkey && typeof this.authService.loadBlocksForPubkey === "function") {
tasks.push(
@@ -4328,6 +4338,7 @@ class Application {
});
});
// DMs can wait for profile since they need encryption context.
const dmStatePromise = profileStatePromise.then(() => {
if (
activePubkey &&
@@ -4373,7 +4384,8 @@ class Application {
return Promise.resolve();
});
const hashtagPreferencesPromise = profileStatePromise.then(() => {
// Hashtag preferences drive feed filtering — start as soon as relays are ready.
const hashtagPreferencesPromise = relaysReadyPromise.then(() => {
if (
activePubkey &&
this.hashtagPreferences &&
@@ -4387,6 +4399,9 @@ class Application {
this.capturePermissionPromptFromError(
this.hashtagPreferences?.lastLoadError,
);
// Ensure the cached snapshot is updated before the feed renders
// so the feed engine has access to interests/disinterests.
this.updateCachedHashtagPreferences();
})
.catch((error) => {
devLogger.warn(
@@ -4399,13 +4414,9 @@ class Application {
return Promise.resolve();
});
void Promise.allSettled([
profileStatePromise,
listStatePromise,
dmStatePromise,
]);
// Ensure DMs are tracked for cleanup.
void dmStatePromise;
void nwcPromise;
void hashtagPreferencesPromise;
if (activePubkey) {
const seedBlacklist = () => {
@@ -4438,20 +4449,37 @@ class Application {
);
}
});
// Subscriptions are loaded in list tasks to align with per-category loading states.
}
// Show loading state immediately while lists are syncing.
try {
this.reinitializeVideoListView({ reason: "login", postLoginResult });
} catch (error) {
devLogger.warn("Failed to reinitialize video list view after login:", error);
}
this.lastIdentityRefreshPromise = this.refreshAllVideoGrids({
reason: "auth-login",
forceMainReload: true,
});
// Wait for the critical feed-filtering lists (blocks, subscriptions,
// hashtag preferences) to settle before rendering the video grid. This
// prevents the feed from briefly showing unfiltered content. We use a
// time-boxed wait so the UI isn't blocked indefinitely if decryption stalls.
const FEED_SYNC_TIMEOUT_MS = 8000;
const feedSyncPromise = Promise.race([
Promise.allSettled([
listStatePromise,
hashtagPreferencesPromise,
profileStatePromise,
]),
new Promise((resolve) => setTimeout(resolve, FEED_SYNC_TIMEOUT_MS)),
]).catch(() => null);
// Chain the grid refresh after the feed data is available.
this.lastIdentityRefreshPromise = feedSyncPromise
.then(() =>
this.refreshAllVideoGrids({
reason: "auth-login",
forceMainReload: true,
}),
);
this.lastIdentityRefreshPromise
.catch((error) => {
devLogger.error("Failed to refresh video grids after login:", error);
+13 -3
View File
@@ -623,10 +623,11 @@ export default class AuthService {
postLoginError: null,
};
const { detail: postLoginDetail, completionPromise } = this.applyPostLoginState({
const { detail: postLoginDetail, completionPromise, relaysReadyPromise } = this.applyPostLoginState({
deferBlocks: true,
});
detail.postLogin = postLoginDetail;
detail.relaysReadyPromise = relaysReadyPromise;
const postLoginPromise = Promise.resolve(completionPromise)
.then((postLogin) => {
@@ -694,7 +695,8 @@ export default class AuthService {
};
if (!activePubkey) {
return { detail, completionPromise: Promise.resolve(detail) };
const resolved = Promise.resolve(detail);
return { detail, completionPromise: resolved, relaysReadyPromise: resolved };
}
const runOperation = async (operation) => {
@@ -718,6 +720,13 @@ export default class AuthService {
const schedule = (callback) => Promise.resolve().then(() => callback());
// Expose relay readiness separately so list loading can start immediately
// after relays are available, without waiting for profile to complete.
let resolveRelaysReady;
const relaysReadyPromise = new Promise((resolve) => {
resolveRelaysReady = resolve;
});
const completionPromise = (async () => {
if (this.relayManager && typeof this.relayManager.loadRelayList === "function") {
const relayOp = {
@@ -738,6 +747,7 @@ export default class AuthService {
};
await runOperation(relayOp);
}
resolveRelaysReady(detail);
const concurrentOps = [];
@@ -775,7 +785,7 @@ export default class AuthService {
return detail;
})();
return { detail, completionPromise };
return { detail, completionPromise, relaysReadyPromise };
}
createBlocksLoadOperation(
+32 -20
View File
@@ -502,14 +502,19 @@ class SubscriptionsManager {
);
const hadCachedSnapshot = cachedSnapshot.hasSnapshot;
const shouldForceFullFetch = !cachedSnapshot.hasSnapshot;
let events = await nostrClient.fetchListIncrementally({
kind: SUBSCRIPTION_SET_KIND,
pubkey: normalizedUserPubkey,
dTag: SUBSCRIPTION_LIST_IDENTIFIER,
relayUrls,
since: shouldForceFullFetch ? 0 : (cachedSnapshot.createdAt || 0),
timeoutMs: 12000,
});
// Fetch user and session actor subscription lists in parallel to reduce
// total relay wait time during login.
const fetchPromises = [
nostrClient.fetchListIncrementally({
kind: SUBSCRIPTION_SET_KIND,
pubkey: normalizedUserPubkey,
dTag: SUBSCRIPTION_LIST_IDENTIFIER,
relayUrls,
since: shouldForceFullFetch ? 0 : (cachedSnapshot.createdAt || 0),
timeoutMs: 12000,
}),
];
const normalizedSessionActorPubkey = normalizeNostrPubkey(
nostrClient?.sessionActor?.pubkey,
@@ -525,17 +530,22 @@ class SubscriptionsManager {
),
);
const shouldForceSessionFetch = !sessionCachedSnapshot.hasSnapshot;
const sessionEvents = await nostrClient.fetchListIncrementally({
kind: SUBSCRIPTION_SET_KIND,
pubkey: normalizedSessionActorPubkey,
dTag: SUBSCRIPTION_LIST_IDENTIFIER,
relayUrls,
since: shouldForceSessionFetch ? 0 : (sessionCachedSnapshot.createdAt || 0),
timeoutMs: 12000,
});
if (sessionEvents.length) {
events = events.concat(sessionEvents);
}
fetchPromises.push(
nostrClient.fetchListIncrementally({
kind: SUBSCRIPTION_SET_KIND,
pubkey: normalizedSessionActorPubkey,
dTag: SUBSCRIPTION_LIST_IDENTIFIER,
relayUrls,
since: shouldForceSessionFetch ? 0 : (sessionCachedSnapshot.createdAt || 0),
timeoutMs: 12000,
}),
);
}
const fetchResults = await Promise.all(fetchPromises);
let events = fetchResults[0] || [];
if (shouldFetchSessionActor && fetchResults[1]?.length) {
events = events.concat(fetchResults[1]);
}
const mergedEvents = [];
@@ -809,7 +819,9 @@ class SubscriptionsManager {
try {
// Race against a timeout so the UI doesn't hang indefinitely if relays stall.
const timeoutMs = 6000;
// The timeout must accommodate relay fetch (~12s) + decryption (~3-12s per
// scheme attempt), so we use 30s to give the full pipeline enough room.
const timeoutMs = 30000;
await Promise.race([
loader,
new Promise((_, reject) =>