diff options
| author | Fuwn <[email protected]> | 2026-02-23 22:02:59 -0800 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2026-02-23 22:02:59 -0800 |
| commit | 7904b2366a28fca2585b5f5ec0588412e10f0c94 (patch) | |
| tree | e57236200c0f377d0d06f1c90231551f7be3dbed | |
| parent | feat: localize accessibility fallback and value strings (diff) | |
| download | sora-testing-main.tar.xz sora-testing-main.zip | |
| -rw-r--r-- | Sora/Data/Booru/BooruManager.swift | 10 | ||||
| -rw-r--r-- | Sora/Data/Settings/SettingsManager.swift | 20 | ||||
| -rw-r--r-- | SoraTests/SettingsManagerSyncTests.swift | 77 | ||||
| -rw-r--r-- | SoraTests/ViewDerivedDataTests.swift | 63 |
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( |