fix(relay): forward ephemeral events to live subscribers (NIP-01)
LiveEventStore.query had a race window between emitting EOSE and registering as a SharedFlow collector — any event emitted in that window was lost because newEventStream has replay=0. The race was usually masked by SQLite write latency for persisted kinds, but it fired reliably for ephemeral kinds (20000-29999) where store.insert is a no-op. Per NIP-01 ephemeral events are not persisted but MUST still reach matching active subscriptions. Fixed by registering the live collector before signalling EOSE via Flow.onSubscription. Adds two tests: one proves an ephemeral event reaches an active subscriber, the other proves it isn't persisted (a follow-up REQ returns zero events).
This commit is contained in:
+17
-11
@@ -25,6 +25,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip01Core.store.IEventStore
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.onSubscription
|
||||
|
||||
/**
|
||||
* A reactive event store that combines historical data retrieval with live event streaming.
|
||||
@@ -56,18 +57,23 @@ class LiveEventStore(
|
||||
onEach: (Event) -> Unit,
|
||||
onEose: () -> Unit,
|
||||
) {
|
||||
// 1. Replay stored events matching filters.
|
||||
store.query(filters, onEach)
|
||||
|
||||
// 2. Signal end of stored events.
|
||||
onEose()
|
||||
|
||||
// 3. Stream live events until cancelled.
|
||||
newEventStream.collect { newEvent ->
|
||||
if (filters.any { it.match(newEvent) }) {
|
||||
onEach(newEvent)
|
||||
// Order matters: register the live collector BEFORE replaying
|
||||
// stored events and signalling EOSE. Otherwise an event emitted
|
||||
// between EOSE and `collect` is lost because [newEventStream] has
|
||||
// replay=0. The race is only occasionally visible for kinds the
|
||||
// store persists (insert latency masks it) but fires reliably for
|
||||
// ephemeral kinds (20000-29999) where insert is a no-op — and
|
||||
// ephemeral events MUST still reach matching live subscribers per
|
||||
// NIP-01.
|
||||
newEventStream
|
||||
.onSubscription {
|
||||
store.query(filters, onEach)
|
||||
onEose()
|
||||
}.collect { newEvent ->
|
||||
if (filters.any { it.match(newEvent) }) {
|
||||
onEach(newEvent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun count(filters: List<Filter>) = store.count(filters)
|
||||
|
||||
Reference in New Issue
Block a user