mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
fix(nostr): serialize nip-07 requests to prevent extension timeouts and crashes
Introduced a request queue in `js/nostr/nip07Permissions.js` to serialize all interactions with the NIP-07 extension (`window.nostr`). Updated `js/userBlocks.js`, `js/services/hashtagPreferencesService.js`, and `js/auth/signingAdapter.js` to wrap direct extension calls with `runNip07WithRetry`, ensuring they participate in the queue. This resolves "message channel closed" errors and timeouts caused by concurrent requests flooding the extension during sync operations. Co-authored-by: PR0M3TH3AN <[email protected]>
This commit is contained in:
1 parent
dc7257855a
commit
498909d60e
4 files changed
+130
-46
No files matched your search
@@ -3,6 +3,7 @@ import { signEventWithPrivateKey } from "../nostr/publishHelpers.js";
|
||||
import { ensureNostrTools, getCachedNostrTools } from "../nostr/toolkit.js";
|
||||
import { normalizeActorKey } from "../nostr/watchHistory.js";
|
||||
import { buildHttpAuthEvent } from "../nostrEventSchemas.js";
|
||||
import { runNip07WithRetry } from "../nostr/nip07Permissions.js";
|
||||
|
||||
const DEFAULT_TEST_PUBKEY = "f".repeat(64);
|
||||
const DEFAULT_DEV_DISPLAY_NAME = "Ephemeral dev signer";
|
||||
@@ -31,22 +32,37 @@ export function createNip07SigningAdapter({ extension } = {}) {
|
||||
(typeof window !== "undefined" && window?.nostr ? window.nostr : null);
|
||||
|
||||
const getPubkey = async () => {
|
||||
if (!resolvedExtension || typeof resolvedExtension.getPublicKey !== "function") {
|
||||
if (
|
||||
!resolvedExtension ||
|
||||
typeof resolvedExtension.getPublicKey !== "function"
|
||||
) {
|
||||
throw new Error("NIP-07 extension is unavailable.");
|
||||
}
|
||||
return normalizePubkey(await resolvedExtension.getPublicKey());
|
||||
return normalizePubkey(
|
||||
await runNip07WithRetry(() => resolvedExtension.getPublicKey(), {
|
||||
label: "extension.getPublicKey",
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const getDisplayName = async () => {
|
||||
if (!resolvedExtension || typeof resolvedExtension.getMetadata !== "function") {
|
||||
if (
|
||||
!resolvedExtension ||
|
||||
typeof resolvedExtension.getMetadata !== "function"
|
||||
) {
|
||||
return "";
|
||||
}
|
||||
const metadata = await resolvedExtension.getMetadata();
|
||||
const metadata = await runNip07WithRetry(
|
||||
() => resolvedExtension.getMetadata(),
|
||||
{ label: "extension.getMetadata" },
|
||||
);
|
||||
if (!metadata || typeof metadata !== "object") {
|
||||
return "";
|
||||
}
|
||||
const displayName =
|
||||
typeof metadata.display_name === "string" ? metadata.display_name.trim() : "";
|
||||
typeof metadata.display_name === "string"
|
||||
? metadata.display_name.trim()
|
||||
: "";
|
||||
if (displayName) {
|
||||
return displayName;
|
||||
}
|
||||
@@ -54,17 +70,27 @@ export function createNip07SigningAdapter({ extension } = {}) {
|
||||
};
|
||||
|
||||
const signEvent = async (event) => {
|
||||
if (!resolvedExtension || typeof resolvedExtension.signEvent !== "function") {
|
||||
if (
|
||||
!resolvedExtension ||
|
||||
typeof resolvedExtension.signEvent !== "function"
|
||||
) {
|
||||
throw new Error("NIP-07 extension is missing signEvent.");
|
||||
}
|
||||
return resolvedExtension.signEvent(event);
|
||||
return runNip07WithRetry(() => resolvedExtension.signEvent(event), {
|
||||
label: "extension.signEvent",
|
||||
});
|
||||
};
|
||||
|
||||
const signMessage = async (message) => {
|
||||
if (!resolvedExtension || typeof resolvedExtension.signMessage !== "function") {
|
||||
if (
|
||||
!resolvedExtension ||
|
||||
typeof resolvedExtension.signMessage !== "function"
|
||||
) {
|
||||
throw new Error("NIP-07 extension does not support signMessage.");
|
||||
}
|
||||
return resolvedExtension.signMessage(message);
|
||||
return runNip07WithRetry(() => resolvedExtension.signMessage(message), {
|
||||
label: "extension.signMessage",
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -30,6 +30,11 @@ export const DEFAULT_NIP07_PERMISSION_METHODS = Object.freeze([
|
||||
...DEFAULT_NIP07_ENCRYPTION_METHODS,
|
||||
]);
|
||||
|
||||
// Module-level mutex 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();
|
||||
|
||||
export function getEnableVariantTimeoutMs() {
|
||||
const overrideValue =
|
||||
typeof globalThis !== "undefined" &&
|
||||
@@ -192,40 +197,61 @@ export async function runNip07WithRetry(
|
||||
retryMultiplier = 2,
|
||||
} = {},
|
||||
) {
|
||||
// We wrap the operation invocation to ensure it returns a fresh promise each time
|
||||
// we attempt it. This is critical for retries: if the first attempt stalls (dropped
|
||||
// by extension), re-awaiting the same promise would just keep waiting on the dead request.
|
||||
const invokeOperation = () => Promise.resolve(operation());
|
||||
// We wrap the entire retry logic in a queue task to ensure only one
|
||||
// request hits the extension at a time.
|
||||
const executeTask = async () => {
|
||||
// We wrap the operation invocation to ensure it returns a fresh promise each time
|
||||
// we attempt it. This is critical for retries: if the first attempt stalls (dropped
|
||||
// by extension), re-awaiting the same promise would just keep waiting on the dead request.
|
||||
const invokeOperation = () => Promise.resolve(operation());
|
||||
|
||||
try {
|
||||
return await withNip07Timeout(invokeOperation, {
|
||||
timeoutMs,
|
||||
message: NIP07_LOGIN_TIMEOUT_ERROR_MESSAGE,
|
||||
});
|
||||
} catch (error) {
|
||||
const isTimeoutError =
|
||||
error instanceof Error &&
|
||||
error.message === NIP07_LOGIN_TIMEOUT_ERROR_MESSAGE;
|
||||
try {
|
||||
return await withNip07Timeout(invokeOperation, {
|
||||
timeoutMs,
|
||||
message: NIP07_LOGIN_TIMEOUT_ERROR_MESSAGE,
|
||||
});
|
||||
} catch (error) {
|
||||
const isTimeoutError =
|
||||
error instanceof Error &&
|
||||
error.message === NIP07_LOGIN_TIMEOUT_ERROR_MESSAGE;
|
||||
|
||||
if (!isTimeoutError || retryMultiplier <= 1) {
|
||||
throw error;
|
||||
if (!isTimeoutError || retryMultiplier <= 1) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const extendedTimeout = Math.max(
|
||||
timeoutMs,
|
||||
Math.round(timeoutMs * retryMultiplier),
|
||||
);
|
||||
|
||||
devLogger.warn(
|
||||
`[nostr] ${label} taking longer than ${timeoutMs}ms. Retrying with ${extendedTimeout}ms timeout.`,
|
||||
);
|
||||
|
||||
// On retry, we invoke operation() again to send a fresh request to the extension.
|
||||
return withNip07Timeout(invokeOperation, {
|
||||
timeoutMs: extendedTimeout,
|
||||
message: NIP07_LOGIN_TIMEOUT_ERROR_MESSAGE,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const extendedTimeout = Math.max(
|
||||
timeoutMs,
|
||||
Math.round(timeoutMs * retryMultiplier),
|
||||
);
|
||||
const queueItem = nip07Queue.then(() =>
|
||||
executeTask().then(
|
||||
(res) => ({ ok: true, value: res }),
|
||||
(err) => ({ ok: false, error: err }),
|
||||
),
|
||||
);
|
||||
|
||||
devLogger.warn(
|
||||
`[nostr] ${label} taking longer than ${timeoutMs}ms. Retrying with ${extendedTimeout}ms timeout.`,
|
||||
);
|
||||
// Advance the queue pointer
|
||||
nip07Queue = queueItem.then(() => {});
|
||||
|
||||
// On retry, we invoke operation() again to send a fresh request to the extension.
|
||||
return withNip07Timeout(invokeOperation, {
|
||||
timeoutMs: extendedTimeout,
|
||||
message: NIP07_LOGIN_TIMEOUT_ERROR_MESSAGE,
|
||||
});
|
||||
// Await the specific result for this call
|
||||
const result = await queueItem;
|
||||
if (result.ok) {
|
||||
return result.value;
|
||||
}
|
||||
throw result.error;
|
||||
}
|
||||
|
||||
export async function requestEnablePermissions(
|
||||
|
||||
@@ -19,6 +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";
|
||||
|
||||
const LOG_PREFIX = "[HashtagPreferences]";
|
||||
const HASHTAG_IDENTIFIER = "bitvid:tag-preferences";
|
||||
@@ -761,7 +762,11 @@ class HashtagPreferencesService {
|
||||
if (typeof nostrApi.nip04?.decrypt === "function") {
|
||||
registerDecryptor(
|
||||
"nip04",
|
||||
(payload) => nostrApi.nip04.decrypt(userPubkey, payload),
|
||||
(payload) =>
|
||||
runNip07WithRetry(
|
||||
() => nostrApi.nip04.decrypt(userPubkey, payload),
|
||||
{ label: "nip04.decrypt" },
|
||||
),
|
||||
"extension",
|
||||
);
|
||||
}
|
||||
@@ -774,7 +779,10 @@ class HashtagPreferencesService {
|
||||
if (typeof nip44.decrypt === "function") {
|
||||
registerDecryptor(
|
||||
"nip44",
|
||||
(payload) => nip44.decrypt(userPubkey, payload),
|
||||
(payload) =>
|
||||
runNip07WithRetry(() => nip44.decrypt(userPubkey, payload), {
|
||||
label: "nip44.decrypt",
|
||||
}),
|
||||
"extension",
|
||||
);
|
||||
}
|
||||
@@ -784,13 +792,19 @@ class HashtagPreferencesService {
|
||||
if (nip44v2 && typeof nip44v2.decrypt === "function") {
|
||||
registerDecryptor(
|
||||
"nip44_v2",
|
||||
(payload) => nip44v2.decrypt(userPubkey, payload),
|
||||
(payload) =>
|
||||
runNip07WithRetry(() => nip44v2.decrypt(userPubkey, payload), {
|
||||
label: "nip44.v2.decrypt",
|
||||
}),
|
||||
"extension",
|
||||
);
|
||||
if (!decryptors.has("nip44")) {
|
||||
registerDecryptor(
|
||||
"nip44",
|
||||
(payload) => nip44v2.decrypt(userPubkey, payload),
|
||||
(payload) =>
|
||||
runNip07WithRetry(() => nip44v2.decrypt(userPubkey, payload), {
|
||||
label: "nip44.v2.decrypt",
|
||||
}),
|
||||
"extension",
|
||||
);
|
||||
}
|
||||
|
||||
+24
-6
@@ -20,6 +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";
|
||||
|
||||
class TinyEventEmitter {
|
||||
constructor() {
|
||||
@@ -874,33 +875,50 @@ class UserBlockListManager {
|
||||
if (typeof nostrApi.nip04?.decrypt === "function") {
|
||||
registerDecryptor(
|
||||
"nip04",
|
||||
(payload) => nostrApi.nip04.decrypt(normalized, payload),
|
||||
(payload) =>
|
||||
runNip07WithRetry(
|
||||
() => nostrApi.nip04.decrypt(normalized, payload),
|
||||
{ label: "nip04.decrypt" },
|
||||
),
|
||||
"extension",
|
||||
);
|
||||
}
|
||||
|
||||
const nip44 =
|
||||
nostrApi.nip44 && typeof nostrApi.nip44 === "object" ? nostrApi.nip44 : null;
|
||||
nostrApi.nip44 && typeof nostrApi.nip44 === "object"
|
||||
? nostrApi.nip44
|
||||
: null;
|
||||
if (nip44) {
|
||||
if (typeof nip44.decrypt === "function") {
|
||||
registerDecryptor(
|
||||
"nip44",
|
||||
(payload) => nip44.decrypt(normalized, payload),
|
||||
(payload) =>
|
||||
runNip07WithRetry(() => nip44.decrypt(normalized, payload), {
|
||||
label: "nip44.decrypt",
|
||||
}),
|
||||
"extension",
|
||||
);
|
||||
}
|
||||
|
||||
const nip44v2 = nip44.v2 && typeof nip44.v2 === "object" ? nip44.v2 : null;
|
||||
const nip44v2 =
|
||||
nip44.v2 && typeof nip44.v2 === "object" ? nip44.v2 : null;
|
||||
if (nip44v2 && typeof nip44v2.decrypt === "function") {
|
||||
registerDecryptor(
|
||||
"nip44_v2",
|
||||
(payload) => nip44v2.decrypt(normalized, payload),
|
||||
(payload) =>
|
||||
runNip07WithRetry(() => nip44v2.decrypt(normalized, payload), {
|
||||
label: "nip44.v2.decrypt",
|
||||
}),
|
||||
"extension",
|
||||
);
|
||||
if (!decryptors.has("nip44")) {
|
||||
registerDecryptor(
|
||||
"nip44",
|
||||
(payload) => nip44v2.decrypt(normalized, payload),
|
||||
(payload) =>
|
||||
runNip07WithRetry(
|
||||
() => nip44v2.decrypt(normalized, payload),
|
||||
{ label: "nip44.v2.decrypt" },
|
||||
),
|
||||
"extension",
|
||||
);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user