feat(quartz): add EventInterner so deserialized events share canonical instances

Process-wide interner that canonicalises Event instances by id —
whenever the same event id is decoded twice (relay duplicates,
projection seeds, FS re-reads, etc.), every consumer sees the same
object reference. Backed by weak references so entries vanish once
no live consumer (projection slot, UI state) holds the event.

Shape:
- expect class EventInterner in nip01Core/core/, with a Default
  process-wide instance.
- jvmAndroid actual: ConcurrentHashMap<HexKey, WeakReference<Event>>
  pre-sized to 5_000 (load factor 0.75). On-access cleanup atomically
  removes dead entries via map.remove(key, ref). intern() uses
  putIfAbsent + retry-on-dead-canonical to be race-safe.
- apple / linux actuals: passthrough (no canonicalisation). Kotlin/
  Native has weak refs but no built-in concurrent map; rather than
  ship a half-baked impl on Apple targets we skip canonicalisation
  there. Can be revisited if iOS wants the memory savings.

Wire-up:
- Event.fromJson now routes through EventInterner.Default.intern,
  so every deserialised event becomes canonical for free. In-process
  events (signer.sign(...) etc.) aren't auto-interned — the
  canonicaliser pays off for events that re-occur from multiple
  sources, which signed-locally events don't.

Tests (jvmTest):
- internReturnsFirstInstance / internCollapsesDuplicates:
  identity is preserved across equivalent decodes.
- getReturnsLiveEntry / getReturnsNullForUnknownId.
- getEvictsDeadEntries: weak ref cleared by GC, on-access
  cleanup drops the entry.
- draftChurnDoesNotLeak: 100 distinct drafts churn through the
  interner with no strong refs; map shrinks back to ~0 after GC.
- defaultInstanceIsShared: the global Default interner is process-wide.
- 7/7 pass; 240/240 store + projection tests still green.

https://claude.ai/code/session_01Jny85MTu1ynKgFBgysfWu5
This commit is contained in:
Claude
2026-04-30 13:27:33 +00:00
parent ae6343cd73
commit e909866d26
6 changed files with 356 additions and 1 deletions
@@ -0,0 +1,136 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip01Core.core
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
import com.vitorpamplona.quartz.utils.Secp256k1Instance
import kotlin.test.AfterTest
import kotlin.test.BeforeTest
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull
import kotlin.test.assertSame
import kotlin.test.assertTrue
class EventInternerTest {
private val signer = NostrSignerSync()
private lateinit var interner: EventInterner
@BeforeTest
fun setUp() {
Secp256k1Instance
interner = EventInterner()
}
@AfterTest
fun tearDown() {
interner.clear()
}
@Test
fun internReturnsFirstInstance() {
val original = signer.sign(TextNoteEvent.build("hello", createdAt = 100))
val canonical = interner.intern(original)
assertSame(original, canonical)
assertEquals(1, interner.size())
}
@Test
fun internCollapsesDuplicates() {
val a = signer.sign(TextNoteEvent.build("hello", createdAt = 100))
// Round-trip through OptimizedJsonMapper directly (NOT
// Event.fromJson, which would already intern via Default) so
// we get a non-identical-but-equal event for our isolated
// interner to deduplicate.
val b = OptimizedJsonMapper.fromJson(a.toJson()) as TextNoteEvent
assertEquals(a.id, b.id)
val firstCanonical = interner.intern(a)
val secondCanonical = interner.intern(b)
assertSame(firstCanonical, secondCanonical)
assertSame(a, secondCanonical)
assertEquals(1, interner.size())
}
@Test
fun getReturnsLiveEntry() {
val a = signer.sign(TextNoteEvent.build("hello", createdAt = 100))
interner.intern(a)
assertSame(a, interner.get(a.id))
}
@Test
fun getReturnsNullForUnknownId() {
assertNull(interner.get("0".repeat(64)))
}
@Test
fun getEvictsDeadEntries() {
// Confine the event to a helper so no strong ref leaks into
// this method's stack frame after it returns. The interner's
// WeakReference is then the only thing holding it.
val id = internAndDiscard(interner)
for (i in 0..40) {
System.gc()
Thread.sleep(20)
if (interner.get(id) == null) break
}
assertNull(interner.get(id))
assertEquals(0, interner.size(), "dead entry should be evicted on access")
}
private fun internAndDiscard(interner: EventInterner): HexKey {
val ev = signer.sign(TextNoteEvent.build("hello", createdAt = 100))
interner.intern(ev)
return ev.id
}
@Test
fun draftChurnDoesNotLeak() {
// 100 distinct drafts, no strong references retained.
val ids = ArrayList<HexKey>(100)
for (i in 0 until 100) {
val e = signer.sign(TextNoteEvent.build("draft-$i", createdAt = 1_000L + i))
ids.add(e.id)
interner.intern(e)
}
// Force GC + sweep — calling get() on every id triggers
// the on-access cleanup path.
for (i in 0..40) {
System.gc()
Thread.sleep(20)
ids.forEach { interner.get(it) }
if (interner.size() == 0) break
}
assertTrue(interner.size() <= 5, "expected most drafts to be evicted, got ${interner.size()}")
}
@Test
fun defaultInstanceIsShared() {
val a = signer.sign(TextNoteEvent.build("hello", createdAt = 100))
val canonical = EventInterner.Default.intern(a)
// A second intern call from anywhere returns the same canonical.
val b = EventInterner.Default.intern(a)
assertSame(canonical, b)
EventInterner.Default.clear()
}
}