Fix grocery sync deletions not persisting and Reminders race condition
Stop cascading syncs by adding an isReconciling flag so that reconcileFromServer no longer triggers scheduleSync via addItem/deleteItem. Make Reminders write-only by removing the diff/sync logic from the onDataChanged callback. Fetch fresh server state in RecipeView reconcile instead of using stale local cache. Track pending removal recipe IDs via DataStore so performInitialSync can push deletions for recipes whose grocery keys have already been removed from groceryDict. Fix a race condition in RemindersGroceryStore where EKEventStoreChanged notifications triggered load() before saveMappings() finished writing to disk, causing the correct in-memory state to be overwritten with stale data. Add ignoreNextExternalChange flag to skip self-triggered reloads. Restyle the add/remove all grocery button to match the Plan recipe button style using Label, subheadline font, and rounded rectangle background. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -35,6 +35,11 @@ class RemindersGroceryStore {
|
||||
private let mappingPath = "reminder_mappings.data"
|
||||
private var mappingStore = ReminderMappingStore()
|
||||
|
||||
/// When true, the next `EKEventStoreChanged` notification is skipped because
|
||||
/// it was triggered by our own save. Prevents a race where `load()` reads stale
|
||||
/// mapping data from disk before `saveMappings()` finishes writing.
|
||||
private var ignoreNextExternalChange = false
|
||||
|
||||
init() {
|
||||
NotificationCenter.default.addObserver(
|
||||
forName: .EKEventStoreChanged,
|
||||
@@ -43,6 +48,10 @@ class RemindersGroceryStore {
|
||||
) { [weak self] _ in
|
||||
Task { @MainActor in
|
||||
guard let self else { return }
|
||||
if self.ignoreNextExternalChange {
|
||||
self.ignoreNextExternalChange = false
|
||||
return
|
||||
}
|
||||
await self.load()
|
||||
self.onDataChanged?()
|
||||
}
|
||||
@@ -104,11 +113,13 @@ class RemindersGroceryStore {
|
||||
reminder.title = itemName
|
||||
reminder.calendar = calendar
|
||||
do {
|
||||
ignoreNextExternalChange = true
|
||||
try eventStore.save(reminder, commit: true)
|
||||
let name = recipeName ?? "-"
|
||||
addMapping(reminderIdentifier: reminder.calendarItemIdentifier, recipeId: recipeId, recipeName: name, itemName: itemName)
|
||||
appendToCache(itemName: itemName, recipeId: recipeId, recipeName: name)
|
||||
} catch {
|
||||
ignoreNextExternalChange = false
|
||||
Logger.view.error("Failed to save reminder: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
@@ -132,8 +143,10 @@ class RemindersGroceryStore {
|
||||
}
|
||||
}
|
||||
do {
|
||||
ignoreNextExternalChange = true
|
||||
try eventStore.commit()
|
||||
} catch {
|
||||
ignoreNextExternalChange = false
|
||||
Logger.view.error("Failed to commit reminders: \(error.localizedDescription)")
|
||||
}
|
||||
saveMappings()
|
||||
@@ -153,8 +166,10 @@ class RemindersGroceryStore {
|
||||
let reminders = await fetchReminders(matching: predicate, in: store)
|
||||
for reminder in reminders where reminder.calendarItemIdentifier == identifier {
|
||||
do {
|
||||
self.ignoreNextExternalChange = true
|
||||
try self.eventStore.remove(reminder, commit: true)
|
||||
} catch {
|
||||
self.ignoreNextExternalChange = false
|
||||
Logger.view.error("Failed to remove reminder: \(error.localizedDescription)")
|
||||
}
|
||||
break
|
||||
@@ -181,8 +196,10 @@ class RemindersGroceryStore {
|
||||
}
|
||||
}
|
||||
do {
|
||||
self.ignoreNextExternalChange = true
|
||||
try self.eventStore.commit()
|
||||
} catch {
|
||||
self.ignoreNextExternalChange = false
|
||||
Logger.view.error("Failed to commit reminder removal: \(error.localizedDescription)")
|
||||
}
|
||||
self.mappingStore.recipes.removeValue(forKey: recipeId)
|
||||
@@ -209,8 +226,10 @@ class RemindersGroceryStore {
|
||||
}
|
||||
}
|
||||
do {
|
||||
self.ignoreNextExternalChange = true
|
||||
try self.eventStore.commit()
|
||||
} catch {
|
||||
self.ignoreNextExternalChange = false
|
||||
Logger.view.error("Failed to commit reminder removal: \(error.localizedDescription)")
|
||||
}
|
||||
self.mappingStore.recipes = [:]
|
||||
|
||||
Reference in New Issue
Block a user