mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
fix(nostr): harden nip-07 adapter and fix hashtag sync issues
- Wrapped NIP-07 adapter extension calls in `retryNip07Call` to handle transient failures and timeouts robustly. - Updated `HashtagPreferencesService` to autosave changes to relays immediately on modification. - Added race condition protection in `HashtagPreferencesService.load` to prevent overwriting optimistic local updates with stale relay data. Co-authored-by: PR0M3TH3AN <[email protected]>
This commit is contained in:
1 parent
de463a86f4
commit
5c1d1d340b
2 files changed
+77
-15
No files matched your search
@@ -1,6 +1,30 @@
|
||||
// js/nostr/adapters/nip07Adapter.js
|
||||
|
||||
import { normalizeActorKey } from "../watchHistory.js";
|
||||
import { runNip07WithRetry } from "../nip07Permissions.js";
|
||||
|
||||
async function retryNip07Call(operation, label) {
|
||||
let lastError = null;
|
||||
const attempts = 2; // Retry once on failure
|
||||
for (let i = 0; i < attempts; i++) {
|
||||
try {
|
||||
return await runNip07WithRetry(operation, { label });
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
// Don't retry if user explicitly rejected
|
||||
if (
|
||||
error?.message?.toLowerCase().includes("denied") ||
|
||||
error?.message?.toLowerCase().includes("rejected")
|
||||
) {
|
||||
throw error;
|
||||
}
|
||||
if (i < attempts - 1) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
}
|
||||
}
|
||||
}
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
function resolveNip44Module(extension) {
|
||||
const nip44 = extension?.nip44 && typeof extension.nip44 === "object" ? extension.nip44 : null;
|
||||
@@ -54,27 +78,51 @@ export async function createNip07Adapter(extension) {
|
||||
const nip44 = resolveNip44Module(resolvedExtension);
|
||||
|
||||
const nip04Encrypt =
|
||||
nip04 && typeof nip04.encrypt === "function" ? nip04.encrypt.bind(nip04) : null;
|
||||
nip04 && typeof nip04.encrypt === "function"
|
||||
? (pubkey, plaintext) =>
|
||||
retryNip07Call(() => nip04.encrypt(pubkey, plaintext), "nip04.encrypt")
|
||||
: null;
|
||||
|
||||
const nip04Decrypt =
|
||||
nip04 && typeof nip04.decrypt === "function" ? nip04.decrypt.bind(nip04) : null;
|
||||
nip04 && typeof nip04.decrypt === "function"
|
||||
? (pubkey, ciphertext) =>
|
||||
retryNip07Call(() => nip04.decrypt(pubkey, ciphertext), "nip04.decrypt")
|
||||
: null;
|
||||
|
||||
const nip44Encrypt =
|
||||
nip44 && typeof nip44.encrypt === "function" ? nip44.encrypt.bind(nip44) : null;
|
||||
nip44 && typeof nip44.encrypt === "function"
|
||||
? (pubkey, plaintext) =>
|
||||
retryNip07Call(() => nip44.encrypt(pubkey, plaintext), "nip44.encrypt")
|
||||
: null;
|
||||
|
||||
const nip44Decrypt =
|
||||
nip44 && typeof nip44.decrypt === "function" ? nip44.decrypt.bind(nip44) : null;
|
||||
nip44 && typeof nip44.decrypt === "function"
|
||||
? (pubkey, ciphertext) =>
|
||||
retryNip07Call(() => nip44.decrypt(pubkey, ciphertext), "nip44.decrypt")
|
||||
: null;
|
||||
|
||||
const requestPermissions = async (methods = []) => {
|
||||
if (typeof resolvedExtension.requestPermissions === "function") {
|
||||
return resolvedExtension.requestPermissions({
|
||||
permissions: Array.isArray(methods) ? methods : [],
|
||||
});
|
||||
return retryNip07Call(
|
||||
() =>
|
||||
resolvedExtension.requestPermissions({
|
||||
permissions: Array.isArray(methods) ? methods : [],
|
||||
}),
|
||||
"extension.requestPermissions",
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof resolvedExtension.enable === "function") {
|
||||
if (Array.isArray(methods) && methods.length) {
|
||||
return resolvedExtension.enable({ permissions: methods.map((method) => ({ method })) });
|
||||
return retryNip07Call(
|
||||
() =>
|
||||
resolvedExtension.enable({
|
||||
permissions: methods.map((method) => ({ method })),
|
||||
}),
|
||||
"extension.enable",
|
||||
);
|
||||
}
|
||||
return resolvedExtension.enable();
|
||||
return retryNip07Call(() => resolvedExtension.enable(), "extension.enable");
|
||||
}
|
||||
|
||||
return { ok: false, error: new Error("permission-request-unavailable") };
|
||||
@@ -85,17 +133,17 @@ export async function createNip07Adapter(extension) {
|
||||
pubkey,
|
||||
metadata: async () =>
|
||||
typeof resolvedExtension.getMetadata === "function"
|
||||
? resolvedExtension.getMetadata()
|
||||
? retryNip07Call(() => resolvedExtension.getMetadata(), "extension.getMetadata")
|
||||
: null,
|
||||
relays: async () =>
|
||||
typeof resolvedExtension.getRelays === "function"
|
||||
? resolvedExtension.getRelays()
|
||||
? retryNip07Call(() => resolvedExtension.getRelays(), "extension.getRelays")
|
||||
: null,
|
||||
signEvent: async (event) => {
|
||||
if (typeof resolvedExtension.signEvent !== "function") {
|
||||
throw new Error("NIP-07 extension missing signEvent.");
|
||||
}
|
||||
return resolvedExtension.signEvent(event);
|
||||
return retryNip07Call(() => resolvedExtension.signEvent(event), "extension.signEvent");
|
||||
},
|
||||
requestPermissions,
|
||||
destroy: async () => {},
|
||||
|
||||
@@ -341,6 +341,9 @@ class HashtagPreferencesService {
|
||||
|
||||
if (!hadInterest || removedFromDisinterests) {
|
||||
this.emitChange("interest-added", { tag: normalized });
|
||||
this.publish().catch((err) =>
|
||||
userLogger.warn(`${LOG_PREFIX} Auto-save failed`, err),
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -355,6 +358,9 @@ class HashtagPreferencesService {
|
||||
|
||||
this.interests.delete(normalized);
|
||||
this.emitChange("interest-removed", { tag: normalized });
|
||||
this.publish().catch((err) =>
|
||||
userLogger.warn(`${LOG_PREFIX} Auto-save failed`, err),
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -370,6 +376,9 @@ class HashtagPreferencesService {
|
||||
|
||||
if (!hadDisinterest || removedFromInterests) {
|
||||
this.emitChange("disinterest-added", { tag: normalized });
|
||||
this.publish().catch((err) =>
|
||||
userLogger.warn(`${LOG_PREFIX} Auto-save failed`, err),
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -384,6 +393,9 @@ class HashtagPreferencesService {
|
||||
|
||||
this.disinterests.delete(normalized);
|
||||
this.emitChange("disinterest-removed", { tag: normalized });
|
||||
this.publish().catch((err) =>
|
||||
userLogger.warn(`${LOG_PREFIX} Auto-save failed`, err),
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -567,9 +579,11 @@ class HashtagPreferencesService {
|
||||
const currentCreatedAt = Number(this.eventCreatedAt) || 0;
|
||||
const latestCreatedAt = Number(latest.created_at) || 0;
|
||||
|
||||
if (wasLoadedForUser && currentCreatedAt > latestCreatedAt) {
|
||||
userLogger.warn(
|
||||
`${LOG_PREFIX} Ignoring stale preferences event (remote: ${latestCreatedAt}, local: ${currentCreatedAt}).`,
|
||||
// Protection against overwriting optimistic updates:
|
||||
// If the local version is newer (because user just edited), ignore the remote fetch.
|
||||
if (wasLoadedForUser && currentCreatedAt >= latestCreatedAt) {
|
||||
devLogger.log(
|
||||
`${LOG_PREFIX} Ignoring stale/concurrent preferences event (remote: ${latestCreatedAt}, local: ${currentCreatedAt}).`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user