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 com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlin.concurrent.Volatile
|
||||||
|
|
||||||
class PoolEventOutbox {
|
class PoolEventOutbox {
|
||||||
// @Volatile so the polling path (INostrClient.pendingPublishRelaysFor)
|
// @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.core.Event
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
|
import kotlin.concurrent.Volatile
|
||||||
|
|
||||||
class PoolEventOutboxState(
|
class PoolEventOutboxState(
|
||||||
val event: Event,
|
val event: Event,
|
||||||
|
|||||||
+1
-1
@@ -65,7 +65,7 @@ class InProcessWebSocket(
|
|||||||
|
|
||||||
override fun connect() {
|
override fun connect() {
|
||||||
if (session != null) return
|
if (session != null) return
|
||||||
val newScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
val newScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
||||||
val newIncoming = Channel<String>(UNLIMITED)
|
val newIncoming = Channel<String>(UNLIMITED)
|
||||||
val s = server.connect { json -> out.onMessage(json) }
|
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
|
* Recently-accepted event ids → expiry epoch second. Bounded to
|
||||||
* [MAX_REPLAY_ENTRIES] (LRU eviction); each entry expires after
|
* [MAX_REPLAY_ENTRIES] (insertion-order eviction); each entry expires
|
||||||
* `2 × toleranceSeconds` (twice the accepted window so a token
|
* after `2 × toleranceSeconds` (twice the accepted window so a token
|
||||||
* can't be reused by an attacker who buffers across the boundary).
|
* can't be reused by an attacker who buffers across the boundary).
|
||||||
*
|
*
|
||||||
* Guarded by [seenLock] so the eviction sweep + insertion are
|
* Guarded by [seenLock] so the eviction sweep + insertion are
|
||||||
* atomic. We use a coroutine [Mutex] so the type works in KMP
|
* atomic. We use a coroutine [Mutex] so the type works in KMP
|
||||||
* commonMain (no `synchronized` block).
|
* commonMain (no `synchronized` block).
|
||||||
*/
|
*/
|
||||||
private val seenEventIds: LinkedHashMap<String, Long> =
|
private val seenEventIds: LinkedHashMap<String, Long> = LinkedHashMap()
|
||||||
object : LinkedHashMap<String, Long>(64, 0.75f, true) {
|
|
||||||
override fun removeEldestEntry(eldest: Map.Entry<String, Long>?): Boolean = size > MAX_REPLAY_ENTRIES
|
|
||||||
}
|
|
||||||
|
|
||||||
private val seenLock = Mutex()
|
private val seenLock = Mutex()
|
||||||
|
|
||||||
@@ -146,6 +143,15 @@ class Nip98AuthVerifier(
|
|||||||
if (seenEventIds.put(event.id, expiry) != null) {
|
if (seenEventIds.put(event.id, expiry) != null) {
|
||||||
return Result.Malformed("replay: this NIP-98 token has already been used")
|
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)
|
return Result.Verified(event.pubKey)
|
||||||
|
|||||||
Reference in New Issue
Block a user