summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Sora/Data/Booru/BooruManager.swift10
-rw-r--r--Sora/Data/Settings/SettingsManager.swift20
-rw-r--r--SoraTests/SettingsManagerSyncTests.swift77
-rw-r--r--SoraTests/ViewDerivedDataTests.swift63
4 files changed, 161 insertions, 9 deletions
diff --git a/Sora/Data/Booru/BooruManager.swift b/Sora/Data/Booru/BooruManager.swift
index 3c4374e..2957fcd 100644
--- a/Sora/Data/Booru/BooruManager.swift
+++ b/Sora/Data/Booru/BooruManager.swift
@@ -30,6 +30,7 @@ class BooruManager: ObservableObject { // swiftlint:disable:this type_body_leng
private let showHeldMoebooruPosts: Bool
private var urlCache: [String: URL] = [:]
private var lastPostCount = 0
+ private var cachedMinimumPostID: Int?
// MARK: - Computed Properties
var tags: [String] {
@@ -340,9 +341,7 @@ class BooruManager: ObservableObject { // swiftlint:disable:this type_body_leng
guard page > 1 else { return "1" }
guard !hasExplicitSortTag(in: tags) else { return String(page) }
- guard let minimumPostID = posts.lazy.compactMap({ Int($0.id) }).min() else {
- return String(page)
- }
+ guard let minimumPostID = cachedMinimumPostID else { return String(page) }
return "b\(minimumPostID)"
}
@@ -545,12 +544,17 @@ class BooruManager: ObservableObject { // swiftlint:disable:this type_body_leng
postIndexMap.removeAll()
lastPostCount = 0
+ cachedMinimumPostID = nil
}
endOfData = newPosts.isEmpty
guard !endOfData else { return }
+ if let nextMinimumPostID = newPosts.lazy.compactMap({ Int($0.id) }).min() {
+ cachedMinimumPostID = min(cachedMinimumPostID ?? nextMinimumPostID, nextMinimumPostID)
+ }
+
withTransaction(Transaction(animation: nil)) {
let oldCount = self.posts.count
diff --git a/Sora/Data/Settings/SettingsManager.swift b/Sora/Data/Settings/SettingsManager.swift
index b07b38c..78aef7c 100644
--- a/Sora/Data/Settings/SettingsManager.swift
+++ b/Sora/Data/Settings/SettingsManager.swift
@@ -198,13 +198,17 @@ class SettingsManager: ObservableObject { // swiftlint:disable:this type_body_l
defer { isUpdatingCache = false }
+ let sortedSearchHistory = newValue.sorted { $0.date > $1.date }
+ let payload = SettingsCodec.encodeOnce(sortedSearchHistory)
+
syncableData(
key: "searchHistory",
localData: $searchHistoryData,
- newValue: newValue,
- ) { $0.sorted { $0.date > $1.date } }
+ newValue: sortedSearchHistory,
+ encodedData: payload?.encodedData
+ ) { $0 }
- searchHistoryCache = newValue.sorted { $0.date > $1.date }
+ searchHistoryCache = sortedSearchHistory
pendingSyncKeys.insert(.searchHistory)
triggerBatchedSync()
@@ -427,13 +431,17 @@ class SettingsManager: ObservableObject { // swiftlint:disable:this type_body_l
defer { isUpdatingCache = false }
+ let sortedSearchHistory = newValue.sorted { $0.date > $1.date }
+ let payload = SettingsCodec.encodeOnce(sortedSearchHistory)
+
syncableData(
key: "searchHistory",
localData: $searchHistoryData,
- newValue: newValue,
- ) { $0.sorted { $0.date > $1.date } }
+ newValue: sortedSearchHistory,
+ encodedData: payload?.encodedData
+ ) { $0 }
- searchHistoryCache = newValue.sorted { $0.date > $1.date }
+ searchHistoryCache = sortedSearchHistory
pendingSyncKeys.insert(.searchHistory)
triggerBatchedSync()
diff --git a/SoraTests/SettingsManagerSyncTests.swift b/SoraTests/SettingsManagerSyncTests.swift
index 50106dc..8ed9d83 100644
--- a/SoraTests/SettingsManagerSyncTests.swift
+++ b/SoraTests/SettingsManagerSyncTests.swift
@@ -91,6 +91,83 @@ final class SettingsManagerSyncTests: XCTestCase {
)
}
+ func testSearchHistoryMutationPathReusesSortedPayloadAndAvoidsInlineResort() throws {
+ let source = try loadSource(at: "Sora/Data/Settings/SettingsManager.swift")
+ let searchHistorySetterSection = try extractFunction(
+ named: "var searchHistory: [BooruSearchQuery]",
+ from: source
+ )
+ let updateSearchHistorySection = try extractFunction(
+ named: "func updateSearchHistory(",
+ from: source
+ )
+ let normalizedSetterSection = strippingCommentsAndStrings(from: searchHistorySetterSection)
+ let normalizedUpdateSection = strippingCommentsAndStrings(from: updateSearchHistorySection)
+ let setterSortedHistoryDeclarationCount = tokenCount(
+ matching: #"\blet\s+sortedSearchHistory\s*="#,
+ in: normalizedSetterSection
+ )
+ let setterInlineSortClosureCount = tokenCount(
+ matching: #"syncableData\s*\([\s\S]*?\)\s*\{\s*\$0\s*\.\s*sorted"#,
+ in: normalizedSetterSection
+ )
+ let setterPreEncodedForwardCount = tokenCount(
+ matching: #"syncableData\s*\([\s\S]*?\bencodedData\s*:"#,
+ in: normalizedSetterSection
+ )
+
+ let updateSortedHistoryDeclarationCount = tokenCount(
+ matching: #"\blet\s+sortedSearchHistory\s*="#,
+ in: normalizedUpdateSection
+ )
+ let updateInlineSortClosureCount = tokenCount(
+ matching: #"syncableData\s*\([\s\S]*?\)\s*\{\s*\$0\s*\.\s*sorted"#,
+ in: normalizedUpdateSection
+ )
+ let updatePreEncodedForwardCount = tokenCount(
+ matching: #"syncableData\s*\([\s\S]*?\bencodedData\s*:"#,
+ in: normalizedUpdateSection
+ )
+
+ // swiftlint:disable:next prefer_nimble
+ XCTAssertGreaterThan(
+ setterSortedHistoryDeclarationCount,
+ 0,
+ "searchHistory setter should precompute sorted values once."
+ )
+ // swiftlint:disable:next prefer_nimble
+ XCTAssertEqual(
+ setterInlineSortClosureCount,
+ 0,
+ "searchHistory setter should avoid inline resorting inside syncableData."
+ )
+ // swiftlint:disable:next prefer_nimble
+ XCTAssertGreaterThan(
+ setterPreEncodedForwardCount,
+ 0,
+ "searchHistory setter should forward a pre-encoded payload to syncableData."
+ )
+
+ // swiftlint:disable:next prefer_nimble
+ XCTAssertGreaterThan(
+ updateSortedHistoryDeclarationCount,
+ 0,
+ "updateSearchHistory should precompute sorted values once."
+ )
+ // swiftlint:disable:next prefer_nimble
+ XCTAssertEqual(
+ updateInlineSortClosureCount,
+ 0,
+ "updateSearchHistory should avoid inline resorting inside syncableData."
+ )
+ // swiftlint:disable:next prefer_nimble
+ XCTAssertGreaterThan(
+ updatePreEncodedForwardCount,
+ 0,
+ "updateSearchHistory 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(
diff --git a/SoraTests/ViewDerivedDataTests.swift b/SoraTests/ViewDerivedDataTests.swift
index 6a27667..d19b51a 100644
--- a/SoraTests/ViewDerivedDataTests.swift
+++ b/SoraTests/ViewDerivedDataTests.swift
@@ -872,6 +872,69 @@ final class ViewDerivedDataTests: XCTestCase { // swiftlint:disable:this type_b
)
}
+ func testBooruManagerDanbooruPaginationCachesMinimumPostIDForCursorToken() throws {
+ let source = try loadSource(at: "Sora/Data/Booru/BooruManager.swift")
+ let pageTokenFunctionSection = try extractFunction(
+ named: "private func danbooruPageToken(for page: Int, tags: [String]) -> String",
+ from: source
+ )
+ let updatePostsSection = try extractFunction(
+ named: "private func updatePosts(_ newPosts: [BooruPost], replace: Bool)",
+ from: source
+ )
+ let cachedMinimumPostIDStorageCount = tokenCount(
+ matching: #"private\s+var\s+cachedMinimumPostID:\s*Int\?"#,
+ in: source
+ )
+ let cachedTokenGuardCount = tokenCount(
+ matching: #"guard\s+let\s+minimumPostID\s*=\s*cachedMinimumPostID"#,
+ in: pageTokenFunctionSection
+ )
+ let directPostsMinScanCount = tokenCount(
+ matching: #"posts\s*\.\s*lazy\s*\.\s*compactMap\s*\(\s*\{\s*Int\(\$0\.id\)\s*\}\s*\)\s*\.\s*min"#,
+ in: pageTokenFunctionSection
+ )
+ let cacheInvalidationOnReplaceCount = tokenCount(
+ matching: #"cachedMinimumPostID\s*=\s*nil"#,
+ in: updatePostsSection
+ )
+ let cacheRefreshCount = tokenCount(
+ matching: #"cachedMinimumPostID\s*=\s*min\("#,
+ in: updatePostsSection
+ )
+
+ // swiftlint:disable:next prefer_nimble
+ XCTAssertGreaterThan(
+ cachedMinimumPostIDStorageCount,
+ 0,
+ "BooruManager should store a cached minimum post ID for Danbooru pagination."
+ )
+ // swiftlint:disable:next prefer_nimble
+ XCTAssertGreaterThan(
+ cachedTokenGuardCount,
+ 0,
+ "Danbooru page token generation should use cached minimum post ID."
+ )
+ // swiftlint:disable:next prefer_nimble
+ XCTAssertEqual(
+ directPostsMinScanCount,
+ 0,
+ "Danbooru page token generation should avoid rescanning all posts for minimum ID."
+ )
+ // swiftlint:disable:next prefer_nimble
+ XCTAssertGreaterThan(
+ cacheInvalidationOnReplaceCount,
+ 0,
+ "Replacing posts should invalidate cached Danbooru minimum post ID."
+ )
+ // swiftlint:disable:next prefer_nimble
+ XCTAssertGreaterThan(
+ cacheRefreshCount,
+ 0,
+ "Appending posts should refresh cached Danbooru minimum post ID incrementally."
+ )
+ }
+
func testBooruManagerDoesNotForceClientSidePostResort() throws {
let source = try loadSource(at: "Sora/Data/Booru/BooruManager.swift")
let retrySection = try extractFunction(