From 83ad516a910a44f64690c5c4aca0a2b4362f02a5 Mon Sep 17 00:00:00 2001 From: Bryan Montz Date: Sun, 8 Sep 2024 07:53:52 -0500 Subject: [PATCH 1/2] fixes and improvements related to Core Data usage #1443 --- CHANGELOG.md | 1 + Nos/Controller/PersistenceController.swift | 97 +++++++------------ Nos/Controller/SearchController.swift | 6 +- Nos/Models/CoreData/Event+CoreDataClass.swift | 38 ++++---- Nos/NosApp.swift | 2 +- Nos/Service/EventProcessor.swift | 4 +- Nos/Service/Relay/RelayService.swift | 28 +++--- Nos/Views/AppView.swift | 2 +- .../Components/Button/NoteOptionsButton.swift | 2 +- Nos/Views/Components/GoldenPostView.swift | 2 +- Nos/Views/Components/ThreadView.swift | 4 +- Nos/Views/Discover/DiscoverTab.swift | 2 +- Nos/Views/Fixtures/PreviewData.swift | 2 +- Nos/Views/Note/NoteView.swift | 2 +- Nos/Views/Profile/ProfileView.swift | 2 +- Nos/Views/Relay/RelayDetailView.swift | 2 +- Nos/Views/Relay/RelayView.swift | 4 +- .../TestHelpers/SQLiteStoreTestCase.swift | 20 ++++ NosTests/Views/EventObservationTests.swift | 2 +- 19 files changed, 111 insertions(+), 111 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a1aed678f..0fd0881c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed a bug where nostr entities in URLs were treated like quoted note links. - Added in-app profile photo editing. - Changed "Name" to "Display Name" on the Edit Profile View. +- Fixes and improvements related to Core Data usage. ### Internal Changes - Included the npub in the properties list sent to analytics. diff --git a/Nos/Controller/PersistenceController.swift b/Nos/Controller/PersistenceController.swift index 62e17487c..bd7027d2c 100644 --- a/Nos/Controller/PersistenceController.swift +++ b/Nos/Controller/PersistenceController.swift @@ -2,24 +2,24 @@ import CoreData import Logger import Dependencies -class PersistenceController { +final class PersistenceController { @Dependency(\.currentUser) var currentUser @Dependency(\.crashReporting) var crashReporting /// Increment this to delete core data on update - static let version = 3 - static let versionKey = "NosPersistenceControllerVersion" + private static let version = 3 + private static let versionKey = "NosPersistenceControllerVersion" static var preview: PersistenceController = { let controller = PersistenceController(inMemory: true) - let viewContext = controller.container.viewContext + let viewContext = controller.viewContext return controller }() static var empty: PersistenceController = { let result = PersistenceController(inMemory: true) - let viewContext = result.container.viewContext + let viewContext = result.viewContext return result }() @@ -28,22 +28,18 @@ class PersistenceController { } /// A context for parsing Nostr events from relays. - lazy var parseContext = { - newBackgroundContext() - }() + private(set) lazy var parseContext = newBackgroundContext() /// A context for Views to do expensive queries that we want to keep off the viewContext. - lazy var backgroundViewContext = { - self.newBackgroundContext() - }() + private(set) lazy var backgroundViewContext = newBackgroundContext() var sqliteURL: URL? { container.persistentStoreDescriptions.first?.url } private(set) var container: NSPersistentContainer - private var model: NSManagedObjectModel - private var inMemory: Bool + private let model: NSManagedObjectModel + private let inMemory: Bool init(containerName: String = "Nos", inMemory: Bool = false, erase: Bool = false) { self.inMemory = inMemory @@ -53,57 +49,19 @@ class PersistenceController { setUp(erasingPrevious: erase) } - func tearDown() throws { - for store in container.persistentStoreCoordinator.persistentStores { - try container.persistentStoreCoordinator.remove(store) - } - - try container.persistentStoreDescriptions.forEach { storeDescription in - try container.persistentStoreCoordinator.destroyPersistentStore( - at: storeDescription.url!, - ofType: NSSQLiteStoreType, - options: nil - ) - } - - viewContext.reset() - backgroundViewContext.reset() - parseContext.reset() - } - - func setUp(erasingPrevious: Bool) { + private func setUp(erasingPrevious: Bool) { if inMemory { container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null") } loadPersistentStores(from: container, erasingPrevious: erasingPrevious) - container.viewContext.automaticallyMergesChangesFromParent = true - let mergeType = NSMergePolicyType.mergeByPropertyStoreTrumpMergePolicyType - container.viewContext.mergePolicy = NSMergePolicy(merge: mergeType) + viewContext.automaticallyMergesChangesFromParent = true + viewContext.mergePolicy = NSMergePolicy.mergeByPropertyStoreTrump } - #if DEBUG - func resetForTesting() { - container = NSPersistentContainer(name: "Nos", managedObjectModel: model) - if !inMemory { - container.loadPersistentStores(completionHandler: { (storeDescription, _) in - guard let storeURL = storeDescription.url else { - Log.error("Could not get store URL") - return - } - Self.clearCoreData(store: storeURL, in: self.container) - }) - } - setUp(erasingPrevious: true) - viewContext.reset() - backgroundViewContext.reset() - parseContext.reset() - } - #endif - private func loadPersistentStores(from container: NSPersistentContainer, erasingPrevious: Bool) { - container.loadPersistentStores(completionHandler: { (storeDescription, error) in + container.loadPersistentStores { storeDescription, error in // Drop database if necessary if Self.loadVersionFromDisk() < Self.version || erasingPrevious { @@ -124,7 +82,7 @@ class PersistenceController { } fatalError("Could not initialize database \(error), \(error.userInfo)") } - }) + } } @MainActor @@ -135,7 +93,7 @@ class PersistenceController { } } - static func clearCoreData(store storeURL: URL, in container: NSPersistentContainer) { + private static func clearCoreData(store storeURL: URL, in container: NSPersistentContainer) { Log.info("Dropping Core Data...") do { try container.persistentStoreCoordinator.destroyPersistentStore(at: storeURL, type: .sqlite) @@ -144,6 +102,7 @@ class PersistenceController { } } +#if DEBUG func loadSampleData(context: NSManagedObjectContext) async throws { guard let sampleFile = Bundle.current.url(forResource: "sample_data", withExtension: "json") else { Log.error("Error: bad sample file location") @@ -178,6 +137,24 @@ class PersistenceController { } } + func resetForTesting() { + container = NSPersistentContainer(name: "Nos", managedObjectModel: model) + if !inMemory { + container.loadPersistentStores(completionHandler: { (storeDescription, _) in + guard let storeURL = storeDescription.url else { + Log.error("Could not get store URL") + return + } + Self.clearCoreData(store: storeURL, in: self.container) + }) + } + setUp(erasingPrevious: true) + viewContext.reset() + backgroundViewContext.reset() + parseContext.reset() + } +#endif + func newBackgroundContext() -> NSManagedObjectContext { let context = container.newBackgroundContext() context.automaticallyMergesChangesFromParent = true @@ -185,15 +162,15 @@ class PersistenceController { return context } - static func loadVersionFromDisk() -> Int { + private static func loadVersionFromDisk() -> Int { UserDefaults.standard.integer(forKey: Self.versionKey) } - static func saveVersionToDisk(_ newVersion: Int) { + private static func saveVersionToDisk(_ newVersion: Int) { UserDefaults.standard.set(newVersion, forKey: Self.versionKey) } - /// Cleans up uneeded entities from the database. Our local database is really just a cache, and we need to + /// Cleans up unneeded entities from the database. Our local database is really just a cache, and we need to /// invalidate old items to keep it from growing indefinitely. /// /// This should only be called once right at app launch. diff --git a/Nos/Controller/SearchController.swift b/Nos/Controller/SearchController.swift index 44b531ea8..e1314ad28 100644 --- a/Nos/Controller/SearchController.swift +++ b/Nos/Controller/SearchController.swift @@ -49,9 +49,7 @@ class SearchController: ObservableObject { /// The timer for showing the "not finding results" view. Resets any time the query is changed. private var timer: Timer? - private lazy var context: NSManagedObjectContext = { - persistenceController.viewContext - }() + private lazy var context = persistenceController.viewContext /// The amount of time, in seconds, to remain in the `.loading` state until switching to `.stillLoading`. private let stillLoadingTime: TimeInterval = 10 @@ -141,7 +139,7 @@ class SearchController: ObservableObject { authorResults = [] } - func note(fromPublicKey publicKeyString: String) -> Event? { + private func note(fromPublicKey publicKeyString: String) -> Event? { let strippedString = publicKeyString.trimmingCharacters( in: NSCharacterSet.whitespacesAndNewlines ) diff --git a/Nos/Models/CoreData/Event+CoreDataClass.swift b/Nos/Models/CoreData/Event+CoreDataClass.swift index 27b5b8f08..d3b5670f6 100644 --- a/Nos/Models/CoreData/Event+CoreDataClass.swift +++ b/Nos/Models/CoreData/Event+CoreDataClass.swift @@ -10,9 +10,6 @@ public class Event: NosManagedObject, VerifiableEvent { @Dependency(\.currentUser) @ObservationIgnored private var currentUser var pubKey: String { author?.hexadecimalPublicKey ?? "" } - - static var replyNoteReferences = "kind = 1 AND ANY eventReferences.referencedEvent.identifier == %@ " + - "AND author.muted = false" @nonobjc public class func allEventsRequest() -> NSFetchRequest { let fetchRequest = NSFetchRequest(entityName: "Event") @@ -113,7 +110,6 @@ public class Event: NosManagedObject, VerifiableEvent { @nonobjc public class func lastReceived(for user: Author) -> NSFetchRequest { let fetchRequest = NSFetchRequest(entityName: "Event") - fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \Event.createdAt, ascending: false)] fetchRequest.predicate = NSPredicate(format: "author != %@", user) fetchRequest.fetchLimit = 1 fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \Event.receivedAt, ascending: false)] @@ -128,6 +124,11 @@ public class Event: NosManagedObject, VerifiableEvent { guard let noteID else { return emptyRequest() } + + let replyNoteReferences = "kind = 1 " + + "AND ANY eventReferences.referencedEvent.identifier == %@ " + + "AND author.muted = false" + let fetchRequest = NSFetchRequest(entityName: "Event") fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \Event.receivedAt, ascending: false)] fetchRequest.predicate = NSPredicate( @@ -480,8 +481,8 @@ public class Event: NosManagedObject, VerifiableEvent { // MARK: - Creating - func createIfNecessary( - jsonEvent: JSONEvent, + static func createIfNecessary( + jsonEvent: JSONEvent, relay: Relay?, context: NSManagedObjectContext ) throws -> Event? { @@ -562,7 +563,7 @@ public class Event: NosManagedObject, VerifiableEvent { } /// Populates an event stub (with only its ID set) using the data in the given JSON. - func hydrate(from jsonEvent: JSONEvent, relay: Relay?, in context: NSManagedObjectContext) throws { + private func hydrate(from jsonEvent: JSONEvent, relay: Relay?, in context: NSManagedObjectContext) throws { assert(isStub, "Tried to hydrate an event that isn't a stub. This is a programming error") // if this stub was created with a replaceableIdentifier and author, it won't have an identifier yet @@ -629,7 +630,11 @@ public class Event: NosManagedObject, VerifiableEvent { } } - func hydrateContactList(from jsonEvent: JSONEvent, author newAuthor: Author, context: NSManagedObjectContext) { + private func hydrateContactList( + from jsonEvent: JSONEvent, + author newAuthor: Author, + context: NSManagedObjectContext + ) { guard createdAt! > newAuthor.lastUpdatedContactList ?? Date.distantPast else { return } @@ -682,7 +687,7 @@ public class Event: NosManagedObject, VerifiableEvent { } } - func hydrateDefault(from jsonEvent: JSONEvent, context: NSManagedObjectContext) { + private func hydrateDefault(from jsonEvent: JSONEvent, context: NSManagedObjectContext) { let newEventReferences = NSMutableOrderedSet() let newAuthorReferences = NSMutableOrderedSet() for jsonTag in jsonEvent.tags { @@ -706,7 +711,7 @@ public class Event: NosManagedObject, VerifiableEvent { authorReferences = newAuthorReferences } - func hydrateMetaData(from jsonEvent: JSONEvent, author newAuthor: Author, context: NSManagedObjectContext) { + private func hydrateMetaData(from jsonEvent: JSONEvent, author newAuthor: Author, context: NSManagedObjectContext) { guard createdAt! > newAuthor.lastUpdatedMetadata ?? Date.distantPast else { // This is old data return @@ -738,7 +743,7 @@ public class Event: NosManagedObject, VerifiableEvent { seenOnRelays.insert(relay) } - func hydrateMuteList(from jsonEvent: JSONEvent, context: NSManagedObjectContext) { + private func hydrateMuteList(from jsonEvent: JSONEvent, context: NSManagedObjectContext) { let mutedKeys = jsonEvent.tags.map { $0[1] } let request = Author.allAuthorsRequest(muted: true) @@ -766,19 +771,15 @@ public class Event: NosManagedObject, VerifiableEvent { } /// Tries to parse a new event out of the given jsonEvent's `content` field. - @discardableResult - func parseContent(from jsonEvent: JSONEvent, context: NSManagedObjectContext) -> Event? { + private func parseContent(from jsonEvent: JSONEvent, context: NSManagedObjectContext) { do { if let contentData = jsonEvent.content.data(using: .utf8) { let jsonEvent = try JSONDecoder().decode(JSONEvent.self, from: contentData) - return try Event().createIfNecessary(jsonEvent: jsonEvent, relay: nil, context: context) + _ = try Event.createIfNecessary(jsonEvent: jsonEvent, relay: nil, context: context) } } catch { Log.error("Could not parse content for jsonEvent: \(jsonEvent)") - return nil } - - return nil } // MARK: - Preloading and Caching @@ -887,9 +888,8 @@ public class Event: NosManagedObject, VerifiableEvent { @Dependency(\.persistenceController) var persistenceController let context = persistenceController.backgroundViewContext - _ = try? Event.findOrCreateStubBy(id: quotedNoteID, context: context) - await context.perform { + _ = try? Event.findOrCreateStubBy(id: quotedNoteID, context: context) try? context.save() } diff --git a/Nos/NosApp.swift b/Nos/NosApp.swift index 7733193f6..fbc27ff61 100644 --- a/Nos/NosApp.swift +++ b/Nos/NosApp.swift @@ -26,7 +26,7 @@ struct NosApp: App { var body: some Scene { WindowGroup { AppView() - .environment(\.managedObjectContext, persistenceController.container.viewContext) + .environment(\.managedObjectContext, persistenceController.viewContext) .environmentObject(relayService) .environmentObject(router) .environment(appController) diff --git a/Nos/Service/EventProcessor.swift b/Nos/Service/EventProcessor.swift index 264ef2184..d0e132d69 100644 --- a/Nos/Service/EventProcessor.swift +++ b/Nos/Service/EventProcessor.swift @@ -11,7 +11,7 @@ enum EventProcessor { in parseContext: NSManagedObjectContext, skipVerification: Bool = false ) throws -> Event? { - if let event = try Event().createIfNecessary(jsonEvent: jsonEvent, relay: relay, context: parseContext) { + if let event = try Event.createIfNecessary(jsonEvent: jsonEvent, relay: relay, context: parseContext) { relay.unwrap { do { try event.trackDelete(on: $0, context: parseContext) @@ -79,7 +79,7 @@ enum EventProcessor { from relay: Relay?, in persistenceController: PersistenceController ) throws -> [Event] { - let parseContext = persistenceController.container.viewContext + let parseContext = persistenceController.viewContext return try parse(jsonData: jsonData, from: relay, in: parseContext) } } diff --git a/Nos/Service/Relay/RelayService.swift b/Nos/Service/Relay/RelayService.swift index 90718e59f..dba9a34ff 100644 --- a/Nos/Service/Relay/RelayService.swift +++ b/Nos/Service/Relay/RelayService.swift @@ -16,7 +16,6 @@ class RelayService: ObservableObject { private var backgroundProcessTimer: AsyncTimer? private var eventProcessingLoop: Task? private var backgroundContext: NSManagedObjectContext - private var parseContext: NSManagedObjectContext private var processingQueue = DispatchQueue(label: "RelayService-processing", qos: .userInitiated) private var parseQueue = ParseQueue() @@ -30,7 +29,6 @@ class RelayService: ObservableObject { self.subscriptionManager = subscriptionManager @Dependency(\.persistenceController) var persistenceController self.backgroundContext = persistenceController.newBackgroundContext() - self.parseContext = persistenceController.parseContext Task { await self.subscriptionManager.set(socketQueue: processingQueue, delegate: self) } @@ -63,7 +61,7 @@ class RelayService: ObservableObject { }) Task { @MainActor in - currentUser.viewContext = persistenceController.container.viewContext + currentUser.viewContext = persistenceController.viewContext } NotificationCenter.default.addObserver( @@ -381,12 +379,16 @@ extension RelayService { return false } else { let remainingEventCount = await parseQueue.count - try await self.parseContext.perform { + try await persistenceController.parseContext.perform { var savedEvents = 0 for (event, socket) in eventData { - let relay = self.relay(from: socket, in: self.parseContext) + let relay = self.relay(from: socket, in: self.persistenceController.parseContext) do { - if try EventProcessor.parse(jsonEvent: event, from: relay, in: self.parseContext) != nil { + if try EventProcessor.parse( + jsonEvent: event, + from: relay, + in: self.persistenceController.parseContext + ) != nil { savedEvents += 1 } } catch { @@ -402,7 +404,7 @@ extension RelayService { if remainingEventCount >= 1000 && remainingEventCount < 1030 { self.crashReporting.report("Parse queue is large: currently 1000+ events") } - try self.parseContext.saveIfNeeded() + try self.persistenceController.parseContext.saveIfNeeded() try self.persistenceController.viewContext.saveIfNeeded() } return true @@ -410,7 +412,7 @@ extension RelayService { } } -// MARK: Relay Communciation +// MARK: Relay Communication extension RelayService { private func parseOK(_ responseArray: [Any], _ socket: WebSocket) async { @@ -716,12 +718,12 @@ extension RelayService { /// These events could have failed to publish becuase the relay was offline, or because the user was offline. Often /// the user has relay in their list that they don't have write access to so eventually this function will stop /// trying to republish the same event. - @MainActor func retryFailedPublishes() async { + @MainActor private func retryFailedPublishes() async { guard let userKey = currentUser.author?.hexadecimalPublicKey else { return } - await self.backgroundContext.perform { + await backgroundContext.perform { guard let user = try? Author.find(by: userKey, context: self.backgroundContext) else { return @@ -731,10 +733,12 @@ extension RelayService { // Try to publish each of these again to each relay that failed. for event in eventsToRetry { + guard let jsonEvent = event.codable else { continue } + let missedRelays = event.shouldBePublishedTo.subtracting(event.publishedTo) + let missedAddresses = missedRelays.compactMap { $0.address } - for missedRelay in missedRelays { - guard let missedAddress = missedRelay.address, let jsonEvent = event.codable else { continue } + for missedAddress in missedAddresses { Task { if let socket = await self.subscriptionManager.socket(for: missedAddress) { // Publish again to this socket diff --git a/Nos/Views/AppView.swift b/Nos/Views/AppView.swift index a165e39df..d84d64707 100644 --- a/Nos/Views/AppView.swift +++ b/Nos/Views/AppView.swift @@ -194,7 +194,7 @@ struct AppView_Previews: PreviewProvider { static var previewData = PreviewData() static var persistenceController = PersistenceController.preview - static var previewContext = persistenceController.container.viewContext + static var previewContext = persistenceController.viewContext static var relayService = previewData.relayService static var router = Router() static var currentUser = previewData.currentUser diff --git a/Nos/Views/Components/Button/NoteOptionsButton.swift b/Nos/Views/Components/Button/NoteOptionsButton.swift index f63c04dab..fed0f0074 100644 --- a/Nos/Views/Components/Button/NoteOptionsButton.swift +++ b/Nos/Views/Components/Button/NoteOptionsButton.swift @@ -116,7 +116,7 @@ struct NoteOptionsButton: View { struct NoteOptionsButton_Previews: PreviewProvider { static var previewData = PreviewData() static var persistenceController = PersistenceController.preview - static var previewContext = persistenceController.container.viewContext + static var previewContext = persistenceController.viewContext static var currentUser = previewData.currentUser static var shortNote: Event { diff --git a/Nos/Views/Components/GoldenPostView.swift b/Nos/Views/Components/GoldenPostView.swift index be735652a..53f923998 100644 --- a/Nos/Views/Components/GoldenPostView.swift +++ b/Nos/Views/Components/GoldenPostView.swift @@ -123,7 +123,7 @@ struct GoldenPostView: View { struct GoldenPostView_Previews: PreviewProvider { static var persistenceController = PersistenceController.preview - static var previewContext = persistenceController.container.viewContext + static var previewContext = persistenceController.viewContext static var shortNote: Event { let note = Event(context: previewContext) diff --git a/Nos/Views/Components/ThreadView.swift b/Nos/Views/Components/ThreadView.swift index c84678e04..97986d6e1 100644 --- a/Nos/Views/Components/ThreadView.swift +++ b/Nos/Views/Components/ThreadView.swift @@ -68,11 +68,11 @@ struct ThreadView: View { struct ThreadView_Previews: PreviewProvider { static var previewData = PreviewData() static var persistenceController = PersistenceController.preview - static var previewContext = persistenceController.container.viewContext + static var previewContext = persistenceController.viewContext static var router = Router() static var emptyPersistenceController = PersistenceController.empty - static var emptyPreviewContext = emptyPersistenceController.container.viewContext + static var emptyPreviewContext = emptyPersistenceController.viewContext static var emptyRelayService = previewData.relayService static var currentUser = previewData.currentUser diff --git a/Nos/Views/Discover/DiscoverTab.swift b/Nos/Views/Discover/DiscoverTab.swift index 44ca3ae79..603f6f5bc 100644 --- a/Nos/Views/Discover/DiscoverTab.swift +++ b/Nos/Views/Discover/DiscoverTab.swift @@ -74,7 +74,7 @@ struct DiscoverTab_Previews: PreviewProvider { static var previewData = PreviewData() static var persistenceController = PersistenceController.preview - static var previewContext = persistenceController.container.viewContext + static var previewContext = persistenceController.viewContext static var currentUser = previewData.currentUser static var router = Router() diff --git a/Nos/Views/Fixtures/PreviewData.swift b/Nos/Views/Fixtures/PreviewData.swift index a19e90b98..4cc7b7342 100644 --- a/Nos/Views/Fixtures/PreviewData.swift +++ b/Nos/Views/Fixtures/PreviewData.swift @@ -28,7 +28,7 @@ struct PreviewData { @Dependency(\.currentUser) var currentUser lazy var previewContext: NSManagedObjectContext = { - persistenceController.container.viewContext + persistenceController.viewContext }() // MARK: - Authors diff --git a/Nos/Views/Note/NoteView.swift b/Nos/Views/Note/NoteView.swift index f244a5a3f..535e5124a 100644 --- a/Nos/Views/Note/NoteView.swift +++ b/Nos/Views/Note/NoteView.swift @@ -171,7 +171,7 @@ struct RepliesView_Previews: PreviewProvider { static var previewData = PreviewData(currentUserKey: KeyFixture.alice) static var persistenceController = PersistenceController.preview - static var previewContext = persistenceController.container.viewContext + static var previewContext = persistenceController.viewContext static var emptyRelayService = previewData.relayService static var router = Router() static var currentUser = previewData.currentUser diff --git a/Nos/Views/Profile/ProfileView.swift b/Nos/Views/Profile/ProfileView.swift index 4ba8ff4f6..6b02023f3 100644 --- a/Nos/Views/Profile/ProfileView.swift +++ b/Nos/Views/Profile/ProfileView.swift @@ -246,7 +246,7 @@ struct ProfileView: View { @Dependency(\.persistenceController) var persistenceController lazy var previewContext: NSManagedObjectContext = { - persistenceController.container.viewContext + persistenceController.viewContext }() var previewData = PreviewData(currentUserKey: KeyFixture.eve) diff --git a/Nos/Views/Relay/RelayDetailView.swift b/Nos/Views/Relay/RelayDetailView.swift index 6bfb3b223..7d28623d3 100644 --- a/Nos/Views/Relay/RelayDetailView.swift +++ b/Nos/Views/Relay/RelayDetailView.swift @@ -79,7 +79,7 @@ struct RelayDetailView: View { } struct RelayDetailView_Previews: PreviewProvider { - static var previewContext = PersistenceController.preview.container.viewContext + static var previewContext = PersistenceController.preview.viewContext static var relay: Relay { do { return try Relay.findOrCreate(by: "wss://example.com", context: previewContext) diff --git a/Nos/Views/Relay/RelayView.swift b/Nos/Views/Relay/RelayView.swift index aeac17156..ba27f19f6 100644 --- a/Nos/Views/Relay/RelayView.swift +++ b/Nos/Views/Relay/RelayView.swift @@ -228,9 +228,9 @@ struct RelayView: View { struct RelayView_Previews: PreviewProvider { static var previewData = PreviewData() - static var previewContext = PersistenceController.preview.container.viewContext + static var previewContext = PersistenceController.preview.viewContext - static var emptyContext = PersistenceController.empty.container.viewContext + static var emptyContext = PersistenceController.empty.viewContext static var user: Author { let author = Author(context: previewContext) diff --git a/NosTests/TestHelpers/SQLiteStoreTestCase.swift b/NosTests/TestHelpers/SQLiteStoreTestCase.swift index 56e3bada9..e437b980b 100644 --- a/NosTests/TestHelpers/SQLiteStoreTestCase.swift +++ b/NosTests/TestHelpers/SQLiteStoreTestCase.swift @@ -22,3 +22,23 @@ class SQLiteStoreTestCase: XCTestCase { } // swiftlint:enable implicitly_unwrapped_optional + +extension PersistenceController { + func tearDown() throws { + for store in container.persistentStoreCoordinator.persistentStores { + try container.persistentStoreCoordinator.remove(store) + } + + try container.persistentStoreDescriptions.forEach { storeDescription in + try container.persistentStoreCoordinator.destroyPersistentStore( + at: storeDescription.url!, + ofType: NSSQLiteStoreType, + options: nil + ) + } + + viewContext.reset() + backgroundViewContext.reset() + parseContext.reset() + } +} diff --git a/NosTests/Views/EventObservationTests.swift b/NosTests/Views/EventObservationTests.swift index 491b547a9..092646fc6 100644 --- a/NosTests/Views/EventObservationTests.swift +++ b/NosTests/Views/EventObservationTests.swift @@ -56,7 +56,7 @@ final class EventObservationTests: CoreDataTestCase { fullEvent.content = eventContent let view = EventObservationTestView() - ViewHosting.host(view: view.environment(\.managedObjectContext, persistenceController.container.viewContext)) + ViewHosting.host(view: view.environment(\.managedObjectContext, persistenceController.viewContext)) // sanity check let expectNullContent = view.inspection.inspect { view in let eventContentInView = try view.find(ViewType.Text.self).string() From 361dabb44d35d7eea4510bfd75315e9a607ef250 Mon Sep 17 00:00:00 2001 From: Bryan Montz Date: Tue, 10 Sep 2024 06:25:45 -0500 Subject: [PATCH 2/2] addressed PR feedback --- Nos/Controller/PersistenceController.swift | 108 +++++++++++---------- Nos/Service/Relay/RelayService.swift | 7 +- 2 files changed, 57 insertions(+), 58 deletions(-) diff --git a/Nos/Controller/PersistenceController.swift b/Nos/Controller/PersistenceController.swift index bd7027d2c..7f32f6838 100644 --- a/Nos/Controller/PersistenceController.swift +++ b/Nos/Controller/PersistenceController.swift @@ -102,59 +102,6 @@ final class PersistenceController { } } -#if DEBUG - func loadSampleData(context: NSManagedObjectContext) async throws { - guard let sampleFile = Bundle.current.url(forResource: "sample_data", withExtension: "json") else { - Log.error("Error: bad sample file location") - return - } - - guard let sampleData = try? Data(contentsOf: sampleFile) else { - print("Error: Debug data not found") - return - } - - Event.deleteAll(context: context) - context.reset() - - guard let events = try? EventProcessor.parse(jsonData: sampleData, from: nil, in: context) else { - print("Error: Could not parse events") - return - } - - print("Successfully preloaded \(events.count) events") - - let verifiedEvents = Event.all(context: context) - print("Successfully fetched \(verifiedEvents.count) events") - - // Force follow sample data users; This will be wiped if you sync with a relay. - let authors = Author.all(context: context) - let follows = try context.fetch(Follow.followsRequest(sources: authors)) - - if let publicKey = currentUser.publicKeyHex { - let currentAuthor = try Author.findOrCreate(by: publicKey, context: context) - currentAuthor.follows = Set(follows) - } - } - - func resetForTesting() { - container = NSPersistentContainer(name: "Nos", managedObjectModel: model) - if !inMemory { - container.loadPersistentStores(completionHandler: { (storeDescription, _) in - guard let storeURL = storeDescription.url else { - Log.error("Could not get store URL") - return - } - Self.clearCoreData(store: storeURL, in: self.container) - }) - } - setUp(erasingPrevious: true) - viewContext.reset() - backgroundViewContext.reset() - parseContext.reset() - } -#endif - func newBackgroundContext() -> NSManagedObjectContext { let context = container.newBackgroundContext() context.automaticallyMergesChangesFromParent = true @@ -207,3 +154,58 @@ final class PersistenceController { } } } + +#if DEBUG +extension PersistenceController { + func loadSampleData(context: NSManagedObjectContext) async throws { + guard let sampleFile = Bundle.current.url(forResource: "sample_data", withExtension: "json") else { + Log.error("Error: bad sample file location") + return + } + + guard let sampleData = try? Data(contentsOf: sampleFile) else { + print("Error: Debug data not found") + return + } + + Event.deleteAll(context: context) + context.reset() + + guard let events = try? EventProcessor.parse(jsonData: sampleData, from: nil, in: context) else { + print("Error: Could not parse events") + return + } + + print("Successfully preloaded \(events.count) events") + + let verifiedEvents = Event.all(context: context) + print("Successfully fetched \(verifiedEvents.count) events") + + // Force follow sample data users; This will be wiped if you sync with a relay. + let authors = Author.all(context: context) + let follows = try context.fetch(Follow.followsRequest(sources: authors)) + + if let publicKey = currentUser.publicKeyHex { + let currentAuthor = try Author.findOrCreate(by: publicKey, context: context) + currentAuthor.follows = Set(follows) + } + } + + func resetForTesting() { + container = NSPersistentContainer(name: "Nos", managedObjectModel: model) + if !inMemory { + container.loadPersistentStores(completionHandler: { (storeDescription, _) in + guard let storeURL = storeDescription.url else { + Log.error("Could not get store URL") + return + } + Self.clearCoreData(store: storeURL, in: self.container) + }) + } + setUp(erasingPrevious: true) + viewContext.reset() + backgroundViewContext.reset() + parseContext.reset() + } +} +#endif diff --git a/Nos/Service/Relay/RelayService.swift b/Nos/Service/Relay/RelayService.swift index dba9a34ff..607902ca1 100644 --- a/Nos/Service/Relay/RelayService.swift +++ b/Nos/Service/Relay/RelayService.swift @@ -384,11 +384,8 @@ extension RelayService { for (event, socket) in eventData { let relay = self.relay(from: socket, in: self.persistenceController.parseContext) do { - if try EventProcessor.parse( - jsonEvent: event, - from: relay, - in: self.persistenceController.parseContext - ) != nil { + let context = self.persistenceController.parseContext + if try EventProcessor.parse(jsonEvent: event, from: relay, in: context) != nil { savedEvents += 1 } } catch {