damus

nostr ios client
git clone git://jb55.com/damus
Log | Files | Refs | README | LICENSE

commit 640fbf23eab2d5522b2676743db606046cff8ad7
parent 3d0448a92912418ca788f565bf29907c1d8639f0
Author: Terry Yiu <git@tyiu.xyz>
Date:   Mon,  3 Jul 2023 21:36:22 -0400

Fix UI bug with user search and fix race conditions on profiles NIP-05 cache

Signed-off-by: Terry Yiu <git@tyiu.xyz>
Reviewed-by: William Casarin <jb55@jb55.com>
Signed-off-by: William Casarin <jb55@jb55.com>

Diffstat:
Mdamus/Models/HomeModel.swift | 2+-
Mdamus/Models/Trie.swift | 4++--
Mdamus/Nostr/Profiles.swift | 28++++++++++++++++++++--------
Mdamus/Views/SearchResultsView.swift | 5++---
MdamusTests/UserSearchCacheTests.swift | 7+++++--
5 files changed, 30 insertions(+), 16 deletions(-)

diff --git a/damus/Models/HomeModel.swift b/damus/Models/HomeModel.swift @@ -726,7 +726,7 @@ func process_metadata_profile(our_pubkey: String, profiles: Profiles, profile: P } Task { @MainActor in - profiles.validated[ev.pubkey] = validated + profiles.set_validated(ev.pubkey, nip05: validated) profiles.nip05_pubkey[nip05] = ev.pubkey notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile)) } diff --git a/damus/Models/Trie.swift b/damus/Models/Trie.swift @@ -50,7 +50,6 @@ extension Trie { } // Perform breadth-first search from matching branch and collect values from all descendants. - let exactMatches = Array(currentNode.exactMatchValues) var substringMatches = Set<V>(currentNode.substringMatchValues) var queue = Array(currentNode.children.values) @@ -61,7 +60,8 @@ extension Trie { queue.append(contentsOf: node.children.values) } - return exactMatches + substringMatches + // Prioritize exact matches to be returned first, and then remove exact matches from the set of partial substring matches that are appended afterward. + return Array(currentNode.exactMatchValues) + (substringMatches.subtracting(currentNode.exactMatchValues)) } /// Inserts value of type V into this trie for the specified key. This function stores all substring endings of the key, not only the key itself. diff --git a/damus/Nostr/Profiles.swift b/damus/Nostr/Profiles.swift @@ -13,12 +13,16 @@ class Profiles { /// This queue is used to synchronize access to the profiles dictionary, which /// prevents data races from crashing the app. - private var queue = DispatchQueue(label: "io.damus.profiles", + private var profiles_queue = DispatchQueue(label: "io.damus.profiles", qos: .userInteractive, attributes: .concurrent) + + private var validated_queue = DispatchQueue(label: "io.damus.profiles.validated", + qos: .userInteractive, + attributes: .concurrent) private var profiles: [String: TimestampedProfile] = [:] - var validated: [String: NIP05] = [:] + private var validated: [String: NIP05] = [:] var nip05_pubkey: [String: String] = [:] var zappers: [String: String] = [:] @@ -31,11 +35,19 @@ class Profiles { } func is_validated(_ pk: String) -> NIP05? { - validated[pk] + validated_queue.sync { + validated[pk] + } + } + + func set_validated(_ pk: String, nip05: NIP05?) { + validated_queue.async(flags: .barrier) { + self.validated[pk] = nip05 + } } func enumerated() -> EnumeratedSequence<[String: TimestampedProfile]> { - return queue.sync { + return profiles_queue.sync { return profiles.enumerated() } } @@ -45,7 +57,7 @@ class Profiles { } func add(id: String, profile: TimestampedProfile) { - queue.async(flags: .barrier) { + profiles_queue.async(flags: .barrier) { let old_timestamped_profile = self.profiles[id] self.profiles[id] = profile self.user_search_cache.updateProfile(id: id, profiles: self, oldProfile: old_timestamped_profile?.profile, newProfile: profile.profile) @@ -62,14 +74,14 @@ class Profiles { func lookup(id: String) -> Profile? { var profile: Profile? - queue.sync { + profiles_queue.sync { profile = profiles[id]?.profile } return profile ?? database.get(id: id) } func lookup_with_timestamp(id: String) -> TimestampedProfile? { - queue.sync { + profiles_queue.sync { return profiles[id] } } @@ -77,7 +89,7 @@ class Profiles { func has_fresh_profile(id: String) -> Bool { // check memory first var profile: Profile? - queue.sync { + profiles_queue.sync { profile = profiles[id]?.profile } if profile != nil { diff --git a/damus/Views/SearchResultsView.swift b/damus/Views/SearchResultsView.swift @@ -194,7 +194,6 @@ func search_profiles(profiles: Profiles, search: String) -> [SearchedUser] { let matched_pubkeys = profiles.user_search_cache.search(key: new) return matched_pubkeys - .map { ($0, profiles.lookup(id: $0)) } - .filter { $1 != nil } - .map { SearchedUser(profile: $1, pubkey: $0) } + .map { SearchedUser(profile: profiles.lookup(id: $0), pubkey: $0) } + .filter { $0.profile != nil } } diff --git a/damusTests/UserSearchCacheTests.swift b/damusTests/UserSearchCacheTests.swift @@ -19,8 +19,9 @@ final class UserSearchCacheTests: XCTestCase { if let keypair { let pubkey = keypair.pubkey + let validatedNip05 = try XCTUnwrap(NIP05.parse(nip05)) - damusState.profiles.validated[pubkey] = NIP05.parse(nip05) + damusState.profiles.set_validated(pubkey, nip05: validatedNip05) let profile = Profile(name: "tyiu", display_name: "Terry Yiu", about: nil, picture: nil, banner: nil, website: nil, lud06: nil, lud16: nil, nip05: nip05, damus_donation: nil) let timestampedProfile = TimestampedProfile(profile: profile, timestamp: 0, event: test_event) @@ -50,7 +51,9 @@ final class UserSearchCacheTests: XCTestCase { let keypair = try XCTUnwrap(keypair) let newNip05 = "_@other.xyz" - damusState.profiles.validated[keypair.pubkey] = NIP05.parse(newNip05) + let validatedNewNip05 = try XCTUnwrap(NIP05.parse(newNip05)) + + damusState.profiles.set_validated(keypair.pubkey, nip05: NIP05.parse(newNip05)) let newProfile = Profile(name: "whoami", display_name: "T-DAWG", about: nil, picture: nil, banner: nil, website: nil, lud06: nil, lud16: nil, nip05: newNip05, damus_donation: nil) let newTimestampedProfile = TimestampedProfile(profile: newProfile, timestamp: 1000, event: test_event)