diff --git a/js/nostr/adapters/nip07Adapter.js b/js/nostr/adapters/nip07Adapter.js index dbe0f564..37bf5068 100644 --- a/js/nostr/adapters/nip07Adapter.js +++ b/js/nostr/adapters/nip07Adapter.js @@ -42,7 +42,7 @@ function resolveNip44Module(extension) { return nip44; } -export async function createNip07Adapter(initialExtension) { +export async function createNip07Adapter(initialExtension, { preloadedPubkey } = {}) { const getExtension = () => { if (typeof window !== "undefined" && window.nostr) { return window.nostr; @@ -86,9 +86,10 @@ export async function createNip07Adapter(initialExtension) { }; const pubkey = - typeof bootstrapExtension.getPublicKey === "function" + preloadedPubkey || + (typeof bootstrapExtension.getPublicKey === "function" ? normalizeActorKey(await getPublicKey()) - : ""; + : ""); // Wrapper builders to ensure dynamic lookup at call time const createNip04Encrypt = () => (pubkey, plaintext, options = {}) => { diff --git a/js/nostr/client.js b/js/nostr/client.js index 9c473f41..893edabb 100644 --- a/js/nostr/client.js +++ b/js/nostr/client.js @@ -3053,11 +3053,19 @@ export class NostrClient { return existingSigner; } + // Optimization: Check if a signer was registered while we were waiting for the extension call + const raceWinner = resolveActiveSigner(extensionPubkey || normalizedPubkey); + if (raceWinner && typeof raceWinner.signEvent === "function") { + return raceWinner; + } + if (typeof extension.signEvent !== "function") { return existingSigner; } - const adapter = await createNip07Adapter(extension); + const adapter = await createNip07Adapter(extension, { + preloadedPubkey: extensionPubkey, + }); if (extensionPubkey) { adapter.pubkey = extensionPubkey; } diff --git a/js/subscriptions.js b/js/subscriptions.js index 6a61cc8d..bccb873e 100644 --- a/js/subscriptions.js +++ b/js/subscriptions.js @@ -491,7 +491,7 @@ class SubscriptionsManager { pubkey: normalizedUserPubkey, dTag: SUBSCRIPTION_LIST_IDENTIFIER, relayUrls, - since: shouldForceFullFetch ? 0 : undefined, + since: shouldForceFullFetch ? 0 : (cachedSnapshot.createdAt || 0), timeoutMs: 12000, }); @@ -514,7 +514,7 @@ class SubscriptionsManager { pubkey: normalizedSessionActorPubkey, dTag: SUBSCRIPTION_LIST_IDENTIFIER, relayUrls, - since: shouldForceSessionFetch ? 0 : undefined, + since: shouldForceSessionFetch ? 0 : (sessionCachedSnapshot.createdAt || 0), timeoutMs: 12000, }); if (sessionEvents.length) { diff --git a/js/userBlocks.js b/js/userBlocks.js index 0f4724f0..4601dee6 100644 --- a/js/userBlocks.js +++ b/js/userBlocks.js @@ -908,11 +908,14 @@ class UserBlockListManager { } this.ensureBlockListSubscription(normalized, relays); - const localBlocks = this._loadLocal(normalized); - const hasLocalData = !!localBlocks; + const localData = this._loadLocal(normalized); + const hasLocalData = !!localData; - if (localBlocks) { - this.blockedPubkeys = localBlocks; + if (localData) { + this.blockedPubkeys = localData.blockedPubkeys; + if (Number.isFinite(localData.createdAt)) { + this.blockEventCreatedAt = localData.createdAt; + } this.loaded = true; this.emitter.emit(USER_BLOCK_EVENTS.CHANGE, { action: "sync", @@ -1385,9 +1388,13 @@ class UserBlockListManager { } }; - // If we don't have local data, we must ignore the sync metadata and force a full fetch (since: 0). - // Otherwise, we might miss data if the metadata store says we are up-to-date but the actual list is missing. - const fetchSince = hasLocalData ? undefined : 0; + // Anchor the fetch to the service's current state to prevent desync. + // If we have cached data, we ask for updates since that timestamp. + // If we have no data, we force a full fetch (since=0). + const fetchSince = + hasLocalData && Number.isFinite(this.blockEventCreatedAt) + ? this.blockEventCreatedAt + : 0; // Concurrent incremental fetch for both kinds const [muteEvents, blockEvents] = await Promise.all([ @@ -1437,7 +1444,15 @@ class UserBlockListManager { } const cached = profileCache.getProfileData(normalizedActorHex, "blocks"); if (Array.isArray(cached)) { - return new Set(cached); + return { blockedPubkeys: new Set(cached), createdAt: null }; + } + if (cached && typeof cached === "object") { + return { + blockedPubkeys: new Set( + Array.isArray(cached.blockedPubkeys) ? cached.blockedPubkeys : [], + ), + createdAt: Number.isFinite(cached.createdAt) ? cached.createdAt : null, + }; } return null; } @@ -1447,11 +1462,11 @@ class UserBlockListManager { if (!normalizedActorHex) { return; } - profileCache.setProfileData( - normalizedActorHex, - "blocks", - Array.from(this.blockedPubkeys), - ); + profileCache.setProfileData(normalizedActorHex, "blocks", { + blockedPubkeys: Array.from(this.blockedPubkeys), + createdAt: this.blockEventCreatedAt, + version: 1, + }); } _getSeedState(actorHex) { diff --git a/tests/subscriptions-manager.test.mjs b/tests/subscriptions-manager.test.mjs index 183eeb42..d325c089 100644 --- a/tests/subscriptions-manager.test.mjs +++ b/tests/subscriptions-manager.test.mjs @@ -1,3 +1,4 @@ +import "./test-helpers/setup-localstorage.mjs"; import { test } from "node:test"; import assert from "node:assert/strict";