feat: reply OK to client-published events

nostrclient never answered a client's EVENT with the NIP-01
`["OK", <id>, <accepted>, <message>]` command result. Clients built on
nostr-tools and similar libraries wait for that reply before treating a
publish as successful, so NWC wallet apps paired through the public
endpoint reported "publish failed" even though the request had been
fanned out and answered.

Relay OKs now flow through the message pool like events and notices.
The router tracks each EVENT a client publishes and replies exactly
once: `true` as soon as any relay accepts, `false` once every relay
connected at publish time has rejected it, after a 10 s timeout, or
immediately when no relay is connected. OKs nobody is waiting on are
dropped at the pump so the shared result map cannot grow unbounded.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013Tbyw6FwjhEJg3gHfPHxWt
This commit is contained in:
Padreug 2026-09-12 13:31:00 +02:00
commit 7aa0ce14db
6 changed files with 277 additions and 4 deletions

View file

@ -27,11 +27,22 @@ class EndOfStoredEventsMessage:
self.url = url
class CommandResultMessage:
"""An `["OK", <event_id>, <accepted>, <message>]` reply from one relay."""
def __init__(self, event_id: str, accepted: bool, message: str, url: str) -> None:
self.event_id = event_id
self.accepted = accepted
self.message = message
self.url = url
class MessagePool:
def __init__(self) -> None:
self.events: Queue[EventMessage] = Queue()
self.notices: Queue[NoticeMessage] = Queue()
self.eose_notices: Queue[EndOfStoredEventsMessage] = Queue()
self.command_results: Queue[CommandResultMessage] = Queue()
self._unique_events: set = set()
self.lock: Lock = Lock()
@ -47,6 +58,9 @@ class MessagePool:
def get_eose_notice(self):
return self.eose_notices.get()
def get_command_result(self):
return self.command_results.get()
def has_events(self):
return self.events.qsize() > 0
@ -56,6 +70,9 @@ class MessagePool:
def has_eose_notices(self):
return self.eose_notices.qsize() > 0
def has_command_results(self):
return self.command_results.qsize() > 0
def _process_message(self, message: str, url: str):
message_json = json.loads(message)
message_type = message_json[0]
@ -75,6 +92,13 @@ class MessagePool:
self.notices.put(NoticeMessage(message_json[1], url))
elif message_type == RelayMessageType.END_OF_STORED_EVENTS:
self.eose_notices.put(EndOfStoredEventsMessage(message_json[1], url))
elif message_type == RelayMessageType.COMMAND_RESULT:
message = message_json[3] if len(message_json) > 3 else ""
self.command_results.put(
CommandResultMessage(
message_json[1], bool(message_json[2]), str(message), url
)
)
def _accept_event(self, event_message: EventMessage):
"""