mirror of
https://github.com/hoornet/vega.git
synced 2026-05-06 20:29:12 -07:00
Override NDK default outbox relays (purplepag.es DNS failure was stalling getZapInfo), add 45s zap timeout, disable outbox on NWC instance, fix follower notification dedup in poller. Zap history rows now show a clickable preview of the original zapped note.
203 lines
6.2 KiB
TypeScript
203 lines
6.2 KiB
TypeScript
import NDK, { NDKEvent, NDKFilter, NDKRelay, NDKRelaySet, NDKSubscriptionCacheUsage } from "@nostr-dev-kit/ndk";
|
|
|
|
// ─── Fetch timeout helper ───────────────────────────────────────────
|
|
|
|
/** Race a promise against a timeout. Returns fallback on timeout. */
|
|
export function withTimeout<T>(promise: Promise<T>, ms: number, fallback: T): Promise<T> {
|
|
return Promise.race([
|
|
promise,
|
|
new Promise<T>((resolve) => setTimeout(() => {
|
|
console.warn(`[Wrystr] Fetch timed out after ${ms}ms`);
|
|
resolve(fallback);
|
|
}, ms)),
|
|
]);
|
|
}
|
|
|
|
export const FEED_TIMEOUT = 8000; // 8s for feed fetches
|
|
export const THREAD_TIMEOUT = 5000; // 5s per thread round-trip
|
|
export const SINGLE_TIMEOUT = 5000; // 5s for single event lookups
|
|
|
|
const EMPTY_SET = new Set<NDKEvent>();
|
|
|
|
/** Fetch events with a timeout — returns empty set if relay hangs. */
|
|
export async function fetchWithTimeout(
|
|
instance: NDK,
|
|
filter: NDKFilter,
|
|
timeoutMs: number,
|
|
relaySet?: NDKRelaySet,
|
|
): Promise<Set<NDKEvent>> {
|
|
const opts = {
|
|
cacheUsage: NDKSubscriptionCacheUsage.ONLY_RELAY,
|
|
groupable: false, // Prevent NDK from batching/reusing subscriptions
|
|
};
|
|
const promise = relaySet
|
|
? instance.fetchEvents(filter, opts, relaySet)
|
|
: instance.fetchEvents(filter, opts);
|
|
return withTimeout(promise, timeoutMs, EMPTY_SET);
|
|
}
|
|
|
|
export const RELAY_STORAGE_KEY = "wrystr_relays";
|
|
|
|
export const FALLBACK_RELAYS = [
|
|
"wss://relay.damus.io",
|
|
"wss://nos.lol",
|
|
"wss://relay.snort.social",
|
|
];
|
|
|
|
// Override NDK's default outbox relays (purplepag.es can have DNS issues)
|
|
export const OUTBOX_RELAYS = [
|
|
"wss://relay.damus.io/",
|
|
"wss://nos.lol/",
|
|
"wss://relay.nostr.band/",
|
|
];
|
|
|
|
export function getStoredRelayUrls(): string[] {
|
|
try {
|
|
const stored = localStorage.getItem(RELAY_STORAGE_KEY);
|
|
if (stored) return JSON.parse(stored);
|
|
} catch { /* ignore */ }
|
|
return FALLBACK_RELAYS;
|
|
}
|
|
|
|
export function saveRelayUrls(urls: string[]) {
|
|
localStorage.setItem(RELAY_STORAGE_KEY, JSON.stringify(urls));
|
|
}
|
|
|
|
let ndk: NDK | null = null;
|
|
let ndkCreatedAt: number | null = null;
|
|
|
|
export function getNDK(): NDK {
|
|
if (!ndk) {
|
|
ndk = new NDK({
|
|
explicitRelayUrls: getStoredRelayUrls(),
|
|
outboxRelayUrls: OUTBOX_RELAYS,
|
|
});
|
|
ndkCreatedAt = Date.now();
|
|
}
|
|
return ndk;
|
|
}
|
|
|
|
export function getNDKUptimeMs(): number | null {
|
|
return ndkCreatedAt ? Date.now() - ndkCreatedAt : null;
|
|
}
|
|
|
|
/**
|
|
* Destroy the current NDK instance and create a fresh one.
|
|
* Preserves the signer (login state) but resets all relay connections.
|
|
* Use as a last resort when relay connections are unrecoverable.
|
|
*/
|
|
export async function resetNDK(): Promise<void> {
|
|
const oldInstance = ndk;
|
|
const oldSigner = oldInstance?.signer ?? null;
|
|
|
|
// Disconnect all relays on old instance
|
|
if (oldInstance?.pool?.relays) {
|
|
for (const relay of oldInstance.pool.relays.values()) {
|
|
try { relay.disconnect(); } catch { /* ignore */ }
|
|
}
|
|
}
|
|
|
|
// Create fresh instance
|
|
ndk = new NDK({
|
|
explicitRelayUrls: getStoredRelayUrls(),
|
|
outboxRelayUrls: OUTBOX_RELAYS,
|
|
});
|
|
ndkCreatedAt = Date.now();
|
|
|
|
// Restore signer so user stays logged in
|
|
if (oldSigner) {
|
|
ndk.signer = oldSigner;
|
|
}
|
|
|
|
// Connect fresh
|
|
console.log("[Wrystr] NDK instance reset — connecting fresh relays");
|
|
await ndk.connect();
|
|
await waitForConnectedRelay(ndk, 5000);
|
|
const relays = Array.from(ndk.pool?.relays?.values() ?? []);
|
|
const connected = relays.filter((r) => r.connected).length;
|
|
console.log(`[Wrystr] Fresh connection: ${connected}/${relays.length} relays connected`);
|
|
}
|
|
|
|
export function addRelay(url: string): void {
|
|
const instance = getNDK();
|
|
const urls = getStoredRelayUrls();
|
|
if (!urls.includes(url)) {
|
|
saveRelayUrls([...urls, url]);
|
|
}
|
|
if (!instance.pool?.relays.has(url)) {
|
|
const relay = new NDKRelay(url, undefined, instance);
|
|
instance.pool?.addRelay(relay, true);
|
|
}
|
|
}
|
|
|
|
export function removeRelay(url: string): void {
|
|
const instance = getNDK();
|
|
// NDK may store URLs with or without trailing slash — check both
|
|
const variants = [url, url.replace(/\/$/, ""), url.replace(/\/?$/, "/")];
|
|
for (const v of variants) {
|
|
const relay = instance.pool?.relays.get(v);
|
|
if (relay) {
|
|
relay.disconnect();
|
|
instance.pool?.relays.delete(v);
|
|
}
|
|
}
|
|
saveRelayUrls(getStoredRelayUrls().filter((u) => u !== url));
|
|
}
|
|
|
|
function waitForConnectedRelay(instance: NDK, timeoutMs = 10000): Promise<void> {
|
|
return new Promise((resolve, _reject) => {
|
|
const timer = setTimeout(() => {
|
|
// Even on timeout, continue — some relays may connect later
|
|
console.warn("Relay connection timeout, continuing anyway");
|
|
resolve();
|
|
}, timeoutMs);
|
|
|
|
const check = () => {
|
|
const relays = Array.from(instance.pool?.relays?.values() ?? []);
|
|
const hasConnected = relays.some((r) => r.connected);
|
|
if (hasConnected) {
|
|
clearTimeout(timer);
|
|
resolve();
|
|
} else {
|
|
setTimeout(check, 300);
|
|
}
|
|
};
|
|
check();
|
|
});
|
|
}
|
|
|
|
export async function connectToRelays(): Promise<void> {
|
|
const instance = getNDK();
|
|
await instance.connect();
|
|
await waitForConnectedRelay(instance);
|
|
}
|
|
|
|
/**
|
|
* Ensure at least one relay is connected.
|
|
* If relays report connected, trust them and return immediately.
|
|
* Only reconnect if zero relays are connected — never force-disconnect working connections.
|
|
*/
|
|
export async function ensureConnected(): Promise<boolean> {
|
|
const instance = getNDK();
|
|
const relays = Array.from(instance.pool?.relays?.values() ?? []);
|
|
const connectedCount = relays.filter((r) => r.connected).length;
|
|
|
|
if (connectedCount > 0) {
|
|
return true; // Trust relay.connected — don't probe or disconnect
|
|
}
|
|
|
|
console.warn(`[Wrystr] No relays connected (${relays.length} in pool) — attempting reconnect`);
|
|
|
|
try {
|
|
await withTimeout(instance.connect(), 4000, undefined);
|
|
await waitForConnectedRelay(instance, 3000);
|
|
const after = Array.from(instance.pool?.relays?.values() ?? []);
|
|
const nowConnected = after.some((r) => r.connected);
|
|
console.log(`[Wrystr] Reconnect ${nowConnected ? "succeeded" : "failed"}`);
|
|
return nowConnected;
|
|
} catch {
|
|
console.error("[Wrystr] Reconnect failed");
|
|
return false;
|
|
}
|
|
}
|