Fix timeline position sync not working due to LazilyDecoding cache not being invalidated upon remote change

This commit is contained in:
Shadowfacts 2023-01-28 13:41:22 -05:00
parent aec5c0b787
commit 7b7c05ff68
4 changed files with 24 additions and 7 deletions

View File

@ -545,6 +545,8 @@ class MastodonCachePersistentStore: NSPersistentCloudKitContainer {
guard let timelinePosition = try? self.viewContext.existingObject(with: id) as? TimelinePosition else { guard let timelinePosition = try? self.viewContext.existingObject(with: id) as? TimelinePosition else {
continue continue
} }
// the kvo observer that clears the LazilyDecoding cache doesn't always fire on remote changes, so do it manually
timelinePosition.changedRemotely()
NotificationCenter.default.post(name: .timelinePositionChanged, object: timelinePosition) NotificationCenter.default.post(name: .timelinePositionChanged, object: timelinePosition)
} }
if changedAccountPrefs { if changedAccountPrefs {

View File

@ -41,6 +41,10 @@ public final class TimelinePosition: NSManagedObject {
self.createdAt = Date() self.createdAt = Date()
} }
func changedRemotely() {
_statusIDs.removeCachedValue()
}
} }
// blergh, this is the simplest way of getting the Timeline into a format that A) CoreData can handle and B) is usable in the predicate // blergh, this is the simplest way of getting the Timeline into a format that A) CoreData can handle and B) is usable in the predicate

View File

@ -18,6 +18,7 @@ public struct LazilyDecoding<Enclosing: NSObject, Value: Codable> {
private let fallback: Value private let fallback: Value
private var value: Value? private var value: Value?
private var observation: NSKeyValueObservation? private var observation: NSKeyValueObservation?
private var skipClearingOnNextUpdate = false
init(from keyPath: ReferenceWritableKeyPath<Enclosing, Data?>, fallback: Value) { init(from keyPath: ReferenceWritableKeyPath<Enclosing, Data?>, fallback: Value) {
self.keyPath = keyPath self.keyPath = keyPath
@ -37,13 +38,16 @@ public struct LazilyDecoding<Enclosing: NSObject, Value: Codable> {
} else { } else {
guard let data = instance[keyPath: wrapper.keyPath] else { return wrapper.fallback } guard let data = instance[keyPath: wrapper.keyPath] else { return wrapper.fallback }
do { do {
let value = try decoder.decode(Box<Value>.self, from: data) let value = try decoder.decode(Box.self, from: data)
wrapper.value = value.value wrapper.value = value.value
wrapper.observation = instance.observe(wrapper.keyPath, changeHandler: { instance, _ in wrapper.observation = instance.observe(wrapper.keyPath, changeHandler: { instance, _ in
var updated = instance[keyPath: storageKeyPath] var wrapper = instance[keyPath: storageKeyPath]
updated.value = nil if wrapper.skipClearingOnNextUpdate {
updated.observation = nil wrapper.skipClearingOnNextUpdate = false
instance[keyPath: storageKeyPath] = updated } else {
wrapper.removeCachedValue()
}
instance[keyPath: storageKeyPath] = wrapper
}) })
instance[keyPath: storageKeyPath] = wrapper instance[keyPath: storageKeyPath] = wrapper
return value.value return value.value
@ -55,12 +59,18 @@ public struct LazilyDecoding<Enclosing: NSObject, Value: Codable> {
set { set {
var wrapper = instance[keyPath: storageKeyPath] var wrapper = instance[keyPath: storageKeyPath]
wrapper.value = newValue wrapper.value = newValue
wrapper.skipClearingOnNextUpdate = true
instance[keyPath: storageKeyPath] = wrapper instance[keyPath: storageKeyPath] = wrapper
let newData = try! encoder.encode(Box(value: newValue)) let newData = try! encoder.encode(Box(value: newValue))
instance[keyPath: wrapper.keyPath] = newData instance[keyPath: wrapper.keyPath] = newData
} }
} }
mutating func removeCachedValue() {
value = nil
observation = nil
}
} }
extension LazilyDecoding { extension LazilyDecoding {
@ -72,7 +82,7 @@ extension LazilyDecoding {
extension LazilyDecoding { extension LazilyDecoding {
// PropertyListEncoder only allows top-level types to be dicts or arrays, which breaks encoding nil-able values. // PropertyListEncoder only allows top-level types to be dicts or arrays, which breaks encoding nil-able values.
// Wrapping everything in a Box ensures that it's always a dict. // Wrapping everything in a Box ensures that it's always a dict.
private struct Box<T: Codable>: Codable { struct Box: Codable {
let value: T let value: Value
} }
} }

View File

@ -589,6 +589,7 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
guard timelinePosition.centerStatusID != centerVisibleStatusID else { guard timelinePosition.centerStatusID != centerVisibleStatusID else {
return false return false
} }
stateRestorationLogger.info("Potential restore with centerStatusID: \(timelinePosition.centerStatusID ?? "<none>")")
if !alwaysPrompt { if !alwaysPrompt {
Task { Task {
_ = await restoreState() _ = await restoreState()