fix: drain buffered event on EOSE in fetchFirst to avoid race

A relay sends its matching events before its EOSE, so both an event and
the relay's completion can sit buffered in their channels at the same
time. The select() over the two channels picks a ready clause at random,
so it could process the doneChannel completion first, empty `remaining`,
and exit the loop while the matching event was still unread — returning
null instead of the event.

On a relay completion, drain the event channel first and treat any
already-buffered event as the result before marking the relay done.
This commit is contained in:
Claude
2026-07-08 19:15:11 +00:00
parent c4c2f41d6b
commit b8b25060fb
@@ -120,7 +120,17 @@ suspend fun INostrClient.fetchFirst(
remaining.clear()
}
doneChannel.onReceive { relay ->
remaining.remove(relay)
// A relay sends its matching events before its EOSE, so an event may
// already be buffered when this completion fires. select() picks a ready
// clause at random, so without this drain we could treat the relay as done
// and exit while its event still sits unread in the channel.
val buffered = eventChannel.tryReceive().getOrNull()
if (buffered != null) {
result = buffered
remaining.clear()
} else {
remaining.remove(relay)
}
}
}
}