refactor(quartz): switch project() builder from channelFlow to flow

channelFlow was overkill for a single-producer chain. The cost was
an extra Channel hop between every projection.snapshot() and the
downstream collector — pure overhead with no concurrency benefit.

flow { } needs the outer collector captured (because inside
changes.onSubscription { ... } and changes.collect { ... } the
implicit `this` is FlowCollector<StoreChange>, not
FlowCollector<ProjectionState<T>>). One `val outer = this` fixes
that.

Backpressure now flows directly: a slow collector suspends emit,
which suspends our changes.collect, which suspends the SharedFlow's
buffer drain — natural propagation, no decoupling Channel in the
middle.

17/17 projection tests pass.

https://claude.ai/code/session_01Jny85MTu1ynKgFBgysfWu5
This commit is contained in:
Claude
2026-04-30 15:48:36 +00:00
parent f789fe4f53
commit 5755ab90b2
@@ -35,7 +35,7 @@ import com.vitorpamplona.quartz.nip62RequestToVanish.RequestToVanishEvent
import com.vitorpamplona.quartz.utils.TimeUtils
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.onSubscription
import kotlinx.coroutines.yield
@@ -410,9 +410,13 @@ class EventStoreProjection<T : Event>(
* NIP-62 vanish handling is scoped by the inner store's `relay`.
*/
fun <T : Event> ObservableEventStore.project(filters: List<Filter>): Flow<ProjectionState<T>> =
channelFlow {
flow {
val projection = EventStoreProjection<T>(this@project, filters)
send(ProjectionState.Loading)
// Capture the outer collector so we can emit ProjectionState
// from inside `changes.onSubscription { }` and `collect { }`,
// where the implicit `this` is FlowCollector<StoreChange>.
val outer = this
emit(ProjectionState.Loading)
// `onSubscription` runs after the SharedFlow subscription is
// active but before we pull events — the buffer absorbs
// emissions arriving during the seed query and we drain them
@@ -422,9 +426,9 @@ fun <T : Event> ObservableEventStore.project(filters: List<Filter>): Flow<Projec
changes
.onSubscription {
projection.seed()
send(projection.snapshot())
outer.emit(projection.snapshot())
}.collect { change ->
if (projection.apply(change)) send(projection.snapshot())
if (projection.apply(change)) outer.emit(projection.snapshot())
}
}