summaryrefslogtreecommitdiff
path: root/Sora/Data/Settings/SettingsManager.swift
diff options
context:
space:
mode:
authorFuwn <[email protected]>2026-02-18 12:07:57 -0800
committerFuwn <[email protected]>2026-02-18 12:15:31 -0800
commitd9b8557de70d51db6037891ededd783b8787af22 (patch)
treece41d697e504d00ce85acf00689aff4596da1638 /Sora/Data/Settings/SettingsManager.swift
parentrefactor: remove duplicate settings serialisation work (diff)
downloadsora-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.swift223
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