From 3016beed7d661ab5fc8ca302e372d121de29aa8b Mon Sep 17 00:00:00 2001 From: dadofsambonzuki Date: Thu, 25 Jun 2026 13:05:26 +0000 Subject: [PATCH] fix: add EOSE timeout fallback and increase handle_missed_events default - Add 10s EOSE timeout guard to prevent the provider from hanging indefinitely if EOSE is never received or lost in transit - Process all queued events after timeout and mark subscriptions ready for real-time processing - Increase handle_missed_events default from 0 to 120s to recover missed requests on reconnect --- README.md | 2 +- migrations.py | 2 +- nwcp.py | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1265fed..0f42480 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ The configuration page of the NWC Service Provider extension is available at `/n | relay | URL of the nostr relay for dispatching and receiving NWC events. Use public relays or a custom one. Specify `nostrclient` to connect to the [nostrclient extension](https://github.com/lnbits/nostrclient). | nostrclient | | provider_key | Nostr secret key of the NWC Service Provider. | Random key generated on install | | relay_alias | Relay URL to display in pairing URLs. Set if different from `relay`. | Empty (uses the `relay` value) | -| handle_missed_events | Number of seconds to look back for processing events missed while offline. Setting it to 0 disables this functionality. | 0 | +| handle_missed_events | Number of seconds to look back for processing events missed while offline. Setting it to 0 disables this functionality. | 120 | > [!WARNING] > diff --git a/migrations.py b/migrations.py index 1a3a9b7..f3c9200 100644 --- a/migrations.py +++ b/migrations.py @@ -117,5 +117,5 @@ async def m006_default_config3(db): VALUES ('handle_missed_events', :value) ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value; """, - {"value": "0"}, + {"value": "120"}, ) diff --git a/nwcp.py b/nwcp.py index efa5279..a0ba55e 100644 --- a/nwcp.py +++ b/nwcp.py @@ -124,6 +124,9 @@ def __init__( # Periodic info event resend loop self.info_event_task = None + # EOSE timeout task (fallback if relay never sends EOSE) + self.eose_timeout_task = None + # Subscription self.sub = None self.rate_limit: dict[str, RateLimit] = {} @@ -270,10 +273,43 @@ async def _ratelimit(self, unit: str, max_sleep_time: int = 120) -> None: await asyncio.sleep(limit.backoff) limit.last_attempt_time = int(time.time()) + async def _process_stale_events(self): + if not self.sub: + return + stales = self.sub.get_stale() + if not stales: + return + logger.debug(f"Processing {len(stales)} stale events (EOSE timeout)") + for stale in stales: + try: + await self._handle_request(stale) + except Exception as e: + logger.error(f"Error handling stale event: {e}") + + async def _eose_timeout(self): + await asyncio.sleep(10) + if self._is_shutting_down(): + return + if not self.sub: + return + if not (self.sub.requests_eose and self.sub.responses_eose): + logger.warning( + "EOSE not received within 10s timeout, " + "processing pending events anyway" + ) + self.sub.requests_eose = True + self.sub.responses_eose = True + await self._process_stale_events() + async def _subscribe(self): """ [Re]Subscribe to receive nip 47 requests and responses from the relay """ + # Cancel previous EOSE timeout if any + if self.eose_timeout_task: + self.eose_timeout_task.cancel() + self.eose_timeout_task = None + self.sub = MainSubscription() # Create requests subscription req_filter = { @@ -293,6 +329,8 @@ async def _subscribe(self): # Subscribe await self._send(["REQ", self.sub.requests_sub_id, req_filter]) await self._send(["REQ", self.sub.responses_sub_id, res_filter]) + # Start EOSE timeout guard + self.eose_timeout_task = asyncio.create_task(self._eose_timeout()) async def _on_connection(self, _): """ @@ -469,6 +507,10 @@ async def _on_eose_message(self, msg): # service connection and do not have a response yet, # are considered stale, we will process them now if self.sub.requests_eose and self.sub.responses_eose: + # Cancel the EOSE timeout since we got both EOSEs + if self.eose_timeout_task: + self.eose_timeout_task.cancel() + self.eose_timeout_task = None stales = self.sub.get_stale() for stale in stales: await self._handle_request(stale) @@ -629,6 +671,12 @@ async def cleanup(self): self.info_event_task.cancel() except Exception as e: logger.warning("Error closing info event loop: " + str(e)) + # cancel eose timeout + try: + if self.eose_timeout_task: + self.eose_timeout_task.cancel() + except Exception as e: + logger.warning("Error closing eose timeout task: " + str(e)) # close the websocket try: if self.ws: