Jelajahi Sumber

Refactor date fetch functions to be thread-safe; combine them in generic

Deniz Cengiz 1 tahun lalu
induk
melakukan
7615a0fb54
1 mengubah file dengan 53 tambahan dan 65 penghapusan
  1. 53 65
      Model/JSONImporter.swift

+ 53 - 65
Model/JSONImporter.swift

@@ -56,74 +56,42 @@ class JSONImporter {
         return try decoder.decode(T.self, from: data)
     }
 
-    /// Retrieves the set of dates for all glucose values currently stored in CoreData.
+    /// Fetches a set of unique `Date` values for a specific `NSManagedObject` type from Core Data.
     ///
-    /// - Parameters: the start and end dates to fetch glucose values, inclusive
-    /// - Returns: A set of dates corresponding to existing glucose readings.
-    /// - Throws: An error if the fetch operation fails.
-    private func fetchGlucoseDates(start: Date, end: Date) async throws -> Set<Date> {
-        let allReadings = try await coreDataStack.fetchEntitiesAsync(
-            ofType: GlucoseStored.self,
-            onContext: context,
-            predicate: .predicateForDateBetween(start: start, end: end),
-            key: "date",
-            ascending: false
-        ) as? [GlucoseStored] ?? []
-
-        return Set(allReadings.compactMap(\.date))
-    }
-
-    /// Retrieves the set of timestamps for all pump events currently stored in CoreData.
+    /// This helper function is used to retrieve all existing date-like values (e.g., `date`, `timestamp`, `deliverAt`)
+    /// from a given entity type within a specified time range. It wraps the fetch and transformation
+    /// in a `context.perform` block to ensure thread safety when used on private background contexts.
     ///
-    /// - Parameters: the start and end dates to fetch pump events, inclusive
-    /// - Returns: A set of dates corresponding to existing pump events.
-    /// - Throws: An error if the fetch operation fails.
-    private func fetchPumpTimestamps(start: Date, end: Date) async throws -> Set<Date> {
-        let allPumpEvents = try await coreDataStack.fetchEntitiesAsync(
-            ofType: PumpEventStored.self,
-            onContext: context,
-            predicate: .predicateForTimestampBetween(start: start, end: end),
-            key: "timestamp",
-            ascending: false
-        ) as? [PumpEventStored] ?? []
-
-        return Set(allPumpEvents.compactMap(\.timestamp))
-    }
-
-    /// Retrieves the set of timestamps for all carb entries currently stored in CoreData.
+    /// - Parameters:
+    ///   - type: The `NSManagedObject` subclass to fetch (e.g., `GlucoseStored.self`, `PumpEventStored.self`)
+    ///   - predicate: A preconstructed predicate that filters the entity by date/timestamp range.
+    ///   - sortKey: The string name of the date-like field used to sort the fetch results. **This must match the key used in Core Data.**
+    ///   - dateKeyPath: A key path pointing to the `Date?` property on the entity used to extract the actual date value from each record.
     ///
-    /// - Parameters: the start and end dates to fetch carb entries, inclusive
-    /// - Returns: A set of dates corresponding to existing carb entries.
-    /// - Throws: An error if the fetch operation fails.
-    private func fetchCarbEntryDates(start: Date, end: Date) async throws -> Set<Date> {
-        let allCarbEntryDates = try await coreDataStack.fetchEntitiesAsync(
-            ofType: CarbEntryStored.self,
+    /// - Returns: A `Set<Date>` containing all non-nil date values from the fetched entities.
+    /// - Throws: `CoreDataError.fetchError` if casting the fetched objects fails, or if the fetch itself fails.
+
+    private func fetchDates<T: NSManagedObject>(
+        ofType type: T.Type,
+        predicate: NSPredicate,
+        sortKey: String,
+        dateKeyPath: KeyPath<T, Date?>
+    ) async throws -> Set<Date> {
+        let fetched = try await coreDataStack.fetchEntitiesAsync(
+            ofType: type,
             onContext: context,
-            predicate: .predicateForDateBetween(start: start, end: end),
-            key: "date",
+            predicate: predicate,
+            key: sortKey,
             ascending: false
-        ) as? [CarbEntryStored] ?? []
-
-        return Set(allCarbEntryDates.compactMap(\.date))
-    }
+        )
 
-    /// Retrieves the set of dates for all oref determinations currently stored in CoreData.
-    ///
-    /// - Parameters:
-    ///   - start: the start to fetch from; inclusive
-    ///   - end: the end date to fetch to; inclusive
-    /// - Returns: A set of dates corresponding to existing determinations.
-    /// - Throws: An error if the fetch operation fails.
-    private func fetchDeterminationDates(start: Date, end: Date) async throws -> Set<Date> {
-        let determinations = try await coreDataStack.fetchEntitiesAsync(
-            ofType: OrefDetermination.self,
-            onContext: context,
-            predicate: .predicateForDeliverAtBetween(start: start, end: end),
-            key: "deliverAt",
-            ascending: false
-        ) as? [OrefDetermination] ?? []
+        return try await context.perform {
+            guard let typed = fetched as? [T] else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
 
-        return Set(determinations.compactMap(\.deliverAt))
+            return Set(typed.compactMap { $0[keyPath: dateKeyPath] })
+        }
     }
 
     /// Imports glucose history from a JSON file into CoreData.
@@ -141,7 +109,12 @@ class JSONImporter {
     func importGlucoseHistory(url: URL, now: Date) async throws {
         let twentyFourHoursAgo = now - 24.hours.timeInterval
         let glucoseHistoryFull: [BloodGlucose] = try readJsonFile(url: url)
-        let existingDates = try await fetchGlucoseDates(start: twentyFourHoursAgo, end: now)
+        let existingDates = try await fetchDates(
+            ofType: GlucoseStored.self,
+            predicate: .predicateForDateBetween(start: twentyFourHoursAgo, end: now),
+            sortKey: "date",
+            dateKeyPath: \.date
+        )
 
         // only import glucose values from the last 24 hours that don't exist
         let glucoseHistory = glucoseHistoryFull
@@ -232,7 +205,12 @@ class JSONImporter {
     func importPumpHistory(url: URL, now: Date) async throws {
         let twentyFourHoursAgo = now - 24.hours.timeInterval
         let pumpHistoryRaw: [PumpHistoryEvent] = try readJsonFile(url: url)
-        let existingTimestamps = try await fetchPumpTimestamps(start: twentyFourHoursAgo, end: now)
+        let existingTimestamps = try await fetchDates(
+            ofType: PumpEventStored.self,
+            predicate: .predicateForTimestampBetween(start: twentyFourHoursAgo, end: now),
+            sortKey: "timestamp",
+            dateKeyPath: \.timestamp
+        )
         let pumpHistoryFiltered = pumpHistoryRaw
             .filter { $0.timestamp >= twentyFourHoursAgo && $0.timestamp <= now && !existingTimestamps.contains($0.timestamp) }
 
@@ -272,7 +250,12 @@ class JSONImporter {
     func importCarbHistory(url: URL, now: Date) async throws {
         let twentyFourHoursAgo = now - 24.hours.timeInterval
         let carbHistoryFull: [CarbsEntry] = try readJsonFile(url: url)
-        let existingDates = try await fetchCarbEntryDates(start: twentyFourHoursAgo, end: now)
+        let existingDates = try await fetchDates(
+            ofType: CarbEntryStored.self,
+            predicate: .predicateForDateBetween(start: twentyFourHoursAgo, end: now),
+            sortKey: "date",
+            dateKeyPath: \.date
+        )
 
         // Only import carb entries from the last 24 hours that do not exist yet in Core Data
         // Only import "true" carb entries; ignore all FPU entries (aka carb equivalents)
@@ -314,7 +297,12 @@ class JSONImporter {
         let twentyFourHoursAgo = now - 24.hours.timeInterval
         let enactedDetermination: Determination = try readJsonFile(url: enactedUrl)
         let suggestedDetermination: Determination = try readJsonFile(url: suggestedUrl)
-        let existingDates = try await fetchDeterminationDates(start: twentyFourHoursAgo, end: now)
+        let existingDates = try await fetchDates(
+            ofType: OrefDetermination.self,
+            predicate: .predicateForDeliverAtBetween(start: twentyFourHoursAgo, end: now),
+            sortKey: "deliverAt",
+            dateKeyPath: \.deliverAt
+        )
 
         /// Helper function to check if entries are from within the last 24 hours that do not yet exist in Core Data
         func checkDeterminationDate(_ date: Date) -> Bool {