diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/Event.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/Event.kt index 576fe6813..906773852 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/Event.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/Event.kt @@ -67,7 +67,7 @@ open class Event( fun toJson(): String = OptimizedJsonMapper.toJson(this) companion object { - fun fromJson(json: String): Event = EventInterner.Default.intern(OptimizedJsonMapper.fromJson(json)) + fun fromJson(json: String): Event = OptimizedJsonMapper.fromJson(json) fun fromJsonOrNull(json: String) = try { diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventStore.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventStore.kt index 0a0f1a75b..ddabf901a 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventStore.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventStore.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.quartz.nip01Core.store.sqlite import androidx.sqlite.driver.bundled.BundledSQLiteDriver import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.EventInterner import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.normalizeRelayUrl @@ -36,8 +37,9 @@ class EventStore( dbName: String? = "events.db", val relay: NormalizedRelayUrl? = "wss://quartz.local/".normalizeRelayUrl(), val indexStrategy: IndexingStrategy = DefaultIndexingStrategy(), + val interner: EventInterner = EventInterner.Default, ) : IEventStore { - val store = SQLiteEventStore(BundledSQLiteDriver(), dbName, relay, indexStrategy) + val store = SQLiteEventStore(BundledSQLiteDriver(), dbName, relay, indexStrategy, interner = interner) override suspend fun insert(event: Event) = store.insertEvent(event) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryBuilder.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryBuilder.kt index fa6ad1ffe..51db325bc 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryBuilder.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryBuilder.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.quartz.nip01Core.store.sqlite import androidx.sqlite.SQLiteConnection import androidx.sqlite.SQLiteStatement import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.EventInterner import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Kind import com.vitorpamplona.quartz.nip01Core.core.OptimizedJsonMapper @@ -35,6 +36,7 @@ class QueryBuilder( val fts: FullTextSearchModule, val hasher: (db: SQLiteConnection) -> TagNameValueHasher, val indexStrategy: IndexingStrategy, + val interner: EventInterner = EventInterner.Default, ) { // ------------ // Main methods @@ -232,16 +234,23 @@ class QueryBuilder( } } - private fun SQLiteStatement.toEvent() = - EventFactory.create( - getText(0), - getText(1), - getLong(2), - getInt(3), - OptimizedJsonMapper.fromJsonToTagArray(getText(4)), - getText(5), - getText(6), - ) + @Suppress("UNCHECKED_CAST") + private fun SQLiteStatement.toEvent(): T { + val event = + EventFactory.create( + getText(0), + getText(1), + getLong(2), + getInt(3), + OptimizedJsonMapper.fromJsonToTagArray(getText(4)), + getText(5), + getText(6), + ) + // Events read from durable storage were valid when written, so + // interning their reconstruction is safe. Multiple projections + // re-querying the same id end up sharing one Event instance. + return interner.intern(event) as T + } private fun SQLiteStatement.toRawEvent() = RawEvent( diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/SQLiteEventStore.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/SQLiteEventStore.kt index 2da5cf49c..cc6e56f71 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/SQLiteEventStore.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/SQLiteEventStore.kt @@ -25,6 +25,7 @@ import androidx.sqlite.SQLiteDriver import androidx.sqlite.SQLiteException import androidx.sqlite.driver.bundled.BundledSQLiteDriver import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.EventInterner import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Kind import com.vitorpamplona.quartz.nip01Core.core.OptimizedJsonMapper @@ -41,6 +42,7 @@ class SQLiteEventStore( val relay: NormalizedRelayUrl? = null, val indexStrategy: IndexingStrategy = DefaultIndexingStrategy(), val numReaders: Int = 4, + val interner: EventInterner = EventInterner.Default, ) { companion object { const val DATABASE_VERSION = 2 @@ -68,6 +70,7 @@ class SQLiteEventStore( fullTextSearchModule, seedModule::hasher, indexStrategy, + interner, ) val modules = diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/BaseDBTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/BaseDBTest.kt index 300ceb4cc..a89aaa9b1 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/BaseDBTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/BaseDBTest.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.quartz.nip01Core.store.sqlite +import com.vitorpamplona.quartz.nip01Core.core.EventInterner import com.vitorpamplona.quartz.utils.Secp256k1Instance import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -63,6 +64,12 @@ open class BaseDBTest { EventStore( dbName = null, indexStrategy = indexStrategy, + // Each store gets its own interner so + // parallel forEachDB runs don't share a + // canonical Event for the same id — + // tests sign with random sigs that + // would otherwise cross-pollinate. + interner = EventInterner(), ) } } diff --git a/quartz/src/jvmMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/fs/FsEventStore.kt b/quartz/src/jvmMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/fs/FsEventStore.kt index 8da5d14b5..50a606573 100644 --- a/quartz/src/jvmMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/fs/FsEventStore.kt +++ b/quartz/src/jvmMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/fs/FsEventStore.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.quartz.nip01Core.store.fs import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.EventInterner import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.isAddressable import com.vitorpamplona.quartz.nip01Core.core.isEphemeral @@ -75,6 +76,7 @@ open class FsEventStore( * verification re-canonicalises — so format is purely a UX choice. */ private val eventToJson: (Event) -> String = Event::toJson, + private val interner: EventInterner = EventInterner.Default, ) : IEventStore { private val layout = FsLayout(root) private val hasher: TagNameValueHasher @@ -508,7 +510,10 @@ open class FsEventStore( val p = layout.canonical(id) if (!p.exists()) return null return try { - Event.fromJson(p.readText()) + // Events on disk were valid when written, so interning + // their reconstruction is safe and lets multiple readers + // of the same id share one Event instance. + interner.intern(Event.fromJson(p.readText())) } catch (_: java.nio.file.NoSuchFileException) { null }