mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-29 00:42:30 +00:00
Optimize post-login sequence to load relays before other data
Prioritizes fetching the user's relay list (NIP-65) immediately after login, ensuring that subsequent data fetches (profile, contact list, DMs, feed) utilize the user's preferred relays instead of default ones. This prevents "slow" or incomplete data loading issues caused by relying on defaults. - Refactored `authService.applyPostLoginState` to sequence relay loading before profile/block loading. - Refactored `app.handleAuthLogin` to await relay loading before initiating dependent fetches (Hashtags, Moderation, DMs, NWC, Lists, Feed). - Updated unit tests in `tests/auth-service.test.mjs` to verify sequential execution and correct return values. Co-authored-by: PR0M3TH3AN <[email protected]>
This commit is contained in:
1 parent
8a3e337e97
commit
fdc070479b
3 files changed
+123
-240
No files matched your search
@@ -4080,10 +4080,6 @@ class Application {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const hasVideoParam = urlParams.has("v");
|
||||
|
||||
if (!normalizedView || normalizedView === "most-recent-videos") {
|
||||
setHashView("for-you", { preserveVideoParam: hasVideoParam });
|
||||
}
|
||||
|
||||
const rawProviderId =
|
||||
typeof detail?.providerId === "string" ? detail.providerId.trim() : "";
|
||||
const rawAuthType =
|
||||
@@ -4100,75 +4096,6 @@ class Application {
|
||||
};
|
||||
const activePubkey = detail?.pubkey || this.pubkey;
|
||||
|
||||
Promise.resolve()
|
||||
.then(() =>
|
||||
this.handleModerationSettingsChange({
|
||||
settings: getModerationSettings(),
|
||||
skipRefresh: true,
|
||||
}),
|
||||
)
|
||||
.catch((error) => {
|
||||
devLogger.warn(
|
||||
"Failed to sync moderation settings after login:",
|
||||
error,
|
||||
);
|
||||
});
|
||||
|
||||
if (loginContext.identityChanged) {
|
||||
this.resetHashtagPreferencesState();
|
||||
try {
|
||||
this.watchHistoryTelemetry?.resetPlaybackLoggingState?.();
|
||||
this.watchHistoryTelemetry?.refreshPreferenceSettings?.();
|
||||
} catch (error) {
|
||||
devLogger.warn(
|
||||
"Failed to refresh watch history telemetry after identity change:",
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const hashtagPreferencesPromise = Promise.resolve(
|
||||
this.loadHashtagPreferencesForPubkey(loginContext.pubkey, {
|
||||
allowPermissionPrompt: false,
|
||||
}),
|
||||
).catch((error) => {
|
||||
devLogger.warn(
|
||||
"[Application] Background hashtag preferences load failed:",
|
||||
error
|
||||
);
|
||||
});
|
||||
|
||||
if (this.zapController) {
|
||||
try {
|
||||
this.zapController.setVisibility(
|
||||
Boolean(this.currentVideo?.lightningAddress),
|
||||
);
|
||||
} catch (error) {
|
||||
devLogger.warn("[Application] Failed to refresh zap visibility after login:", error);
|
||||
}
|
||||
}
|
||||
|
||||
const shouldReopenZap = this.pendingModalZapOpen;
|
||||
this.pendingModalZapOpen = false;
|
||||
if (shouldReopenZap) {
|
||||
const hasLightning = Boolean(this.currentVideo?.lightningAddress);
|
||||
if (hasLightning && this.videoModal?.openZapDialog) {
|
||||
Promise.resolve()
|
||||
.then(() => this.videoModal.openZapDialog())
|
||||
.then((opened) => {
|
||||
if (opened) {
|
||||
this.zapController?.open();
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
devLogger.warn(
|
||||
"[Application] Failed to reopen zap dialog after login:",
|
||||
error,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const initialLoadingState = {
|
||||
profile: activePubkey ? "loading" : "idle",
|
||||
lists: activePubkey ? "loading" : "idle",
|
||||
@@ -4184,28 +4111,6 @@ class Application {
|
||||
authLoadingState: this.authLoadingState,
|
||||
});
|
||||
|
||||
const accessControlReadyPromise =
|
||||
accessControl && typeof accessControl.ensureReady === "function"
|
||||
? Promise.resolve()
|
||||
.then(() => accessControl.ensureReady())
|
||||
.catch((error) => {
|
||||
userLogger.error(
|
||||
"[Application] Failed to refresh admin lists after login:",
|
||||
error,
|
||||
);
|
||||
throw error;
|
||||
})
|
||||
: null;
|
||||
|
||||
const scheduleAfterFirstRender = () =>
|
||||
new Promise((resolve) => {
|
||||
if (typeof requestAnimationFrame === "function") {
|
||||
requestAnimationFrame(() => requestAnimationFrame(resolve));
|
||||
} else {
|
||||
setTimeout(resolve, 0);
|
||||
}
|
||||
});
|
||||
|
||||
const cachedProfile =
|
||||
detail?.postLogin && typeof detail.postLogin === "object"
|
||||
? detail.postLogin.profile || null
|
||||
@@ -4221,78 +4126,6 @@ class Application {
|
||||
}
|
||||
}
|
||||
|
||||
const startDmHydration = () => {
|
||||
const dmTasks = [];
|
||||
if (activePubkey) {
|
||||
dmTasks.push(
|
||||
Promise.resolve()
|
||||
.then(() =>
|
||||
this.nostrService.loadDirectMessages({
|
||||
actorPubkey: activePubkey,
|
||||
limit: 50,
|
||||
decryptLimit: 50,
|
||||
initialLoad: true,
|
||||
}),
|
||||
)
|
||||
.catch((error) => {
|
||||
devLogger.warn(
|
||||
"[Application] Failed to sync direct messages during login:",
|
||||
error,
|
||||
);
|
||||
throw error;
|
||||
}),
|
||||
);
|
||||
|
||||
if (
|
||||
typeof this.nostrService.ensureDirectMessageSubscription === "function"
|
||||
) {
|
||||
dmTasks.push(
|
||||
Promise.resolve()
|
||||
.then(() =>
|
||||
this.nostrService.ensureDirectMessageSubscription({
|
||||
actorPubkey: activePubkey,
|
||||
}),
|
||||
)
|
||||
.catch((error) => {
|
||||
devLogger.warn(
|
||||
"[Application] Failed to subscribe to direct messages during login:",
|
||||
error,
|
||||
);
|
||||
throw error;
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return dmTasks.length
|
||||
? Promise.allSettled(dmTasks).then((results) => {
|
||||
const success = results.every(
|
||||
(result) => result.status === "fulfilled",
|
||||
);
|
||||
this.updateAuthLoadingState({
|
||||
dms: success ? "ready" : "error",
|
||||
});
|
||||
return success;
|
||||
})
|
||||
: Promise.resolve(this.updateAuthLoadingState({ dms: "idle" }));
|
||||
};
|
||||
|
||||
const dmStatePromise = Promise.resolve()
|
||||
.then(() => scheduleAfterFirstRender())
|
||||
.then(() => startDmHydration())
|
||||
.catch((error) => {
|
||||
devLogger.warn("[Application] Delayed DM hydration failed:", error);
|
||||
this.updateAuthLoadingState({ dms: "error" });
|
||||
return false;
|
||||
});
|
||||
|
||||
const nwcPromise = Promise.resolve()
|
||||
.then(() => this.nwcSettingsService.onLogin(loginContext))
|
||||
.catch((error) => {
|
||||
devLogger.error("Failed to process NWC settings during login:", error);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (this.profileController) {
|
||||
try {
|
||||
const maybePromise = this.profileController.handleAuthLogin(detail);
|
||||
@@ -4314,8 +4147,22 @@ class Application {
|
||||
this.renderSavedProfiles();
|
||||
}
|
||||
|
||||
const accessControlReadyPromise =
|
||||
accessControl && typeof accessControl.ensureReady === "function"
|
||||
? Promise.resolve()
|
||||
.then(() => accessControl.ensureReady())
|
||||
.catch((error) => {
|
||||
userLogger.error(
|
||||
"[Application] Failed to refresh admin lists after login:",
|
||||
error,
|
||||
);
|
||||
throw error;
|
||||
})
|
||||
: null;
|
||||
|
||||
const profileStatePromise = Promise.resolve(postLoginPromise)
|
||||
.then((postLogin) => {
|
||||
.then(async (postLogin) => {
|
||||
// 1. Relays and Profile are now loaded (sequentially or efficiently by authService)
|
||||
const nextProfile = postLogin?.profile || cachedProfile;
|
||||
if (activePubkey && nextProfile) {
|
||||
this.updateActiveProfileUI(activePubkey, nextProfile);
|
||||
|
||||
+68
-64
@@ -697,79 +697,83 @@ export default class AuthService {
|
||||
return { detail, completionPromise: Promise.resolve(detail) };
|
||||
}
|
||||
|
||||
const runOperation = async (operation) => {
|
||||
try {
|
||||
const value = await operation.promise;
|
||||
const resolved = operation.onFulfilled(value);
|
||||
detail[operation.name] = resolved;
|
||||
if (typeof operation.emit === "function") {
|
||||
operation.emit(resolved, null);
|
||||
}
|
||||
return resolved;
|
||||
} catch (error) {
|
||||
const resolved = operation.onRejected(error);
|
||||
detail[operation.name] = resolved;
|
||||
if (typeof operation.emit === "function") {
|
||||
operation.emit(resolved, error);
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
};
|
||||
|
||||
const schedule = (callback) => Promise.resolve().then(() => callback());
|
||||
|
||||
const operations = [];
|
||||
|
||||
if (!deferBlocks) {
|
||||
const blocksOperation = this.createBlocksLoadOperation(activePubkey, schedule);
|
||||
if (blocksOperation) {
|
||||
operations.push(blocksOperation);
|
||||
const completionPromise = (async () => {
|
||||
if (this.relayManager && typeof this.relayManager.loadRelayList === "function") {
|
||||
const relayOp = {
|
||||
name: "relaysLoaded",
|
||||
promise: schedule(() => this.relayManager.loadRelayList(activePubkey)),
|
||||
onFulfilled: () => true,
|
||||
onRejected: (error) => {
|
||||
this.log("[AuthService] Failed to load relay list", error);
|
||||
return false;
|
||||
},
|
||||
emit: (value, error) => {
|
||||
this.emit("relaysLoaded", {
|
||||
pubkey: activePubkey,
|
||||
relaysLoaded: value,
|
||||
error: error ?? null,
|
||||
});
|
||||
},
|
||||
};
|
||||
await runOperation(relayOp);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.relayManager && typeof this.relayManager.loadRelayList === "function") {
|
||||
operations.push({
|
||||
name: "relaysLoaded",
|
||||
promise: schedule(() => this.relayManager.loadRelayList(activePubkey)),
|
||||
onFulfilled: () => true,
|
||||
onRejected: (error) => {
|
||||
this.log("[AuthService] Failed to load relay list", error);
|
||||
return false;
|
||||
},
|
||||
emit: (value, error) => {
|
||||
this.emit("relaysLoaded", {
|
||||
pubkey: activePubkey,
|
||||
relaysLoaded: value,
|
||||
error: error ?? null,
|
||||
const concurrentOps = [];
|
||||
|
||||
if (!deferBlocks) {
|
||||
const blocksOperation = this.createBlocksLoadOperation(activePubkey, schedule);
|
||||
if (blocksOperation) {
|
||||
concurrentOps.push(blocksOperation);
|
||||
}
|
||||
}
|
||||
|
||||
concurrentOps.push({
|
||||
name: "profile",
|
||||
promise: schedule(() => this.loadOwnProfile(activePubkey)),
|
||||
onFulfilled: (value) => {
|
||||
const profile = value || FALLBACK_PROFILE;
|
||||
this.setProfileCacheEntry(activePubkey, profile, {
|
||||
persist: true,
|
||||
reason: "post-login",
|
||||
});
|
||||
return profile;
|
||||
},
|
||||
onRejected: (error) => {
|
||||
this.log("[AuthService] Failed to load own profile", error);
|
||||
const fallbackProfile = cachedProfile || FALLBACK_PROFILE;
|
||||
this.setProfileCacheEntry(activePubkey, fallbackProfile, {
|
||||
persist: true,
|
||||
reason: "post-login-fallback",
|
||||
});
|
||||
return fallbackProfile;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
operations.push({
|
||||
name: "profile",
|
||||
promise: schedule(() => this.loadOwnProfile(activePubkey)),
|
||||
onFulfilled: (value) => {
|
||||
const profile = value || FALLBACK_PROFILE;
|
||||
this.setProfileCacheEntry(activePubkey, profile, {
|
||||
persist: true,
|
||||
reason: "post-login",
|
||||
});
|
||||
return profile;
|
||||
},
|
||||
onRejected: (error) => {
|
||||
this.log("[AuthService] Failed to load own profile", error);
|
||||
const fallbackProfile = cachedProfile || FALLBACK_PROFILE;
|
||||
this.setProfileCacheEntry(activePubkey, fallbackProfile, {
|
||||
persist: true,
|
||||
reason: "post-login-fallback",
|
||||
});
|
||||
return fallbackProfile;
|
||||
},
|
||||
});
|
||||
await Promise.allSettled(concurrentOps.map(runOperation));
|
||||
|
||||
const wrappedPromises = operations.map((operation) =>
|
||||
operation.promise
|
||||
.then((value) => {
|
||||
const resolved = operation.onFulfilled(value);
|
||||
detail[operation.name] = resolved;
|
||||
if (typeof operation.emit === "function") {
|
||||
operation.emit(resolved, null);
|
||||
}
|
||||
return resolved;
|
||||
})
|
||||
.catch((error) => {
|
||||
const resolved = operation.onRejected(error);
|
||||
detail[operation.name] = resolved;
|
||||
if (typeof operation.emit === "function") {
|
||||
operation.emit(resolved, error);
|
||||
}
|
||||
return resolved;
|
||||
}),
|
||||
);
|
||||
|
||||
const completionPromise = Promise.allSettled(wrappedPromises).then(() => detail);
|
||||
return detail;
|
||||
})();
|
||||
|
||||
return { detail, completionPromise };
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
setActiveProfilePubkey,
|
||||
getProfileCacheMap,
|
||||
} from "../js/state/cache.js";
|
||||
import { profileCache } from "../js/state/profileCache.js";
|
||||
|
||||
if (typeof globalThis.window === "undefined") {
|
||||
globalThis.window = {};
|
||||
@@ -61,6 +62,12 @@ function resetState() {
|
||||
if (cache && typeof cache.clear === "function") {
|
||||
cache.clear();
|
||||
}
|
||||
if (profileCache && typeof profileCache.reset === "function") {
|
||||
profileCache.reset();
|
||||
}
|
||||
if (typeof localStorage !== "undefined") {
|
||||
localStorage.clear();
|
||||
}
|
||||
}
|
||||
|
||||
await (async () => {
|
||||
@@ -94,13 +101,15 @@ await (async () => {
|
||||
|
||||
const promise = service.applyPostLoginState();
|
||||
|
||||
await Promise.resolve();
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
|
||||
assert.deepEqual(calls, ["blocks:start", "relays:start", "profile:start"]);
|
||||
assert.deepEqual(calls, ["relays:start", "blocks:start", "profile:start"]);
|
||||
|
||||
resolveBlocks();
|
||||
|
||||
const detail = await promise;
|
||||
const result = await promise;
|
||||
const detail = result.detail;
|
||||
await result.completionPromise;
|
||||
|
||||
assert.equal(detail.pubkey, SAMPLE_PUBKEY);
|
||||
assert.equal(detail.blocksLoaded, true);
|
||||
@@ -130,12 +139,22 @@ await (async () => {
|
||||
|
||||
service.loadOwnProfile = () => Promise.reject(new Error("profile failed"));
|
||||
|
||||
const detail = await service.applyPostLoginState();
|
||||
const result = service.applyPostLoginState();
|
||||
const detail = result.detail;
|
||||
await result.completionPromise;
|
||||
|
||||
assert.equal(detail.pubkey, SAMPLE_PUBKEY);
|
||||
assert.equal(detail.blocksLoaded, false);
|
||||
assert.equal(detail.relaysLoaded, false);
|
||||
assert.equal(detail.profile, null);
|
||||
assert.deepEqual(detail.profile, {
|
||||
name: "Unknown",
|
||||
picture: "assets/svg/default-profile.svg",
|
||||
about: "",
|
||||
website: "",
|
||||
banner: "",
|
||||
lud16: "",
|
||||
lud06: "",
|
||||
});
|
||||
|
||||
assert.equal(logs.length, 3);
|
||||
assert(logs.some(({ message }) => message.includes("block list")));
|
||||
@@ -147,13 +166,23 @@ await (async () => {
|
||||
resetState();
|
||||
|
||||
const service = new AuthService();
|
||||
const detail = await service.applyPostLoginState();
|
||||
const result = service.applyPostLoginState();
|
||||
const detail = result.detail;
|
||||
await result.completionPromise;
|
||||
|
||||
assert.deepEqual(detail, {
|
||||
pubkey: null,
|
||||
blocksLoaded: false,
|
||||
relaysLoaded: false,
|
||||
profile: null,
|
||||
profile: {
|
||||
name: "Unknown",
|
||||
picture: "assets/svg/default-profile.svg",
|
||||
about: "",
|
||||
website: "",
|
||||
banner: "",
|
||||
lud16: "",
|
||||
lud06: "",
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
||||
@@ -168,7 +197,10 @@ await (async () => {
|
||||
profile: { name: "Stub" },
|
||||
};
|
||||
|
||||
service.applyPostLoginState = async () => postLogin;
|
||||
service.applyPostLoginState = () => ({
|
||||
detail: postLogin,
|
||||
completionPromise: Promise.resolve(postLogin),
|
||||
});
|
||||
|
||||
const detail = await service.login(SAMPLE_PUBKEY);
|
||||
|
||||
|
||||
Reference in new issue
Block a user