diff options
| author | Fuwn <[email protected]> | 2026-02-18 12:07:57 -0800 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2026-02-18 12:15:31 -0800 |
| commit | d9b8557de70d51db6037891ededd783b8787af22 (patch) | |
| tree | ce41d697e504d00ce85acf00689aff4596da1638 /Sora/Data/Settings/SettingsManager.swift | |
| parent | refactor: remove duplicate settings serialisation work (diff) | |
| download | sora-testing-d9b8557de70d51db6037891ededd783b8787af22.tar.xz sora-testing-d9b8557de70d51db6037891ededd783b8787af22.zip | |
refactor: coalesce settings sync and skip unchanged writes
Diffstat (limited to 'Sora/Data/Settings/SettingsManager.swift')
| -rw-r--r-- | Sora/Data/Settings/SettingsManager.swift | 223 |
1 files changed, 130 insertions, 93 deletions
diff --git a/Sora/Data/Settings/SettingsManager.swift b/Sora/Data/Settings/SettingsManager.swift index f42030f..d7bfc44 100644 --- a/Sora/Data/Settings/SettingsManager.swift +++ b/Sora/Data/Settings/SettingsManager.swift @@ -52,6 +52,7 @@ class SettingsManager: ObservableObject { // swiftlint:disable:this type_body_l private var thumbnailQualityCache: BooruPostFileType = .preview private var isUpdatingCache = false private var pendingSyncKeys: Set<SettingsSyncKey> = [] + private let syncCoordinator = SettingsSyncCoordinator() // MARK: - Codable Properties @AppStorage("bookmarks") @@ -531,15 +532,32 @@ class SettingsManager: ObservableObject { // swiftlint:disable:this type_body_l pendingSyncKeys.removeAll() - Task.detached { [weak self] in - await self?.performBatchedSync(for: keysToSync) + Task { @MainActor [weak self] in + guard let self else { return } + + let shouldStartDraining = await syncCoordinator.enqueue(keysToSync) + + guard shouldStartDraining else { return } + + while true { + let nextBatch = await syncCoordinator.dequeueBatch() + + guard !nextBatch.isEmpty else { break } + + performBatchedSync(for: nextBatch) + } } } - // swiftlint:disable:next async_without_await - private func performBatchedSync(for keys: Set<SettingsSyncKey>) async { + private func performBatchedSync(for keys: Set<SettingsSyncKey>) { + var didChange = false + for key in keys { - triggerSyncIfNeeded(for: key) + didChange = triggerSyncIfNeeded(for: key) || didChange + } + + if didChange { + objectWillChange.send() } } @@ -641,28 +659,21 @@ class SettingsManager: ObservableObject { // swiftlint:disable:this type_body_l }.value } - // swiftlint:disable:next cyclomatic_complexity - private func triggerSyncIfNeeded(for key: SettingsSyncKey) { - guard enableSync else { return } - - Task.detached { [weak self] in - guard let self else { return } - - switch key { - case .bookmarks: - var iCloudBookmarks: [SettingsBookmark] = [] + // swiftlint:disable cyclomatic_complexity + @discardableResult + private func triggerSyncIfNeeded(for key: SettingsSyncKey) -> Bool { + guard enableSync else { return false } - if let iCloudData = NSUbiquitousKeyValueStore.default.data(forKey: "bookmarks") { - iCloudBookmarks = - await Self - .decode([SettingsBookmark].self, from: iCloudData) ?? [] - } + var didChange = false - let localBookmarks = - await Self.decode([SettingsBookmark].self, from: bookmarksData) ?? [] - let mergedBookmarksDict = (localBookmarks + iCloudBookmarks).reduce( - into: [UUID: SettingsBookmark]() - ) { dict, bookmark in + switch key { + case .bookmarks: + let iCloudData = NSUbiquitousKeyValueStore.default.data(forKey: "bookmarks") + let iCloudBookmarks = + iCloudData.flatMap { Self.decode([SettingsBookmark].self, from: $0) } ?? [] + let localBookmarks = Self.decode([SettingsBookmark].self, from: bookmarksData) ?? [] + let mergedBookmarks = (localBookmarks + iCloudBookmarks) + .reduce(into: [UUID: SettingsBookmark]()) { dict, bookmark in if let existing = dict[bookmark.id] { if bookmark.date > existing.date { dict[bookmark.id] = bookmark @@ -671,28 +682,36 @@ class SettingsManager: ObservableObject { // swiftlint:disable:this type_body_l dict[bookmark.id] = bookmark } } - let mergedBookmarks = Array(mergedBookmarksDict.values).sorted { $0.date > $1.date } - - if let encoded = await Self.encode(mergedBookmarks) { - NSUbiquitousKeyValueStore.default.set(encoded, forKey: "bookmarks") - - await MainActor.run { - self.bookmarksData = encoded + .values + .sorted { lhs, rhs in + if lhs.date == rhs.date { + return lhs.id.uuidString < rhs.id.uuidString } + + return lhs.date > rhs.date } - case .searchHistory: - var iCloudHistory: [BooruSearchQuery] = [] + bookmarksCache = mergedBookmarks - if let iCloudData = NSUbiquitousKeyValueStore.default.data(forKey: "searchHistory") { - iCloudHistory = await Self.decode([BooruSearchQuery].self, from: iCloudData) ?? [] + if let encoded = Self.encode(mergedBookmarks) { + if iCloudData != encoded { + NSUbiquitousKeyValueStore.default.set(encoded, forKey: "bookmarks") + didChange = true } - let localHistory = - await Self.decode([BooruSearchQuery].self, from: searchHistoryData) ?? [] - let mergedHistoryDict = (localHistory + iCloudHistory).reduce( - into: [UUID: BooruSearchQuery]() - ) { dict, entry in + if bookmarksData != encoded { + bookmarksData = encoded + didChange = true + } + } + + case .searchHistory: + let iCloudData = NSUbiquitousKeyValueStore.default.data(forKey: "searchHistory") + let iCloudHistory = + iCloudData.flatMap { Self.decode([BooruSearchQuery].self, from: $0) } ?? [] + let localHistory = Self.decode([BooruSearchQuery].self, from: searchHistoryData) ?? [] + let mergedHistory = (localHistory + iCloudHistory) + .reduce(into: [UUID: BooruSearchQuery]()) { dict, entry in if let existing = dict[entry.id] { if entry.date > existing.date { dict[entry.id] = entry @@ -701,58 +720,62 @@ class SettingsManager: ObservableObject { // swiftlint:disable:this type_body_l dict[entry.id] = entry } } - let mergedHistory = Array(mergedHistoryDict.values).sorted { $0.date > $1.date } - - if let encoded = await Self.encode(mergedHistory) { - NSUbiquitousKeyValueStore.default.set(encoded, forKey: "searchHistory") - - await MainActor.run { - self.searchHistoryData = encoded + .values + .sorted { lhs, rhs in + if lhs.date == rhs.date { + return lhs.id.uuidString < rhs.id.uuidString } + + return lhs.date > rhs.date } - case .customProviders: - var iCloudProviders: [BooruProviderCustom] = [] + searchHistoryCache = mergedHistory - if let iCloudData = NSUbiquitousKeyValueStore.default.data(forKey: "customProviders") { - iCloudProviders = - await Self - .decode([BooruProviderCustom].self, from: iCloudData) ?? [] + if let encoded = Self.encode(mergedHistory) { + if iCloudData != encoded { + NSUbiquitousKeyValueStore.default.set(encoded, forKey: "searchHistory") + didChange = true } - let localProviders = - await Self.decode([BooruProviderCustom].self, from: customProvidersData) ?? [] - let mergedProvidersDict = (localProviders + iCloudProviders).reduce( - into: [UUID: BooruProviderCustom]() - ) { dict, provider in + if searchHistoryData != encoded { + searchHistoryData = encoded + didChange = true + } + } + + case .customProviders: + let iCloudData = NSUbiquitousKeyValueStore.default.data(forKey: "customProviders") + let iCloudProviders = + iCloudData.flatMap { Self.decode([BooruProviderCustom].self, from: $0) } ?? [] + let localProviders = Self.decode([BooruProviderCustom].self, from: customProvidersData) ?? [] + let mergedProviders = (localProviders + iCloudProviders) + .reduce(into: [UUID: BooruProviderCustom]()) { dict, provider in if dict[provider.id] == nil { dict[provider.id] = provider } } - let mergedProviders = Array(mergedProvidersDict.values) + .values + .sorted { $0.id.uuidString < $1.id.uuidString } - if let encoded = await Self.encode(mergedProviders) { + if let encoded = Self.encode(mergedProviders) { + if iCloudData != encoded { NSUbiquitousKeyValueStore.default.set(encoded, forKey: "customProviders") - - await MainActor.run { - self.customProvidersData = encoded - } + didChange = true } - case .favorites: - var iCloudFavorites: [SettingsFavoritePost] = [] - - if let iCloudData = NSUbiquitousKeyValueStore.default.data(forKey: "favorites") { - iCloudFavorites = - await Self - .decode([SettingsFavoritePost].self, from: iCloudData) ?? [] + if customProvidersData != encoded { + customProvidersData = encoded + didChange = true } + } - let localFavorites = - await Self.decode([SettingsFavoritePost].self, from: favoritesData) ?? [] - let mergedFavoritesDict = (localFavorites + iCloudFavorites).reduce( - into: [UUID: SettingsFavoritePost]() - ) { dict, favorite in + case .favorites: + let iCloudData = NSUbiquitousKeyValueStore.default.data(forKey: "favorites") + let iCloudFavorites = + iCloudData.flatMap { Self.decode([SettingsFavoritePost].self, from: $0) } ?? [] + let localFavorites = Self.decode([SettingsFavoritePost].self, from: favoritesData) ?? [] + let mergedFavorites = (localFavorites + iCloudFavorites) + .reduce(into: [UUID: SettingsFavoritePost]()) { dict, favorite in if let existing = dict[favorite.id] { if favorite.date > existing.date { dict[favorite.id] = favorite @@ -761,25 +784,33 @@ class SettingsManager: ObservableObject { // swiftlint:disable:this type_body_l dict[favorite.id] = favorite } } - let mergedFavorites = Array(mergedFavoritesDict.values).sorted { $0.date > $1.date } + .values + .sorted { lhs, rhs in + if lhs.date == rhs.date { + return lhs.id.uuidString < rhs.id.uuidString + } - if let encoded = await Self.encode(mergedFavorites) { - NSUbiquitousKeyValueStore.default.set(encoded, forKey: "favorites") + return lhs.date > rhs.date + } - await MainActor.run { - self.favoritesData = encoded - } + favoritesCache = mergedFavorites + + if let encoded = Self.encode(mergedFavorites) { + if iCloudData != encoded { + NSUbiquitousKeyValueStore.default.set(encoded, forKey: "favorites") + didChange = true } - } - await MainActor.run { - self.loadBookmarksCache() - self.loadFavoritesCache() - self.loadSearchHistoryCache() - self.objectWillChange.send() + if favoritesData != encoded { + favoritesData = encoded + didChange = true + } } } + + return didChange } + // swiftlint:enable cyclomatic_complexity // MARK: Cache Loaders private func loadCache<T: Decodable & Sendable>( @@ -896,10 +927,16 @@ class SettingsManager: ObservableObject { // swiftlint:disable:this type_body_l } func triggerSyncIfNeededForAll() { - self.triggerSyncIfNeeded(for: .bookmarks) - self.triggerSyncIfNeeded(for: .favorites) - self.triggerSyncIfNeeded(for: .searchHistory) - self.triggerSyncIfNeeded(for: .customProviders) + let keysToSync: [SettingsSyncKey] = [.bookmarks, .favorites, .searchHistory, .customProviders] + var didChange = false + + for keyToSync in keysToSync { + didChange = triggerSyncIfNeeded(for: keyToSync) || didChange + } + + if didChange { + objectWillChange.send() + } } // MARK: Bookmark Management |