feat(server-02): promote EventStore to global-sequence store with subscribeAll
StoredEvent gains sessionSequence: Long (per-session monotonic); existing sequence field becomes the global monotonic cursor across the entire store. SqliteEventStore allocates both counters inside the same BEGIN IMMEDIATE transaction and emits to a global MutableSharedFlow after commit. InMemoryEventStore mirrors this with an AtomicLong global counter and a suspend-on-overflow SharedFlow. EventStore interface gains subscribeAll() and lastGlobalSequence(). Contract tests updated and extended to cover global-sequence and cross-session subscribeAll invariants.
This commit is contained in:
+66
-14
@@ -1,11 +1,16 @@
|
||||
package com.correx.testing.contracts.fixtures.events.store
|
||||
|
||||
import com.correx.core.events.events.StoredEvent
|
||||
import com.correx.core.events.stores.EventStore
|
||||
import com.correx.core.events.types.EventId
|
||||
import com.correx.core.events.types.SessionId
|
||||
import com.correx.testing.contracts.utils.ConcurrencyRunner
|
||||
import com.correx.testing.fixtures.EventFixtures
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.yield
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.assertThrows
|
||||
@@ -83,22 +88,22 @@ abstract class EventStoreContractTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `sequences are strictly increasing per session`(): Unit = runBlocking {
|
||||
fun `sessionSequences are strictly increasing per session`(): Unit = runBlocking {
|
||||
val store = store()
|
||||
|
||||
repeat(100) {
|
||||
store.append(EventFixtures.newEvent(EventId("e$it"), SessionId("s1")))
|
||||
}
|
||||
|
||||
val seqs = store.read(SessionId("s1")).map { it.sequence }
|
||||
val sessionSeqs = store.read(SessionId("s1")).map { it.sessionSequence }
|
||||
sessionSeqs.zipWithNext().forEach { (a, b) -> Assertions.assertTrue(a < b) }
|
||||
|
||||
seqs.zipWithNext().forEach { (a, b) ->
|
||||
Assertions.assertTrue(a < b)
|
||||
}
|
||||
val globalSeqs = store.read(SessionId("s1")).map { it.sequence }
|
||||
globalSeqs.zipWithNext().forEach { (a, b) -> Assertions.assertTrue(a < b) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `sequences are isolated per session`(): Unit = runBlocking {
|
||||
fun `sessionSequences are isolated per session and global sequence spans all`(): Unit = runBlocking {
|
||||
val store = store()
|
||||
|
||||
repeat(50) {
|
||||
@@ -106,11 +111,17 @@ abstract class EventStoreContractTest {
|
||||
store.append(EventFixtures.newEvent(EventId("b$it"), SessionId("B")))
|
||||
}
|
||||
|
||||
val aSeq = store.read(SessionId("A")).map { it.sequence }
|
||||
val bSeq = store.read(SessionId("B")).map { it.sequence }
|
||||
val aSessionSeq = store.read(SessionId("A")).map { it.sessionSequence }
|
||||
val bSessionSeq = store.read(SessionId("B")).map { it.sessionSequence }
|
||||
|
||||
Assertions.assertEquals(aSeq.sorted(), aSeq)
|
||||
Assertions.assertEquals(bSeq.sorted(), bSeq)
|
||||
Assertions.assertEquals(aSessionSeq.sorted(), aSessionSeq)
|
||||
Assertions.assertEquals(bSessionSeq.sorted(), bSessionSeq)
|
||||
Assertions.assertEquals(1L, aSessionSeq.first())
|
||||
Assertions.assertEquals(1L, bSessionSeq.first())
|
||||
|
||||
val allGlobal = (store.read(SessionId("A")) + store.read(SessionId("B")))
|
||||
.map { it.sequence }.sorted()
|
||||
allGlobal.zipWithNext().forEach { (a, b) -> Assertions.assertTrue(a < b) }
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -127,7 +138,7 @@ abstract class EventStoreContractTest {
|
||||
|
||||
Assertions.assertEquals(50, result.size)
|
||||
|
||||
val seqs = result.map { it.sequence }
|
||||
val seqs = result.map { it.sessionSequence }
|
||||
Assertions.assertEquals(seqs.sorted(), seqs)
|
||||
}
|
||||
|
||||
@@ -149,7 +160,7 @@ abstract class EventStoreContractTest {
|
||||
|
||||
val all = store.read(SessionId("s1"))
|
||||
|
||||
val seqs = all.map { it.sequence }
|
||||
val seqs = all.map { it.sessionSequence }
|
||||
|
||||
Assertions.assertEquals(seqs.sorted(), seqs)
|
||||
}
|
||||
@@ -179,7 +190,48 @@ abstract class EventStoreContractTest {
|
||||
val first = store.readFrom(SessionId("s1"), 3)
|
||||
val second = store.readFrom(SessionId("s1"), 7)
|
||||
|
||||
Assertions.assertTrue(first.all { it.sequence > 3 })
|
||||
Assertions.assertTrue(second.all { it.sequence > 7 })
|
||||
Assertions.assertTrue(first.all { it.sessionSequence > 3 })
|
||||
Assertions.assertTrue(second.all { it.sessionSequence > 7 })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `lastGlobalSequence returns 0 when store is empty`(): Unit = runBlocking {
|
||||
val store = store()
|
||||
Assertions.assertEquals(0L, store.lastGlobalSequence())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `lastGlobalSequence equals highest sequence after appends`(): Unit = runBlocking {
|
||||
val store = store()
|
||||
|
||||
store.append(EventFixtures.newEvent(EventId("x1"), SessionId("s1")))
|
||||
store.append(EventFixtures.newEvent(EventId("x2"), SessionId("s2")))
|
||||
store.append(EventFixtures.newEvent(EventId("x3"), SessionId("s1")))
|
||||
|
||||
val allSeqs = (store.read(SessionId("s1")) + store.read(SessionId("s2")))
|
||||
.map { it.sequence }
|
||||
|
||||
Assertions.assertEquals(allSeqs.max(), store.lastGlobalSequence())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `subscribeAll emits events from all sessions`(): Unit = runBlocking {
|
||||
val store = store()
|
||||
val received = mutableListOf<StoredEvent>()
|
||||
|
||||
val job = launch {
|
||||
store.subscribeAll().collect { received.add(it) }
|
||||
}
|
||||
yield()
|
||||
|
||||
store.append(EventFixtures.newEvent(EventId("p1"), SessionId("alpha")))
|
||||
store.append(EventFixtures.newEvent(EventId("q1"), SessionId("beta")))
|
||||
delay(100)
|
||||
job.cancel()
|
||||
|
||||
Assertions.assertEquals(2, received.size)
|
||||
val sessionIds = received.map { it.metadata.sessionId.value }.toSet()
|
||||
Assertions.assertTrue("alpha" in sessionIds)
|
||||
Assertions.assertTrue("beta" in sessionIds)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user