fix(quartz): make commonMain compile for Kotlin/Native (iOS)
@Volatile resolves via kotlin.jvm on JVM but needs an explicit kotlin.concurrent.Volatile import on Native; Dispatchers.IO is internal on Native, so the in-process WebSocket switches to Dispatchers.Default (no blocking I/O on that path); and Native's LinkedHashMap is final without removeEldestEntry, so Nip98AuthVerifier's replay cache now caps itself with an explicit insertion-order eviction loop (access-order was unused — the cache is write-only). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+1
@@ -29,6 +29,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.EventCmd
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlin.concurrent.Volatile
|
||||
|
||||
class PoolEventOutbox {
|
||||
// @Volatile so the polling path (INostrClient.pendingPublishRelaysFor)
|
||||
|
||||
+1
@@ -23,6 +23,7 @@ package com.vitorpamplona.quartz.nip01Core.relay.client.pool
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlin.concurrent.Volatile
|
||||
|
||||
class PoolEventOutboxState(
|
||||
val event: Event,
|
||||
|
||||
+1
-1
@@ -65,7 +65,7 @@ class InProcessWebSocket(
|
||||
|
||||
override fun connect() {
|
||||
if (session != null) return
|
||||
val newScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
val newScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
||||
val newIncoming = Channel<String>(UNLIMITED)
|
||||
val s = server.connect { json -> out.onMessage(json) }
|
||||
|
||||
|
||||
+12
-6
@@ -55,18 +55,15 @@ class Nip98AuthVerifier(
|
||||
) {
|
||||
/**
|
||||
* Recently-accepted event ids → expiry epoch second. Bounded to
|
||||
* [MAX_REPLAY_ENTRIES] (LRU eviction); each entry expires after
|
||||
* `2 × toleranceSeconds` (twice the accepted window so a token
|
||||
* [MAX_REPLAY_ENTRIES] (insertion-order eviction); each entry expires
|
||||
* after `2 × toleranceSeconds` (twice the accepted window so a token
|
||||
* can't be reused by an attacker who buffers across the boundary).
|
||||
*
|
||||
* Guarded by [seenLock] so the eviction sweep + insertion are
|
||||
* atomic. We use a coroutine [Mutex] so the type works in KMP
|
||||
* commonMain (no `synchronized` block).
|
||||
*/
|
||||
private val seenEventIds: LinkedHashMap<String, Long> =
|
||||
object : LinkedHashMap<String, Long>(64, 0.75f, true) {
|
||||
override fun removeEldestEntry(eldest: Map.Entry<String, Long>?): Boolean = size > MAX_REPLAY_ENTRIES
|
||||
}
|
||||
private val seenEventIds: LinkedHashMap<String, Long> = LinkedHashMap()
|
||||
|
||||
private val seenLock = Mutex()
|
||||
|
||||
@@ -146,6 +143,15 @@ class Nip98AuthVerifier(
|
||||
if (seenEventIds.put(event.id, expiry) != null) {
|
||||
return Result.Malformed("replay: this NIP-98 token has already been used")
|
||||
}
|
||||
// Cap entries: drop oldest by insertion order. Equivalent to
|
||||
// the JDK LinkedHashMap.removeEldestEntry hook we used before,
|
||||
// but works in KMP commonMain.
|
||||
while (seenEventIds.size > MAX_REPLAY_ENTRIES) {
|
||||
val eldest = seenEventIds.keys.iterator()
|
||||
if (!eldest.hasNext()) break
|
||||
eldest.next()
|
||||
eldest.remove()
|
||||
}
|
||||
}
|
||||
|
||||
return Result.Verified(event.pubKey)
|
||||
|
||||
Reference in New Issue
Block a user