mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-11 16:57:39 +00:00
refactor: reconcile duplicate LIMITS code and rename client accessory
Cleanup after adding LimitsMessage + the client-side cache: - Remove the unused experimental LIMITS prototype (experimental/limits/Limits.kt + LimitProcessor.kt). Its @Serializable model duplicated LimitsMessage (minus auth_for_read/auth_for_write and the Message wiring); the processor's clamp/reject logic is superseded by the server-side LimitsPolicy and will be reincarnated as pure helpers on LimitsMessage. Both were prototypes with no references (preserved in git history). - Rename the client accessory RelayLimits -> RelayLimitsTracker so it no longer collides on simple name with the relay-server-side nip01Core.relay.server.policies.RelayLimits (the operator-configured limits a relay enforces and advertises). Updates AppModules and the test. No behavior change. quartz:jvmTest (RelayLimitsTrackerTest 5/5) and amethyst play-flavor compile are green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01464jkunWPtYhTReoc3fUQQ
This commit is contained in:
@@ -122,7 +122,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.RelayLogger
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.RelayOfflineTracker
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.limits.RelayLimits
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.limits.RelayLimitsTracker
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.stats.RelayReqStats
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.stats.RelayStats
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.CachingEventDecoder
|
||||
@@ -712,7 +712,7 @@ class AppModules(
|
||||
val relayStats = RelayStats(client)
|
||||
|
||||
// Caches the latest LIMITS (rights + limits) each relay advertises.
|
||||
val relayLimits = RelayLimits(client)
|
||||
val relayLimits = RelayLimitsTracker(client)
|
||||
|
||||
// Resource-usage ledger: relay traffic/reconnect + connection-time,
|
||||
// foreground-time, process-CPU, and signature-verification collectors.
|
||||
|
||||
-113
@@ -1,113 +0,0 @@
|
||||
/*
|
||||
* 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.experimental.limits
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip13Pow.pow
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
class LimitProcessor {
|
||||
fun wrapFilterToLimits(
|
||||
filter: Filter,
|
||||
sendingStr: String,
|
||||
limits: Limits,
|
||||
): Filter? {
|
||||
var newFilter: Filter? = filter
|
||||
|
||||
if (limits.canRead != null && !limits.canRead) {
|
||||
newFilter = null
|
||||
}
|
||||
|
||||
if (limits.maxLimit != null && filter.limit != null && filter.limit > limits.maxLimit) {
|
||||
newFilter = filter.copy(limit = limits.maxLimit)
|
||||
}
|
||||
|
||||
if (!limits.acceptedEventKinds.isNullOrEmpty() && !filter.kinds.isNullOrEmpty()) {
|
||||
val intersect = filter.kinds.filter { it in limits.acceptedEventKinds }
|
||||
if (intersect.isNotEmpty()) {
|
||||
newFilter = filter.copy(kinds = intersect)
|
||||
} else {
|
||||
newFilter = null
|
||||
}
|
||||
}
|
||||
|
||||
if (!limits.blockedEventKinds.isNullOrEmpty() && !filter.kinds.isNullOrEmpty()) {
|
||||
val intersect = filter.kinds.filter { it !in limits.blockedEventKinds }
|
||||
if (intersect.isNotEmpty()) {
|
||||
newFilter = filter.copy(kinds = intersect)
|
||||
} else {
|
||||
newFilter = null
|
||||
}
|
||||
}
|
||||
|
||||
if (limits.maxMessageLength != null && sendingStr.length > limits.maxMessageLength) {
|
||||
// TODO: figure out how to dynamically reduce filter size
|
||||
newFilter = null
|
||||
}
|
||||
|
||||
return newFilter
|
||||
}
|
||||
|
||||
fun canSendEvent(
|
||||
ev: Event,
|
||||
sendingStr: String,
|
||||
limits: Limits,
|
||||
): Boolean {
|
||||
if (limits.canWrite != null && !limits.canWrite) return false
|
||||
|
||||
if (!limits.acceptedEventKinds.isNullOrEmpty() && ev.kind !in limits.acceptedEventKinds) return false
|
||||
if (!limits.blockedEventKinds.isNullOrEmpty() && ev.kind in limits.blockedEventKinds) return false
|
||||
|
||||
if (limits.minPoW != null && ev.pow() < limits.minPoW) return false
|
||||
if (limits.maxEventTags != null && ev.tags.size > limits.maxEventTags) return false
|
||||
if (limits.maxContentLength != null && ev.content.length > limits.maxContentLength) return false
|
||||
|
||||
if (limits.createdAtMillisecsAgo != null && ev.createdAt < TimeUtils.now() - limits.createdAtMillisecsAgo) return false
|
||||
if (limits.createdAtMillisecsAhead != null && ev.createdAt > TimeUtils.now() + limits.createdAtMillisecsAhead) return false
|
||||
|
||||
if (limits.requiredTags != null && !matchAll(ev, limits.requiredTags)) return false
|
||||
|
||||
if (limits.maxMessageLength != null && sendingStr.length > limits.maxMessageLength) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun matchAll(
|
||||
ev: Event,
|
||||
requiredTags: Array<Array<String>>,
|
||||
): Boolean =
|
||||
requiredTags.all { requiredTag ->
|
||||
if (requiredTag.isNotEmpty()) {
|
||||
if (requiredTag.getOrNull(1) == null) {
|
||||
ev.tags.any { eventTag ->
|
||||
eventTag.getOrNull(0) == requiredTag[0]
|
||||
}
|
||||
} else {
|
||||
ev.tags.any { eventTag ->
|
||||
eventTag.getOrNull(0) == requiredTag[0] && eventTag.getOrNull(1) == requiredTag[1]
|
||||
}
|
||||
}
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
/*
|
||||
* 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.experimental.limits
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
class Limits(
|
||||
@SerialName("can_write") val canWrite: Boolean?,
|
||||
@SerialName("can_read") val canRead: Boolean?,
|
||||
@SerialName("accepted_event_kinds") val acceptedEventKinds: Set<Int>?,
|
||||
@SerialName("blocked_event_kinds") val blockedEventKinds: Set<Int>?,
|
||||
@SerialName("min_pow_difficulty") val minPoW: Int?,
|
||||
@SerialName("max_message_length") val maxMessageLength: Int?,
|
||||
@SerialName("max_subscriptions") val maxSubscriptions: Int?,
|
||||
@SerialName("max_filters") val maxFilters: Int?,
|
||||
@SerialName("max_limit") val maxLimit: Int?,
|
||||
@SerialName("max_event_tags") val maxEventTags: Int?,
|
||||
@SerialName("max_content_length") val maxContentLength: Int?,
|
||||
@SerialName("created_at_msecs_ago") val createdAtMillisecsAgo: Long?,
|
||||
@SerialName("created_at_msecs_ahead") val createdAtMillisecsAhead: Long?,
|
||||
@SerialName("filter_rate_limit") val filterRateLimit: Long?,
|
||||
@SerialName("publishing_rate_limit") val publishingRateLimit: Long?,
|
||||
@SerialName("required_tags") val requiredTags: Array<Array<String>>?,
|
||||
)
|
||||
+7
-3
@@ -47,8 +47,12 @@ import kotlinx.coroutines.flow.update
|
||||
* The cache is connection-scoped: a relay's entry is dropped on disconnect, so a
|
||||
* stale limit from a previous session never leaks into a new one. A fresh
|
||||
* connection re-advertises `LIMITS` on connect.
|
||||
*
|
||||
* Named `…Tracker` to avoid colliding with the relay-*server* side's
|
||||
* [com.vitorpamplona.quartz.nip01Core.relay.server.policies.RelayLimits], which
|
||||
* is the operator-configured source of truth a relay enforces and advertises.
|
||||
*/
|
||||
class RelayLimits(
|
||||
class RelayLimitsTracker(
|
||||
val client: INostrClient,
|
||||
) {
|
||||
// onIncomingMessage / onDisconnected fire on the per-relay socket dispatcher
|
||||
@@ -88,13 +92,13 @@ class RelayLimits(
|
||||
}
|
||||
|
||||
init {
|
||||
Log.d("RelayLimits", "Init, Subscribe")
|
||||
Log.d("RelayLimitsTracker", "Init, Subscribe")
|
||||
client.addConnectionListener(clientListener)
|
||||
}
|
||||
|
||||
fun destroy() {
|
||||
// makes sure to run
|
||||
Log.d("RelayLimits", "Destroy, Unsubscribe")
|
||||
Log.d("RelayLimitsTracker", "Destroy, Unsubscribe")
|
||||
client.removeConnectionListener(clientListener)
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -33,7 +33,7 @@ import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class RelayLimitsTest {
|
||||
class RelayLimitsTrackerTest {
|
||||
private class CapturingClient(
|
||||
private val delegate: INostrClient = EmptyNostrClient(),
|
||||
) : INostrClient by delegate {
|
||||
@@ -62,10 +62,10 @@ class RelayLimitsTest {
|
||||
override fun disconnect() = Unit
|
||||
}
|
||||
|
||||
private fun setup(): Pair<RelayLimits, RelayConnectionListener> {
|
||||
private fun setup(): Pair<RelayLimitsTracker, RelayConnectionListener> {
|
||||
val client = CapturingClient()
|
||||
val limits = RelayLimits(client)
|
||||
val listener = client.captured ?: error("RelayLimits did not register a listener")
|
||||
val limits = RelayLimitsTracker(client)
|
||||
val listener = client.captured ?: error("RelayLimitsTracker did not register a listener")
|
||||
return limits to listener
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user