Fix stale relay list: replace UserDefaults hex lookup with ndb query
Replace the fragile `latestRelayListEventIdHex` UserDefaults lookup in `getLatestNIP65RelayListEvent()` with a direct nostrdb query using `ndb.query(filters:maxResults:)` on the AUTHOR_KINDS index. This eliminates the stale-hex failure mode where relay add/remove operations would silently fall back to bootstrap or year-old relay lists. Add an in-memory `lastSetRelayList` cache to bridge the nostrdb async write gap — `set()` populates it, `getUserCurrentRelayList()` checks it first, and `load()` clears it once ndb has committed. Remove `latestRelayListEventIdHex` from: Delegate protocol, DamusState, SaveKeysView, UserSettingsStore, and all test mocks. Includes 5 regression tests that fail before the fix and pass after: - testRemoveRelayDoesNotFallBackToBootstrapList - testCacheBridgesAsyncWriteGap - testRapidSequentialRemovesDoNotReintroduceRelays - testLoadClearsCacheAndReadsFromNdb - testNdbQueryFindsRelayListWithoutStoredHex Changelog-Fixed: Fix stale relay list causing inability to disconnect relays Closes: #3537 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: alltheseas <alltheseas@users.noreply.github.com>
This commit is contained in:
committed by
Daniel D’Aquino
co-authored by
Claude Opus 4.6
parent
537734397f
commit
2e10c0b4c5
@@ -304,10 +304,7 @@ extension NostrNetworkManager {
|
||||
|
||||
/// The keypair to use for relay authentication and updating relay lists
|
||||
var keypair: Keypair { get }
|
||||
|
||||
/// The latest relay list event id hex
|
||||
var latestRelayListEventIdHex: String? { get set } // TODO: Update this once we have full NostrDB query support
|
||||
|
||||
|
||||
/// The latest contact list `NostrEvent`
|
||||
///
|
||||
/// Note: Read-only access, because `NostrNetworkManager` does not manage contact lists.
|
||||
|
||||
@@ -18,9 +18,14 @@ extension NostrNetworkManager {
|
||||
private var delegate: Delegate
|
||||
private let pool: RelayPool
|
||||
private let reader: SubscriptionManager
|
||||
|
||||
|
||||
private var relayListObserverTask: Task<Void, Never>? = nil
|
||||
private var walletUpdatesObserverTask: AnyCancellable? = nil
|
||||
|
||||
/// In-memory cache of the most recently set relay list.
|
||||
/// Bridges the gap between sending an event to nostrdb (async write) and it being queryable.
|
||||
@MainActor
|
||||
private var lastSetRelayList: NIP65.RelayList?
|
||||
|
||||
init(delegate: Delegate, pool: RelayPool, reader: SubscriptionManager) {
|
||||
self.delegate = delegate
|
||||
@@ -60,9 +65,11 @@ extension NostrNetworkManager {
|
||||
|
||||
/// Gets the user's current relay list.
|
||||
///
|
||||
/// It attempts to get a NIP-65 relay list from the local database, or falls back to a legacy list.
|
||||
/// It attempts to get the in-memory cache first (to bridge the nostrdb async write gap),
|
||||
/// then a NIP-65 relay list from the local database, or falls back to a legacy list.
|
||||
@MainActor
|
||||
func getUserCurrentRelayList() -> NIP65.RelayList? {
|
||||
if let lastSetRelayList { return lastSetRelayList }
|
||||
if let latestRelayListEvent = try? self.getLatestNIP65RelayList() { return latestRelayListEvent }
|
||||
if let latestRelayListEvent = try? self.getLatestKind3RelayList() { return latestRelayListEvent }
|
||||
if let latestRelayListEvent = try? self.getLatestUserDefaultsRelayList() { return latestRelayListEvent }
|
||||
@@ -80,17 +87,18 @@ extension NostrNetworkManager {
|
||||
return list
|
||||
}
|
||||
|
||||
/// Gets the latest NIP-65 relay list event from NostrDB.
|
||||
///
|
||||
/// Gets the latest NIP-65 relay list event from NostrDB via query.
|
||||
///
|
||||
/// This is `private` because it is part of internal logic. Callers should use the higher level functions.
|
||||
///
|
||||
/// It is recommended to use this function only if the NostrEvent metadata is needed. For cases where only the relay list info is needed, use `getLatestNIP65RelayList` instead.
|
||||
///
|
||||
/// - Returns: The latest NIP-65 relay list NdbNote
|
||||
private func getLatestNIP65RelayListEvent() -> NdbNote? {
|
||||
guard let latestRelayListEventId = delegate.latestRelayListEventIdHex else { return nil }
|
||||
guard let latestRelayListEventId = NoteId(hex: latestRelayListEventId) else { return nil }
|
||||
return try? delegate.ndb.lookup_note_and_copy(latestRelayListEventId)
|
||||
let filter = NostrFilter(kinds: [.relay_list], limit: 1, authors: [delegate.keypair.pubkey])
|
||||
guard let ndbFilter = try? NdbFilter(from: filter) else { return nil }
|
||||
guard let noteKey = try? delegate.ndb.query(filters: [ndbFilter], maxResults: 1).first else { return nil }
|
||||
return try? delegate.ndb.lookup_note_by_key_and_copy(noteKey)
|
||||
}
|
||||
|
||||
/// Gets the latest `kind:3` relay list from NostrDB.
|
||||
@@ -176,17 +184,19 @@ extension NostrNetworkManager {
|
||||
func set(userRelayList: NIP65.RelayList) async throws(UpdateError) {
|
||||
guard let fullKeypair = delegate.keypair.to_full() else { throw .notAuthorizedToChangeRelayList }
|
||||
guard let relayListEvent = userRelayList.toNostrEvent(keypair: fullKeypair) else { throw .cannotFormRelayListEvent }
|
||||
|
||||
|
||||
await MainActor.run { self.lastSetRelayList = userRelayList }
|
||||
|
||||
await self.apply(newRelayList: self.computeRelaysToConnectTo(with: userRelayList))
|
||||
|
||||
|
||||
await self.pool.send(.event(relayListEvent)) // This will send to NostrDB as well, which will locally save that NIP-65 event
|
||||
self.delegate.latestRelayListEventIdHex = relayListEvent.id.hex() // Make sure we are able to recall this event from NostrDB
|
||||
}
|
||||
|
||||
// MARK: - Syncing our saved user relay list with the active `RelayPool`
|
||||
|
||||
/// Loads the current user relay list
|
||||
func load() async {
|
||||
await MainActor.run { self.lastSetRelayList = nil } // Clear cache; ndb has had time to commit by now
|
||||
await self.apply(newRelayList: self.relaysToConnectTo())
|
||||
}
|
||||
|
||||
|
||||
@@ -230,11 +230,6 @@ fileprivate extension DamusState {
|
||||
var ndb: Ndb
|
||||
var keypair: Keypair
|
||||
|
||||
var latestRelayListEventIdHex: String? {
|
||||
get { self.settings.latestRelayListEventIdHex }
|
||||
set { self.settings.latestRelayListEventIdHex = newValue }
|
||||
}
|
||||
|
||||
@MainActor
|
||||
var latestContactListEvent: NostrEvent? { self.contacts.event }
|
||||
var bootstrapRelays: [RelayURL] { get_default_bootstrap_relays() }
|
||||
|
||||
@@ -166,7 +166,6 @@ struct SaveKeysView: View {
|
||||
// Save the ID to user settings so that we can easily find it later.
|
||||
let settings = UserSettingsStore.globally_load_for(pubkey: account.pubkey)
|
||||
settings.latest_contact_event_id_hex = first_contact_event.id.hex()
|
||||
settings.latestRelayListEventIdHex = first_relay_list_event.id.hex()
|
||||
}
|
||||
|
||||
func handle_event(relay: RelayURL, ev: NostrConnectionEvent) async {
|
||||
|
||||
@@ -411,10 +411,6 @@ class UserSettingsStore: ObservableObject {
|
||||
@Setting(key: "draft_event_ids", default_value: nil)
|
||||
var draft_event_ids: [String]?
|
||||
|
||||
// TODO: Get rid of this once we have NostrDB query capabilities integrated
|
||||
@Setting(key: "latest_relay_list_event_id", default_value: nil)
|
||||
var latestRelayListEventIdHex: String?
|
||||
|
||||
// MARK: Helper types
|
||||
|
||||
enum NotificationsMode: String, CaseIterable, Identifiable, StringCodable, Equatable {
|
||||
|
||||
Reference in New Issue
Block a user