Files
bitvid/js/nostr/signRequestQueue.js

131 lines
3.3 KiB
JavaScript

import { devLogger } from "../utils/logger.js";
const DEFAULT_SIGN_TIMEOUT_MS = 20_000;
const signQueues = new WeakMap();
const SIGN_ERROR_CODES = new Set([
"permission-denied",
"timeout",
"not-capable",
"signer-disconnected",
]);
function resolveTimeoutMs(timeoutMs) {
const candidate = Number(timeoutMs);
if (Number.isFinite(candidate) && candidate > 0) {
return Math.floor(candidate);
}
return DEFAULT_SIGN_TIMEOUT_MS;
}
function toError(value, message) {
if (value instanceof Error) {
return value;
}
const error = new Error(message);
error.details = value;
return error;
}
function normalizeSignError(error) {
const normalized = toError(error, "signEvent failed");
const existingCode = typeof normalized.code === "string" ? normalized.code : "";
if (SIGN_ERROR_CODES.has(existingCode)) {
return normalized;
}
const message = typeof normalized.message === "string"
? normalized.message.toLowerCase()
: "";
if (
existingCode === "extension-permission-denied" ||
message.includes("permission") ||
message.includes("denied") ||
message.includes("rejected")
) {
normalized.code = "permission-denied";
return normalized;
}
if (
existingCode === "nostr-extension-missing" ||
message.includes("disconnected") ||
message.includes("not connected") ||
message.includes("connection closed") ||
message.includes("extension missing")
) {
normalized.code = "signer-disconnected";
return normalized;
}
if (existingCode === "sign-event-unavailable") {
normalized.code = "not-capable";
return normalized;
}
return normalized;
}
function createSignError(code, originalError) {
const error = toError(
originalError,
originalError?.message || `signEvent failed with ${code}`,
);
error.code = code;
if (originalError && originalError !== error) {
error.details = originalError;
}
return error;
}
function runWithTimeout(operation, timeoutMs) {
const resolvedTimeout = resolveTimeoutMs(timeoutMs);
let timeoutId;
let timedOut = false;
const signPromise = Promise.resolve().then(operation);
const timeoutPromise = new Promise((_, reject) => {
timeoutId = setTimeout(() => {
timedOut = true;
reject(createSignError("timeout", new Error(
`signEvent timed out after ${resolvedTimeout}ms`,
)));
}, resolvedTimeout);
});
return Promise.race([signPromise, timeoutPromise]).finally(() => {
if (timeoutId) {
clearTimeout(timeoutId);
}
if (timedOut) {
void signPromise.catch((error) => {
devLogger.warn("[nostr] signEvent resolved after timeout:", error);
});
}
});
}
export async function queueSignEvent(signer, event, options = {}) {
if (!signer || typeof signer !== "object") {
throw createSignError("not-capable", new Error("signer-unavailable"));
}
if (typeof signer.signEvent !== "function") {
throw createSignError("not-capable", new Error("sign-event-unavailable"));
}
const queue = signQueues.get(signer) || Promise.resolve();
const runSign = async () => runWithTimeout(
() => signer.signEvent(event),
options.timeoutMs,
);
const queued = queue.then(runSign, runSign);
signQueues.set(signer, queued.catch(() => {}));
try {
return await queued;
} catch (error) {
throw normalizeSignError(error);
}
}