diff options
| author | Adam Soltys <[email protected]> | 2026-03-07 10:39:57 -0800 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-03-07 10:39:57 -0800 |
| commit | 63249aed9dd62e59aa302ddcf7c3a419effc43b3 (patch) | |
| tree | 48e89bcc0c58a92753a75d5d8d9e9dea4291dc87 /src/lib | |
| parent | ad0b4009b2b2ae9173fffb41f0cfa7b35edcb303 (diff) | |
| download | coinos-ui-63249aed9dd62e59aa302ddcf7c3a419effc43b3.tar.gz coinos-ui-63249aed9dd62e59aa302ddcf7c3a419effc43b3.zip | |
improve message delivery with websocket reconnect and push data transport
- Enable auto-reconnect on relay connections so dropped WebSockets recover
- Connect relays in parallel with 5s timeout for faster startup
- Add visibility change handler to catch up missed messages on tab return
- Forward kind 445 events via push notifications as a data channel
- Show "reconnecting..." status in chat UI when connections are down
Co-Authored-By: Claude Opus 4.6 <[email protected]>
Diffstat (limited to 'src/lib')
| -rw-r--r-- | src/lib/messaging.ts | 3 | ||||
| -rw-r--r-- | src/lib/mls-relay.ts | 152 |
2 files changed, 144 insertions, 11 deletions
diff --git a/src/lib/messaging.ts b/src/lib/messaging.ts index 4cebd2b0..a647a657 100644 --- a/src/lib/messaging.ts +++ b/src/lib/messaging.ts @@ -17,6 +17,9 @@ export { subscribeToMessages, fetchGroupHistory, decryptMediaUrl, + isConnected, + onConnectionChange, + catchUpMessages, } from "$lib/mls-relay"; export type { GroupInfo } from "$lib/mls-relay"; diff --git a/src/lib/mls-relay.ts b/src/lib/mls-relay.ts index d29cd535..bdd33935 100644 --- a/src/lib/mls-relay.ts +++ b/src/lib/mls-relay.ts @@ -129,6 +129,90 @@ let _activeSub: { } | null = null; // --------------------------------------------------------------------------- +// Connection status tracking +// --------------------------------------------------------------------------- + +let _connectionListeners: ((connected: boolean) => void)[] = []; +let _isConnected = false; + +export function isConnected(): boolean { + return _isConnected; +} + +export function onConnectionChange(cb: (connected: boolean) => void): () => void { + _connectionListeners.push(cb); + return () => { + _connectionListeners = _connectionListeners.filter((l) => l !== cb); + }; +} + +function _updateConnectionStatus(): void { + if (!_activeSub) { + if (_isConnected) { + _isConnected = false; + _connectionListeners.forEach((cb) => cb(false)); + } + return; + } + const connected = _activeSub.connectedRelays.some((r: any) => r.connected); + if (connected !== _isConnected) { + _isConnected = connected; + _connectionListeners.forEach((cb) => cb(connected)); + } +} + +/** Fetch recent messages to catch up after disconnect or background period. */ +export async function catchUpMessages(): Promise<void> { + if (!_activeSub) return; + const { user, seen, onNewRumour } = _activeSub; + const client = getMarmotClient(user.pubkey); + + try { + await client.loadAllGroups(); + const groups = client.groups; + if (groups.length === 0) return; + + const hTags: string[] = []; + const catchUpRelays = new Set(_activeSub.relays); + for (const g of groups) { + registerGroupIds(g); + hTags.push(getNostrGroupId(g)); + const r = g.relays; + if (r) r.forEach((url: string) => catchUpRelays.add(url)); + } + + const since = Math.floor(Date.now() / 1000) - 300; + const events = await pool.querySync([...catchUpRelays], { + kinds: [445], + "#h": hTags, + since, + limit: 200, + }); + + events.sort((a, b) => a.created_at - b.created_at); + + let newCount = 0; + for (const event of events) { + if (seen.has(event.id)) continue; + seen.add(event.id); + const rumour = await handleGroupMessageEvent( + event as NostrEvent, + user, + client, + ); + if (rumour) { + onNewRumour(rumour); + newCount++; + } + } + if (newCount > 0) + console.log(`[mls] caught up ${newCount} missed messages`); + } catch (e) { + console.error("[mls] catch-up failed:", e); + } +} + +// --------------------------------------------------------------------------- // Key package management (kind 443) // --------------------------------------------------------------------------- @@ -569,9 +653,22 @@ export async function subscribeToMessages( console.error("[mls] Failed to fetch historical welcomes:", e); } - for (const url of allRelayUrls) { + // Pre-compute group subscription filters + const groups = client.groups; + const hTags: string[] = []; + for (const g of groups) { + const nostrId = getNostrGroupId(g); + subscribedGroups.add(nostrId); + hTags.push(nostrId); + } + + // Connect to relays in parallel with auto-reconnect + const setupRelay = async (url: string) => { try { - const relay = await Relay.connect(url); + const relay = await Relay.connect(url, { + enableReconnect: true, + timeout: 5000, + }); connectedRelays.push(relay); const welcomeSub = relay.subscribe( @@ -591,14 +688,7 @@ export async function subscribeToMessages( ); closers.push(() => welcomeSub.close()); - const groups = client.groups; - if (groups.length > 0) { - const hTags: string[] = []; - for (const g of groups) { - const nostrId = getNostrGroupId(g); - subscribedGroups.add(nostrId); - hTags.push(nostrId); - } + if (hTags.length > 0) { const msgSub = relay.subscribe( [{ kinds: [445], "#h": hTags }], { @@ -613,8 +703,47 @@ export async function subscribeToMessages( closers.push(() => msgSub.close()); } } catch (e) { - console.error("[mls-relay] Failed to connect:", url, e); + console.warn("[mls-relay] Failed to connect:", url); } + }; + + await Promise.allSettled([...allRelayUrls].map(setupRelay)); + _updateConnectionStatus(); + + // Catch up on missed messages when tab becomes visible again + const handleVisibility = () => { + if (document.visibilityState === "visible") { + _updateConnectionStatus(); + catchUpMessages(); + } + }; + document.addEventListener("visibilitychange", handleVisibility); + closers.push(() => + document.removeEventListener("visibilitychange", handleVisibility), + ); + + // Listen for push-delivered events from service worker + if (typeof navigator !== "undefined" && navigator.serviceWorker) { + const handleSWMessage = async (evt: MessageEvent) => { + if (evt.data?.type !== "mls-event") return; + try { + const event = JSON.parse(evt.data.event); + if (seen.has(event.id)) return; + seen.add(event.id); + const rumour = await handleGroupMessageEvent( + event as NostrEvent, + user, + client, + ); + if (rumour) onNewRumour(rumour); + } catch (e) { + console.error("[mls] Failed to process push event:", e); + } + }; + navigator.serviceWorker.addEventListener("message", handleSWMessage); + closers.push(() => + navigator.serviceWorker.removeEventListener("message", handleSWMessage), + ); } // Publish key package @@ -624,6 +753,7 @@ export async function subscribeToMessages( return () => { _activeSub = null; + _updateConnectionStatus(); inviteReader.removeAllListeners("newInvite"); closers.forEach((c) => c()); }; |
