refactor(quartz): intern only on store reads, inject per-store interner

The previous commit interned at Event.fromJson, which is too early —
the deserializer can produce events with manipulated id fields that
don't match their content. Move interning to the post-validation
boundary: events are only canonicalised when read back from durable
storage, where they were valid when written.

- Revert Event.fromJson to plain OptimizedJsonMapper.fromJson.
- Revert ObservableEventStore.insert interning. Substituting the
  caller's event for a previously-cached one with the same id but
  potentially different sig was a write-side surprise; we leave the
  caller's instance alone.
- Intern at SQLiteStatement.toEvent (read deserialization).
- Intern at FsEventStore.readEvent (FS reads).

The interner is also now constructor-injected through every store
layer (SQLiteEventStore, EventStore, QueryBuilder, FsEventStore)
defaulting to EventInterner.Default. BaseDBTest gives each parallel
forEachDB store its own EventInterner — without this, parallel test
runs that sign "same id, different sig" events through the shared
Default would see the first run's sig leak across all DBs.

All 240 store + projection + interner tests pass.

https://claude.ai/code/session_01Jny85MTu1ynKgFBgysfWu5
This commit is contained in:
Claude
2026-04-30 13:45:01 +00:00
parent e909866d26
commit 7f613d6003
6 changed files with 39 additions and 13 deletions
@@ -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(),
)
}
}