add ProfileDatabase class to read and write profiles to disk
This commit is contained in:
@@ -261,6 +261,7 @@
|
||||
501F8C822A0224EB001AFC1D /* KeychainStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 501F8C812A0224EB001AFC1D /* KeychainStorageTests.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 */; };
|
||||
5C42E78C29DB76D90086AAC1 /* EmptyUserSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C42E78B29DB76D90086AAC1 /* EmptyUserSearchView.swift */; };
|
||||
@@ -686,6 +687,7 @@
|
||||
501F8C812A0224EB001AFC1D /* KeychainStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainStorageTests.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>"; };
|
||||
5C42E78B29DB76D90086AAC1 /* EmptyUserSearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyUserSearchView.swift; sourceTree = "<group>"; };
|
||||
@@ -1000,6 +1002,7 @@
|
||||
4C75EFBA2804A34C0006080F /* ProofOfWork.swift */,
|
||||
4CEE2AEC2805B22500AB5EEF /* NostrRequest.swift */,
|
||||
4CACA9DB280C38C000D9BBE8 /* Profiles.swift */,
|
||||
501F8C5929FF70F5001AFC1D /* ProfileDatabase.swift */,
|
||||
4C3BEFD32819DE8F00B3DE84 /* NostrKind.swift */,
|
||||
4C363A8F28247A1D006E126D /* NostrLink.swift */,
|
||||
50088DA029E8271A008A1FDF /* WebSocket.swift */,
|
||||
@@ -1651,6 +1654,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 */,
|
||||
|
||||
110
damus/Nostr/ProfileDatabase.swift
Normal file
110
damus/Nostr/ProfileDatabase.swift
Normal file
@@ -0,0 +1,110 @@
|
||||
//
|
||||
// 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?
|
||||
|
||||
init() {
|
||||
set_up()
|
||||
}
|
||||
|
||||
private 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: profile_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) -> PersistedProfile? {
|
||||
let request = NSFetchRequest<PersistedProfile>(entityName: entity_name)
|
||||
request.predicate = NSPredicate(format: "id == %@", id)
|
||||
request.fetchLimit = 1
|
||||
return try? persistent_container?.viewContext.fetch(request).first
|
||||
}
|
||||
|
||||
// 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) throws {
|
||||
guard let context = background_context else {
|
||||
throw ProfileDatabaseError.missing_context
|
||||
}
|
||||
|
||||
var persisted_profile: PersistedProfile?
|
||||
if let profile = get_persisted(id: id) {
|
||||
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: entity_name, into: context) as? PersistedProfile
|
||||
persisted_profile?.id = id
|
||||
}
|
||||
persisted_profile?.copyValues(from: profile)
|
||||
persisted_profile?.last_update = last_update
|
||||
|
||||
try context.save()
|
||||
}
|
||||
|
||||
func get(id: String) -> Profile? {
|
||||
guard let profile = get_persisted(id: id) else {
|
||||
return nil
|
||||
}
|
||||
return Profile(persisted_profile: profile)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user