diff options
| author | Adam Soltys <[email protected]> | 2026-08-03 08:05:16 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-08-03 08:05:16 -0700 |
| commit | 59d0991115465106d960d1983fdd1b7c94aef7e6 (patch) | |
| tree | def53fccb7235fb6d7c92298b3a60424e5c2e38f | |
| parent | 86101d5ff381ca9832a4e4c17c47d8350e2d12d0 (diff) | |
| download | coinosv3-59d0991115465106d960d1983fdd1b7c94aef7e6.tar.gz coinosv3-59d0991115465106d960d1983fdd1b7c94aef7e6.zip | |
Fix the service-worker ReferenceError; stop dialing dead relays
sw.js threw "NWC_WAKE_FALLBACK is not defined" on every load: the
template substitution used String.replace with a string pattern, which
replaces only the FIRST match — and that was the mention inside the
comment above, leaving the real placeholder literal in the output
(parsed as a block plus a bare identifier). replaceAll fixes it, the
comment no longer spells the token, and {{VERSION}} got the same
treatment. Both build profiles now emit a service worker that parses
and activates cleanly.
The relay noise: a browser logs failed WebSocket connections itself and
JS cannot suppress that line, so the fix is to stop making the doomed
connection. nostr.js now probes each new relay once, remembers
unreachable ones for ten minutes, and filters them out of every
subscribe/query/publish (never down to an empty set). Publish
rejections that look like connection failures mark the relay sick
rather than logging a scary "refused" line. relay.damus.io — down all
week — also leaves the profile relay list.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Wvj4aoeMZwA7VUGdJP9Swi
| -rw-r--r-- | build.js | 6 | ||||
| -rw-r--r-- | src/nostr.js | 52 |
2 files changed, 50 insertions, 8 deletions
@@ -58,7 +58,7 @@ self.addEventListener('activate', (e) => { // NWC push handling lives in src/sw-nwc.js (bundled and appended below when // the nwc+ark features ship): it tries to ANSWER the request from the pouch -// and falls back to a notification. {{NWC_WAKE_FALLBACK}} carries a +// and falls back to a notification. The placeholder below carries a // notify-only handler for builds without those features. {{NWC_WAKE_FALLBACK}} @@ -259,8 +259,8 @@ if (import.meta.main) { const feats = enabledFeatures(); const nwcSw = feats.includes('nwc') && feats.includes('ark'); const swBody = SW - .replace('{{VERSION}}', version) - .replace('{{NWC_WAKE_FALLBACK}}', nwcSw ? '' : SW_WAKE_ONLY); + .replaceAll('{{VERSION}}', version) + .replaceAll('{{NWC_WAKE_FALLBACK}}', nwcSw ? '' : SW_WAKE_ONLY); await Bun.write('dist/sw.js', nwcSw ? swBody + '\n' + await buildSwNwc() : swBody); for (const f of STATIC) await Bun.write('dist/' + f, Bun.file('static/' + f)); // default avatars, self-hosted since coinos.io's copies are half-broken diff --git a/src/nostr.js b/src/nostr.js index c644ef5..cf3cc88 100644 --- a/src/nostr.js +++ b/src/nostr.js @@ -30,27 +30,69 @@ const pool = new SimplePool({ enablePing: true, enableReconnect: true }); // feature's configuration. NWC needs this: the relays it advertises in a // connection URI must be exactly the ones it listens on, or a client's // request lands somewhere the wallet isn't and it waits forever. +// ---- relay health --------------------------------------------------------- +// A relay that refuses connections (damus has been down for days at a time) +// otherwise gets retried on every profile fetch, every sync, every chat +// subscribe — each attempt logging a browser-level WebSocket error we cannot +// catch from JS. So we remember failures and stop dialing for a while. +const SICK_MS = 10 * 60_000; +const sick = new Map(); // url -> until +export function markRelaySick(url, why) { + if (!url) return; + if (!sick.has(url)) console.warn(`nostr: ${url} unreachable (${why || 'connect failed'}) — skipping for 10 min`); + sick.set(url, Date.now() + SICK_MS); +} +// Drop known-bad relays, but never hand back an empty set: if everything is +// sick the network probably is, and one doomed attempt beats doing nothing. +function liveRelays(list) { + const now = Date.now(); + for (const [u, until] of sick) if (until < now) sick.delete(u); + const ok = (list || []).filter((u) => !sick.has(u)); + return ok.length ? ok : (list || []); +} +// Dial a relay once in the background to learn whether it's reachable; a +// rejection here is the only reliable signal nostr-tools gives us. +const probed = new Set(); +function probeRelays(list) { + for (const url of list || []) { + if (probed.has(url) || sick.has(url)) continue; + probed.add(url); + Promise.resolve(pool.ensureRelay(url, { connectionTimeout: 6000 })) + .then(() => {}) + .catch((e) => markRelaySick(url, e && e.message)) + .finally(() => setTimeout(() => probed.delete(url), SICK_MS)); + } +} + export function subscribeOn(relays, filter, onEvent, extra = {}) { try { // nostr-tools ≥2.23 takes a single filter object here; wrapping it in an // array serializes as ["REQ",id,[{...}]] which every relay rejects with // "provided filter is not an object" — and the subscription dies silently. - const sub = pool.subscribeMany(relays, filter, { onevent: onEvent, ...extra }); + const live = liveRelays(relays); + probeRelays(relays); + const sub = pool.subscribeMany(live, filter, { onevent: onEvent, ...extra }); return () => { try { sub.close(); } catch {} }; } catch { return () => {}; } } export async function queryOn(relays, filter, maxWait = 1500) { - try { return await pool.querySync(relays, filter, { maxWait }); } catch { return []; } + probeRelays(relays); + try { return await pool.querySync(liveRelays(relays), filter, { maxWait }); } catch { return []; } } export async function publishOn(relays, evt) { try { - const results = await Promise.allSettled(pool.publish(relays, evt)); + const live = liveRelays(relays); + const results = await Promise.allSettled(pool.publish(live, evt)); // A rejected publish is a relay refusing the event (policy, rate limit, // ban). Swallowing that silently once hid a relay-side rejection behind // what looked like a client bug — always say which relay refused and why. results.forEach((r, i) => { if (r.status === 'rejected') { - console.warn(`nostr: publish kind ${evt.kind} refused by ${relays[i]}:`, r.reason?.message || r.reason); + const msg = r.reason?.message || String(r.reason || ''); + // "relay connection errored/closed" means the socket never came up — + // that's a sick relay, not a rejected event. + if (/connect|closed|errored|timeout|network/i.test(msg)) markRelaySick(live[i], msg); + else console.warn(`nostr: publish kind ${evt.kind} refused by ${live[i]}:`, msg); } }); return results.some((r) => r.status === 'fulfilled'); @@ -73,7 +115,7 @@ export function npubOf(pkHex) { try { return npubEncode(pkHex); } catch { return // Profiles live across the network, so look them up on popular relays (broader // than the sync relays). -export const PROFILE_RELAYS = ['wss://relay.damus.io', 'wss://nos.lol', 'wss://relay.primal.net', 'wss://relay.coinos.io']; +export const PROFILE_RELAYS = ['wss://relay.coinos.io', 'wss://nos.lol', 'wss://relay.primal.net']; // Fetch a recipient's profile (name + picture) for a pubkey, newest across relays. export async function fetchNostrProfile(pubkeyHex, relays = PROFILE_RELAYS) { let events; |
