From 041abf9e37b8354d43408cbdb2720a1e7daeaeaa Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Jul 2026 17:21:00 +0000 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_01464jkunWPtYhTReoc3fUQQ --- .../com/vitorpamplona/amethyst/AppModules.kt | 4 +- .../experimental/limits/LimitProcessor.kt | 113 ------------------ .../quartz/experimental/limits/Limits.kt | 44 ------- .../{RelayLimits.kt => RelayLimitsTracker.kt} | 10 +- ...imitsTest.kt => RelayLimitsTrackerTest.kt} | 8 +- 5 files changed, 13 insertions(+), 166 deletions(-) delete mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/limits/LimitProcessor.kt delete mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/limits/Limits.kt rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/limits/{RelayLimits.kt => RelayLimitsTracker.kt} (92%) rename quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/limits/{RelayLimitsTest.kt => RelayLimitsTrackerTest.kt} (95%) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt index 5cbb2cc366..92a2454665 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt @@ -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. diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/limits/LimitProcessor.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/limits/LimitProcessor.kt deleted file mode 100644 index 3bd5129456..0000000000 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/limits/LimitProcessor.kt +++ /dev/null @@ -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>, - ): 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 - } - } -} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/limits/Limits.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/limits/Limits.kt deleted file mode 100644 index 2526e377f9..0000000000 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/limits/Limits.kt +++ /dev/null @@ -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?, - @SerialName("blocked_event_kinds") val blockedEventKinds: Set?, - @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>?, -) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/limits/RelayLimits.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/limits/RelayLimitsTracker.kt similarity index 92% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/limits/RelayLimits.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/limits/RelayLimitsTracker.kt index 36acfaf475..a8ffa37d63 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/limits/RelayLimits.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/limits/RelayLimitsTracker.kt @@ -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) } } diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/limits/RelayLimitsTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/limits/RelayLimitsTrackerTest.kt similarity index 95% rename from quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/limits/RelayLimitsTest.kt rename to quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/limits/RelayLimitsTrackerTest.kt index b88f2e2dfc..6547aeb9bb 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/limits/RelayLimitsTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/limits/RelayLimitsTrackerTest.kt @@ -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 { + private fun setup(): Pair { 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 }