Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fixes and improvements related to Core Data usage #1443 #1471

Merged
merged 4 commits into from
Sep 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fixed issue where relay metadata is never updated. [#1472](https://github.com/planetary-social/nos/issues/1472)
- Updated the copy on the 3 dots note menu. [#1028](https://github.com/planetary-social/nos/issues/1028)
- Added functionality to share notes link through the 3 dots note menu. [#1272](https://github.com/planetary-social/nos/issues/1272)
- Fixes and improvements related to Core Data usage. [#1443](https://github.com/planetary-social/nos/issues/1443)

### Internal Changes
- Use NIP-92 media metadata to display media in the proper orientation. Currently behind the “Enable new media display” feature flag. [#1172](https://github.com/planetary-social/nos/issues/1172)
Expand Down
167 changes: 73 additions & 94 deletions Nos/Controller/PersistenceController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}()

Expand All @@ -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
Expand All @@ -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)
}

#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()
viewContext.automaticallyMergesChangesFromParent = true
viewContext.mergePolicy = NSMergePolicy.mergeByPropertyStoreTrump
}
#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 {
Expand All @@ -124,7 +82,7 @@ class PersistenceController {
}
fatalError("Could not initialize database \(error), \(error.userInfo)")
}
})
}
}

@MainActor
Expand All @@ -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)
Expand All @@ -144,56 +102,22 @@ class 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 newBackgroundContext() -> NSManagedObjectContext {
let context = container.newBackgroundContext()
context.automaticallyMergesChangesFromParent = true
context.mergePolicy = NSMergePolicy.mergeByPropertyStoreTrump
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
bryanmontz marked this conversation as resolved.
Show resolved Hide resolved
/// invalidate old items to keep it from growing indefinitely.
///
/// This should only be called once right at app launch.
Expand Down Expand Up @@ -230,3 +154,58 @@ 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
6 changes: 2 additions & 4 deletions Nos/Controller/SearchController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
)
Expand Down
Loading
Loading