diff --git a/js/nostr/adapters/nip07Adapter.js b/js/nostr/adapters/nip07Adapter.js index dbdde2be..0c52dd4b 100644 --- a/js/nostr/adapters/nip07Adapter.js +++ b/js/nostr/adapters/nip07Adapter.js @@ -3,12 +3,12 @@ import { normalizeActorKey } from "../watchHistory.js"; import { runNip07WithRetry } from "../nip07Permissions.js"; -async function retryNip07Call(operation, label) { +async function retryNip07Call(operation, label, priority) { let lastError = null; const attempts = 2; // Retry once on failure for (let i = 0; i < attempts; i++) { try { - return await runNip07WithRetry(operation, { label }); + return await runNip07WithRetry(operation, { label, priority }); } catch (error) { lastError = error; // Don't retry if user explicitly rejected @@ -97,40 +97,56 @@ export async function createNip07Adapter(initialExtension) { const hasNip44 = !!bootstrapNip44; // Wrapper builders to ensure dynamic lookup at call time - const createNip04Encrypt = () => (pubkey, plaintext) => { + const createNip04Encrypt = () => (pubkey, plaintext, options = {}) => { const ext = getExtension(); const nip04 = ext?.nip04; if (!nip04 || typeof nip04.encrypt !== "function") { throw new Error("NIP-04 encryption unavailable on active extension."); } - return retryNip07Call(() => nip04.encrypt(pubkey, plaintext), "nip04.encrypt"); + return retryNip07Call( + () => nip04.encrypt(pubkey, plaintext), + "nip04.encrypt", + options?.priority, + ); }; - const createNip04Decrypt = () => (pubkey, ciphertext) => { + const createNip04Decrypt = () => (pubkey, ciphertext, options = {}) => { const ext = getExtension(); const nip04 = ext?.nip04; if (!nip04 || typeof nip04.decrypt !== "function") { throw new Error("NIP-04 decryption unavailable on active extension."); } - return retryNip07Call(() => nip04.decrypt(pubkey, ciphertext), "nip04.decrypt"); + return retryNip07Call( + () => nip04.decrypt(pubkey, ciphertext), + "nip04.decrypt", + options?.priority, + ); }; - const createNip44Encrypt = () => (pubkey, plaintext) => { + const createNip44Encrypt = () => (pubkey, plaintext, options = {}) => { const ext = getExtension(); const nip44 = resolveNip44Module(ext); if (!nip44 || typeof nip44.encrypt !== "function") { throw new Error("NIP-44 encryption unavailable on active extension."); } - return retryNip07Call(() => nip44.encrypt(pubkey, plaintext), "nip44.encrypt"); + return retryNip07Call( + () => nip44.encrypt(pubkey, plaintext), + "nip44.encrypt", + options?.priority, + ); }; - const createNip44Decrypt = () => (pubkey, ciphertext) => { + const createNip44Decrypt = () => (pubkey, ciphertext, options = {}) => { const ext = getExtension(); const nip44 = resolveNip44Module(ext); if (!nip44 || typeof nip44.decrypt !== "function") { throw new Error("NIP-44 decryption unavailable on active extension."); } - return retryNip07Call(() => nip44.decrypt(pubkey, ciphertext), "nip44.decrypt"); + return retryNip07Call( + () => nip44.decrypt(pubkey, ciphertext), + "nip44.decrypt", + options?.priority, + ); }; const requestPermissions = async (methods = []) => { diff --git a/js/nostr/adapters/nip46Adapter.js b/js/nostr/adapters/nip46Adapter.js index 7813e5f6..ec5fe36b 100644 --- a/js/nostr/adapters/nip46Adapter.js +++ b/js/nostr/adapters/nip46Adapter.js @@ -65,16 +65,20 @@ export async function createNip46Adapter(client) { }; if (typeof resolvedClient.nip04Encrypt === "function") { - signer.nip04Encrypt = resolvedClient.nip04Encrypt.bind(resolvedClient); + signer.nip04Encrypt = (pubkey, plaintext, options) => + resolvedClient.nip04Encrypt(pubkey, plaintext, options); } if (typeof resolvedClient.nip04Decrypt === "function") { - signer.nip04Decrypt = resolvedClient.nip04Decrypt.bind(resolvedClient); + signer.nip04Decrypt = (pubkey, ciphertext, options) => + resolvedClient.nip04Decrypt(pubkey, ciphertext, options); } if (typeof resolvedClient.nip44Encrypt === "function") { - signer.nip44Encrypt = resolvedClient.nip44Encrypt.bind(resolvedClient); + signer.nip44Encrypt = (pubkey, plaintext, options) => + resolvedClient.nip44Encrypt(pubkey, plaintext, options); } if (typeof resolvedClient.nip44Decrypt === "function") { - signer.nip44Decrypt = resolvedClient.nip44Decrypt.bind(resolvedClient); + signer.nip44Decrypt = (pubkey, ciphertext, options) => + resolvedClient.nip44Decrypt(pubkey, ciphertext, options); } signer.capabilities.nip04 = Boolean( diff --git a/js/nostr/nip07Permissions.js b/js/nostr/nip07Permissions.js index 181ecb84..f85a3a03 100644 --- a/js/nostr/nip07Permissions.js +++ b/js/nostr/nip07Permissions.js @@ -30,10 +30,65 @@ export const DEFAULT_NIP07_PERMISSION_METHODS = Object.freeze([ ...DEFAULT_NIP07_ENCRYPTION_METHODS, ]); -// Module-level mutex to serialize NIP-07 extension requests. +export const NIP07_PRIORITY = Object.freeze({ + HIGH: 10, + NORMAL: 5, + LOW: 1, +}); + +class Nip07RequestQueue { + constructor() { + this.queue = []; + this.running = false; + } + + enqueue(task, priority = NIP07_PRIORITY.NORMAL) { + return new Promise((resolve, reject) => { + this.queue.push({ + task, + priority: Number.isFinite(priority) ? priority : NIP07_PRIORITY.NORMAL, + resolve, + reject, + addedAt: Date.now(), + }); + // Sort by priority (descending), then by insertion time (ascending) for fairness + this.queue.sort((a, b) => { + if (b.priority !== a.priority) { + return b.priority - a.priority; + } + return a.addedAt - b.addedAt; + }); + this.process(); + }); + } + + async process() { + if (this.running) return; + this.running = true; + + while (this.queue.length > 0) { + const item = this.queue.shift(); + if (!item) break; + + const { task, resolve, reject } = item; + + try { + const result = await task(); + resolve(result); + } catch (error) { + reject(error); + } + } + + this.running = false; + } +} + +// Module-level priority queue to serialize NIP-07 extension requests. // This prevents "message channel closed" errors caused by race conditions -// when multiple components (blocks, DMs, auth) query the extension simultaneously. -let nip07Queue = Promise.resolve(); +// when multiple components (blocks, DMs, auth) query the extension simultaneously, +// while allowing critical tasks (e.g. blocklist decryption) to jump the line. +const requestQueue = new Nip07RequestQueue(); export function getEnableVariantTimeoutMs() { const overrideValue = @@ -195,6 +250,7 @@ export async function runNip07WithRetry( label = "NIP-07 operation", timeoutMs = NIP07_LOGIN_TIMEOUT_MS, retryMultiplier = 2, + priority = NIP07_PRIORITY.NORMAL, } = {}, ) { // We wrap the entire retry logic in a queue task to ensure only one @@ -236,22 +292,7 @@ export async function runNip07WithRetry( } }; - const queueItem = nip07Queue.then(() => - executeTask().then( - (res) => ({ ok: true, value: res }), - (err) => ({ ok: false, error: err }), - ), - ); - - // Advance the queue pointer - nip07Queue = queueItem.then(() => {}); - - // Await the specific result for this call - const result = await queueItem; - if (result.ok) { - return result.value; - } - throw result.error; + return requestQueue.enqueue(executeTask, priority); } export async function requestEnablePermissions( diff --git a/js/nostr/nip46Client.js b/js/nostr/nip46Client.js index da574157..e0832b57 100644 --- a/js/nostr/nip46Client.js +++ b/js/nostr/nip46Client.js @@ -2105,12 +2105,16 @@ export class Nip46RpcClient { } } - async nip04Encrypt(pubkey, plaintext) { + async nip04Encrypt(pubkey, plaintext, options = {}) { if (!pubkey || !plaintext) { throw new Error("Pubkey and plaintext are required for NIP-04 encryption."); } + const priority = + Number.isFinite(options?.priority) + ? options.priority + : NIP46_PRIORITY.LOW; const result = await this.sendRpc("nip04_encrypt", [pubkey, plaintext], { - priority: NIP46_PRIORITY.LOW, + priority, }); if (typeof result !== "string") { throw new Error("Remote signer returned invalid NIP-04 ciphertext."); @@ -2118,12 +2122,16 @@ export class Nip46RpcClient { return result; } - async nip04Decrypt(pubkey, ciphertext) { + async nip04Decrypt(pubkey, ciphertext, options = {}) { if (!pubkey || !ciphertext) { throw new Error("Pubkey and ciphertext are required for NIP-04 decryption."); } + const priority = + Number.isFinite(options?.priority) + ? options.priority + : NIP46_PRIORITY.LOW; const result = await this.sendRpc("nip04_decrypt", [pubkey, ciphertext], { - priority: NIP46_PRIORITY.LOW, + priority, }); if (typeof result !== "string") { throw new Error("Remote signer returned invalid NIP-04 plaintext."); @@ -2131,12 +2139,16 @@ export class Nip46RpcClient { return result; } - async nip44Encrypt(pubkey, plaintext) { + async nip44Encrypt(pubkey, plaintext, options = {}) { if (!pubkey || !plaintext) { throw new Error("Pubkey and plaintext are required for NIP-44 encryption."); } + const priority = + Number.isFinite(options?.priority) + ? options.priority + : NIP46_PRIORITY.LOW; const result = await this.sendRpc("nip44_encrypt", [pubkey, plaintext], { - priority: NIP46_PRIORITY.LOW, + priority, }); if (typeof result !== "string") { throw new Error("Remote signer returned invalid NIP-44 ciphertext."); @@ -2144,12 +2156,16 @@ export class Nip46RpcClient { return result; } - async nip44Decrypt(pubkey, ciphertext) { + async nip44Decrypt(pubkey, ciphertext, options = {}) { if (!pubkey || !ciphertext) { throw new Error("Pubkey and ciphertext are required for NIP-44 decryption."); } + const priority = + Number.isFinite(options?.priority) + ? options.priority + : NIP46_PRIORITY.LOW; const result = await this.sendRpc("nip44_decrypt", [pubkey, ciphertext], { - priority: NIP46_PRIORITY.LOW, + priority, }); if (typeof result !== "string") { throw new Error("Remote signer returned invalid NIP-44 plaintext."); @@ -2168,13 +2184,25 @@ export class Nip46RpcClient { pubkey: this.userPubkey, signEvent: (event) => this.signEvent(event), nip04: { - encrypt: (pubkey, plaintext) => this.nip04Encrypt(pubkey, plaintext), - decrypt: (pubkey, ciphertext) => this.nip04Decrypt(pubkey, ciphertext), + encrypt: (pubkey, plaintext, options) => + this.nip04Encrypt(pubkey, plaintext, options), + decrypt: (pubkey, ciphertext, options) => + this.nip04Decrypt(pubkey, ciphertext, options), }, nip44: { - encrypt: (pubkey, plaintext) => this.nip44Encrypt(pubkey, plaintext), - decrypt: (pubkey, ciphertext) => this.nip44Decrypt(pubkey, ciphertext), + encrypt: (pubkey, plaintext, options) => + this.nip44Encrypt(pubkey, plaintext, options), + decrypt: (pubkey, ciphertext, options) => + this.nip44Decrypt(pubkey, ciphertext, options), }, + nip04Encrypt: (pubkey, plaintext, options) => + this.nip04Encrypt(pubkey, plaintext, options), + nip04Decrypt: (pubkey, ciphertext, options) => + this.nip04Decrypt(pubkey, ciphertext, options), + nip44Encrypt: (pubkey, plaintext, options) => + this.nip44Encrypt(pubkey, plaintext, options), + nip44Decrypt: (pubkey, ciphertext, options) => + this.nip44Decrypt(pubkey, ciphertext, options), }; } diff --git a/js/services/hashtagPreferencesService.js b/js/services/hashtagPreferencesService.js index 715f964f..2e4ce5c2 100644 --- a/js/services/hashtagPreferencesService.js +++ b/js/services/hashtagPreferencesService.js @@ -19,7 +19,7 @@ import { normalizeHashtag } from "../utils/hashtagNormalization.js"; import { profileCache } from "../state/profileCache.js"; import { DEFAULT_RELAY_URLS } from "../nostr/toolkit.js"; import { relayManager } from "../relayManager.js"; -import { runNip07WithRetry } from "../nostr/nip07Permissions.js"; +import { runNip07WithRetry, NIP07_PRIORITY } from "../nostr/nip07Permissions.js"; const LOG_PREFIX = "[HashtagPreferences]"; const HASHTAG_IDENTIFIER = "bitvid:tag-preferences"; @@ -739,12 +739,18 @@ class HashtagPreferencesService { if (signerHasNip44) { registerDecryptor( "nip44", - (payload) => signer.nip44Decrypt(userPubkey, payload), + (payload) => + signer.nip44Decrypt(userPubkey, payload, { + priority: NIP07_PRIORITY.HIGH, + }), "active-signer", ); registerDecryptor( "nip44_v2", - (payload) => signer.nip44Decrypt(userPubkey, payload), + (payload) => + signer.nip44Decrypt(userPubkey, payload, { + priority: NIP07_PRIORITY.HIGH, + }), "active-signer", ); } @@ -752,7 +758,10 @@ class HashtagPreferencesService { if (signerHasNip04) { registerDecryptor( "nip04", - (payload) => signer.nip04Decrypt(userPubkey, payload), + (payload) => + signer.nip04Decrypt(userPubkey, payload, { + priority: NIP07_PRIORITY.HIGH, + }), "active-signer", ); } @@ -765,7 +774,7 @@ class HashtagPreferencesService { (payload) => runNip07WithRetry( () => nostrApi.nip04.decrypt(userPubkey, payload), - { label: "nip04.decrypt" }, + { label: "nip04.decrypt", priority: NIP07_PRIORITY.HIGH }, ), "extension", ); @@ -780,9 +789,13 @@ class HashtagPreferencesService { registerDecryptor( "nip44", (payload) => - runNip07WithRetry(() => nip44.decrypt(userPubkey, payload), { - label: "nip44.decrypt", - }), + runNip07WithRetry( + () => nip44.decrypt(userPubkey, payload), + { + label: "nip44.decrypt", + priority: NIP07_PRIORITY.HIGH, + }, + ), "extension", ); } @@ -793,18 +806,26 @@ class HashtagPreferencesService { registerDecryptor( "nip44_v2", (payload) => - runNip07WithRetry(() => nip44v2.decrypt(userPubkey, payload), { - label: "nip44.v2.decrypt", - }), + runNip07WithRetry( + () => nip44v2.decrypt(userPubkey, payload), + { + label: "nip44.v2.decrypt", + priority: NIP07_PRIORITY.HIGH, + }, + ), "extension", ); if (!decryptors.has("nip44")) { registerDecryptor( "nip44", (payload) => - runNip07WithRetry(() => nip44v2.decrypt(userPubkey, payload), { - label: "nip44.v2.decrypt", - }), + runNip07WithRetry( + () => nip44v2.decrypt(userPubkey, payload), + { + label: "nip44.v2.decrypt", + priority: NIP07_PRIORITY.HIGH, + }, + ), "extension", ); } diff --git a/js/subscriptions.js b/js/subscriptions.js index 146401fd..4c2bfc21 100644 --- a/js/subscriptions.js +++ b/js/subscriptions.js @@ -31,6 +31,7 @@ import { devLogger, userLogger } from "./utils/logger.js"; import moderationService from "./services/moderationService.js"; import nostrService from "./services/nostrService.js"; import { profileCache } from "./state/profileCache.js"; +import { runNip07WithRetry, NIP07_PRIORITY } from "./nostr/nip07Permissions.js"; const SUBSCRIPTION_SET_KIND = getNostrEventSchema(NOTE_TYPES.SUBSCRIPTION_LIST)?.kind ?? 30000; @@ -776,22 +777,36 @@ class SubscriptionsManager { }; if (signerHasNip44) { - registerDecryptor("nip44", (payload) => signer.nip44Decrypt(userPubkey, payload)); - registerDecryptor( - "nip44_v2", - (payload) => signer.nip44Decrypt(userPubkey, payload) + registerDecryptor("nip44", (payload) => + signer.nip44Decrypt(userPubkey, payload, { + priority: NIP07_PRIORITY.HIGH, + }), + ); + registerDecryptor("nip44_v2", (payload) => + signer.nip44Decrypt(userPubkey, payload, { + priority: NIP07_PRIORITY.HIGH, + }), ); } if (signerHasNip04) { - registerDecryptor("nip04", (payload) => signer.nip04Decrypt(userPubkey, payload)); + registerDecryptor("nip04", (payload) => + signer.nip04Decrypt(userPubkey, payload, { + priority: NIP07_PRIORITY.HIGH, + }), + ); } if (nostrApi) { if (typeof nostrApi.nip04?.decrypt === "function") { - registerDecryptor( - "nip04", - (payload) => nostrApi.nip04.decrypt(userPubkey, payload) + registerDecryptor("nip04", (payload) => + runNip07WithRetry( + () => nostrApi.nip04.decrypt(userPubkey, payload), + { + label: "nip04.decrypt", + priority: NIP07_PRIORITY.HIGH, + }, + ), ); } @@ -801,23 +816,38 @@ class SubscriptionsManager { : null; if (nip44) { if (typeof nip44.decrypt === "function") { - registerDecryptor( - "nip44", - (payload) => nip44.decrypt(userPubkey, payload) + registerDecryptor("nip44", (payload) => + runNip07WithRetry( + () => nip44.decrypt(userPubkey, payload), + { + label: "nip44.decrypt", + priority: NIP07_PRIORITY.HIGH, + }, + ), ); } const nip44v2 = nip44.v2 && typeof nip44.v2 === "object" ? nip44.v2 : null; if (nip44v2 && typeof nip44v2.decrypt === "function") { - registerDecryptor( - "nip44_v2", - (payload) => nip44v2.decrypt(userPubkey, payload) + registerDecryptor("nip44_v2", (payload) => + runNip07WithRetry( + () => nip44v2.decrypt(userPubkey, payload), + { + label: "nip44.v2.decrypt", + priority: NIP07_PRIORITY.HIGH, + }, + ), ); if (!decryptors.has("nip44")) { - registerDecryptor( - "nip44", - (payload) => nip44v2.decrypt(userPubkey, payload) + registerDecryptor("nip44", (payload) => + runNip07WithRetry( + () => nip44v2.decrypt(userPubkey, payload), + { + label: "nip44.v2.decrypt", + priority: NIP07_PRIORITY.HIGH, + }, + ), ); } } diff --git a/js/userBlocks.js b/js/userBlocks.js index 2a348fbc..1687fc2f 100644 --- a/js/userBlocks.js +++ b/js/userBlocks.js @@ -20,7 +20,7 @@ import { import { profileCache } from "./state/profileCache.js"; import { DEFAULT_RELAY_URLS } from "./nostr/toolkit.js"; import { relayManager } from "./relayManager.js"; -import { runNip07WithRetry } from "./nostr/nip07Permissions.js"; +import { runNip07WithRetry, NIP07_PRIORITY } from "./nostr/nip07Permissions.js"; class TinyEventEmitter { constructor() { @@ -852,12 +852,18 @@ class UserBlockListManager { if (signerHasNip44) { registerDecryptor( "nip44", - (payload) => signer.nip44Decrypt(normalized, payload), + (payload) => + signer.nip44Decrypt(normalized, payload, { + priority: NIP07_PRIORITY.HIGH, + }), "active-signer", ); registerDecryptor( "nip44_v2", - (payload) => signer.nip44Decrypt(normalized, payload), + (payload) => + signer.nip44Decrypt(normalized, payload, { + priority: NIP07_PRIORITY.HIGH, + }), "active-signer", ); } @@ -865,7 +871,10 @@ class UserBlockListManager { if (signerHasNip04) { registerDecryptor( "nip04", - (payload) => signer.nip04Decrypt(normalized, payload), + (payload) => + signer.nip04Decrypt(normalized, payload, { + priority: NIP07_PRIORITY.HIGH, + }), "active-signer", ); } @@ -878,7 +887,7 @@ class UserBlockListManager { (payload) => runNip07WithRetry( () => nostrApi.nip04.decrypt(normalized, payload), - { label: "nip04.decrypt" }, + { label: "nip04.decrypt", priority: NIP07_PRIORITY.HIGH }, ), "extension", ); @@ -895,6 +904,7 @@ class UserBlockListManager { (payload) => runNip07WithRetry(() => nip44.decrypt(normalized, payload), { label: "nip44.decrypt", + priority: NIP07_PRIORITY.HIGH, }), "extension", ); @@ -908,6 +918,7 @@ class UserBlockListManager { (payload) => runNip07WithRetry(() => nip44v2.decrypt(normalized, payload), { label: "nip44.v2.decrypt", + priority: NIP07_PRIORITY.HIGH, }), "extension", ); @@ -917,7 +928,7 @@ class UserBlockListManager { (payload) => runNip07WithRetry( () => nip44v2.decrypt(normalized, payload), - { label: "nip44.v2.decrypt" }, + { label: "nip44.v2.decrypt", priority: NIP07_PRIORITY.HIGH }, ), "extension", );