epic-12: after epic audit and init commit

This commit is contained in:
2026-05-16 11:42:00 +04:00
commit c77277af0b
461 changed files with 28958 additions and 0 deletions
+13
View File
@@ -0,0 +1,13 @@
plugins {
id 'java-library'
id 'org.jetbrains.kotlin.jvm'
id 'org.jetbrains.kotlin.plugin.serialization'
}
dependencies {
implementation(project(":core:events"))
implementation(project(":core:sessions"))
implementation "org.xerial:sqlite-jdbc"
testImplementation(testFixtures(project(":testing:contracts")))
testImplementation "org.junit.jupiter:junit-jupiter"
}
@@ -0,0 +1,68 @@
package com.correx.infrastructure.persistence
import com.correx.core.events.events.NewEvent
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 java.util.concurrent.*
import java.util.concurrent.atomic.*
class InMemoryEventStore : EventStore {
private val streams = ConcurrentHashMap<SessionId, MutableList<StoredEvent>>()
private val sequences = ConcurrentHashMap<SessionId, AtomicLong>()
private val seenEventIds = ConcurrentHashMap.newKeySet<EventId>()
override fun append(event: NewEvent): StoredEvent {
val stream = streams.computeIfAbsent(event.metadata.sessionId) { mutableListOf() }
synchronized(stream) {
if (!seenEventIds.add(event.metadata.eventId)) {
error("duplicate event_id: ${event.metadata.eventId}")
}
return doAppend(event, stream)
}
}
override fun appendAll(events: List<NewEvent>): List<StoredEvent> {
if (events.isEmpty()) return emptyList()
val sessionId = events.first().metadata.sessionId
val stream = streams.computeIfAbsent(sessionId) { mutableListOf() }
synchronized(stream) {
return events.map { doAppend(it, stream) }
}
}
override fun read(sessionId: SessionId): List<StoredEvent> {
return streams[sessionId]
?.sortedBy { it.sequence }
?: emptyList()
}
override fun readFrom(sessionId: SessionId, fromSequence: Long): List<StoredEvent> {
return streams[sessionId]
?.filter { it.sequence > fromSequence }
?.toList()
?: emptyList()
}
override fun lastSequence(sessionId: SessionId): Long? {
return sequences[sessionId]?.get()
}
private fun doAppend(event: NewEvent, stream: MutableList<StoredEvent>): StoredEvent {
val seq = sequences.computeIfAbsent(event.metadata.sessionId) { AtomicLong(0) }
.incrementAndGet()
val stored = StoredEvent(metadata = event.metadata, sequence = seq, payload = event.payload)
// safety: enforce ordering invariant
if (stream.isNotEmpty() && stream.last().sequence >= stored.sequence) {
error("sequence violation for session ${event.metadata.sessionId}")
}
stream.add(stored)
return stored
}
}
@@ -0,0 +1,215 @@
package com.correx.infrastructure.persistence
import com.correx.core.events.events.EventMetadata
import com.correx.core.events.events.NewEvent
import com.correx.core.events.events.StoredEvent
import com.correx.core.events.serialization.JsonEventSerializer
import com.correx.core.events.serialization.eventJson
import com.correx.core.events.stores.EventStore
import com.correx.core.events.types.CausationId
import com.correx.core.events.types.CorrelationId
import com.correx.core.events.types.EventId
import com.correx.core.events.types.SessionId
import com.correx.infrastructure.persistence.util.JDBCHelper.transaction
import kotlinx.datetime.Instant
import java.sql.Connection
import java.sql.ResultSet
class SqliteEventStore(
private val connection: Connection,
private val jsonSerializer: JsonEventSerializer = JsonEventSerializer(eventJson)
) : EventStore {
init {
connection.createStatement().use { stmt ->
stmt.execute(
"""
CREATE TABLE IF NOT EXISTS events (
event_id TEXT PRIMARY KEY,
session_id TEXT NOT NULL,
sequence INTEGER NOT NULL,
timestamp TEXT NOT NULL,
schema_version INTEGER NOT NULL,
causation_id TEXT,
correlation_id TEXT,
payload TEXT NOT NULL
);
""".trimIndent()
)
}
}
override fun append(event: NewEvent): StoredEvent {
return connection.transaction {
val existing = findByEventId(event.metadata.eventId)
check(existing == null) { "duplicate event_id: ${event.metadata.eventId}" }
val seq = nextSequence(event.metadata.sessionId)
val stored = StoredEvent(
metadata = event.metadata,
sequence = seq,
payload = event.payload
)
insert(stored)
stored
}
}
override fun appendAll(events: List<NewEvent>): List<StoredEvent> {
if (events.isEmpty()) return emptyList()
val sessionId = events.first().metadata.sessionId
return connection.transaction {
events.map { event ->
val existing = findByEventId(event.metadata.eventId)
check(existing == null) { "duplicate event_id: ${event.metadata.eventId}" }
val seq = nextSequence(sessionId)
val stored = StoredEvent(
metadata = event.metadata,
sequence = seq,
payload = event.payload
)
insert(stored)
stored
}
}
}
override fun read(sessionId: SessionId): List<StoredEvent> =
connection.prepareStatement(
"""
SELECT * FROM events
WHERE session_id = ?
ORDER BY sequence ASC
""".trimIndent()
).use { ps ->
ps.setString(1, sessionId.value)
ps.executeQuery().use { rs ->
buildList {
while (rs.next()) {
add(map(rs))
}
}
}
}
override fun readFrom(sessionId: SessionId, fromSequence: Long): List<StoredEvent> =
connection.prepareStatement(
"""
SELECT * FROM events
WHERE session_id = ?
AND sequence > ?
ORDER BY sequence ASC
""".trimIndent()
).use { ps ->
ps.setString(1, sessionId.value)
ps.setLong(2, fromSequence)
ps.executeQuery().use { rs ->
buildList {
while (rs.next()) {
add(map(rs))
}
}
}
}
override fun lastSequence(sessionId: SessionId): Long? =
connection.prepareStatement(
"""
SELECT MAX(sequence)
FROM events
WHERE session_id = ?
""".trimIndent()
).use { ps ->
ps.setString(1, sessionId.value)
ps.executeQuery().use { rs ->
if (rs.next()) rs.getLong(1).takeIf { !rs.wasNull() }
else null
}
}
// ---------- helpers ----------
private fun nextSequence(sessionId: SessionId): Long =
connection.prepareStatement(
"""
SELECT COALESCE(MAX(sequence), 0)
FROM events
WHERE session_id = ?
""".trimIndent()
).use { ps ->
ps.setString(1, sessionId.value)
ps.executeQuery().use { rs ->
rs.next()
rs.getLong(1) + 1
}
}
@Suppress("MagicNumber")
private fun insert(event: StoredEvent) {
connection.prepareStatement(
"""
INSERT INTO events (
event_id,
session_id,
sequence,
timestamp,
schema_version,
causation_id,
correlation_id,
payload
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""".trimIndent()
).use { ps ->
ps.setString(1, event.metadata.eventId.value)
ps.setString(2, event.metadata.sessionId.value)
ps.setLong(3, event.sequence)
ps.setString(4, event.metadata.timestamp.toString())
ps.setInt(5, event.metadata.schemaVersion)
ps.setString(6, event.metadata.causationId?.value)
ps.setString(7, event.metadata.correlationId?.value)
ps.setString(8, jsonSerializer.serialize(event.payload))
ps.executeUpdate()
}
}
private fun findByEventId(eventId: EventId): StoredEvent? =
connection.prepareStatement(
"""
SELECT * FROM events
WHERE event_id = ?
""".trimIndent()
).use { ps ->
ps.setString(1, eventId.value)
ps.executeQuery().use { rs ->
if (rs.next()) map(rs) else null
}
}
private fun map(rs: ResultSet): StoredEvent =
StoredEvent(
metadata = EventMetadata(
eventId = EventId(rs.getString("event_id")),
sessionId = SessionId(rs.getString("session_id")),
timestamp = Instant.parse(rs.getString("timestamp")),
schemaVersion = rs.getInt("schema_version"),
causationId = rs.getString("causation_id")?.let { CausationId(it) },
correlationId = rs.getString("correlation_id")?.let { CorrelationId(it) }
),
sequence = rs.getLong("sequence"),
payload = jsonSerializer.deserialize(rs.getString("payload"))
)
}
@@ -0,0 +1,23 @@
package com.correx.infrastructure.persistence.util
import java.sql.Connection
import java.sql.SQLException
object JDBCHelper {
inline fun <T> Connection.transaction(block: () -> T): T {
val oldAutoCommit = autoCommit
try {
autoCommit = false
val result = block()
commit()
return result
} catch (e: SQLException) {
rollback()
throw e
} finally {
autoCommit = oldAutoCommit
}
}
}
@@ -0,0 +1,8 @@
package com.correx.infrastructure.persistence
import com.correx.testing.contracts.fixtures.events.store.EventStoreContractTest
import com.correx.core.events.stores.EventStore
class InMemoryEventStoreTest : EventStoreContractTest() {
override fun store(): EventStore = InMemoryEventStore()
}
@@ -0,0 +1,23 @@
package com.correx.infrastructure.persistence
import com.correx.core.events.stores.EventStore
import com.correx.core.sessions.projections.replay.DefaultEventReplayer
import com.correx.core.sessions.projections.replay.EventReplayer
import com.correx.core.sessions.SessionCounterProjection
import com.correx.core.sessions.SessionCounterState
import com.correx.testing.contracts.fixtures.projections.SessionReplayContractTest
class InMemoryReplayTest : SessionReplayContractTest() {
override fun store(): EventStore =
InMemoryEventStore()
override fun replayer(
store: EventStore
): EventReplayer<SessionCounterState> {
return DefaultEventReplayer(
store,
SessionCounterProjection("s1")
)
}
}
@@ -0,0 +1,12 @@
package com.correx.infrastructure.persistence
import com.correx.testing.contracts.fixtures.events.store.EventStoreContractTest
import com.correx.core.events.stores.EventStore
import java.sql.DriverManager
class SqliteEventStoreTest : EventStoreContractTest() {
override fun store(): EventStore {
val conn = DriverManager.getConnection("jdbc:sqlite::memory:")
return SqliteEventStore(conn)
}
}
@@ -0,0 +1,28 @@
package com.correx.infrastructure.persistence
import com.correx.core.events.stores.EventStore
import com.correx.core.sessions.projections.replay.DefaultEventReplayer
import com.correx.core.sessions.projections.replay.EventReplayer
import com.correx.core.sessions.SessionCounterProjection
import com.correx.core.sessions.SessionCounterState
import com.correx.testing.contracts.fixtures.projections.SessionReplayContractTest
import java.sql.DriverManager
class SqliteReplayTest : SessionReplayContractTest() {
override fun store(): EventStore {
val connection =
DriverManager.getConnection("jdbc:sqlite::memory:")
return SqliteEventStore(connection)
}
override fun replayer(
store: EventStore
): EventReplayer<SessionCounterState> {
return DefaultEventReplayer(
store,
SessionCounterProjection("s1")
)
}
}