Merge pull request #1880 from PR0M3TH3AN/feat/prioritize-profile-sync-17123164066717764298

Prioritize profile and feed data synchronization
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2026-01-28 10:12:06 -05:00
commit 2dc23a5bb4
7 files changed
+233 -82

No files matched your search

+26 -10
View File
@@ -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 = []) => {
+8 -4
View File
@@ -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(
+60 -19
View File
@@ -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(
+40 -12
View File
@@ -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),
};
}
+35 -14
View File
@@ -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",
);
}
+47 -17
View File
@@ -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,
},
),
);
}
}
+17 -6
View File
@@ -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",
);