summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFuwn <[email protected]>2026-02-18 11:45:06 -0800
committerFuwn <[email protected]>2026-02-18 11:57:36 -0800
commit07c533116dd267e3e6e70d673f97bbb87f600f1f (patch)
treee744ebf8b78bc23e4639cef9b4483942140c4055
parenttest: add baseline test harness and performance baseline report (diff)
downloadsora-testing-07c533116dd267e3e6e70d673f97bbb87f600f1f.tar.xz
sora-testing-07c533116dd267e3e6e70d673f97bbb87f600f1f.zip
refactor: remove duplicate settings serialisation work
-rw-r--r--Sora/Data/Settings/SettingsCodec.swift43
-rw-r--r--Sora/Data/Settings/SettingsManager.swift79
-rw-r--r--SoraTests/SettingsManagerSyncTests.swift92
3 files changed, 185 insertions, 29 deletions
diff --git a/Sora/Data/Settings/SettingsCodec.swift b/Sora/Data/Settings/SettingsCodec.swift
new file mode 100644
index 0000000..4ec361a
--- /dev/null
+++ b/Sora/Data/Settings/SettingsCodec.swift
@@ -0,0 +1,43 @@
+import Foundation
+import SwiftUI
+
+@MainActor
+enum SettingsCodec {
+ private static let sharedJSONEncoder = JSONEncoder()
+ private static let sharedJSONDecoder = JSONDecoder()
+
+ static func encode<T: Encodable>(_ value: T) -> Data? {
+ try? sharedJSONEncoder.encode(value)
+ }
+
+ static func decode<T: Decodable>(_ type: T.Type, from data: Data) -> T? {
+ try? sharedJSONDecoder.decode(type, from: data)
+ }
+
+ static func encodeOnce<T: Encodable>(_ value: T) -> (value: T, encodedData: Data)? {
+ guard let encodedData = encode(value) else { return nil }
+
+ return (value: value, encodedData: encodedData)
+ }
+
+ @discardableResult
+ static func applyIfChanged(
+ encodedData: Data,
+ localData: Binding<Data>,
+ key: String,
+ enableSync: Bool
+ ) -> Bool {
+ guard localData.wrappedValue != encodedData else {
+ return false
+ }
+
+ localData.wrappedValue = encodedData
+
+ if enableSync {
+ NSUbiquitousKeyValueStore.default.set(encodedData, forKey: key)
+ NSUbiquitousKeyValueStore.default.synchronize()
+ }
+
+ return true
+ }
+}
diff --git a/Sora/Data/Settings/SettingsManager.swift b/Sora/Data/Settings/SettingsManager.swift
index ed29ec3..f42030f 100644
--- a/Sora/Data/Settings/SettingsManager.swift
+++ b/Sora/Data/Settings/SettingsManager.swift
@@ -92,13 +92,17 @@ class SettingsManager: ObservableObject { // swiftlint:disable:this type_body_l
defer { isUpdatingCache = false }
+ let sortedBookmarks = newValue.sorted { $0.date > $1.date }
+ let payload = SettingsCodec.encodeOnce(sortedBookmarks)
+
syncableData(
key: "bookmarks",
localData: $bookmarksData,
- newValue: newValue
- ) { $0.sorted { $0.date > $1.date } }
+ newValue: sortedBookmarks,
+ encodedData: payload?.encodedData
+ ) { $0 }
- bookmarksCache = newValue.sorted { $0.date > $1.date }
+ bookmarksCache = sortedBookmarks
pendingSyncKeys.insert(.bookmarks)
triggerBatchedSync()
@@ -115,13 +119,17 @@ class SettingsManager: ObservableObject { // swiftlint:disable:this type_body_l
defer { isUpdatingCache = false }
+ let sortedFavorites = newValue.sorted { $0.date > $1.date }
+ let payload = SettingsCodec.encodeOnce(sortedFavorites)
+
syncableData(
key: "favorites",
localData: $favoritesData,
- newValue: newValue
- ) { $0.sorted { $0.date > $1.date } }
+ newValue: sortedFavorites,
+ encodedData: payload?.encodedData
+ ) { $0 }
- favoritesCache = newValue.sorted { $0.date > $1.date }
+ favoritesCache = sortedFavorites
pendingSyncKeys.insert(.favorites)
triggerBatchedSync()
@@ -348,40 +356,48 @@ class SettingsManager: ObservableObject { // swiftlint:disable:this type_body_l
thumbnailQualityCache = _thumbnailQuality
}
- func updateBookmarks(_ newValue: [SettingsBookmark]) async {
+ func updateBookmarks(_ newValue: [SettingsBookmark], encodedData: Data? = nil) async {
guard !isUpdatingCache else { return }
isUpdatingCache = true
defer { isUpdatingCache = false }
+ let sortedBookmarks = newValue.sorted { $0.date > $1.date }
+ let resolvedEncodedData = encodedData ?? SettingsCodec.encode(sortedBookmarks)
+
syncableData(
key: "bookmarks",
localData: $bookmarksData,
- newValue: newValue
- ) { $0.sorted { $0.date > $1.date } }
+ newValue: sortedBookmarks,
+ encodedData: resolvedEncodedData
+ ) { $0 }
- bookmarksCache = newValue.sorted { $0.date > $1.date }
+ bookmarksCache = sortedBookmarks
await backupBookmarks()
pendingSyncKeys.insert(.bookmarks)
triggerBatchedSync()
}
- func updateFavorites(_ newValue: [SettingsFavoritePost]) async {
+ func updateFavorites(_ newValue: [SettingsFavoritePost], encodedData: Data? = nil) async {
guard !isUpdatingCache else { return }
isUpdatingCache = true
defer { isUpdatingCache = false }
+ let sortedFavorites = newValue.sorted { $0.date > $1.date }
+ let resolvedEncodedData = encodedData ?? SettingsCodec.encode(sortedFavorites)
+
syncableData(
key: "favorites",
localData: $favoritesData,
- newValue: newValue
- ) { $0.sorted { $0.date > $1.date } }
+ newValue: sortedFavorites,
+ encodedData: resolvedEncodedData
+ ) { $0 }
- favoritesCache = newValue.sorted { $0.date > $1.date }
+ favoritesCache = sortedFavorites
await backupFavorites()
pendingSyncKeys.insert(.favorites)
@@ -454,11 +470,11 @@ class SettingsManager: ObservableObject { // swiftlint:disable:this type_body_l
// MARK: - Private Helpers
private static func encode<T: Encodable>(_ value: T) -> Data? {
- try? JSONEncoder().encode(value)
+ SettingsCodec.encode(value)
}
private static func decode<T: Decodable>(_ type: T.Type, from data: Data) -> T? {
- try? JSONDecoder().decode(type, from: data)
+ SettingsCodec.decode(type, from: data)
}
private func syncableData<T: Codable>(
@@ -490,23 +506,22 @@ class SettingsManager: ObservableObject { // swiftlint:disable:this type_body_l
key: String,
localData: Binding<Data>,
newValue: [T],
+ encodedData: Data? = nil,
sort: ([T]) -> [T]
) {
let sortedValues = sort(newValue)
- guard let encoded = Self.encode(sortedValues) else {
+ guard let encoded = encodedData ?? SettingsCodec.encode(sortedValues) else {
localData.wrappedValue = Data()
return
}
- if localData.wrappedValue != encoded {
- localData.wrappedValue = encoded
-
- if enableSync {
- NSUbiquitousKeyValueStore.default.set(encoded, forKey: key)
- NSUbiquitousKeyValueStore.default.synchronize()
- }
- }
+ SettingsCodec.applyIfChanged(
+ encodedData: encoded,
+ localData: localData,
+ key: key,
+ enableSync: enableSync
+ )
}
private func triggerBatchedSync() {
@@ -909,9 +924,12 @@ class SettingsManager: ObservableObject { // swiftlint:disable:this type_body_l
SettingsBookmark(provider: provider, tags: normalizedTags, folder: folder)
)
- if let data = Self.encode(updatedBookmarks), data.count < 1_000_000 { // 1 MB
+ let sortedBookmarks = updatedBookmarks.sorted { $0.date > $1.date }
+ if let payload = SettingsCodec.encodeOnce(sortedBookmarks),
+ payload.encodedData.count < 1_000_000
+ { // 1 MB
Task {
- await updateBookmarks(updatedBookmarks)
+ await updateBookmarks(payload.value, encodedData: payload.encodedData)
}
} else {
debugPrint("SettingsManager.addBookmark: iCloud data limit exceeded")
@@ -1024,9 +1042,12 @@ class SettingsManager: ObservableObject { // swiftlint:disable:this type_body_l
SettingsFavoritePost(post: post, provider: provider, folder: folder)
)
- if let data = Self.encode(updatedFavorites), data.count < 1_000_000 { // 1 MB
+ let sortedFavorites = updatedFavorites.sorted { $0.date > $1.date }
+ if let payload = SettingsCodec.encodeOnce(sortedFavorites),
+ payload.encodedData.count < 1_000_000
+ { // 1 MB
Task {
- await updateFavorites(updatedFavorites)
+ await updateFavorites(payload.value, encodedData: payload.encodedData)
}
} else {
debugPrint("SettingsManager.addFavorite: iCloud data limit exceeded")
diff --git a/SoraTests/SettingsManagerSyncTests.swift b/SoraTests/SettingsManagerSyncTests.swift
index 27f05f5..72cf2ad 100644
--- a/SoraTests/SettingsManagerSyncTests.swift
+++ b/SoraTests/SettingsManagerSyncTests.swift
@@ -1,7 +1,98 @@
import Foundation
import XCTest
+// swiftlint:disable type_body_length
final class SettingsManagerSyncTests: XCTestCase {
+ func testBookmarkMutationPathReusesEncodedPayload() throws {
+ let source = try loadSource(at: "Sora/Data/Settings/SettingsManager.swift")
+ let addBookmarkSection = try extractFunction(
+ named: "func addBookmark(provider: BooruProvider, tags: [String], folder: UUID? = nil)",
+ from: source
+ )
+ let updateBookmarksSection = try extractFunction(named: "func updateBookmarks(", from: source)
+ let bookmarksSetterSection = try extractFunction(named: "var bookmarks: [SettingsBookmark]", from: source)
+ let normalizedAddBookmark = strippingCommentsAndStrings(from: addBookmarkSection)
+ let normalizedUpdateBookmarks = strippingCommentsAndStrings(from: updateBookmarksSection)
+ let normalizedBookmarksSetter = strippingCommentsAndStrings(from: bookmarksSetterSection)
+
+ let addBookmarkInlineEncodeCount = tokenCount(
+ matching: #"Self\s*\.\s*encode\s*\(\s*updatedBookmarks\s*\)"#,
+ in: normalizedAddBookmark
+ )
+ let updateBookmarksPreEncodedForwardCount = tokenCount(
+ matching: #"syncableData\s*\([\s\S]*?\bencodedData\s*:"#,
+ in: normalizedUpdateBookmarks
+ )
+ let bookmarksSetterPreEncodedForwardCount = tokenCount(
+ matching: #"syncableData\s*\([\s\S]*?\bencodedData\s*:"#,
+ in: normalizedBookmarksSetter
+ )
+
+ // swiftlint:disable:next prefer_nimble
+ XCTAssertEqual(
+ addBookmarkInlineEncodeCount,
+ 0,
+ "addBookmark should not pre-encode and then trigger another encode downstream."
+ )
+ // swiftlint:disable:next prefer_nimble
+ XCTAssertGreaterThan(
+ updateBookmarksPreEncodedForwardCount,
+ 0,
+ "updateBookmarks should forward a pre-encoded payload to syncableData to avoid duplicate encoding."
+ )
+ // swiftlint:disable:next prefer_nimble
+ XCTAssertGreaterThan(
+ bookmarksSetterPreEncodedForwardCount,
+ 0,
+ "bookmarks setter should forward a pre-encoded payload to syncableData."
+ )
+ }
+
+ func testFavoriteMutationPathReusesEncodedPayload() throws {
+ let source = try loadSource(at: "Sora/Data/Settings/SettingsManager.swift")
+ let addFavoriteSection = try extractFunction(
+ named: "func addFavorite(post: BooruPost, provider: BooruProvider, folder: UUID? = nil)",
+ from: source
+ )
+ let updateFavoritesSection = try extractFunction(named: "func updateFavorites(", from: source)
+ let favoritesSetterSection = try extractFunction(named: "var favorites: [SettingsFavoritePost]", from: source)
+ let normalizedAddFavorite = strippingCommentsAndStrings(from: addFavoriteSection)
+ let normalizedUpdateFavorites = strippingCommentsAndStrings(from: updateFavoritesSection)
+ let normalizedFavoritesSetter = strippingCommentsAndStrings(from: favoritesSetterSection)
+
+ let addFavoriteInlineEncodeCount = tokenCount(
+ matching: #"Self\s*\.\s*encode\s*\(\s*updatedFavorites\s*\)"#,
+ in: normalizedAddFavorite
+ )
+ let updateFavoritesPreEncodedForwardCount = tokenCount(
+ matching: #"syncableData\s*\([\s\S]*?\bencodedData\s*:"#,
+ in: normalizedUpdateFavorites
+ )
+ let favoritesSetterPreEncodedForwardCount = tokenCount(
+ matching: #"syncableData\s*\([\s\S]*?\bencodedData\s*:"#,
+ in: normalizedFavoritesSetter
+ )
+
+ // swiftlint:disable:next prefer_nimble
+ XCTAssertEqual(
+ addFavoriteInlineEncodeCount,
+ 0,
+ "addFavorite should not pre-encode and then trigger another encode downstream."
+ )
+ // swiftlint:disable:next prefer_nimble
+ XCTAssertGreaterThan(
+ updateFavoritesPreEncodedForwardCount,
+ 0,
+ "updateFavorites should forward a pre-encoded payload to syncableData to avoid duplicate encoding."
+ )
+ // swiftlint:disable:next prefer_nimble
+ XCTAssertGreaterThan(
+ favoritesSetterPreEncodedForwardCount,
+ 0,
+ "favorites setter should forward a pre-encoded payload to syncableData."
+ )
+ }
+
func testBatchedSyncPathGuardsUnchangedPayloadWrites() throws {
let source = try loadSource(at: "Sora/Data/Settings/SettingsManager.swift")
let triggerSyncSection = try extractFunction(
@@ -235,3 +326,4 @@ final class SettingsManagerSyncTests: XCTestCase {
return regex.numberOfMatches(in: source, range: range)
}
}
+// swiftlint:enable type_body_length