From f303b95741ac07496cbb8aaf2060242edcec5fb3 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Wed, 5 Nov 2025 09:38:06 -0800 Subject: [PATCH] fix(nostr): Better subscript management and preventing race conditions --- src/components/app-body/nostr-chat/index.js | 74 +++++++++++++++++-- .../app-body/nostr-chat/message-list.js | 2 +- src/config/index.js | 7 +- 3 files changed, 75 insertions(+), 8 deletions(-) diff --git a/src/components/app-body/nostr-chat/index.js b/src/components/app-body/nostr-chat/index.js index e51e44c..6c3f8a8 100644 --- a/src/components/app-body/nostr-chat/index.js +++ b/src/components/app-body/nostr-chat/index.js @@ -197,6 +197,10 @@ function NostrChat (props) { const subId = generateSubId(`group-${selectedChannel}`) const filter = { limit: 10, kinds: [42], '#e': [selectedChannel] } + // Track if EOSE has been called + let eoseCalled = false + let eoseTimeoutId = null + const subscription = restClient.current.createSubscription(subId, filter, { onEvent: (ev) => { console.log('Group post retrieved from REST API', ev.content) @@ -207,8 +211,17 @@ function NostrChat (props) { } }, onEose: () => { + eoseCalled = true + if (eoseTimeoutId) { + clearTimeout(eoseTimeoutId) + eoseTimeoutId = null + } if (!selectedChannelIsDm) { - setLoadedMessages(true) + // Use setTimeout to ensure state updates from onEvent callbacks are processed + // before setting loadedMessages to true + setTimeout(() => { + setLoadedMessages(true) + }, 100) } }, onClosed: (message) => { @@ -221,11 +234,24 @@ function NostrChat (props) { subscriptionsRef.current[subId] = subscription + // Set up EOSE timeout fallback - if EOSE doesn't arrive within 10 seconds, set loadedMessages anyway + const EOSE_TIMEOUT_MS = 10000 // 10 seconds + eoseTimeoutId = setTimeout(() => { + if (!eoseCalled && !selectedChannelIsDm) { + console.warn(`EOSE timeout reached for subscription ${subId} - setting loadedMessages to true`) + setLoadedMessages(true) + } + }, EOSE_TIMEOUT_MS) + // Capture values for cleanup const subscriptionsRefValue = subscriptionsRef.current const restClientValue = restClient.current return () => { + // Clear EOSE timeout if it exists + if (eoseTimeoutId) { + clearTimeout(eoseTimeoutId) + } // Close subscription on component unmount or selected channel changes console.log('Close existing subscription for group channel') if (subscriptionsRefValue[subId]) { @@ -233,7 +259,11 @@ function NostrChat (props) { delete subscriptionsRefValue[subId] } restClientValue.closeSubscription(subId).catch(err => { - console.warn('Error closing subscription:', err) + // Subscription already closed is not an error - this is expected behavior + const errorMessage = err?.message || err?.toString() || '' + if (!errorMessage.includes('not found') && !errorMessage.includes('already closed')) { + console.warn('Error closing subscription:', err) + } }) } }, [onMsgRead, selectedChannel, selectedChannelIsDm, nostrQueries, channelsLoaded, deletedChats]) @@ -255,6 +285,10 @@ function NostrChat (props) { { limit: 10, kinds: [4], '#p': [dmPubKey], authors: [nostrKeyPair.pubHex] } // sent messages ] + // Track if EOSE has been called + let eoseCalled = false + let eoseTimeoutId = null + const subscription = restClient.current.createSubscription(subId, filters, { onEvent: (ev) => { console.log('DM post retrieved from REST API', ev.content) @@ -268,8 +302,17 @@ function NostrChat (props) { } }, onEose: () => { + eoseCalled = true + if (eoseTimeoutId) { + clearTimeout(eoseTimeoutId) + eoseTimeoutId = null + } if (selectedChannelIsDm) { - setLoadedMessages(true) + // Use setTimeout to ensure state updates from onEvent callbacks are processed + // before setting loadedMessages to true + setTimeout(() => { + setLoadedMessages(true) + }, 100) } }, onClosed: (message) => { @@ -282,11 +325,24 @@ function NostrChat (props) { subscriptionsRef.current[subId] = subscription + // Set up EOSE timeout fallback - if EOSE doesn't arrive within 10 seconds, set loadedMessages anyway + const EOSE_TIMEOUT_MS = 10000 // 10 seconds + eoseTimeoutId = setTimeout(() => { + if (!eoseCalled && selectedChannelIsDm) { + console.warn(`EOSE timeout reached for subscription ${subId} - setting loadedMessages to true`) + setLoadedMessages(true) + } + }, EOSE_TIMEOUT_MS) + // Capture values for cleanup const subscriptionsRefValue = subscriptionsRef.current const restClientValue = restClient.current return () => { + // Clear EOSE timeout if it exists + if (eoseTimeoutId) { + clearTimeout(eoseTimeoutId) + } // Close subscription on component unmount or selected channel changes console.log('Close existing subscription for private channel') if (subscriptionsRefValue[subId]) { @@ -294,7 +350,11 @@ function NostrChat (props) { delete subscriptionsRefValue[subId] } restClientValue.closeSubscription(subId).catch(err => { - console.warn('Error closing subscription:', err) + // Subscription already closed is not an error - this is expected behavior + const errorMessage = err?.message || err?.toString() || '' + if (!errorMessage.includes('not found') && !errorMessage.includes('already closed')) { + console.warn('Error closing subscription:', err) + } }) } }, [onMsgRead, selectedChannel, selectedChannelIsDm, nostrQueries, bchWalletState, decryptMsg]) @@ -374,7 +434,11 @@ function NostrChat (props) { delete subscriptionsRefValue[subId] } restClientValue.closeSubscription(subId).catch(err => { - console.warn('Error closing subscription:', err) + // Subscription already closed is not an error - this is expected behavior + const errorMessage = err?.message || err?.toString() || '' + if (!errorMessage.includes('not found') && !errorMessage.includes('already closed')) { + console.warn('Error closing subscription:', err) + } }) } }, [handleIncomingDms, appData, nostrQueries, dmListLoaded, channelsLoaded]) diff --git a/src/components/app-body/nostr-chat/message-list.js b/src/components/app-body/nostr-chat/message-list.js index cef3650..ad44f54 100644 --- a/src/components/app-body/nostr-chat/message-list.js +++ b/src/components/app-body/nostr-chat/message-list.js @@ -13,7 +13,7 @@ import { Spinner } from 'react-bootstrap' function MessageList (props) { const { messages, loadedMessages } = props console.log('loadedMessages', loadedMessages) - const [groupedMessages, setGroupedMessages] = useState([]) + const [groupedMessages, setGroupedMessages] = useState({}) const msgContainerRef = useRef() // Group messages by date const groupMessagesByDate = useCallback((messages) => { diff --git a/src/config/index.js b/src/config/index.js index a876b2a..316bd88 100644 --- a/src/config/index.js +++ b/src/config/index.js @@ -14,12 +14,15 @@ const config = { ghRepo: 'https://github.com/Permissionless-Software-Foundation/bch-dex-taker-v2', radicleUrl: 'https://app.radicle.network/seeds/maple.radicle.garden/rad:git:hnrkd5cjwwb5tzx37hq9uqm5ubon7ee468xcy/remotes/hyyycncbn9qzqmobnhjq9rry6t4mbjiadzjoyhaknzxjcz3cxkpfpc', - // dexServer: 'https://dex-api.fullstack.cash', - dexServer: 'http://localhost:5700', + dexServer: 'https://dex-api.fullstack.cash', + // dexServer: 'http://localhost:5700', nostrTopic: 'bch-dex-test-topic-02', + // REST API endpoint for Nostr relay interactions (primary interface) nostrRestApiUrl: 'https://nostr-relay-api.psfoundation.info', + // nostrRestApiUrl: 'http://localhost:5942', + // Legacy relay URLs kept for reference (may be used in tags, but actual connections use REST API) nostrRelay: 'wss://nostr-relay.psfoundation.info', nostrRelays: [