commit 05503024ccadd1e76be9be4cbeaad72a8a73f3d3
parent e4e477a2acbe7c790ff15ebca418159aa3ddbcfa
Author: William Casarin <jb55@jb55.com>
Date: Fri, 26 May 2023 12:11:43 -0700
Profile Caching
Changelog-Added: Add profile caching
Diffstat:
11 files changed, 454 insertions(+), 39 deletions(-)
diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj
@@ -267,6 +267,10 @@
50088DA129E8271A008A1FDF /* WebSocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50088DA029E8271A008A1FDF /* WebSocket.swift */; };
501F8C802A0220E1001AFC1D /* KeychainStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 501F8C7F2A0220E1001AFC1D /* KeychainStorage.swift */; };
501F8C822A0224EB001AFC1D /* KeychainStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 501F8C812A0224EB001AFC1D /* KeychainStorageTests.swift */; };
+ 5019CADD2A0FB0A9000069E1 /* ProfileDatabaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5019CADC2A0FB0A9000069E1 /* ProfileDatabaseTests.swift */; };
+ 501F8C5529FF5EF6001AFC1D /* PersistedProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 501F8C5429FF5EF6001AFC1D /* PersistedProfile.swift */; };
+ 501F8C5829FF5FC5001AFC1D /* Damus.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 501F8C5629FF5FC5001AFC1D /* Damus.xcdatamodeld */; };
+ 501F8C5A29FF70F5001AFC1D /* ProfileDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 501F8C5929FF70F5001AFC1D /* ProfileDatabase.swift */; };
50A50A8D29A09E1C00C01BE7 /* RequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50A50A8C29A09E1C00C01BE7 /* RequestTests.swift */; };
50B5685329F97CB400A23243 /* CredentialHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B5685229F97CB400A23243 /* CredentialHandler.swift */; };
50DA11262A16A23F00236234 /* Launch.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 50DA11252A16A23F00236234 /* Launch.storyboard */; };
@@ -699,6 +703,10 @@
50088DA029E8271A008A1FDF /* WebSocket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebSocket.swift; sourceTree = "<group>"; };
501F8C7F2A0220E1001AFC1D /* KeychainStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainStorage.swift; sourceTree = "<group>"; };
501F8C812A0224EB001AFC1D /* KeychainStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainStorageTests.swift; sourceTree = "<group>"; };
+ 5019CADC2A0FB0A9000069E1 /* ProfileDatabaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileDatabaseTests.swift; sourceTree = "<group>"; };
+ 501F8C5429FF5EF6001AFC1D /* PersistedProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistedProfile.swift; sourceTree = "<group>"; };
+ 501F8C5729FF5FC5001AFC1D /* Damus.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Damus.xcdatamodel; sourceTree = "<group>"; };
+ 501F8C5929FF70F5001AFC1D /* ProfileDatabase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileDatabase.swift; sourceTree = "<group>"; };
50A50A8C29A09E1C00C01BE7 /* RequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestTests.swift; sourceTree = "<group>"; };
50B5685229F97CB400A23243 /* CredentialHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CredentialHandler.swift; sourceTree = "<group>"; };
50DA11252A16A23F00236234 /* Launch.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Launch.storyboard; sourceTree = "<group>"; };
@@ -1006,6 +1014,7 @@
4C75EFAB28049CC80006080F /* Nostr */ = {
isa = PBXGroup;
children = (
+ 501F8C5329FF5EE2001AFC1D /* CoreData */,
4CE6DF1527F8DEBF00C66700 /* RelayConnection.swift */,
4C75EFA527FF87A20006080F /* Nostr.swift */,
4C75EFAE28049D340006080F /* NostrFilter.swift */,
@@ -1016,6 +1025,7 @@
4C75EFBA2804A34C0006080F /* ProofOfWork.swift */,
4CEE2AEC2805B22500AB5EEF /* NostrRequest.swift */,
4CACA9DB280C38C000D9BBE8 /* Profiles.swift */,
+ 501F8C5929FF70F5001AFC1D /* ProfileDatabase.swift */,
4C3BEFD32819DE8F00B3DE84 /* NostrKind.swift */,
4C363A8F28247A1D006E126D /* NostrLink.swift */,
50088DA029E8271A008A1FDF /* WebSocket.swift */,
@@ -1302,6 +1312,7 @@
4CB883AD2976FA9300DC99E7 /* FormatTests.swift */,
3A3040EC29A5CB86008A0F29 /* ReplyDescriptionTests.swift */,
3A3040EE29A8FEE9008A0F29 /* EventDetailBarTests.swift */,
+ 5019CADC2A0FB0A9000069E1 /* ProfileDatabaseTests.swift */,
3A3040F229A91366008A0F29 /* ProfileViewTests.swift */,
3A30410029AB12AA008A0F29 /* EventGroupViewTests.swift */,
4C8D00D329E3C5D40036AF10 /* NIP19Tests.swift */,
@@ -1393,6 +1404,15 @@
path = Images;
sourceTree = "<group>";
};
+ 501F8C5329FF5EE2001AFC1D /* CoreData */ = {
+ isa = PBXGroup;
+ children = (
+ 501F8C5429FF5EF6001AFC1D /* PersistedProfile.swift */,
+ 501F8C5629FF5FC5001AFC1D /* Damus.xcdatamodeld */,
+ );
+ path = CoreData;
+ sourceTree = "<group>";
+ };
7C0F392D29B57C8F0039859C /* Extensions */ = {
isa = PBXGroup;
children = (
@@ -1674,6 +1694,7 @@
4CD348EF29C3659D00497EB2 /* ImageUploadModel.swift in Sources */,
4C7D096E2A0AEA0400943473 /* ScannerCoordinator.swift in Sources */,
4C3BEFDC281DCE6100B3DE84 /* Liked.swift in Sources */,
+ 501F8C5A29FF70F5001AFC1D /* ProfileDatabase.swift in Sources */,
4CF0ABE7298444FD00D66079 /* MutedEventView.swift in Sources */,
9C83F89329A937B900136C08 /* TextViewWrapper.swift in Sources */,
4CF0ABE12981A83900D66079 /* MutelistView.swift in Sources */,
@@ -1769,6 +1790,7 @@
4C1A9A2729DDE31900516EAC /* TranslationSettingsView.swift in Sources */,
4CB8838629656C8B00DC99E7 /* NIP05.swift in Sources */,
4CF0ABD82981980C00D66079 /* Lists.swift in Sources */,
+ 501F8C5829FF5FC5001AFC1D /* Damus.xcdatamodeld in Sources */,
4C30AC8029A6A53F00E2BD5A /* ProfilePicturesView.swift in Sources */,
4C8EC52529D1FA6C0085D9A8 /* DamusColors.swift in Sources */,
4C5C7E6A284EDE2E00A22DF5 /* SearchResultsView.swift in Sources */,
@@ -1823,6 +1845,7 @@
F7908E92298B0F0700AB113A /* RelayDetailView.swift in Sources */,
4C3A1D332960DB0500558C0F /* Markdown.swift in Sources */,
4CE879552996BAB900F758CC /* RelayPaidDetail.swift in Sources */,
+ 501F8C5529FF5EF6001AFC1D /* PersistedProfile.swift in Sources */,
4CF0ABD42980996B00D66079 /* Report.swift in Sources */,
4C06670B28FDE64700038D2A /* damus.c in Sources */,
4C1A9A2529DDDF2600516EAC /* ZapSettingsView.swift in Sources */,
@@ -1856,6 +1879,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
+ 5019CADD2A0FB0A9000069E1 /* ProfileDatabaseTests.swift in Sources */,
3A3040ED29A5CB86008A0F29 /* ReplyDescriptionTests.swift in Sources */,
4C8D00D429E3C5D40036AF10 /* NIP19Tests.swift in Sources */,
3A30410129AB12AA008A0F29 /* EventGroupViewTests.swift in Sources */,
@@ -2375,6 +2399,19 @@
productName = secp256k1;
};
/* End XCSwiftPackageProductDependency section */
+
+/* Begin XCVersionGroup section */
+ 501F8C5629FF5FC5001AFC1D /* Damus.xcdatamodeld */ = {
+ isa = XCVersionGroup;
+ children = (
+ 501F8C5729FF5FC5001AFC1D /* Damus.xcdatamodel */,
+ );
+ currentVersion = 501F8C5729FF5FC5001AFC1D /* Damus.xcdatamodel */;
+ path = Damus.xcdatamodeld;
+ sourceTree = "<group>";
+ versionGroupType = wrapper.xcdatamodel;
+ };
+/* End XCVersionGroup section */
};
rootObject = 4CE6DEDB27F7A08100C66700 /* Project object */;
}
diff --git a/damus/Models/FollowersModel.swift b/damus/Models/FollowersModel.swift
@@ -57,7 +57,7 @@ class FollowersModel: ObservableObject {
func load_profiles(relay_id: String) {
var filter = NostrFilter.filter_profiles
- let authors = find_profiles_to_fetch_pk(profiles: damus_state.profiles, event_pubkeys: contacts ?? [])
+ let authors = find_profiles_to_fetch_from_keys(profiles: damus_state.profiles, pks: contacts ?? [])
if authors.isEmpty {
return
}
diff --git a/damus/Models/FollowingModel.swift b/damus/Models/FollowingModel.swift
@@ -24,7 +24,7 @@ class FollowingModel {
var f = NostrFilter.filter_kinds([NostrKind.metadata.rawValue])
f.authors = self.contacts.reduce(into: Array<String>()) { acc, pk in
// don't fetch profiles we already have
- if damus_state.profiles.lookup(id: pk) != nil {
+ if damus_state.profiles.has_fresh_profile(id: pk) {
return
}
acc.append(pk)
diff --git a/damus/Models/SearchHomeModel.swift b/damus/Models/SearchHomeModel.swift
@@ -90,20 +90,6 @@ class SearchHomeModel: ObservableObject {
}
}
-func find_profiles_to_fetch_pk(profiles: Profiles, event_pubkeys: [String]) -> [String] {
- var pubkeys = Set<String>()
-
- for pk in event_pubkeys {
- if profiles.lookup(id: pk) != nil {
- continue
- }
-
- pubkeys.insert(pk)
- }
-
- return Array(pubkeys)
-}
-
func find_profiles_to_fetch(profiles: Profiles, load: PubkeysToLoad, cache: EventCache) -> [String] {
switch load {
case .from_events(let events):
@@ -114,17 +100,7 @@ func find_profiles_to_fetch(profiles: Profiles, load: PubkeysToLoad, cache: Even
}
func find_profiles_to_fetch_from_keys(profiles: Profiles, pks: [String]) -> [String] {
- var pubkeys = Set<String>()
-
- for pk in pks {
- if profiles.lookup(id: pk) != nil {
- continue
- }
-
- pubkeys.insert(pk)
- }
-
- return Array(pubkeys)
+ Array(Set(pks.filter { pk in !profiles.has_fresh_profile(id: pk) }))
}
func find_profiles_to_fetch_from_events(profiles: Profiles, events: [NostrEvent], cache: EventCache) -> [String] {
@@ -132,11 +108,11 @@ func find_profiles_to_fetch_from_events(profiles: Profiles, events: [NostrEvent]
for ev in events {
// lookup profiles from boosted events
- if ev.known_kind == .boost, let bev = ev.get_inner_event(cache: cache), profiles.lookup(id: bev.pubkey) == nil {
+ if ev.known_kind == .boost, let bev = ev.get_inner_event(cache: cache), !profiles.has_fresh_profile(id: bev.pubkey) {
pubkeys.insert(bev.pubkey)
}
- if profiles.lookup(id: ev.pubkey) == nil {
+ if !profiles.has_fresh_profile(id: ev.pubkey) {
pubkeys.insert(ev.pubkey)
}
}
diff --git a/damus/Nostr/CoreData/Damus.xcdatamodeld/Damus.xcdatamodel/contents b/damus/Nostr/CoreData/Damus.xcdatamodeld/Damus.xcdatamodel/contents
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21754" systemVersion="22E261" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
+ <entity name="PersistedProfile" representedClassName="PersistedProfile" syncable="YES">
+ <attribute name="about" optional="YES" attributeType="String"/>
+ <attribute name="banner" optional="YES" attributeType="String"/>
+ <attribute name="damus_donation" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
+ <attribute name="display_name" optional="YES" attributeType="String"/>
+ <attribute name="id" optional="YES" attributeType="String"/>
+ <attribute name="last_update" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
+ <attribute name="lud06" optional="YES" attributeType="String"/>
+ <attribute name="lud16" optional="YES" attributeType="String"/>
+ <attribute name="name" optional="YES" attributeType="String"/>
+ <attribute name="network_pull_date" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
+ <attribute name="nip05" optional="YES" attributeType="String"/>
+ <attribute name="picture" optional="YES" attributeType="String"/>
+ <attribute name="website" optional="YES" attributeType="String"/>
+ </entity>
+</model>+
\ No newline at end of file
diff --git a/damus/Nostr/CoreData/PersistedProfile.swift b/damus/Nostr/CoreData/PersistedProfile.swift
@@ -0,0 +1,39 @@
+//
+// PersistedProfile.swift
+// damus
+//
+// Created by Bryan Montz on 4/30/23.
+//
+
+import Foundation
+import CoreData
+
+@objc(PersistedProfile)
+final class PersistedProfile: NSManagedObject {
+ @NSManaged var id: String?
+ @NSManaged var name: String?
+ @NSManaged var display_name: String?
+ @NSManaged var about: String?
+ @NSManaged var picture: String?
+ @NSManaged var banner: String?
+ @NSManaged var website: String?
+ @NSManaged var lud06: String?
+ @NSManaged var lud16: String?
+ @NSManaged var nip05: String?
+ @NSManaged var damus_donation: Int16
+ @NSManaged var last_update: Date? // The date that the profile was last updated by the user
+ @NSManaged var network_pull_date: Date? // The date we got this profile from a relay (for staleness checking)
+
+ func copyValues(from profile: Profile) {
+ name = profile.name
+ display_name = profile.display_name
+ about = profile.about
+ picture = profile.picture
+ banner = profile.banner
+ website = profile.website
+ lud06 = profile.lud06
+ lud16 = profile.lud16
+ nip05 = profile.nip05
+ damus_donation = profile.damus_donation != nil ? Int16(profile.damus_donation!) : 0
+ }
+}
diff --git a/damus/Nostr/Nostr.swift b/damus/Nostr/Nostr.swift
@@ -24,6 +24,19 @@ class Profile: Codable {
self.damus_donation = damus_donation
}
+ convenience init(persisted_profile: PersistedProfile) {
+ self.init(name: persisted_profile.name,
+ display_name: persisted_profile.display_name,
+ about: persisted_profile.about,
+ picture: persisted_profile.picture,
+ banner: persisted_profile.banner,
+ website: persisted_profile.website,
+ lud06: persisted_profile.lud06,
+ lud16: persisted_profile.lud16,
+ nip05: persisted_profile.nip05,
+ damus_donation: Int(persisted_profile.damus_donation))
+ }
+
private func str(_ str: String) -> String? {
return get_val(str)
}
diff --git a/damus/Nostr/ProfileDatabase.swift b/damus/Nostr/ProfileDatabase.swift
@@ -0,0 +1,181 @@
+//
+// ProfileDatabase.swift
+// damus
+//
+// Created by Bryan Montz on 4/30/23.
+//
+
+import Foundation
+import CoreData
+
+enum ProfileDatabaseError: Error {
+ case missing_context
+ case outdated_input
+}
+
+final class ProfileDatabase {
+
+ private let entity_name = "PersistedProfile"
+ private var persistent_container: NSPersistentContainer?
+ private var background_context: NSManagedObjectContext?
+ private let cache_url: URL
+
+ /// This queue is used to synchronize access to the network_pull_date_cache dictionary, which
+ /// prevents data races from crashing the app.
+ private var queue = DispatchQueue(label: "io.damus.profile_db",
+ qos: .userInteractive,
+ attributes: .concurrent)
+ private var network_pull_date_cache = [String: Date]()
+
+ init(cache_url: URL = ProfileDatabase.profile_cache_url) {
+ self.cache_url = cache_url
+ set_up()
+ }
+
+ private static var profile_cache_url: URL {
+ (FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first?.appendingPathComponent("profiles"))!
+ }
+
+ private var persistent_store_description: NSPersistentStoreDescription {
+ let description = NSPersistentStoreDescription(url: cache_url)
+ description.type = NSSQLiteStoreType
+ description.setOption(true as NSNumber, forKey: NSMigratePersistentStoresAutomaticallyOption)
+ description.setOption(true as NSNumber, forKey: NSInferMappingModelAutomaticallyOption)
+ description.setOption(true as NSNumber, forKey: NSSQLiteManualVacuumOption)
+ return description
+ }
+
+ private var object_model: NSManagedObjectModel? {
+ guard let url = Bundle.main.url(forResource: "Damus", withExtension: "momd") else {
+ return nil
+ }
+ return NSManagedObjectModel(contentsOf: url)
+ }
+
+ private func set_up() {
+ guard let object_model else {
+ print("⚠️ Warning: ProfileDatabase failed to load its object model")
+ return
+ }
+
+ persistent_container = NSPersistentContainer(name: "Damus", managedObjectModel: object_model)
+ persistent_container?.persistentStoreDescriptions = [persistent_store_description]
+ persistent_container?.loadPersistentStores { _, error in
+ if let error {
+ print("WARNING: ProfileDatabase failed to load: \(error)")
+ }
+ }
+
+ persistent_container?.viewContext.automaticallyMergesChangesFromParent = true
+ persistent_container?.viewContext.mergePolicy = NSMergePolicy(merge: .mergeByPropertyObjectTrumpMergePolicyType)
+
+ background_context = persistent_container?.newBackgroundContext()
+ background_context?.mergePolicy = NSMergePolicy(merge: .mergeByPropertyObjectTrumpMergePolicyType)
+ }
+
+ private func get_persisted(id: String, context: NSManagedObjectContext) -> PersistedProfile? {
+ let request = NSFetchRequest<PersistedProfile>(entityName: entity_name)
+ request.predicate = NSPredicate(format: "id == %@", id)
+ request.fetchLimit = 1
+ return try? context.fetch(request).first
+ }
+
+ func get_network_pull_date(id: String) -> Date? {
+ var pull_date: Date?
+ queue.sync {
+ pull_date = network_pull_date_cache[id]
+ }
+ if let pull_date {
+ return pull_date
+ }
+
+ let request = NSFetchRequest<PersistedProfile>(entityName: entity_name)
+ request.predicate = NSPredicate(format: "id == %@", id)
+ request.fetchLimit = 1
+ request.propertiesToFetch = ["network_pull_date"]
+ guard let profile = try? persistent_container?.viewContext.fetch(request).first else {
+ return nil
+ }
+
+ queue.async(flags: .barrier) {
+ self.network_pull_date_cache[id] = profile.network_pull_date
+ }
+ return profile.network_pull_date
+ }
+
+ // MARK: - Public
+
+ /// Updates or inserts a new Profile into the local database. Rejects profiles whose update date
+ /// is older than one we already have. Database writes occur on a background context for best performance.
+ /// - Parameters:
+ /// - id: Profile id (pubkey)
+ /// - profile: Profile object to be stored
+ /// - last_update: Date that the Profile was updated
+ func upsert(id: String, profile: Profile, last_update: Date) async throws {
+ guard let context = background_context else {
+ throw ProfileDatabaseError.missing_context
+ }
+
+ try await context.perform {
+ var persisted_profile: PersistedProfile?
+ if let profile = self.get_persisted(id: id, context: context) {
+ if let existing_last_update = profile.last_update, last_update < existing_last_update {
+ throw ProfileDatabaseError.outdated_input
+ } else {
+ persisted_profile = profile
+ }
+ } else {
+ persisted_profile = NSEntityDescription.insertNewObject(forEntityName: self.entity_name, into: context) as? PersistedProfile
+ persisted_profile?.id = id
+ }
+ persisted_profile?.copyValues(from: profile)
+ persisted_profile?.last_update = last_update
+
+ let pull_date = Date.now
+ persisted_profile?.network_pull_date = pull_date
+ self.queue.async(flags: .barrier) {
+ self.network_pull_date_cache[id] = pull_date
+ }
+
+ try context.save()
+ }
+ }
+
+ func get(id: String) -> Profile? {
+ guard let container = persistent_container,
+ let profile = get_persisted(id: id, context: container.viewContext) else {
+ return nil
+ }
+ return Profile(persisted_profile: profile)
+ }
+
+ var count: Int {
+ let request = NSFetchRequest<PersistedProfile>(entityName: entity_name)
+ let count = try? persistent_container?.viewContext.count(for: request)
+ return count ?? 0
+ }
+
+ func remove_all_profiles() throws {
+ guard let context = background_context, let container = persistent_container else {
+ throw ProfileDatabaseError.missing_context
+ }
+
+ queue.async(flags: .barrier) {
+ self.network_pull_date_cache.removeAll()
+ }
+
+ let request = NSFetchRequest<NSFetchRequestResult>(entityName: entity_name)
+ let batch_delete_request = NSBatchDeleteRequest(fetchRequest: request)
+ batch_delete_request.resultType = .resultTypeObjectIDs
+
+ let result = try container.persistentStoreCoordinator.execute(batch_delete_request, with: context) as! NSBatchDeleteResult
+
+ // NSBatchDeleteRequest is an NSPersistentStoreRequest, which operates on disk. So now we'll manually update our in-memory context.
+ if let object_ids = result.result as? [NSManagedObjectID] {
+ let changes: [AnyHashable: Any] = [
+ NSDeletedObjectsKey: object_ids
+ ]
+ NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [context])
+ }
+ }
+}
diff --git a/damus/Nostr/Profiles.swift b/damus/Nostr/Profiles.swift
@@ -6,11 +6,11 @@
//
import Foundation
-import UIKit
-
class Profiles {
+ static let db_freshness_threshold: TimeInterval = 24 * 60 * 60
+
/// 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",
@@ -22,8 +22,10 @@ class Profiles {
var nip05_pubkey: [String: String] = [:]
var zappers: [String: String] = [:]
+ private let database = ProfileDatabase()
+
func is_validated(_ pk: String) -> NIP05? {
- return validated[pk]
+ validated[pk]
}
func enumerated() -> EnumeratedSequence<[String: TimestampedProfile]> {
@@ -33,23 +35,29 @@ class Profiles {
}
func lookup_zapper(pubkey: String) -> String? {
- if let zapper = zappers[pubkey] {
- return zapper
- }
-
- return nil
+ zappers[pubkey]
}
func add(id: String, profile: TimestampedProfile) {
queue.async(flags: .barrier) {
self.profiles[id] = profile
}
+
+ Task {
+ do {
+ try await database.upsert(id: id, profile: profile.profile, last_update: Date(timeIntervalSince1970: TimeInterval(profile.timestamp)))
+ } catch {
+ print("⚠️ Warning: Profiles failed to save a profile: \(error)")
+ }
+ }
}
func lookup(id: String) -> Profile? {
+ var profile: Profile?
queue.sync {
- return profiles[id]?.profile
+ profile = profiles[id]?.profile
}
+ return profile ?? database.get(id: id)
}
func lookup_with_timestamp(id: String) -> TimestampedProfile? {
@@ -57,6 +65,23 @@ class Profiles {
return profiles[id]
}
}
+
+ func has_fresh_profile(id: String) -> Bool {
+ // check memory first
+ var profile: Profile?
+ queue.sync {
+ profile = profiles[id]?.profile
+ }
+ if profile != nil {
+ return true
+ }
+
+ // then disk
+ guard let pull_date = database.get_network_pull_date(id: id) else {
+ return false
+ }
+ return Date.now.timeIntervalSince(pull_date) < Profiles.db_freshness_threshold
+ }
}
diff --git a/damusTests/ProfileDatabaseTests.swift b/damusTests/ProfileDatabaseTests.swift
@@ -0,0 +1,124 @@
+//
+// ProfileDatabaseTests.swift
+// damusTests
+//
+// Created by Bryan Montz on 5/13/23.
+//
+
+import XCTest
+@testable import damus
+
+class ProfileDatabaseTests: XCTestCase {
+
+ static let cache_url = (FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first?.appendingPathComponent("test-profiles"))!
+ let database = ProfileDatabase(cache_url: ProfileDatabaseTests.cache_url)
+
+ override func tearDownWithError() throws {
+ // This method is called after the invocation of each test method in the class.
+ try database.remove_all_profiles()
+ }
+
+ var test_profile: Profile {
+ Profile(name: "test-name",
+ display_name: "test-display-name",
+ about: "test-about",
+ picture: "test-picture",
+ banner: "test-banner",
+ website: "test-website",
+ lud06: "test-lud06",
+ lud16: "test-lud16",
+ nip05: "test-nip05",
+ damus_donation: 100)
+ }
+
+ func testStoreAndRetrieveProfile() async throws {
+ let id = "test-id"
+
+ let profile = test_profile
+
+ // make sure it's not there yet
+ XCTAssertNil(database.get(id: id))
+
+ // store the profile
+ try await database.upsert(id: id, profile: profile, last_update: .now)
+
+ // read the profile out of the database
+ let retrievedProfile = try XCTUnwrap(database.get(id: id))
+
+ XCTAssertEqual(profile.name, retrievedProfile.name)
+ XCTAssertEqual(profile.display_name, retrievedProfile.display_name)
+ XCTAssertEqual(profile.about, retrievedProfile.about)
+ XCTAssertEqual(profile.picture, retrievedProfile.picture)
+ XCTAssertEqual(profile.banner, retrievedProfile.banner)
+ XCTAssertEqual(profile.website, retrievedProfile.website)
+ XCTAssertEqual(profile.lud06, retrievedProfile.lud06)
+ XCTAssertEqual(profile.lud16, retrievedProfile.lud16)
+ XCTAssertEqual(profile.nip05, retrievedProfile.nip05)
+ XCTAssertEqual(profile.damus_donation, retrievedProfile.damus_donation)
+ }
+
+ func testRejectOutdatedProfile() async throws {
+ let id = "test-id"
+
+ // store a profile
+ let profile = test_profile
+ let profile_last_updated = Date.now
+ try await database.upsert(id: id, profile: profile, last_update: profile_last_updated)
+
+ // try to store a profile with the same id but the last_update date is older than the previously stored profile
+ let outdatedProfile = test_profile
+ let outdated_last_updated = profile_last_updated.addingTimeInterval(-60)
+
+ do {
+ try await database.upsert(id: id, profile: outdatedProfile, last_update: outdated_last_updated)
+ XCTFail("expected to throw error")
+ } catch let error as ProfileDatabaseError {
+ XCTAssertEqual(error, ProfileDatabaseError.outdated_input)
+ } catch {
+ XCTFail("not the expected error")
+ }
+ }
+
+ func testUpdateExistingProfile() async throws {
+ let id = "test-id"
+
+ // store a profile
+ let profile = test_profile
+ let profile_last_update = Date.now
+ try await database.upsert(id: id, profile: profile, last_update: profile_last_update)
+
+ // update the same profile
+ let updated_profile = test_profile
+ updated_profile.nip05 = "updated-nip05"
+ let updated_profile_last_update = profile_last_update.addingTimeInterval(60)
+ try await database.upsert(id: id, profile: updated_profile, last_update: updated_profile_last_update)
+
+ // retrieve the profile and make sure it was updated
+ let retrieved_profile = database.get(id: id)
+ XCTAssertEqual(retrieved_profile?.nip05, "updated-nip05")
+ }
+
+ func testStoreMultipleAndRemoveAllProfiles() async throws {
+ XCTAssertEqual(database.count, 0)
+
+ // store a profile
+ let id = "test-id"
+ let profile = test_profile
+ let profile_last_update = Date.now
+ try await database.upsert(id: id, profile: profile, last_update: profile_last_update)
+
+ XCTAssertEqual(database.count, 1)
+
+ // store another profile
+ let id2 = "test-id-2"
+ let profile2 = test_profile
+ let profile_last_update2 = Date.now
+ try await database.upsert(id: id2, profile: profile2, last_update: profile_last_update2)
+
+ XCTAssertEqual(database.count, 2)
+
+ try database.remove_all_profiles()
+
+ XCTAssertEqual(database.count, 0)
+ }
+}
diff --git a/damusTests/ZapTests.swift b/damusTests/ZapTests.swift
@@ -11,7 +11,8 @@ import XCTest
final class ZapTests: XCTestCase {
override func setUpWithError() throws {
- // Put setup code here. This method is called before the invocation of each test method in the class.
+ let db = ProfileDatabase()
+ try db.remove_all_profiles()
}
override func tearDownWithError() throws {