mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
Speed up Nostr login and fix list sync, signer persistence, and feed registration
The NIP-07 login flow was extremely slow because list decryption (blocks, subscriptions, hashtag preferences) started before the signer was ready. On page refresh, all three services failed with "permission-required" and scheduled 3-second retry delays, adding 6-10 seconds to every login. Key fixes: - List loading now waits for the permission pre-grant to complete before attempting decryption, ensuring the signer is available on first try - Extension wait timeout increased from 3s to 5s for slower extensions - Decrypt timeouts reduced from 20s to 10s (signer is guaranteed ready) - Auto-login now forwards authType/providerId from saved profiles - Storage tab unlock falls back to ensureActiveSignerForPubkey when the global signer is not yet registered - Watch history feed registration adds defensive re-registration and try/catch fallback to prevent "Feed not registered" errors - Decryption scheme cache TTL extended from 10 minutes to 2 hours - ensureExtensionPermissions waits for extension injection when there are outstanding permissions to request (was returning immediately) https://claude.ai/code/session_01T4wwPu7m5qp1Nxx3H2v57j
This commit is contained in:
10 files changed
+101
-29
No files matched your search
@@ -1159,9 +1159,26 @@ class Application {
|
||||
async _initAutoLogin() {
|
||||
const savedPubKey = this.activeProfilePubkey;
|
||||
if (savedPubKey) {
|
||||
// Auto-login if a pubkey was saved
|
||||
// Look up the saved profile entry to forward authType and providerId.
|
||||
// Without this, the login flow defaults to "nip07" and the signer
|
||||
// restoration path cannot distinguish NIP-07 from NIP-46 or nsec,
|
||||
// which leads to incorrect signer setup on page refresh.
|
||||
const savedProfiles = this.authService.cloneSavedProfiles();
|
||||
const normalizedSaved = this.normalizeHexPubkey(savedPubKey) || savedPubKey;
|
||||
const savedEntry = savedProfiles.find((entry) => {
|
||||
const entryPubkey = this.normalizeHexPubkey(entry?.pubkey);
|
||||
return entryPubkey && entryPubkey === normalizedSaved;
|
||||
});
|
||||
const loginOptions = { persistActive: false };
|
||||
if (savedEntry?.authType) {
|
||||
loginOptions.authType = savedEntry.authType;
|
||||
}
|
||||
if (savedEntry?.providerId) {
|
||||
loginOptions.providerId = savedEntry.providerId;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.authService.login(savedPubKey, { persistActive: false });
|
||||
await this.authService.login(savedPubKey, loginOptions);
|
||||
} catch (error) {
|
||||
devLogger.error("Auto-login failed:", error);
|
||||
if (error && error.code === "site-lockdown") {
|
||||
|
||||
@@ -222,13 +222,16 @@ export function createAuthSessionCoordinator(deps) {
|
||||
// render needs all three to be settled. Loading them concurrently shaves
|
||||
// 20-30 seconds off the critical login path.
|
||||
//
|
||||
// PERF: Lists no longer wait for the permission pre-grant to complete.
|
||||
// The NIP-07 extension already granted decrypt permissions during the
|
||||
// login flow (ensureExtensionPermissions is called in loginWithExtension),
|
||||
// so the signer adapter's decrypt functions work immediately. Waiting for
|
||||
// the redundant pre-grant was adding 2-5 seconds of unnecessary latency
|
||||
// before list loading could even begin.
|
||||
const listStatePromise = relaysReadyPromise.then(async () => {
|
||||
// Lists wait for the permission pre-grant to settle before attempting
|
||||
// decryption. During auto-login (page refresh), the NIP-07 extension
|
||||
// may not be ready yet — without waiting, all three services would fail
|
||||
// with "permission-required" and schedule 3-second retry delays, adding
|
||||
// 6-10 seconds to the critical login path. By waiting for the pre-grant
|
||||
// (which itself waits for the extension and requests permissions), the
|
||||
// signer is guaranteed to be available when decryption starts. This does
|
||||
// NOT slow down fresh logins because the pre-grant resolves immediately
|
||||
// when permissions were already granted during loginWithExtension().
|
||||
const listStatePromise = Promise.all([relaysReadyPromise, permissionPromise]).then(async () => {
|
||||
const parallelListTasks = [];
|
||||
|
||||
if (
|
||||
|
||||
@@ -484,6 +484,7 @@ export function createFeedCoordinator(deps) {
|
||||
|
||||
registerWatchHistoryFeed() {
|
||||
if (!this.feedEngine || typeof this.feedEngine.registerFeed !== "function") {
|
||||
devLogger.warn("[Application] Cannot register watch-history feed: feedEngine not available.");
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
+20
-6
@@ -1400,12 +1400,26 @@ export function createWatchHistoryRenderer(config = {}) {
|
||||
const app = getAppInstance();
|
||||
const engine = app?.feedEngine;
|
||||
if (engine && typeof engine.run === "function") {
|
||||
const runtime = buildWatchHistoryFeedRuntime({
|
||||
actor: actorInput,
|
||||
cursor,
|
||||
forceRefresh,
|
||||
});
|
||||
return engine.run("watch-history", { runtime });
|
||||
// Ensure the watch-history feed is registered. It may not be if the
|
||||
// feed engine was ready but registration was silently skipped during
|
||||
// bootstrap (e.g. timing edge cases).
|
||||
if (
|
||||
typeof engine.getFeedDefinition === "function" &&
|
||||
!engine.getFeedDefinition("watch-history") &&
|
||||
typeof app.registerWatchHistoryFeed === "function"
|
||||
) {
|
||||
app.registerWatchHistoryFeed();
|
||||
}
|
||||
try {
|
||||
const runtime = buildWatchHistoryFeedRuntime({
|
||||
actor: actorInput,
|
||||
cursor,
|
||||
forceRefresh,
|
||||
});
|
||||
return engine.run("watch-history", { runtime });
|
||||
} catch (feedError) {
|
||||
// Fall through to service fallback if feed is still not registered
|
||||
}
|
||||
}
|
||||
const items = await watchHistoryService.loadLatest(actorInput, {
|
||||
allowStale: !forceRefresh,
|
||||
|
||||
+17
-5
@@ -3122,9 +3122,10 @@ export class NostrClient {
|
||||
// Even if permissions are cached, we must ensure the extension object is actually
|
||||
// injected and available on the window before returning success.
|
||||
// This prevents race conditions where the app loads faster than the extension.
|
||||
// 5 seconds gives slower extensions (e.g. Alby) enough time to inject.
|
||||
if (typeof window !== "undefined" && !window.nostr) {
|
||||
try {
|
||||
await waitForNip07Extension(3000);
|
||||
await waitForNip07Extension(5000);
|
||||
} catch (error) {
|
||||
return { ok: false, error: new Error("extension-unavailable") };
|
||||
}
|
||||
@@ -3137,7 +3138,17 @@ export class NostrClient {
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
const extension = typeof window !== "undefined" ? window.nostr : null;
|
||||
let extension = typeof window !== "undefined" ? window.nostr : null;
|
||||
// Wait for the extension to inject if it isn't available yet. The cached
|
||||
// permission state implies a previous session with an extension, so it's
|
||||
// likely to appear once the content script finishes loading.
|
||||
if (!extension && this.extensionPermissionCache && this.extensionPermissionCache.size > 0) {
|
||||
try {
|
||||
extension = await waitForNip07Extension(5000);
|
||||
} catch (error) {
|
||||
// Fall through
|
||||
}
|
||||
}
|
||||
if (!extension) {
|
||||
this.extensionReady = false;
|
||||
return { ok: false, error: new Error("extension-unavailable") };
|
||||
@@ -3237,11 +3248,12 @@ export class NostrClient {
|
||||
typeof window !== "undefined" && window && window.nostr ? window.nostr : null;
|
||||
|
||||
// If the extension is missing but we have cached permissions, it implies the user
|
||||
// previously logged in with an extension. We should wait briefly for injection
|
||||
// to resolve the common race condition where the app loads faster than the extension.
|
||||
// previously logged in with an extension. We should wait for injection to resolve
|
||||
// the common race condition where the app loads faster than the extension. 5 seconds
|
||||
// gives slower extensions (e.g. Alby) enough time to inject window.nostr.
|
||||
if (!extension && this.extensionPermissionCache && this.extensionPermissionCache.size > 0) {
|
||||
try {
|
||||
extension = await waitForNip07Extension(3000);
|
||||
extension = await waitForNip07Extension(5000);
|
||||
} catch (error) {
|
||||
// Fall through to existing signer check
|
||||
}
|
||||
|
||||
@@ -10,7 +10,11 @@
|
||||
* login path where all three services decrypt simultaneously.
|
||||
*/
|
||||
|
||||
const DECRYPTION_CACHE_TTL_MS = 10 * 60 * 1000; // 10 minutes
|
||||
// Extended from 10 minutes to 2 hours. The cache is cleared on logout, so
|
||||
// there's no privacy concern with a longer TTL. A short TTL caused redundant
|
||||
// scheme probing during normal browsing sessions, triggering unnecessary
|
||||
// NIP-07 extension calls that slow down background list re-syncs.
|
||||
const DECRYPTION_CACHE_TTL_MS = 2 * 60 * 60 * 1000; // 2 hours
|
||||
|
||||
/** @type {Map<string, { scheme: string, timestamp: number }>} */
|
||||
const cache = new Map();
|
||||
|
||||
@@ -36,7 +36,11 @@ const LOG_PREFIX = "[HashtagPreferences]";
|
||||
const HASHTAG_IDENTIFIER = "bitvid:tag-preferences";
|
||||
|
||||
const DEFAULT_VERSION = 1;
|
||||
const DECRYPT_TIMEOUT_MS = 20000;
|
||||
// PERF: Reduced from 20s to 10s — the signer is now guaranteed to be ready
|
||||
// before list loading starts (authSessionCoordinator waits for the permission
|
||||
// pre-grant), so decryption should succeed quickly. 10s is a generous fallback
|
||||
// for slow relay responses while still failing fast enough to retry promptly.
|
||||
const DECRYPT_TIMEOUT_MS = 10000;
|
||||
// PERF: Reduced from 10s to 3s for faster recovery during login.
|
||||
const DECRYPT_RETRY_DELAY_MS = 3000;
|
||||
|
||||
|
||||
+5
-1
@@ -44,7 +44,11 @@ import {
|
||||
|
||||
const SUBSCRIPTION_SET_KIND =
|
||||
getNostrEventSchema(NOTE_TYPES.SUBSCRIPTION_LIST)?.kind ?? 30000;
|
||||
const DECRYPT_TIMEOUT_MS = 20000;
|
||||
// PERF: Reduced from 20s to 10s — the signer is now guaranteed to be ready
|
||||
// before list loading starts (authSessionCoordinator waits for the permission
|
||||
// pre-grant), so decryption should succeed quickly. 10s is a generous fallback
|
||||
// for slow relay responses while still failing fast enough to retry promptly.
|
||||
const DECRYPT_TIMEOUT_MS = 10000;
|
||||
// PERF: Reduced from 10s to 3s — extensions that already granted permission
|
||||
// should recover quickly. The longer delay was causing unnecessary wait time
|
||||
// during the critical login path when the first decrypt attempt fails due to
|
||||
|
||||
@@ -9956,10 +9956,20 @@ export class ProfileModalController {
|
||||
const pubkey = this.normalizeHexPubkey(this.getActivePubkey());
|
||||
if (!pubkey) return;
|
||||
|
||||
// We need the active signer.
|
||||
const signer = getActiveSigner();
|
||||
// Or try resolving via client if global one is missing?
|
||||
// ProfileModalController imports getActiveSigner.
|
||||
// We need the active signer. Try the global first, then resolve via the
|
||||
// nostr client which can detect/restore the NIP-07 extension signer.
|
||||
let signer = getActiveSigner();
|
||||
if (
|
||||
!signer &&
|
||||
this.services.nostrClient &&
|
||||
typeof this.services.nostrClient.ensureActiveSignerForPubkey === "function"
|
||||
) {
|
||||
try {
|
||||
signer = await this.services.nostrClient.ensureActiveSignerForPubkey(pubkey);
|
||||
} catch (error) {
|
||||
devLogger.warn("[ProfileModal] Failed to resolve signer for storage unlock:", error);
|
||||
}
|
||||
}
|
||||
|
||||
if (!signer) {
|
||||
this.showError("No active signer found. Please login.");
|
||||
|
||||
+5
-2
@@ -83,8 +83,11 @@ export const USER_BLOCK_EVENTS = Object.freeze({
|
||||
const FAST_BLOCKLIST_RELAY_LIMIT = 3;
|
||||
const FAST_BLOCKLIST_TIMEOUT_MS = 2500;
|
||||
const BACKGROUND_BLOCKLIST_TIMEOUT_MS = 6000;
|
||||
const DECRYPT_TIMEOUT_MS = 20000;
|
||||
const BACKGROUND_DECRYPT_TIMEOUT_MS = 15000;
|
||||
// PERF: Reduced from 20s/15s to 10s/8s — the signer is now guaranteed to be
|
||||
// ready before list loading starts (authSessionCoordinator waits for the
|
||||
// permission pre-grant), so decryption should succeed quickly.
|
||||
const DECRYPT_TIMEOUT_MS = 10000;
|
||||
const BACKGROUND_DECRYPT_TIMEOUT_MS = 8000;
|
||||
// PERF: Reduced from 10s to 3s for faster recovery during login.
|
||||
const DECRYPT_RETRY_DELAY_MS = 3000;
|
||||
const MAX_BLOCKLIST_ENTRIES = 5000;
|
||||
|
||||
Reference in new issue
Block a user