diff --git a/Frameworks/Account/Account.swift b/Frameworks/Account/Account.swift index ab8c2f463..4c03c47f3 100644 --- a/Frameworks/Account/Account.swift +++ b/Frameworks/Account/Account.swift @@ -689,7 +689,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, database.fetchStarredArticleIDsAsync(webFeedIDs: flattenedWebFeeds().webFeedIDs(), completion: completion) } - /// Fetch articleIDs for articles that we should have, but don’t. These articles are not userDeleted, and they are either (starred) or (unread and newer than the article cutoff date). + /// Fetch articleIDs for articles that we should have, but don’t. These articles are not userDeleted, and they are either (starred) or (newer than the article cutoff date). public func fetchArticleIDsForStatusesWithoutArticlesNewerThanCutoffDate(_ completion: @escaping ArticleIDsCompletionBlock) { database.fetchArticleIDsForStatusesWithoutArticlesNewerThanCutoffDate(completion) } diff --git a/Frameworks/Account/Account.xcodeproj/project.pbxproj b/Frameworks/Account/Account.xcodeproj/project.pbxproj index bd6997e20..929b58708 100644 --- a/Frameworks/Account/Account.xcodeproj/project.pbxproj +++ b/Frameworks/Account/Account.xcodeproj/project.pbxproj @@ -99,7 +99,6 @@ 9E1773D7234575AB0056A5A8 /* FeedlyTag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1773D6234575AB0056A5A8 /* FeedlyTag.swift */; }; 9E1773D923458D590056A5A8 /* FeedlyResourceId.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1773D823458D590056A5A8 /* FeedlyResourceId.swift */; }; 9E1773DB234593CF0056A5A8 /* FeedlyResourceIdTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1773DA234593CF0056A5A8 /* FeedlyResourceIdTests.swift */; }; - 9E1AF38B2353D41A008BD1D5 /* FeedlySetStarredArticlesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1AF38A2353D41A008BD1D5 /* FeedlySetStarredArticlesOperation.swift */; }; 9E1D154D233370D800F4944C /* FeedlySyncAllOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1D154C233370D800F4944C /* FeedlySyncAllOperation.swift */; }; 9E1D154F233371DD00F4944C /* FeedlyGetCollectionsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1D154E233371DD00F4944C /* FeedlyGetCollectionsOperation.swift */; }; 9E1D15512334282100F4944C /* FeedlyMirrorCollectionsAsFoldersOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1D15502334282100F4944C /* FeedlyMirrorCollectionsAsFoldersOperation.swift */; }; @@ -113,15 +112,14 @@ 9E1FF8642368EC2400834C24 /* FeedlySyncAllOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1FF8632368EC2400834C24 /* FeedlySyncAllOperationTests.swift */; }; 9E1FF8662368ED7E00834C24 /* TestMarkArticlesService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1FF8652368ED7E00834C24 /* TestMarkArticlesService.swift */; }; 9E1FF8682368EE4900834C24 /* TestGetCollectionsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1FF8672368EE4900834C24 /* TestGetCollectionsService.swift */; }; - 9E3CFFFD2368202000BA7365 /* FeedlySyncUnreadStatusesOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E3CFFFC2368202000BA7365 /* FeedlySyncUnreadStatusesOperationTests.swift */; }; - 9E4828F223617F4A00D68691 /* FeedlySetUnreadArticlesOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E4828F123617F4A00D68691 /* FeedlySetUnreadArticlesOperationTests.swift */; }; + 9E44C90F23C6FF3600CCC286 /* FeedlyIngestStreamArticleIdsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E44C90E23C6FF3600CCC286 /* FeedlyIngestStreamArticleIdsOperation.swift */; }; 9E489E8D2360652C004372EE /* FeedlyGetStreamIdsOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E489E8C2360652C004372EE /* FeedlyGetStreamIdsOperationTests.swift */; }; 9E489E912360ED30004372EE /* FeedlyOrganiseParsedItemsByFeedOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E489E902360ED30004372EE /* FeedlyOrganiseParsedItemsByFeedOperationTests.swift */; }; 9E489E93236101FC004372EE /* FeedlyUpdateAccountFeedsWithItemsOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E489E92236101FC004372EE /* FeedlyUpdateAccountFeedsWithItemsOperationTests.swift */; }; 9E5ABE9A236BE6BD00B5DE9F /* feedly-1-initial in Resources */ = {isa = PBXBuildFile; fileRef = 9E5ABE99236BE6BC00B5DE9F /* feedly-1-initial */; }; + 9E5DE60E23C3F4B70064DA30 /* FeedlyFetchIdsForMissingArticlesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E5DE60D23C3F4B70064DA30 /* FeedlyFetchIdsForMissingArticlesOperation.swift */; }; 9E672394236F7CA0000BE141 /* FeedlyRefreshAccessTokenOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E672393236F7CA0000BE141 /* FeedlyRefreshAccessTokenOperation.swift */; }; 9E672396236F7E68000BE141 /* OAuthAcessTokenRefreshing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E672395236F7E68000BE141 /* OAuthAcessTokenRefreshing.swift */; }; - 9E713653233AD63E00765C84 /* FeedlySetUnreadArticlesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E713652233AD63E00765C84 /* FeedlySetUnreadArticlesOperation.swift */; }; 9E7299D723505E9600DAEFB7 /* FeedlyAddFeedToCollectionOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7299D623505E9600DAEFB7 /* FeedlyAddFeedToCollectionOperation.swift */; }; 9E7299D9235062A200DAEFB7 /* FeedlyResourceProviding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7299D8235062A200DAEFB7 /* FeedlyResourceProviding.swift */; }; 9E784EBE237E890600099B1B /* FeedlyLogoutOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E784EBD237E890600099B1B /* FeedlyLogoutOperation.swift */; }; @@ -129,9 +127,8 @@ 9E79F7742395C9F00031DB98 /* feedly-add-new-feed in Resources */ = {isa = PBXBuildFile; fileRef = 9E79F7732395C9EF0031DB98 /* feedly-add-new-feed */; }; 9E7F88AC235EDDC2009AB9DF /* FeedlyCreateFeedsForCollectionFoldersOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7F88AB235EDDC2009AB9DF /* FeedlyCreateFeedsForCollectionFoldersOperationTests.swift */; }; 9E7F88AE235FBB11009AB9DF /* FeedlyGetStreamContentsOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7F88AD235FBB11009AB9DF /* FeedlyGetStreamContentsOperationTests.swift */; }; - 9E84DC472359A23200D6E809 /* FeedlySyncUnreadStatusesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E84DC462359A23200D6E809 /* FeedlySyncUnreadStatusesOperation.swift */; }; + 9E84DC472359A23200D6E809 /* FeedlyIngestUnreadArticleIdsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E84DC462359A23200D6E809 /* FeedlyIngestUnreadArticleIdsOperation.swift */; }; 9E84DC492359A73600D6E809 /* FeedlyCheckpointOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E84DC482359A73600D6E809 /* FeedlyCheckpointOperation.swift */; }; - 9E85C8E42366FE0100D0F1F7 /* FeedlySyncStarredArticlesOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E85C8E32366FE0100D0F1F7 /* FeedlySyncStarredArticlesOperationTests.swift */; }; 9E85C8E62366FED600D0F1F7 /* TestGetStreamContentsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E85C8E52366FED600D0F1F7 /* TestGetStreamContentsService.swift */; }; 9E85C8E82366FF4200D0F1F7 /* TestGetPagedStreamContentsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E85C8E72366FF4200D0F1F7 /* TestGetPagedStreamContentsService.swift */; }; 9E85C8EB236700E600D0F1F7 /* FeedlyGetEntriesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E85C8E9236700AD00D0F1F7 /* FeedlyGetEntriesOperation.swift */; }; @@ -143,6 +140,7 @@ 9EA643D3239305680018A28C /* FeedlySearchOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EA643D2239305680018A28C /* FeedlySearchOperation.swift */; }; 9EA643D5239306AC0018A28C /* FeedlyFeedsSearchResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EA643D4239306AC0018A28C /* FeedlyFeedsSearchResponse.swift */; }; 9EA643D923945CE00018A28C /* FeedlyAddNewFeedOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EA643D823945CE00018A28C /* FeedlyAddNewFeedOperationTests.swift */; }; + 9EAADA1023C93144003A801F /* TestGetEntriesService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAADA0F23C93144003A801F /* TestGetEntriesService.swift */; }; 9EAEC60C2332FE830085D7C9 /* FeedlyCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAEC60B2332FE830085D7C9 /* FeedlyCollection.swift */; }; 9EAEC60E2332FEC20085D7C9 /* FeedlyFeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAEC60D2332FEC20085D7C9 /* FeedlyFeed.swift */; }; 9EAEC624233315F60085D7C9 /* FeedlyEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAEC623233315F60085D7C9 /* FeedlyEntry.swift */; }; @@ -150,7 +148,8 @@ 9EAEC62823331C350085D7C9 /* FeedlyCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAEC62723331C350085D7C9 /* FeedlyCategory.swift */; }; 9EAEC62A23331EE70085D7C9 /* FeedlyOrigin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAEC62923331EE70085D7C9 /* FeedlyOrigin.swift */; }; 9EB1D576238E6A3900A753D7 /* FeedlyAddNewFeedOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EB1D575238E6A3900A753D7 /* FeedlyAddNewFeedOperation.swift */; }; - 9EC228552362C17F00766EF8 /* FeedlySetStarredArticlesOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EC228542362C17F00766EF8 /* FeedlySetStarredArticlesOperationTests.swift */; }; + 9EBD49C023C67602005AD5CD /* FeedlyDownloadArticlesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EBD49BF23C67602005AD5CD /* FeedlyDownloadArticlesOperation.swift */; }; + 9EBD49C223C67784005AD5CD /* FeedlyEntryIdentifierProviding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EBD49C123C67784005AD5CD /* FeedlyEntryIdentifierProviding.swift */; }; 9EC228572362C7F900766EF8 /* FeedlyCheckpointOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EC228562362C7F900766EF8 /* FeedlyCheckpointOperationTests.swift */; }; 9EC228592362D0EA00766EF8 /* FeedlySendArticleStatusesOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EC228582362D0EA00766EF8 /* FeedlySendArticleStatusesOperationTests.swift */; }; 9EC2285B23639A6500766EF8 /* FeedlySyncStreamContentsOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EC2285A23639A6500766EF8 /* FeedlySyncStreamContentsOperationTests.swift */; }; @@ -173,10 +172,11 @@ 9EEAE073235D01AE00E3FEE4 /* FeedlyGetStreamIdsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EEAE072235D01AE00E3FEE4 /* FeedlyGetStreamIdsService.swift */; }; 9EEAE075235D01C400E3FEE4 /* FeedlyMarkArticlesService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EEAE074235D01C400E3FEE4 /* FeedlyMarkArticlesService.swift */; }; 9EEEF71F23545CB4009E9D80 /* FeedlySendArticleStatusesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EEEF71E23545CB4009E9D80 /* FeedlySendArticleStatusesOperation.swift */; }; - 9EEEF7212355277F009E9D80 /* FeedlySyncStarredArticlesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EEEF7202355277F009E9D80 /* FeedlySyncStarredArticlesOperation.swift */; }; + 9EEEF7212355277F009E9D80 /* FeedlyIngestStarredArticleIdsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EEEF7202355277F009E9D80 /* FeedlyIngestStarredArticleIdsOperation.swift */; }; 9EF1B10323584B4C000A486A /* FeedlySyncStreamContentsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EF1B10223584B4C000A486A /* FeedlySyncStreamContentsOperation.swift */; }; 9EF1B10723590D61000A486A /* FeedlyGetStreamIdsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EF1B10623590D61000A486A /* FeedlyGetStreamIdsOperation.swift */; }; 9EF1B10923590E93000A486A /* FeedlyStreamIds.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EF1B10823590E93000A486A /* FeedlyStreamIds.swift */; }; + 9EF2602C23C91FFE006D160C /* FeedlyGetUpdatedArticleIdsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EF2602B23C91FFE006D160C /* FeedlyGetUpdatedArticleIdsOperation.swift */; }; 9EF35F7A234E830E003AE2AE /* FeedlyCompoundOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EF35F79234E830E003AE2AE /* FeedlyCompoundOperation.swift */; }; /* End PBXBuildFile section */ @@ -322,7 +322,6 @@ 9E1773D6234575AB0056A5A8 /* FeedlyTag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyTag.swift; sourceTree = ""; }; 9E1773D823458D590056A5A8 /* FeedlyResourceId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyResourceId.swift; sourceTree = ""; }; 9E1773DA234593CF0056A5A8 /* FeedlyResourceIdTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyResourceIdTests.swift; sourceTree = ""; }; - 9E1AF38A2353D41A008BD1D5 /* FeedlySetStarredArticlesOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlySetStarredArticlesOperation.swift; sourceTree = ""; }; 9E1D154C233370D800F4944C /* FeedlySyncAllOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlySyncAllOperation.swift; sourceTree = ""; }; 9E1D154E233371DD00F4944C /* FeedlyGetCollectionsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyGetCollectionsOperation.swift; sourceTree = ""; }; 9E1D15502334282100F4944C /* FeedlyMirrorCollectionsAsFoldersOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyMirrorCollectionsAsFoldersOperation.swift; sourceTree = ""; }; @@ -336,15 +335,14 @@ 9E1FF8632368EC2400834C24 /* FeedlySyncAllOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlySyncAllOperationTests.swift; sourceTree = ""; }; 9E1FF8652368ED7E00834C24 /* TestMarkArticlesService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestMarkArticlesService.swift; sourceTree = ""; }; 9E1FF8672368EE4900834C24 /* TestGetCollectionsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestGetCollectionsService.swift; sourceTree = ""; }; - 9E3CFFFC2368202000BA7365 /* FeedlySyncUnreadStatusesOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlySyncUnreadStatusesOperationTests.swift; sourceTree = ""; }; - 9E4828F123617F4A00D68691 /* FeedlySetUnreadArticlesOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlySetUnreadArticlesOperationTests.swift; sourceTree = ""; }; + 9E44C90E23C6FF3600CCC286 /* FeedlyIngestStreamArticleIdsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyIngestStreamArticleIdsOperation.swift; sourceTree = ""; }; 9E489E8C2360652C004372EE /* FeedlyGetStreamIdsOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyGetStreamIdsOperationTests.swift; sourceTree = ""; }; 9E489E902360ED30004372EE /* FeedlyOrganiseParsedItemsByFeedOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyOrganiseParsedItemsByFeedOperationTests.swift; sourceTree = ""; }; 9E489E92236101FC004372EE /* FeedlyUpdateAccountFeedsWithItemsOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyUpdateAccountFeedsWithItemsOperationTests.swift; sourceTree = ""; }; 9E5ABE99236BE6BC00B5DE9F /* feedly-1-initial */ = {isa = PBXFileReference; lastKnownFileType = folder; path = "feedly-1-initial"; sourceTree = ""; }; + 9E5DE60D23C3F4B70064DA30 /* FeedlyFetchIdsForMissingArticlesOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyFetchIdsForMissingArticlesOperation.swift; sourceTree = ""; }; 9E672393236F7CA0000BE141 /* FeedlyRefreshAccessTokenOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyRefreshAccessTokenOperation.swift; sourceTree = ""; }; 9E672395236F7E68000BE141 /* OAuthAcessTokenRefreshing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OAuthAcessTokenRefreshing.swift; sourceTree = ""; }; - 9E713652233AD63E00765C84 /* FeedlySetUnreadArticlesOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlySetUnreadArticlesOperation.swift; sourceTree = ""; }; 9E7299D623505E9600DAEFB7 /* FeedlyAddFeedToCollectionOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyAddFeedToCollectionOperation.swift; sourceTree = ""; }; 9E7299D8235062A200DAEFB7 /* FeedlyResourceProviding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyResourceProviding.swift; sourceTree = ""; }; 9E784EBD237E890600099B1B /* FeedlyLogoutOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyLogoutOperation.swift; sourceTree = ""; }; @@ -352,9 +350,8 @@ 9E79F7732395C9EF0031DB98 /* feedly-add-new-feed */ = {isa = PBXFileReference; lastKnownFileType = folder; path = "feedly-add-new-feed"; sourceTree = ""; }; 9E7F88AB235EDDC2009AB9DF /* FeedlyCreateFeedsForCollectionFoldersOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyCreateFeedsForCollectionFoldersOperationTests.swift; sourceTree = ""; }; 9E7F88AD235FBB11009AB9DF /* FeedlyGetStreamContentsOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyGetStreamContentsOperationTests.swift; sourceTree = ""; }; - 9E84DC462359A23200D6E809 /* FeedlySyncUnreadStatusesOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlySyncUnreadStatusesOperation.swift; sourceTree = ""; }; + 9E84DC462359A23200D6E809 /* FeedlyIngestUnreadArticleIdsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyIngestUnreadArticleIdsOperation.swift; sourceTree = ""; }; 9E84DC482359A73600D6E809 /* FeedlyCheckpointOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyCheckpointOperation.swift; sourceTree = ""; }; - 9E85C8E32366FE0100D0F1F7 /* FeedlySyncStarredArticlesOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlySyncStarredArticlesOperationTests.swift; sourceTree = ""; }; 9E85C8E52366FED600D0F1F7 /* TestGetStreamContentsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestGetStreamContentsService.swift; sourceTree = ""; }; 9E85C8E72366FF4200D0F1F7 /* TestGetPagedStreamContentsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestGetPagedStreamContentsService.swift; sourceTree = ""; }; 9E85C8E9236700AD00D0F1F7 /* FeedlyGetEntriesOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyGetEntriesOperation.swift; sourceTree = ""; }; @@ -366,6 +363,7 @@ 9EA643D2239305680018A28C /* FeedlySearchOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlySearchOperation.swift; sourceTree = ""; }; 9EA643D4239306AC0018A28C /* FeedlyFeedsSearchResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyFeedsSearchResponse.swift; sourceTree = ""; }; 9EA643D823945CE00018A28C /* FeedlyAddNewFeedOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyAddNewFeedOperationTests.swift; sourceTree = ""; }; + 9EAADA0F23C93144003A801F /* TestGetEntriesService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestGetEntriesService.swift; sourceTree = ""; }; 9EAEC60B2332FE830085D7C9 /* FeedlyCollection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyCollection.swift; sourceTree = ""; }; 9EAEC60D2332FEC20085D7C9 /* FeedlyFeed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyFeed.swift; sourceTree = ""; }; 9EAEC623233315F60085D7C9 /* FeedlyEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyEntry.swift; sourceTree = ""; }; @@ -373,7 +371,8 @@ 9EAEC62723331C350085D7C9 /* FeedlyCategory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyCategory.swift; sourceTree = ""; }; 9EAEC62923331EE70085D7C9 /* FeedlyOrigin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyOrigin.swift; sourceTree = ""; }; 9EB1D575238E6A3900A753D7 /* FeedlyAddNewFeedOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyAddNewFeedOperation.swift; sourceTree = ""; }; - 9EC228542362C17F00766EF8 /* FeedlySetStarredArticlesOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlySetStarredArticlesOperationTests.swift; sourceTree = ""; }; + 9EBD49BF23C67602005AD5CD /* FeedlyDownloadArticlesOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyDownloadArticlesOperation.swift; sourceTree = ""; }; + 9EBD49C123C67784005AD5CD /* FeedlyEntryIdentifierProviding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyEntryIdentifierProviding.swift; sourceTree = ""; }; 9EC228562362C7F900766EF8 /* FeedlyCheckpointOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyCheckpointOperationTests.swift; sourceTree = ""; }; 9EC228582362D0EA00766EF8 /* FeedlySendArticleStatusesOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlySendArticleStatusesOperationTests.swift; sourceTree = ""; }; 9EC2285A23639A6500766EF8 /* FeedlySyncStreamContentsOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlySyncStreamContentsOperationTests.swift; sourceTree = ""; }; @@ -396,10 +395,11 @@ 9EEAE072235D01AE00E3FEE4 /* FeedlyGetStreamIdsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyGetStreamIdsService.swift; sourceTree = ""; }; 9EEAE074235D01C400E3FEE4 /* FeedlyMarkArticlesService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyMarkArticlesService.swift; sourceTree = ""; }; 9EEEF71E23545CB4009E9D80 /* FeedlySendArticleStatusesOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlySendArticleStatusesOperation.swift; sourceTree = ""; }; - 9EEEF7202355277F009E9D80 /* FeedlySyncStarredArticlesOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlySyncStarredArticlesOperation.swift; sourceTree = ""; }; + 9EEEF7202355277F009E9D80 /* FeedlyIngestStarredArticleIdsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyIngestStarredArticleIdsOperation.swift; sourceTree = ""; }; 9EF1B10223584B4C000A486A /* FeedlySyncStreamContentsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlySyncStreamContentsOperation.swift; sourceTree = ""; }; 9EF1B10623590D61000A486A /* FeedlyGetStreamIdsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyGetStreamIdsOperation.swift; sourceTree = ""; }; 9EF1B10823590E93000A486A /* FeedlyStreamIds.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyStreamIds.swift; sourceTree = ""; }; + 9EF2602B23C91FFE006D160C /* FeedlyGetUpdatedArticleIdsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyGetUpdatedArticleIdsOperation.swift; sourceTree = ""; }; 9EF35F79234E830E003AE2AE /* FeedlyCompoundOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyCompoundOperation.swift; sourceTree = ""; }; D511EEB5202422BB00712EC3 /* Account_project_debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Account_project_debug.xcconfig; sourceTree = ""; }; D511EEB6202422BB00712EC3 /* Account_target.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Account_target.xcconfig; sourceTree = ""; }; @@ -648,6 +648,7 @@ 9E85C8E72366FF4200D0F1F7 /* TestGetPagedStreamContentsService.swift */, 9E1FF8612368219B00834C24 /* TestGetPagedStreamIdsService.swift */, 9E1FF8672368EE4900834C24 /* TestGetCollectionsService.swift */, + 9EAADA0F23C93144003A801F /* TestGetEntriesService.swift */, 9E1FF8652368ED7E00834C24 /* TestMarkArticlesService.swift */, 9E03C11B235D921400FB6D9E /* FeedlyOperationTests.swift */, 9E03C11D235D976500FB6D9E /* FeedlyGetCollectionsOperationTests.swift */, @@ -657,13 +658,9 @@ 9E489E8C2360652C004372EE /* FeedlyGetStreamIdsOperationTests.swift */, 9E489E902360ED30004372EE /* FeedlyOrganiseParsedItemsByFeedOperationTests.swift */, 9E489E92236101FC004372EE /* FeedlyUpdateAccountFeedsWithItemsOperationTests.swift */, - 9E4828F123617F4A00D68691 /* FeedlySetUnreadArticlesOperationTests.swift */, - 9EC228542362C17F00766EF8 /* FeedlySetStarredArticlesOperationTests.swift */, 9EC228562362C7F900766EF8 /* FeedlyCheckpointOperationTests.swift */, 9EC228582362D0EA00766EF8 /* FeedlySendArticleStatusesOperationTests.swift */, 9EC2285A23639A6500766EF8 /* FeedlySyncStreamContentsOperationTests.swift */, - 9E85C8E32366FE0100D0F1F7 /* FeedlySyncStarredArticlesOperationTests.swift */, - 9E3CFFFC2368202000BA7365 /* FeedlySyncUnreadStatusesOperationTests.swift */, 9E1FF8632368EC2400834C24 /* FeedlySyncAllOperationTests.swift */, 9EC804E2236C18AB0057CFCB /* FeedlySyncAllMockResponseProvider.swift */, 9E1773DA234593CF0056A5A8 /* FeedlyResourceIdTests.swift */, @@ -718,16 +715,18 @@ 9E1D155A2334423300F4944C /* FeedlyOrganiseParsedItemsByFeedOperation.swift */, 9E1D155C233447F000F4944C /* FeedlyUpdateAccountFeedsWithItemsOperation.swift */, 9E85C8E9236700AD00D0F1F7 /* FeedlyGetEntriesOperation.swift */, - 9E713652233AD63E00765C84 /* FeedlySetUnreadArticlesOperation.swift */, - 9E1AF38A2353D41A008BD1D5 /* FeedlySetStarredArticlesOperation.swift */, 9EEEF71E23545CB4009E9D80 /* FeedlySendArticleStatusesOperation.swift */, 9E84DC482359A73600D6E809 /* FeedlyCheckpointOperation.swift */, 9EF1B10223584B4C000A486A /* FeedlySyncStreamContentsOperation.swift */, - 9EEEF7202355277F009E9D80 /* FeedlySyncStarredArticlesOperation.swift */, - 9E84DC462359A23200D6E809 /* FeedlySyncUnreadStatusesOperation.swift */, + 9E44C90E23C6FF3600CCC286 /* FeedlyIngestStreamArticleIdsOperation.swift */, + 9EEEF7202355277F009E9D80 /* FeedlyIngestStarredArticleIdsOperation.swift */, + 9E84DC462359A23200D6E809 /* FeedlyIngestUnreadArticleIdsOperation.swift */, + 9EF2602B23C91FFE006D160C /* FeedlyGetUpdatedArticleIdsOperation.swift */, 9E1D154C233370D800F4944C /* FeedlySyncAllOperation.swift */, 9E672393236F7CA0000BE141 /* FeedlyRefreshAccessTokenOperation.swift */, 9E784EBD237E890600099B1B /* FeedlyLogoutOperation.swift */, + 9E5DE60D23C3F4B70064DA30 /* FeedlyFetchIdsForMissingArticlesOperation.swift */, + 9EBD49BF23C67602005AD5CD /* FeedlyDownloadArticlesOperation.swift */, ); path = Operations; sourceTree = ""; @@ -747,6 +746,7 @@ 9E1773D22345700E0056A5A8 /* FeedlyLink.swift */, 9E1773D6234575AB0056A5A8 /* FeedlyTag.swift */, 9EA643D4239306AC0018A28C /* FeedlyFeedsSearchResponse.swift */, + 9EBD49C123C67784005AD5CD /* FeedlyEntryIdentifierProviding.swift */, ); path = Models; sourceTree = ""; @@ -1000,12 +1000,12 @@ 9EAEC62A23331EE70085D7C9 /* FeedlyOrigin.swift in Sources */, 511B9804237CD4270028BCAA /* FeedIdentifier.swift in Sources */, 84F73CF1202788D90000BCEF /* ArticleFetcher.swift in Sources */, - 9E713653233AD63E00765C84 /* FeedlySetUnreadArticlesOperation.swift in Sources */, 841974251F6DDCE4006346C4 /* AccountDelegate.swift in Sources */, 510BD113232C3E9D002692E4 /* WebFeedMetadataFile.swift in Sources */, 5165D73122837F3400D9D53D /* InitialFeedDownloader.swift in Sources */, 9E784EBE237E890600099B1B /* FeedlyLogoutOperation.swift in Sources */, 9EEEF71F23545CB4009E9D80 /* FeedlySendArticleStatusesOperation.swift in Sources */, + 9EBD49C223C67784005AD5CD /* FeedlyEntryIdentifierProviding.swift in Sources */, 846E77541F6F00E300A165E2 /* AccountManager.swift in Sources */, 515E4EB72324FF8C0057B0E7 /* Credentials.swift in Sources */, 51E490362288C37100C791F0 /* FeedbinDate.swift in Sources */, @@ -1015,6 +1015,7 @@ 9E84DC492359A73600D6E809 /* FeedlyCheckpointOperation.swift in Sources */, 9E85C8EB236700E600D0F1F7 /* FeedlyGetEntriesOperation.swift in Sources */, 9E1D154D233370D800F4944C /* FeedlySyncAllOperation.swift in Sources */, + 9E44C90F23C6FF3600CCC286 /* FeedlyIngestStreamArticleIdsOperation.swift in Sources */, 844B297D2106C7EC004020B3 /* WebFeed.swift in Sources */, 3B826DA72385C81C00FC1ADB /* FeedWranglerAuthorizationResult.swift in Sources */, 9E964EBA23754B4000A7AF2E /* OAuthAccountAuthorizationOperation.swift in Sources */, @@ -1031,10 +1032,10 @@ 9EF1B10323584B4C000A486A /* FeedlySyncStreamContentsOperation.swift in Sources */, 5154367B228EEB28005E1CDF /* FeedbinImportResult.swift in Sources */, 84B2D4D02238CD8A00498ADA /* WebFeedMetadata.swift in Sources */, - 9E84DC472359A23200D6E809 /* FeedlySyncUnreadStatusesOperation.swift in Sources */, + 9E84DC472359A23200D6E809 /* FeedlyIngestUnreadArticleIdsOperation.swift in Sources */, 9EAEC624233315F60085D7C9 /* FeedlyEntry.swift in Sources */, 9EEAE073235D01AE00E3FEE4 /* FeedlyGetStreamIdsService.swift in Sources */, - 9EEEF7212355277F009E9D80 /* FeedlySyncStarredArticlesOperation.swift in Sources */, + 9EEEF7212355277F009E9D80 /* FeedlyIngestStarredArticleIdsOperation.swift in Sources */, 3BC23AB92385ECB100371CBA /* FeedWranglerSubscriptionResult.swift in Sources */, 5144EA49227B497600D19003 /* FeedbinAPICaller.swift in Sources */, 84B99C9F1FAE8D3200ECDEDB /* ContainerPath.swift in Sources */, @@ -1044,6 +1045,7 @@ 55203300229D5D5A009559E0 /* ReaderAPICaller.swift in Sources */, 9E1D154F233371DD00F4944C /* FeedlyGetCollectionsOperation.swift in Sources */, 9EAEC626233318400085D7C9 /* FeedlyStream.swift in Sources */, + 9E5DE60E23C3F4B70064DA30 /* FeedlyFetchIdsForMissingArticlesOperation.swift in Sources */, 3B826DA92385C81C00FC1ADB /* FeedWranglerAPICaller.swift in Sources */, 9EAEC60C2332FE830085D7C9 /* FeedlyCollection.swift in Sources */, 51E3EB41229AF61B00645299 /* AccountError.swift in Sources */, @@ -1052,6 +1054,7 @@ 552032F8229D5D5A009559E0 /* ReaderAPIEntry.swift in Sources */, 552032FB229D5D5A009559E0 /* ReaderAPITag.swift in Sources */, 5165D72822835F7800D9D53D /* FeedFinder.swift in Sources */, + 9EBD49C023C67602005AD5CD /* FeedlyDownloadArticlesOperation.swift in Sources */, 51D58755227F53BE00900287 /* FeedbinTag.swift in Sources */, 9E1D155B2334423300F4944C /* FeedlyOrganiseParsedItemsByFeedOperation.swift in Sources */, 552032FE229D5D5A009559E0 /* ReaderAPIAccountDelegate.swift in Sources */, @@ -1079,10 +1082,10 @@ 9E1773D5234570E30056A5A8 /* FeedlyEntryParser.swift in Sources */, 51BFDECE238B508D00216323 /* ContainerIdentifier.swift in Sources */, 9E1D1555233431A600F4944C /* FeedlyOperation.swift in Sources */, - 9E1AF38B2353D41A008BD1D5 /* FeedlySetStarredArticlesOperation.swift in Sources */, 84F1F06E2243524700DA0616 /* AccountMetadata.swift in Sources */, 9EF1B10723590D61000A486A /* FeedlyGetStreamIdsOperation.swift in Sources */, 84245C851FDDD8CB0074AFBB /* FeedbinSubscription.swift in Sources */, + 9EF2602C23C91FFE006D160C /* FeedlyGetUpdatedArticleIdsOperation.swift in Sources */, 3B826DAA2385C81C00FC1ADB /* FeedWranglerSubscription.swift in Sources */, 3B826DAC2385C81C00FC1ADB /* FeedWranglerAccountDelegate.swift in Sources */, ); @@ -1094,9 +1097,8 @@ files = ( 9EC228572362C7F900766EF8 /* FeedlyCheckpointOperationTests.swift in Sources */, 9E03C122235E62E100FB6D9E /* FeedlyTestSupport.swift in Sources */, - 9E3CFFFD2368202000BA7365 /* FeedlySyncUnreadStatusesOperationTests.swift in Sources */, + 9EAADA1023C93144003A801F /* TestGetEntriesService.swift in Sources */, 9E784EC0237E8BE100099B1B /* FeedlyLogoutOperationTests.swift in Sources */, - 9EC228552362C17F00766EF8 /* FeedlySetStarredArticlesOperationTests.swift in Sources */, 9E03C120235E62A500FB6D9E /* FeedlyMirrorCollectionsAsFoldersOperationTests.swift in Sources */, 9E489E912360ED30004372EE /* FeedlyOrganiseParsedItemsByFeedOperationTests.swift in Sources */, 9E0260CB236FF99A00D122D3 /* FeedlyRefreshAccessTokenOperationTests.swift in Sources */, @@ -1117,10 +1119,8 @@ 51D5875E227F643C00900287 /* AccountFeedbinFolderSyncTest.swift in Sources */, 9EC804E3236C18AB0057CFCB /* FeedlySyncAllMockResponseProvider.swift in Sources */, 9E1FF8682368EE4900834C24 /* TestGetCollectionsService.swift in Sources */, - 9E4828F223617F4A00D68691 /* FeedlySetUnreadArticlesOperationTests.swift in Sources */, 5107A09B227DE49500C7C3C5 /* TestAccountManager.swift in Sources */, 513323082281070D00C30F19 /* AccountFeedbinSyncTest.swift in Sources */, - 9E85C8E42366FE0100D0F1F7 /* FeedlySyncStarredArticlesOperationTests.swift in Sources */, 5107A09D227DE77700C7C3C5 /* TestTransport.swift in Sources */, 5107A099227DE42E00C7C3C5 /* AccountCredentialsTest.swift in Sources */, 9EC2285B23639A6500766EF8 /* FeedlySyncStreamContentsOperationTests.swift in Sources */, diff --git a/Frameworks/Account/AccountTests/Feedly/FeedlyOrganiseParsedItemsByFeedOperationTests.swift b/Frameworks/Account/AccountTests/Feedly/FeedlyOrganiseParsedItemsByFeedOperationTests.swift index 71b29f0b6..eb4e7405d 100644 --- a/Frameworks/Account/AccountTests/Feedly/FeedlyOrganiseParsedItemsByFeedOperationTests.swift +++ b/Frameworks/Account/AccountTests/Feedly/FeedlyOrganiseParsedItemsByFeedOperationTests.swift @@ -28,6 +28,7 @@ class FeedlyOrganiseParsedItemsByFeedOperationTests: XCTestCase { } struct TestParsedItemsProvider: FeedlyParsedItemProviding { + let parsedItemProviderName = "TestParsedItemsProvider" var resource: FeedlyResourceId var parsedEntries: Set } @@ -51,7 +52,6 @@ class FeedlyOrganiseParsedItemsByFeedOperationTests: XCTestCase { let itemsAndFeedIds = organise.parsedItemsKeyedByFeedId XCTAssertEqual(itemsAndFeedIds, entries) - XCTAssertEqual(resource.id, organise.providerName) } func testGroupsOneEntryByFeedId() { @@ -73,7 +73,6 @@ class FeedlyOrganiseParsedItemsByFeedOperationTests: XCTestCase { let itemsAndFeedIds = organise.parsedItemsKeyedByFeedId XCTAssertEqual(itemsAndFeedIds, entries) - XCTAssertEqual(resource.id, organise.providerName) } func testGroupsManyEntriesByFeedId() { @@ -95,6 +94,5 @@ class FeedlyOrganiseParsedItemsByFeedOperationTests: XCTestCase { let itemsAndFeedIds = organise.parsedItemsKeyedByFeedId XCTAssertEqual(itemsAndFeedIds, entries) - XCTAssertEqual(resource.id, organise.providerName) } } diff --git a/Frameworks/Account/AccountTests/Feedly/FeedlySetStarredArticlesOperationTests.swift b/Frameworks/Account/AccountTests/Feedly/FeedlySetStarredArticlesOperationTests.swift deleted file mode 100644 index cdc0be0ad..000000000 --- a/Frameworks/Account/AccountTests/Feedly/FeedlySetStarredArticlesOperationTests.swift +++ /dev/null @@ -1,489 +0,0 @@ -// -// FeedlySetStarredArticlesOperationTests.swift -// AccountTests -// -// Created by Kiel Gillard on 25/10/19. -// Copyright © 2019 Ranchero Software, LLC. All rights reserved. -// - -import XCTest -@testable import Account -import RSParser - -class FeedlySetStarredArticlesOperationTests: XCTestCase { - - private var account: Account! - private let support = FeedlyTestSupport() - - override func setUp() { - super.setUp() - account = support.makeTestAccount() - } - - override func tearDown() { - if let account = account { - support.destroy(account) - } - super.tearDown() - } - - // MARK: - Ensuring Unread Status By Id - - struct TestStarredArticleProvider: FeedlyStarredEntryIdProviding { - var entryIds: Set - } - - func testEmptyArticleIds() { - let testIds = Set() - let provider = TestStarredArticleProvider(entryIds: testIds) - - let setStarred = FeedlySetStarredArticlesOperation(account: account, allStarredEntryIdsProvider: provider, log: support.log) - - let completionExpectation = expectation(description: "Did Finish") - setStarred.completionBlock = { - completionExpectation.fulfill() - } - - OperationQueue.main.addOperation(setStarred) - - waitForExpectations(timeout: 2) - - let fetchIdsExpectation = expectation(description: "Fetch Article Ids") - account.fetchStarredArticleIDs { accountArticlesIDsResult in - do { - let accountArticlesIDs = try accountArticlesIDsResult.get() - XCTAssertTrue(accountArticlesIDs.isEmpty) - XCTAssertEqual(accountArticlesIDs, testIds) - fetchIdsExpectation.fulfill() - } catch { - XCTFail("Error checking articles IDs: \(error)") - } - } - waitForExpectations(timeout: 2) - } - - func testSetOneArticleIdStarred() { - let testIds = Set(["feed/0/article/0"]) - let provider = TestStarredArticleProvider(entryIds: testIds) - - let setStarred = FeedlySetStarredArticlesOperation(account: account, allStarredEntryIdsProvider: provider, log: support.log) - - let completionExpectation = expectation(description: "Did Finish") - setStarred.completionBlock = { - completionExpectation.fulfill() - } - - OperationQueue.main.addOperation(setStarred) - - waitForExpectations(timeout: 2) - - let fetchIdsExpectation = expectation(description: "Fetch Article Ids") - account.fetchStarredArticleIDs { accountArticlesIDsResult in - do { - let accountArticlesIDs = try accountArticlesIDsResult.get() - XCTAssertEqual(accountArticlesIDs.count, testIds.count) - fetchIdsExpectation.fulfill() - } catch { - XCTFail("Error checking articles IDs: \(error)") - } - } - waitForExpectations(timeout: 2) - } - - func testSetManyArticleIdsStarred() { - let testIds = Set((0..<10_000).map { "feed/0/article/\($0)" }) - let provider = TestStarredArticleProvider(entryIds: testIds) - - let setStarred = FeedlySetStarredArticlesOperation(account: account, allStarredEntryIdsProvider: provider, log: support.log) - - let completionExpectation = expectation(description: "Did Finish") - setStarred.completionBlock = { - completionExpectation.fulfill() - } - - OperationQueue.main.addOperation(setStarred) - - waitForExpectations(timeout: 2) - - let fetchIdsExpectation = expectation(description: "Fetch Article Ids") - account.fetchStarredArticleIDs { accountArticlesIDsResult in - do { - let accountArticlesIDs = try accountArticlesIDsResult.get() - XCTAssertEqual(accountArticlesIDs.count, testIds.count) - fetchIdsExpectation.fulfill() - } catch { - XCTFail("Error checking articles IDs: \(error)") - } - } - waitForExpectations(timeout: 2) - } - - func testSetSomeArticleIdsUnstarred() { - let initialStarredIds = Set((0..<1000).map { "feed/0/article/\($0)" }) - - do { - let provider = TestStarredArticleProvider(entryIds: initialStarredIds) - let setStarred = FeedlySetStarredArticlesOperation(account: account, allStarredEntryIdsProvider: provider, log: support.log) - - let completionExpectation = expectation(description: "Did Finish Setting Initial Unreads") - setStarred.completionBlock = { - completionExpectation.fulfill() - } - - OperationQueue.main.addOperation(setStarred) - - waitForExpectations(timeout: 2) - } - - let remainingStarredIds = Set(initialStarredIds.enumerated().filter { $0.offset % 2 > 0 }.map { $0.element }) - let provider = TestStarredArticleProvider(entryIds: remainingStarredIds) - let setStarred = FeedlySetStarredArticlesOperation(account: account, allStarredEntryIdsProvider: provider, log: support.log) - - let completionExpectation = expectation(description: "Did Finish") - setStarred.completionBlock = { - completionExpectation.fulfill() - } - - OperationQueue.main.addOperation(setStarred) - - waitForExpectations(timeout: 2) - - let fetchIdsExpectation = expectation(description: "Fetch Article Ids") - account.fetchStarredArticleIDs { remainingAccountArticlesIDsResult in - do { - let remainingAccountArticlesIDs = try remainingAccountArticlesIDsResult.get() - XCTAssertEqual(remainingAccountArticlesIDs, remainingStarredIds) - fetchIdsExpectation.fulfill() - } catch { - XCTFail("Error checking articles IDs: \(error)") - } - } - waitForExpectations(timeout: 2) - } - - func testSetAllArticleIdsUnstarred() { - let initialStarredIds = Set((0..<1000).map { "feed/0/article/\($0)" }) - - do { - let provider = TestStarredArticleProvider(entryIds: initialStarredIds) - let setStarred = FeedlySetStarredArticlesOperation(account: account, allStarredEntryIdsProvider: provider, log: support.log) - - let completionExpectation = expectation(description: "Did Finish Setting Initial Unreads") - setStarred.completionBlock = { - completionExpectation.fulfill() - } - - OperationQueue.main.addOperation(setStarred) - - waitForExpectations(timeout: 2) - } - - let remainingStarredIds = Set() - let provider = TestStarredArticleProvider(entryIds: remainingStarredIds) - let setStarred = FeedlySetStarredArticlesOperation(account: account, allStarredEntryIdsProvider: provider, log: support.log) - - let completionExpectation = expectation(description: "Did Finish") - setStarred.completionBlock = { - completionExpectation.fulfill() - } - - OperationQueue.main.addOperation(setStarred) - - waitForExpectations(timeout: 2) - - let fetchIdsExpectation = expectation(description: "Fetch Article Ids") - account.fetchStarredArticleIDs { remainingAccountArticlesIDsResult in - do { - let remainingAccountArticlesIDs = try remainingAccountArticlesIDsResult.get() - XCTAssertEqual(remainingAccountArticlesIDs, remainingStarredIds) - fetchIdsExpectation.fulfill() - } catch { - XCTFail("Error checking articles IDs: \(error)") - } - } - waitForExpectations(timeout: 2) - } - - // MARK: - Updating Article Unread Status - - struct TestItemsByFeedProvider: FeedlyParsedItemsByFeedProviding { - var providerName: String - var parsedItemsKeyedByFeedId: [String: Set] - } - - func testSetAllArticlesStarred() { - let testItemsAndFeeds = support.makeParsedItemTestDataFor(numberOfFeeds: 5, numberOfItemsInFeeds: 100) - - do { - let resource = FeedlyCategoryResourceId(id: "user/12345/category/6789") - let provider = TestItemsByFeedProvider(providerName: resource.id, parsedItemsKeyedByFeedId: testItemsAndFeeds) - let update = FeedlyUpdateAccountFeedsWithItemsOperation(account: account, organisedItemsProvider: provider, log: support.log) - - let completionExpectation = expectation(description: "Did Finish") - update.completionBlock = { - completionExpectation.fulfill() - } - - OperationQueue.main.addOperation(update) - - waitForExpectations(timeout: 2) - } - - let testItems = Set(testItemsAndFeeds.flatMap { $0.value }) - let remainingStarredIds = Set(testItems.compactMap { $0.syncServiceID }) - XCTAssertEqual(testItems.count, remainingStarredIds.count, "Not every item has a value for \(\ParsedItem.syncServiceID).") - - let provider = TestStarredArticleProvider(entryIds: remainingStarredIds) - let setStarred = FeedlySetStarredArticlesOperation(account: account, allStarredEntryIdsProvider: provider, log: support.log) - - let completionExpectation = expectation(description: "Did Finish") - setStarred.completionBlock = { - completionExpectation.fulfill() - } - - OperationQueue.main.addOperation(setStarred) - - waitForExpectations(timeout: 2) - - let fetchIdsExpectation = expectation(description: "Fetch Article Ids") - account.fetchStarredArticleIDs { accountArticlesIDsResult in - do { - let accountArticlesIDs = try accountArticlesIDsResult.get() - XCTAssertEqual(accountArticlesIDs, remainingStarredIds) - - let idsOfStarredArticles = Set(try self.account - .fetchArticles(.articleIDs(remainingStarredIds)) - .filter { $0.status.boolStatus(forKey: .starred) == true } - .map { $0.articleID }) - - XCTAssertEqual(idsOfStarredArticles, remainingStarredIds) - fetchIdsExpectation.fulfill() - } catch { - XCTFail("Error checking articles IDs: \(error)") - } - } - waitForExpectations(timeout: 2) - } - - func testSetManyArticlesUnread() { - let testItemsAndFeeds = support.makeParsedItemTestDataFor(numberOfFeeds: 5, numberOfItemsInFeeds: 100) - - do { - let resource = FeedlyCategoryResourceId(id: "user/12345/category/6789") - let provider = TestItemsByFeedProvider(providerName: resource.id, parsedItemsKeyedByFeedId: testItemsAndFeeds) - let update = FeedlyUpdateAccountFeedsWithItemsOperation(account: account, organisedItemsProvider: provider, log: support.log) - - let completionExpectation = expectation(description: "Did Finish") - update.completionBlock = { - completionExpectation.fulfill() - } - - OperationQueue.main.addOperation(update) - - waitForExpectations(timeout: 2) - } - - let testItems = Set(testItemsAndFeeds.flatMap { $0.value }) - let unreadItems = testItems - .enumerated() - .filter { $0.offset % 2 > 0 } - .map { $0.element } - - let remainingStarredIds = Set(unreadItems.compactMap { $0.syncServiceID }) - XCTAssertEqual(unreadItems.count, remainingStarredIds.count, "Not every item has a value for \(\ParsedItem.syncServiceID).") - - let provider = TestStarredArticleProvider(entryIds: remainingStarredIds) - let setStarred = FeedlySetStarredArticlesOperation(account: account, allStarredEntryIdsProvider: provider, log: support.log) - - let completionExpectation = expectation(description: "Did Finish") - setStarred.completionBlock = { - completionExpectation.fulfill() - } - - OperationQueue.main.addOperation(setStarred) - - waitForExpectations(timeout: 2) - - let fetchIdsExpectation = expectation(description: "Fetch Article Ids") - account.fetchStarredArticleIDs { accountArticlesIDsResult in - do { - let accountArticlesIDs = try accountArticlesIDsResult.get() - XCTAssertEqual(accountArticlesIDs, remainingStarredIds) - - let idsOfStarredArticles = Set(try self.account - .fetchArticles(.articleIDs(remainingStarredIds)) - .filter { $0.status.boolStatus(forKey: .starred) == true } - .map { $0.articleID }) - - XCTAssertEqual(idsOfStarredArticles, remainingStarredIds) - fetchIdsExpectation.fulfill() - } catch { - XCTFail("Error checking articles IDs: \(error)") - } - } - waitForExpectations(timeout: 2) - } - - func testSetOneArticleUnread() { - let testItemsAndFeeds = support.makeParsedItemTestDataFor(numberOfFeeds: 5, numberOfItemsInFeeds: 100) - - do { - let resource = FeedlyCategoryResourceId(id: "user/12345/category/6789") - let provider = TestItemsByFeedProvider(providerName: resource.id, parsedItemsKeyedByFeedId: testItemsAndFeeds) - let update = FeedlyUpdateAccountFeedsWithItemsOperation(account: account, organisedItemsProvider: provider, log: support.log) - - let completionExpectation = expectation(description: "Did Finish") - update.completionBlock = { - completionExpectation.fulfill() - } - - OperationQueue.main.addOperation(update) - - waitForExpectations(timeout: 2) - } - - let testItems = Set(testItemsAndFeeds.flatMap { $0.value }) - // Since the test data is completely under the developer's control, not having at least one can be a programmer error. - let remainingStarredIds = Set([testItems.compactMap { $0.syncServiceID }.first!]) - let provider = TestStarredArticleProvider(entryIds: remainingStarredIds) - let setStarred = FeedlySetStarredArticlesOperation(account: account, allStarredEntryIdsProvider: provider, log: support.log) - - let completionExpectation = expectation(description: "Did Finish") - setStarred.completionBlock = { - completionExpectation.fulfill() - } - - OperationQueue.main.addOperation(setStarred) - - waitForExpectations(timeout: 2) - - let fetchIdsExpectation = expectation(description: "Fetch Article Ids") - account.fetchStarredArticleIDs { accountArticlesIDsResult in - do { - let accountArticlesIDs = try accountArticlesIDsResult.get() - XCTAssertEqual(accountArticlesIDs, remainingStarredIds) - - let idsOfStarredArticles = Set(try self.account - .fetchArticles(.articleIDs(remainingStarredIds)) - .filter { $0.status.boolStatus(forKey: .starred) == true } - .map { $0.articleID }) - - XCTAssertEqual(idsOfStarredArticles, remainingStarredIds) - fetchIdsExpectation.fulfill() - } catch { - XCTFail("Error checking articles IDs: \(error)") - } - } - waitForExpectations(timeout: 2) - } - - func testSetNoArticlesRead() { - let testItemsAndFeeds = support.makeParsedItemTestDataFor(numberOfFeeds: 5, numberOfItemsInFeeds: 100) - - do { - let resource = FeedlyCategoryResourceId(id: "user/12345/category/6789") - let provider = TestItemsByFeedProvider(providerName: resource.id, parsedItemsKeyedByFeedId: testItemsAndFeeds) - - let update = FeedlyUpdateAccountFeedsWithItemsOperation(account: account, organisedItemsProvider: provider, log: support.log) - - let completionExpectation = expectation(description: "Did Finish") - update.completionBlock = { - completionExpectation.fulfill() - } - - OperationQueue.main.addOperation(update) - - waitForExpectations(timeout: 2) - } - - let remainingStarredIds = Set() - let provider = TestStarredArticleProvider(entryIds: remainingStarredIds) - let setStarred = FeedlySetStarredArticlesOperation(account: account, allStarredEntryIdsProvider: provider, log: support.log) - - let completionExpectation = expectation(description: "Did Finish") - setStarred.completionBlock = { - completionExpectation.fulfill() - } - - OperationQueue.main.addOperation(setStarred) - - waitForExpectations(timeout: 2) - - let fetchIdsExpectation = expectation(description: "Fetch Article Ids") - account.fetchStarredArticleIDs { accountArticlesIDsResult in - do { - let accountArticlesIDs = try accountArticlesIDsResult.get() - XCTAssertEqual(accountArticlesIDs, remainingStarredIds) - - let idsOfStarredArticles = Set(try self.account - .fetchArticles(.articleIDs(remainingStarredIds)) - .filter { $0.status.boolStatus(forKey: .starred) == true } - .map { $0.articleID }) - - XCTAssertEqual(idsOfStarredArticles, remainingStarredIds) - fetchIdsExpectation.fulfill() - } catch { - XCTFail("Error checking articles IDs: \(error)") - } - } - waitForExpectations(timeout: 2) - } - - func testSetAllArticlesAndArticleIdsWithSomeArticlesIngested() { - let testItemsAndFeeds = support.makeParsedItemTestDataFor(numberOfFeeds: 5, numberOfItemsInFeeds: 100) - let someItemsAndFeeds = Dictionary(uniqueKeysWithValues: testItemsAndFeeds.enumerated().filter { $0.offset % 2 > 0 }.map { $0.element }) - - do { - let resource = FeedlyCategoryResourceId(id: "user/12345/category/6789") - let provider = TestItemsByFeedProvider(providerName: resource.id, parsedItemsKeyedByFeedId: someItemsAndFeeds) - let update = FeedlyUpdateAccountFeedsWithItemsOperation(account: account, organisedItemsProvider: provider, log: support.log) - - let completionExpectation = expectation(description: "Did Finish") - update.completionBlock = { - completionExpectation.fulfill() - } - - OperationQueue.main.addOperation(update) - - waitForExpectations(timeout: 2) - } - - let testItems = Set(testItemsAndFeeds.flatMap { $0.value }) - let remainingStarredIds = Set(testItems.compactMap { $0.syncServiceID }) - XCTAssertEqual(testItems.count, remainingStarredIds.count, "Not every item has a value for \(\ParsedItem.syncServiceID).") - - let provider = TestStarredArticleProvider(entryIds: remainingStarredIds) - let setStarred = FeedlySetStarredArticlesOperation(account: account, allStarredEntryIdsProvider: provider, log: support.log) - - let completionExpectation = expectation(description: "Did Finish") - setStarred.completionBlock = { - completionExpectation.fulfill() - } - - OperationQueue.main.addOperation(setStarred) - - waitForExpectations(timeout: 2) - - let fetchIdsExpectation = expectation(description: "Fetch Article Ids") - account.fetchStarredArticleIDs { accountArticlesIDsResult in - do { - let accountArticlesIDs = try accountArticlesIDsResult.get() - XCTAssertEqual(accountArticlesIDs, remainingStarredIds) - - let someTestItems = Set(someItemsAndFeeds.flatMap { $0.value }) - let someRemainingStarredIdsOfIngestedArticles = Set(someTestItems.compactMap { $0.syncServiceID }) - let idsOfStarredArticles = Set(try self.account - .fetchArticles(.articleIDs(someRemainingStarredIdsOfIngestedArticles)) - .filter { $0.status.boolStatus(forKey: .starred) == true } - .map { $0.articleID }) - - XCTAssertEqual(idsOfStarredArticles, someRemainingStarredIdsOfIngestedArticles) - - fetchIdsExpectation.fulfill() - } catch { - XCTFail("Error checking articles IDs: \(error)") - } - } - waitForExpectations(timeout: 2) - } -} diff --git a/Frameworks/Account/AccountTests/Feedly/FeedlySetUnreadArticlesOperationTests.swift b/Frameworks/Account/AccountTests/Feedly/FeedlySetUnreadArticlesOperationTests.swift deleted file mode 100644 index 4126d6003..000000000 --- a/Frameworks/Account/AccountTests/Feedly/FeedlySetUnreadArticlesOperationTests.swift +++ /dev/null @@ -1,485 +0,0 @@ -// -// FeedlySetUnreadArticlesOperationTests.swift -// AccountTests -// -// Created by Kiel Gillard on 24/10/19. -// Copyright © 2019 Ranchero Software, LLC. All rights reserved. -// - -import XCTest -@testable import Account -import RSParser - -class FeedlySetUnreadArticlesOperationTests: XCTestCase { - - private var account: Account! - private let support = FeedlyTestSupport() - - override func setUp() { - super.setUp() - account = support.makeTestAccount() - } - - override func tearDown() { - if let account = account { - support.destroy(account) - } - super.tearDown() - } - - // MARK: - Ensuring Unread Status By Id - - struct TestUnreadArticleIdProvider: FeedlyUnreadEntryIdProviding { - var entryIds: Set - } - - func testEmptyArticleIds() { - let testIds = Set() - let provider = TestUnreadArticleIdProvider(entryIds: testIds) - - let setUnread = FeedlySetUnreadArticlesOperation(account: account, allUnreadIdsProvider: provider, log: support.log) - - let completionExpectation = expectation(description: "Did Finish") - setUnread.completionBlock = { - completionExpectation.fulfill() - } - - OperationQueue.main.addOperation(setUnread) - - waitForExpectations(timeout: 2) - - let fetchIdsExpectation = expectation(description: "Fetched Articles Ids") - account.fetchUnreadArticleIDs { accountArticlesIDsResult in - do { - let accountArticlesIDs = try accountArticlesIDsResult.get() - XCTAssertTrue(accountArticlesIDs.isEmpty) - XCTAssertEqual(accountArticlesIDs.count, testIds.count) - fetchIdsExpectation.fulfill() - } catch { - XCTFail("Error checking account articles IDs result: \(error)") - } - } - - waitForExpectations(timeout: 2) - } - - func testSetOneArticleIdUnread() { - let testIds = Set(["feed/0/article/0"]) - let provider = TestUnreadArticleIdProvider(entryIds: testIds) - - let setUnread = FeedlySetUnreadArticlesOperation(account: account, allUnreadIdsProvider: provider, log: support.log) - - let completionExpectation = expectation(description: "Did Finish") - setUnread.completionBlock = { - completionExpectation.fulfill() - } - - OperationQueue.main.addOperation(setUnread) - - waitForExpectations(timeout: 2) - - let fetchIdsExpectation = expectation(description: "Fetched Articles Ids") - account.fetchUnreadArticleIDs { accountArticlesIDsResult in - do { - let accountArticlesIDs = try accountArticlesIDsResult.get() - XCTAssertEqual(accountArticlesIDs.count, testIds.count) - fetchIdsExpectation.fulfill() - } catch { - XCTFail("Error checking account articles IDs result: \(error)") - } - } - waitForExpectations(timeout: 2) - } - - func testSetManyArticleIdsUnread() { - let testIds = Set((0..<10_000).map { "feed/0/article/\($0)" }) - let provider = TestUnreadArticleIdProvider(entryIds: testIds) - - let setUnread = FeedlySetUnreadArticlesOperation(account: account, allUnreadIdsProvider: provider, log: support.log) - - let completionExpectation = expectation(description: "Did Finish") - setUnread.completionBlock = { - completionExpectation.fulfill() - } - - OperationQueue.main.addOperation(setUnread) - - waitForExpectations(timeout: 2) - - let fetchIdsExpectation = expectation(description: "Fetched Articles Ids") - account.fetchUnreadArticleIDs { accountArticlesIDsResult in - do { - let accountArticlesIDs = try accountArticlesIDsResult.get() - XCTAssertEqual(accountArticlesIDs.count, testIds.count) - fetchIdsExpectation.fulfill() - } catch { - XCTFail("Error checking account articles IDs result: \(error)") - } - } - waitForExpectations(timeout: 2) - } - - func testSetSomeArticleIdsRead() { - let initialUnreadIds = Set((0..<1000).map { "feed/0/article/\($0)" }) - - do { - let provider = TestUnreadArticleIdProvider(entryIds: initialUnreadIds) - let setUnread = FeedlySetUnreadArticlesOperation(account: account, allUnreadIdsProvider: provider, log: support.log) - - let completionExpectation = expectation(description: "Did Finish Setting Initial Unreads") - setUnread.completionBlock = { - completionExpectation.fulfill() - } - - OperationQueue.main.addOperation(setUnread) - - waitForExpectations(timeout: 2) - } - - let remainingUnreadIds = Set(initialUnreadIds.enumerated().filter { $0.offset % 2 > 0 }.map { $0.element }) - let provider = TestUnreadArticleIdProvider(entryIds: remainingUnreadIds) - let setUnread = FeedlySetUnreadArticlesOperation(account: account, allUnreadIdsProvider: provider, log: support.log) - - let completionExpectation = expectation(description: "Did Finish") - setUnread.completionBlock = { - completionExpectation.fulfill() - } - - OperationQueue.main.addOperation(setUnread) - - waitForExpectations(timeout: 2) - - let fetchIdsExpectation = expectation(description: "Fetched Articles Ids") - account.fetchUnreadArticleIDs { remainingAccountArticlesIDsResult in - do { - let remainingAccountArticlesIDs = try remainingAccountArticlesIDsResult.get() - XCTAssertEqual(remainingAccountArticlesIDs, remainingUnreadIds) - fetchIdsExpectation.fulfill() - } catch { - XCTFail("Error checking account articles IDs result: \(error)") - } - } - waitForExpectations(timeout: 2) - } - - func testSetAllArticleIdsRead() { - let initialUnreadIds = Set((0..<1000).map { "feed/0/article/\($0)" }) - - do { - let provider = TestUnreadArticleIdProvider(entryIds: initialUnreadIds) - let setUnread = FeedlySetUnreadArticlesOperation(account: account, allUnreadIdsProvider: provider, log: support.log) - - let completionExpectation = expectation(description: "Did Finish Setting Initial Unreads") - setUnread.completionBlock = { - completionExpectation.fulfill() - } - - OperationQueue.main.addOperation(setUnread) - - waitForExpectations(timeout: 2) - } - - let remainingUnreadIds = Set() - let provider = TestUnreadArticleIdProvider(entryIds: remainingUnreadIds) - let setUnread = FeedlySetUnreadArticlesOperation(account: account, allUnreadIdsProvider: provider, log: support.log) - - let completionExpectation = expectation(description: "Did Finish") - setUnread.completionBlock = { - completionExpectation.fulfill() - } - - OperationQueue.main.addOperation(setUnread) - - waitForExpectations(timeout: 2) - - let fetchIdsExpectation = expectation(description: "Fetched Articles Ids") - account.fetchUnreadArticleIDs { remainingAccountArticlesIDsResult in - do { - let remainingAccountArticlesIDs = try remainingAccountArticlesIDsResult.get() - XCTAssertEqual(remainingAccountArticlesIDs, remainingUnreadIds) - fetchIdsExpectation.fulfill() - } catch { - XCTFail("Error checking account articles IDs result: \(error)") - } - } - waitForExpectations(timeout: 2) - } - - // MARK: - Updating Article Unread Status - - struct TestItemsByFeedProvider: FeedlyParsedItemsByFeedProviding { - var providerName: String - var parsedItemsKeyedByFeedId: [String: Set] - } - - func testSetAllArticlesUnread() { - let testItemsAndFeeds = support.makeParsedItemTestDataFor(numberOfFeeds: 5, numberOfItemsInFeeds: 100) - - do { - let resource = FeedlyCategoryResourceId(id: "user/12345/category/6789") - let provider = TestItemsByFeedProvider(providerName: resource.id, parsedItemsKeyedByFeedId: testItemsAndFeeds) - let update = FeedlyUpdateAccountFeedsWithItemsOperation(account: account, organisedItemsProvider: provider, log: support.log) - - let completionExpectation = expectation(description: "Did Finish") - update.completionBlock = { - completionExpectation.fulfill() - } - - OperationQueue.main.addOperation(update) - - waitForExpectations(timeout: 2) - } - - let testItems = Set(testItemsAndFeeds.flatMap { $0.value }) - let remainingUnreadIds = Set(testItems.compactMap { $0.syncServiceID }) - XCTAssertEqual(testItems.count, remainingUnreadIds.count, "Not every item has a value for \(\ParsedItem.syncServiceID).") - - let provider = TestUnreadArticleIdProvider(entryIds: remainingUnreadIds) - let setUnread = FeedlySetUnreadArticlesOperation(account: account, allUnreadIdsProvider: provider, log: support.log) - - let completionExpectation = expectation(description: "Did Finish") - setUnread.completionBlock = { - completionExpectation.fulfill() - } - - OperationQueue.main.addOperation(setUnread) - - waitForExpectations(timeout: 2) - - let fetchIdsExpectation = expectation(description: "Fetched Articles Ids") - account.fetchUnreadArticleIDs { accountArticlesIDsResult in - do { - let accountArticlesIDs = try accountArticlesIDsResult.get() - XCTAssertEqual(accountArticlesIDs, remainingUnreadIds) - let idsOfUnreadArticles = Set(try self.account - .fetchArticles(.articleIDs(remainingUnreadIds)) - .filter { $0.status.boolStatus(forKey: .read) == false } - .map { $0.articleID }) - - XCTAssertEqual(idsOfUnreadArticles, remainingUnreadIds) - fetchIdsExpectation.fulfill() - } catch { - XCTFail("Error checking account articles IDs result: \(error)") - } - } - waitForExpectations(timeout: 2) - } - - func testSetManyArticlesUnread() { - let testItemsAndFeeds = support.makeParsedItemTestDataFor(numberOfFeeds: 5, numberOfItemsInFeeds: 100) - - do { - let resource = FeedlyCategoryResourceId(id: "user/12345/category/6789") - let provider = TestItemsByFeedProvider(providerName: resource.id, parsedItemsKeyedByFeedId: testItemsAndFeeds) - let update = FeedlyUpdateAccountFeedsWithItemsOperation(account: account, organisedItemsProvider: provider, log: support.log) - - let completionExpectation = expectation(description: "Did Finish") - update.completionBlock = { - completionExpectation.fulfill() - } - - OperationQueue.main.addOperation(update) - - waitForExpectations(timeout: 2) - } - - let testItems = Set(testItemsAndFeeds.flatMap { $0.value }) - let unreadItems = testItems - .enumerated() - .filter { $0.offset % 2 > 0 } - .map { $0.element } - - let remainingUnreadIds = Set(unreadItems.compactMap { $0.syncServiceID }) - XCTAssertEqual(unreadItems.count, remainingUnreadIds.count, "Not every item has a value for \(\ParsedItem.syncServiceID).") - - let provider = TestUnreadArticleIdProvider(entryIds: remainingUnreadIds) - let setUnread = FeedlySetUnreadArticlesOperation(account: account, allUnreadIdsProvider: provider, log: support.log) - - let completionExpectation = expectation(description: "Did Finish") - setUnread.completionBlock = { - completionExpectation.fulfill() - } - - OperationQueue.main.addOperation(setUnread) - - waitForExpectations(timeout: 2) - - let fetchIdsExpectation = expectation(description: "Fetched Articles Ids") - account.fetchUnreadArticleIDs { accountArticlesIDsResult in - do { - let accountArticlesIDs = try accountArticlesIDsResult.get() - XCTAssertEqual(accountArticlesIDs, remainingUnreadIds) - - let idsOfUnreadArticles = Set(try self.account - .fetchArticles(.articleIDs(remainingUnreadIds)) - .filter { $0.status.boolStatus(forKey: .read) == false } - .map { $0.articleID }) - - XCTAssertEqual(idsOfUnreadArticles, remainingUnreadIds) - fetchIdsExpectation.fulfill() - } catch { - XCTFail("Error checking account articles IDs result: \(error)") - } - } - } - - func testSetOneArticleUnread() { - let testItemsAndFeeds = support.makeParsedItemTestDataFor(numberOfFeeds: 5, numberOfItemsInFeeds: 100) - - do { - let resource = FeedlyCategoryResourceId(id: "user/12345/category/6789") - let provider = TestItemsByFeedProvider(providerName: resource.id, parsedItemsKeyedByFeedId: testItemsAndFeeds) - let update = FeedlyUpdateAccountFeedsWithItemsOperation(account: account, organisedItemsProvider: provider, log: support.log) - - let completionExpectation = expectation(description: "Did Finish") - update.completionBlock = { - completionExpectation.fulfill() - } - - OperationQueue.main.addOperation(update) - - waitForExpectations(timeout: 2) - } - - let testItems = Set(testItemsAndFeeds.flatMap { $0.value }) - // Since the test data is completely under the developer's control, not having at least one can be a programmer error. - let remainingUnreadIds = Set([testItems.compactMap { $0.syncServiceID }.first!]) - let provider = TestUnreadArticleIdProvider(entryIds: remainingUnreadIds) - let setUnread = FeedlySetUnreadArticlesOperation(account: account, allUnreadIdsProvider: provider, log: support.log) - - let completionExpectation = expectation(description: "Did Finish") - setUnread.completionBlock = { - completionExpectation.fulfill() - } - - OperationQueue.main.addOperation(setUnread) - - waitForExpectations(timeout: 2) - - let fetchIdsExpectation = expectation(description: "Fetched Articles Ids") - account.fetchUnreadArticleIDs { accountArticlesIDsResult in - do { - let accountArticlesIDs = try accountArticlesIDsResult.get() - XCTAssertEqual(accountArticlesIDs, remainingUnreadIds) - - let idsOfUnreadArticles = Set(try self.account - .fetchArticles(.articleIDs(remainingUnreadIds)) - .filter { $0.status.boolStatus(forKey: .read) == false } - .map { $0.articleID }) - - XCTAssertEqual(idsOfUnreadArticles, remainingUnreadIds) - fetchIdsExpectation.fulfill() - } catch { - XCTFail("Error checking account articles IDs result: \(error)") - } - } - waitForExpectations(timeout: 2) - } - - func testSetNoArticlesRead() { - let testItemsAndFeeds = support.makeParsedItemTestDataFor(numberOfFeeds: 5, numberOfItemsInFeeds: 100) - - do { - let resource = FeedlyCategoryResourceId(id: "user/12345/category/6789") - let provider = TestItemsByFeedProvider(providerName: resource.id, parsedItemsKeyedByFeedId: testItemsAndFeeds) - - let update = FeedlyUpdateAccountFeedsWithItemsOperation(account: account, organisedItemsProvider: provider, log: support.log) - - let completionExpectation = expectation(description: "Did Finish") - update.completionBlock = { - completionExpectation.fulfill() - } - - OperationQueue.main.addOperation(update) - - waitForExpectations(timeout: 2) - } - - let remainingUnreadIds = Set() - let provider = TestUnreadArticleIdProvider(entryIds: remainingUnreadIds) - let setUnread = FeedlySetUnreadArticlesOperation(account: account, allUnreadIdsProvider: provider, log: support.log) - - let completionExpectation = expectation(description: "Did Finish") - setUnread.completionBlock = { - completionExpectation.fulfill() - } - - OperationQueue.main.addOperation(setUnread) - - waitForExpectations(timeout: 2) - - let fetchIdsExpectation = expectation(description: "Fetched Articles Ids") - account.fetchUnreadArticleIDs { accountArticlesIDsResult in - do { - let accountArticlesIDs = try accountArticlesIDsResult.get() - XCTAssertEqual(accountArticlesIDs, remainingUnreadIds) - - let idsOfUnreadArticles = Set(try self.account - .fetchArticles(.articleIDs(remainingUnreadIds)) - .filter { $0.status.boolStatus(forKey: .read) == false } - .map { $0.articleID }) - - XCTAssertEqual(idsOfUnreadArticles, remainingUnreadIds) - fetchIdsExpectation.fulfill() - } catch { - XCTFail("Error checking account articles IDs result: \(error)") - } - } - } - - func testSetAllArticlesAndArticleIdsWithSomeArticlesIngested() { - let testItemsAndFeeds = support.makeParsedItemTestDataFor(numberOfFeeds: 5, numberOfItemsInFeeds: 100) - let someItemsAndFeeds = Dictionary(uniqueKeysWithValues: testItemsAndFeeds.enumerated().filter { $0.offset % 2 > 0 }.map { $0.element }) - - do { - let resource = FeedlyCategoryResourceId(id: "user/12345/category/6789") - let provider = TestItemsByFeedProvider(providerName: resource.id, parsedItemsKeyedByFeedId: someItemsAndFeeds) - let update = FeedlyUpdateAccountFeedsWithItemsOperation(account: account, organisedItemsProvider: provider, log: support.log) - - let completionExpectation = expectation(description: "Did Finish") - update.completionBlock = { - completionExpectation.fulfill() - } - - OperationQueue.main.addOperation(update) - - waitForExpectations(timeout: 2) - } - - let testItems = Set(testItemsAndFeeds.flatMap { $0.value }) - let remainingUnreadIds = Set(testItems.compactMap { $0.syncServiceID }) - XCTAssertEqual(testItems.count, remainingUnreadIds.count, "Not every item has a value for \(\ParsedItem.syncServiceID).") - - let provider = TestUnreadArticleIdProvider(entryIds: remainingUnreadIds) - let setUnread = FeedlySetUnreadArticlesOperation(account: account, allUnreadIdsProvider: provider, log: support.log) - - let completionExpectation = expectation(description: "Did Finish") - setUnread.completionBlock = { - completionExpectation.fulfill() - } - - OperationQueue.main.addOperation(setUnread) - - waitForExpectations(timeout: 2) - - let fetchIdsExpectation = expectation(description: "Fetched Articles Ids") - account.fetchUnreadArticleIDs { accountArticlesIDsResult in - do { - let accountArticlesIDs = try accountArticlesIDsResult.get() - XCTAssertEqual(accountArticlesIDs, remainingUnreadIds) - - let someTestItems = Set(someItemsAndFeeds.flatMap { $0.value }) - let someRemainingUnreadIdsOfIngestedArticles = Set(someTestItems.compactMap { $0.syncServiceID }) - let idsOfUnreadArticles = Set(try self.account - .fetchArticles(.articleIDs(someRemainingUnreadIdsOfIngestedArticles)) - .filter { $0.status.boolStatus(forKey: .read) == false } - .map { $0.articleID }) - - XCTAssertEqual(idsOfUnreadArticles, someRemainingUnreadIdsOfIngestedArticles) - fetchIdsExpectation.fulfill() - } catch { - XCTFail("Error checking account articles IDs result: \(error)") - } - } - } -} diff --git a/Frameworks/Account/AccountTests/Feedly/FeedlySyncAllOperationTests.swift b/Frameworks/Account/AccountTests/Feedly/FeedlySyncAllOperationTests.swift index 403250314..d6ae72384 100644 --- a/Frameworks/Account/AccountTests/Feedly/FeedlySyncAllOperationTests.swift +++ b/Frameworks/Account/AccountTests/Feedly/FeedlySyncAllOperationTests.swift @@ -57,25 +57,31 @@ class FeedlySyncAllOperationTests: XCTestCase { getGlobalStreamContents.getStreamContentsExpectation = expectation(description: "Get Contents of global.all") getGlobalStreamContents.getStreamContentsExpectation?.isInverted = true - let getStarredContents = TestGetStreamContentsService() - getStarredContents.getStreamContentsExpectation = expectation(description: "Get Contents of global.saved") - getStarredContents.getStreamContentsExpectation?.isInverted = true + let getStarredIds = TestGetStreamIdsService() + getStarredIds.getStreamIdsExpectation = expectation(description: "Get Ids of global.saved") + getStarredIds.getStreamIdsExpectation?.isInverted = true + + let getEntriesService = TestGetEntriesService() + getEntriesService.getEntriesExpectation = expectation(description: "Get Entries") + getEntriesService.getEntriesExpectation?.isInverted = true let progress = DownloadProgress(numberOfTasks: 0) let _ = expectationForCompletion(of: progress) let container = support.makeTestDatabaseContainer() let syncAll = FeedlySyncAllOperation(account: account, - credentials: support.accessToken, - lastSuccessfulFetchStartDate: nil, - markArticlesService: markArticlesService, - getUnreadService: getStreamIdsService, - getCollectionsService: getCollectionsService, - getStreamContentsService: getGlobalStreamContents, - getStarredArticlesService: getStarredContents, - database: container.database, - downloadProgress: progress, - log: support.log) + credentials: support.accessToken, + lastSuccessfulFetchStartDate: nil, + markArticlesService: markArticlesService, + getUnreadService: getStreamIdsService, + getCollectionsService: getCollectionsService, + getStreamContentsService: getGlobalStreamContents, + getStarredService: getStarredIds, + getStreamIdsService: getStreamIdsService, + getEntriesService: getEntriesService, + database: container.database, + downloadProgress: progress, + log: support.log) // If this expectation is not fulfilled, the operation is not calling `didFinish`. let completionExpectation = expectation(description: "Did Finish") diff --git a/Frameworks/Account/AccountTests/Feedly/FeedlySyncStarredArticlesOperationTests.swift b/Frameworks/Account/AccountTests/Feedly/FeedlySyncStarredArticlesOperationTests.swift deleted file mode 100644 index 40d0c39ff..000000000 --- a/Frameworks/Account/AccountTests/Feedly/FeedlySyncStarredArticlesOperationTests.swift +++ /dev/null @@ -1,189 +0,0 @@ -// -// FeedlySyncStarredArticlesOperationTests.swift -// AccountTests -// -// Created by Kiel Gillard on 28/10/19. -// Copyright © 2019 Ranchero Software, LLC. All rights reserved. -// - -import XCTest -@testable import Account - -class FeedlySyncStarredArticlesOperationTests: XCTestCase { - - private var account: Account! - private let support = FeedlyTestSupport() - - override func setUp() { - super.setUp() - account = support.makeTestAccount() - } - - override func tearDown() { - if let account = account { - support.destroy(account) - } - super.tearDown() - } - - func testIngestsOnePageSuccess() { - let service = TestGetStreamContentsService() - let resource = FeedlyCategoryResourceId(id: "user/1234/category/5678") - let items = service.makeMockFeedlyEntryItem() - service.mockResult = .success(FeedlyStream(id: resource.id, updated: nil, continuation: nil, items: items)) - - let getStreamContentsExpectation = expectation(description: "Did Get Page of Stream Contents") - getStreamContentsExpectation.expectedFulfillmentCount = 1 - - service.getStreamContentsExpectation = getStreamContentsExpectation - service.parameterTester = { serviceResource, continuation, serviceNewerThan, serviceUnreadOnly in - XCTAssertEqual(serviceResource.id, resource.id) - XCTAssertNil(serviceNewerThan) - XCTAssertNil(continuation) - XCTAssertNil(serviceUnreadOnly) - } - - let syncStarred = FeedlySyncStarredArticlesOperation(account: account, resource: resource, service: service, log: support.log) - - let completionExpectation = expectation(description: "Did Finish") - syncStarred.completionBlock = { - completionExpectation.fulfill() - } - - OperationQueue.main.addOperation(syncStarred) - - waitForExpectations(timeout: 2) - - let expectedArticleIds = Set(items.map { $0.id }) - let fetchIdsExpectation = expectation(description: "Fetch Article Ids") - account.fetchStarredArticleIDs { starredArticleIdsResult in - do { - let starredArticleIds = try starredArticleIdsResult.get() - let missingIds = expectedArticleIds.subtracting(starredArticleIds) - XCTAssertTrue(missingIds.isEmpty, "These article ids were not marked as starred.") - - // Fetch articles directly because account.fetchArticles(.starred) fetches starred articles for feeds subscribed to. - let expectedArticles = try self.account.fetchArticles(.articleIDs(expectedArticleIds)) - XCTAssertEqual(expectedArticles.count, expectedArticleIds.count, "Did not fetch all the articles.") - - let starredArticles = try self.account.fetchArticles(.articleIDs(starredArticleIds)) - XCTAssertEqual(expectedArticleIds.count, expectedArticles.count) - let missingArticles = expectedArticles.subtracting(starredArticles) - XCTAssertTrue(missingArticles.isEmpty, "These articles should be starred and fetched.") - XCTAssertEqual(expectedArticles, starredArticles) - - fetchIdsExpectation.fulfill() - } catch { - XCTFail("Error checking starred article IDs: \(error)") - } - } - waitForExpectations(timeout: 2) - } - - func testIngestsOnePageFailure() { - let service = TestGetStreamContentsService() - let resource = FeedlyCategoryResourceId(id: "user/1234/category/5678") - - service.mockResult = .failure(URLError(.timedOut)) - - let getStreamContentsExpectation = expectation(description: "Did Get Page of Stream Contents") - getStreamContentsExpectation.expectedFulfillmentCount = 1 - - service.getStreamContentsExpectation = getStreamContentsExpectation - service.parameterTester = { serviceResource, continuation, serviceNewerThan, serviceUnreadOnly in - XCTAssertEqual(serviceResource.id, resource.id) - XCTAssertNil(serviceNewerThan) - XCTAssertNil(continuation) - XCTAssertNil(serviceUnreadOnly) - } - - let syncStarred = FeedlySyncStarredArticlesOperation(account: account, resource: resource, service: service, log: support.log) - - let completionExpectation = expectation(description: "Did Finish") - syncStarred.completionBlock = { - completionExpectation.fulfill() - } - - OperationQueue.main.addOperation(syncStarred) - - waitForExpectations(timeout: 2) - - let fetchIdsExpectation = expectation(description: "Fetch Article Ids") - account.fetchStarredArticleIDs { starredArticleIdsResult in - do { - let starredArticleIds = try starredArticleIdsResult.get() - XCTAssertTrue(starredArticleIds.isEmpty) - fetchIdsExpectation.fulfill() - } catch { - XCTFail("Error checking starred article IDs: \(error)") - } - } - waitForExpectations(timeout: 2) - } - - func testIngestsManyPagesSuccess() { - let service = TestGetPagedStreamContentsService() - let resource = FeedlyCategoryResourceId(id: "user/1234/category/5678") - - let continuations = (1...10).map { "\($0)" } - service.addAtLeastOnePage(for: resource, continuations: continuations, numberOfEntriesPerPage: 10) - - let getStreamContentsExpectation = expectation(description: "Did Get Page of Stream Contents") - getStreamContentsExpectation.expectedFulfillmentCount = 1 + continuations.count - - var remainingContinuations = Set(continuations) - let getStreamPageExpectation = expectation(description: "Did Request Page") - getStreamPageExpectation.expectedFulfillmentCount = 1 + continuations.count - - service.getStreamContentsExpectation = getStreamContentsExpectation - service.parameterTester = { serviceResource, continuation, serviceNewerThan, serviceUnreadOnly in - XCTAssertEqual(serviceResource.id, resource.id) - XCTAssertNil(serviceNewerThan) - XCTAssertNil(serviceUnreadOnly) - - if let continuation = continuation { - XCTAssertTrue(remainingContinuations.contains(continuation)) - remainingContinuations.remove(continuation) - } - - getStreamPageExpectation.fulfill() - } - - let syncStarred = FeedlySyncStarredArticlesOperation(account: account, resource: resource, service: service, log: support.log) - - let completionExpectation = expectation(description: "Did Finish") - syncStarred.completionBlock = { - completionExpectation.fulfill() - } - - OperationQueue.main.addOperation(syncStarred) - - waitForExpectations(timeout: 2) - - // Find articles inserted. - let expectedArticleIds = Set(service.pages.values.map { $0.items }.flatMap { $0 }.map { $0.id }) - let fetchIdsExpectation = expectation(description: "Fetch Article Ids") - account.fetchStarredArticleIDs { starredArticleIdsResult in - do { - let starredArticleIds = try starredArticleIdsResult.get() - let missingIds = expectedArticleIds.subtracting(starredArticleIds) - XCTAssertTrue(missingIds.isEmpty, "These article ids were not marked as starred.") - - // Fetch articles directly because account.fetchArticles(.starred) fetches starred articles for feeds subscribed to. - let expectedArticles = try self.account.fetchArticles(.articleIDs(expectedArticleIds)) - XCTAssertEqual(expectedArticles.count, expectedArticleIds.count, "Did not fetch all the articles.") - - let starredArticles = try self.account.fetchArticles(.articleIDs(starredArticleIds)) - XCTAssertEqual(expectedArticleIds.count, expectedArticles.count) - let missingArticles = expectedArticles.subtracting(starredArticles) - XCTAssertTrue(missingArticles.isEmpty, "These articles should be starred and fetched.") - XCTAssertEqual(expectedArticles, starredArticles) - - fetchIdsExpectation.fulfill() - } catch { - XCTFail("Error checking starred article IDs: \(error)") - } - } - waitForExpectations(timeout: 2) - } -} diff --git a/Frameworks/Account/AccountTests/Feedly/FeedlySyncUnreadStatusesOperationTests.swift b/Frameworks/Account/AccountTests/Feedly/FeedlySyncUnreadStatusesOperationTests.swift deleted file mode 100644 index 09a8ec57a..000000000 --- a/Frameworks/Account/AccountTests/Feedly/FeedlySyncUnreadStatusesOperationTests.swift +++ /dev/null @@ -1,167 +0,0 @@ -// -// FeedlySyncUnreadStatusesOperationTests.swift -// AccountTests -// -// Created by Kiel Gillard on 29/10/19. -// Copyright © 2019 Ranchero Software, LLC. All rights reserved. -// - -import XCTest -@testable import Account - -class FeedlySyncUnreadStatusesOperationTests: XCTestCase { - - private var account: Account! - private let support = FeedlyTestSupport() - - override func setUp() { - super.setUp() - account = support.makeTestAccount() - } - - override func tearDown() { - if let account = account { - support.destroy(account) - } - super.tearDown() - } - - func testIngestsOnePageSuccess() { - let service = TestGetStreamIdsService() - let resource = FeedlyCategoryResourceId(id: "user/1234/category/5678") - let ids = [UUID().uuidString] - service.mockResult = .success(FeedlyStreamIds(continuation: nil, ids: ids)) - - let getStreamIdsExpectation = expectation(description: "Did Get Page of Stream Ids") - getStreamIdsExpectation.expectedFulfillmentCount = 1 - - service.getStreamIdsExpectation = getStreamIdsExpectation - service.parameterTester = { serviceResource, continuation, serviceNewerThan, serviceUnreadOnly in - XCTAssertEqual(serviceResource.id, resource.id) - XCTAssertNil(serviceNewerThan) - XCTAssertNil(continuation) - XCTAssertEqual(serviceUnreadOnly, true) - } - - let syncUnreads = FeedlySyncUnreadStatusesOperation(account: account, resource: resource, service: service, newerThan: nil, log: support.log) - - let completionExpectation = expectation(description: "Did Finish") - syncUnreads.completionBlock = { - completionExpectation.fulfill() - } - - OperationQueue.main.addOperation(syncUnreads) - - waitForExpectations(timeout: 2) - - let expectedArticleIds = Set(ids) - let fetchIdsExpectation = expectation(description: "Fetch Article Ids") - account.fetchUnreadArticleIDs { unreadArticleIdsResult in - do { - let unreadArticleIds = try unreadArticleIdsResult.get() - let missingIds = expectedArticleIds.subtracting(unreadArticleIds) - XCTAssertTrue(missingIds.isEmpty, "These article ids were not marked as unread.") - fetchIdsExpectation.fulfill() - } catch { - XCTFail("Error checking unread article IDs: \(error)") - } - } - waitForExpectations(timeout: 2) - } - - func testIngestsOnePageFailure() { - let service = TestGetStreamIdsService() - let resource = FeedlyCategoryResourceId(id: "user/1234/category/5678") - - service.mockResult = .failure(URLError(.timedOut)) - - let getStreamIdsExpectation = expectation(description: "Did Get Page of Stream Contents") - getStreamIdsExpectation.expectedFulfillmentCount = 1 - - service.getStreamIdsExpectation = getStreamIdsExpectation - service.parameterTester = { serviceResource, continuation, serviceNewerThan, serviceUnreadOnly in - XCTAssertEqual(serviceResource.id, resource.id) - XCTAssertNil(serviceNewerThan) - XCTAssertNil(continuation) - XCTAssertEqual(serviceUnreadOnly, true) - } - - let syncUnreads = FeedlySyncUnreadStatusesOperation(account: account, resource: resource, service: service, newerThan: nil, log: support.log) - - let completionExpectation = expectation(description: "Did Finish") - syncUnreads.completionBlock = { - completionExpectation.fulfill() - } - - OperationQueue.main.addOperation(syncUnreads) - - waitForExpectations(timeout: 2) - - let fetchIdsExpectation = expectation(description: "Fetch Article Ids") - account.fetchUnreadArticleIDs { unreadArticleIdsResult in - do { - let unreadArticleIds = try unreadArticleIdsResult.get() - XCTAssertTrue(unreadArticleIds.isEmpty) - fetchIdsExpectation.fulfill() - } catch { - XCTFail("Error checking unread article IDs: \(error)") - } - } - waitForExpectations(timeout: 2) - } - - func testIngestsManyPagesSuccess() { - let service = TestGetPagedStreamIdsService() - let resource = FeedlyCategoryResourceId(id: "user/1234/category/5678") - - let continuations = (1...10).map { "\($0)" } - service.addAtLeastOnePage(for: resource, continuations: continuations, numberOfEntriesPerPage: 1000) - - let getStreamIdsExpectation = expectation(description: "Did Get Page of Stream Contents") - getStreamIdsExpectation.expectedFulfillmentCount = 1 + continuations.count - - var remainingContinuations = Set(continuations) - let getStreamPageExpectation = expectation(description: "Did Request Page") - getStreamPageExpectation.expectedFulfillmentCount = 1 + continuations.count - - service.getStreamIdsExpectation = getStreamIdsExpectation - service.parameterTester = { serviceResource, continuation, serviceNewerThan, serviceUnreadOnly in - XCTAssertEqual(serviceResource.id, resource.id) - XCTAssertNil(serviceNewerThan) - XCTAssertEqual(serviceUnreadOnly, true) - - if let continuation = continuation { - XCTAssertTrue(remainingContinuations.contains(continuation)) - remainingContinuations.remove(continuation) - } - - getStreamPageExpectation.fulfill() - } - - let syncUnreads = FeedlySyncUnreadStatusesOperation(account: account, resource: resource, service: service, newerThan: nil, log: support.log) - - let completionExpectation = expectation(description: "Did Finish") - syncUnreads.completionBlock = { - completionExpectation.fulfill() - } - - OperationQueue.main.addOperation(syncUnreads) - - waitForExpectations(timeout: 2) - - // Find statuses inserted. - let expectedArticleIds = Set(service.pages.values.map { $0.ids }.flatMap { $0 }) - let fetchIdsExpectation = expectation(description: "Fetch Article Ids") - account.fetchUnreadArticleIDs { unreadArticleIdsResult in - do { - let unreadArticleIds = try unreadArticleIdsResult.get() - let missingIds = expectedArticleIds.subtracting(unreadArticleIds) - XCTAssertTrue(missingIds.isEmpty, "These article ids were not marked as unread.") - fetchIdsExpectation.fulfill() - } catch { - XCTFail("Error checking unread article IDs: \(error)") - } - } - waitForExpectations(timeout: 2) - } -} diff --git a/Frameworks/Account/AccountTests/Feedly/FeedlyUpdateAccountFeedsWithItemsOperationTests.swift b/Frameworks/Account/AccountTests/Feedly/FeedlyUpdateAccountFeedsWithItemsOperationTests.swift index b828f86e5..e2d058688 100644 --- a/Frameworks/Account/AccountTests/Feedly/FeedlyUpdateAccountFeedsWithItemsOperationTests.swift +++ b/Frameworks/Account/AccountTests/Feedly/FeedlyUpdateAccountFeedsWithItemsOperationTests.swift @@ -28,14 +28,14 @@ class FeedlyUpdateAccountFeedsWithItemsOperationTests: XCTestCase { } struct TestItemsByFeedProvider: FeedlyParsedItemsByFeedProviding { - var providerName: String + var parsedItemsByFeedProviderName: String var parsedItemsKeyedByFeedId: [String: Set] } func testUpdateAccountWithEmptyItems() throws { let testItems = support.makeParsedItemTestDataFor(numberOfFeeds: 0, numberOfItemsInFeeds: 0) let resource = FeedlyCategoryResourceId(id: "user/12345/category/6789") - let provider = TestItemsByFeedProvider(providerName: resource.id, parsedItemsKeyedByFeedId: testItems) + let provider = TestItemsByFeedProvider(parsedItemsByFeedProviderName: resource.id, parsedItemsKeyedByFeedId: testItems) let update = FeedlyUpdateAccountFeedsWithItemsOperation(account: account, organisedItemsProvider: provider, log: support.log) @@ -59,7 +59,7 @@ class FeedlyUpdateAccountFeedsWithItemsOperationTests: XCTestCase { func testUpdateAccountWithOneItem() throws { let testItems = support.makeParsedItemTestDataFor(numberOfFeeds: 1, numberOfItemsInFeeds: 1) let resource = FeedlyCategoryResourceId(id: "user/12345/category/6789") - let provider = TestItemsByFeedProvider(providerName: resource.id, parsedItemsKeyedByFeedId: testItems) + let provider = TestItemsByFeedProvider(parsedItemsByFeedProviderName: resource.id, parsedItemsKeyedByFeedId: testItems) let update = FeedlyUpdateAccountFeedsWithItemsOperation(account: account, organisedItemsProvider: provider, log: support.log) @@ -87,7 +87,7 @@ class FeedlyUpdateAccountFeedsWithItemsOperationTests: XCTestCase { func testUpdateAccountWithManyItems() throws { let testItems = support.makeParsedItemTestDataFor(numberOfFeeds: 100, numberOfItemsInFeeds: 100) let resource = FeedlyCategoryResourceId(id: "user/12345/category/6789") - let provider = TestItemsByFeedProvider(providerName: resource.id, parsedItemsKeyedByFeedId: testItems) + let provider = TestItemsByFeedProvider(parsedItemsByFeedProviderName: resource.id, parsedItemsKeyedByFeedId: testItems) let update = FeedlyUpdateAccountFeedsWithItemsOperation(account: account, organisedItemsProvider: provider, log: support.log) @@ -115,7 +115,7 @@ class FeedlyUpdateAccountFeedsWithItemsOperationTests: XCTestCase { func testCancelUpdateAccount() throws { let testItems = support.makeParsedItemTestDataFor(numberOfFeeds: 1, numberOfItemsInFeeds: 1) let resource = FeedlyCategoryResourceId(id: "user/12345/category/6789") - let provider = TestItemsByFeedProvider(providerName: resource.id, parsedItemsKeyedByFeedId: testItems) + let provider = TestItemsByFeedProvider(parsedItemsByFeedProviderName: resource.id, parsedItemsKeyedByFeedId: testItems) let update = FeedlyUpdateAccountFeedsWithItemsOperation(account: account, organisedItemsProvider: provider, log: support.log) diff --git a/Frameworks/Account/AccountTests/Feedly/TestGetEntriesService.swift b/Frameworks/Account/AccountTests/Feedly/TestGetEntriesService.swift new file mode 100644 index 000000000..7af98b393 --- /dev/null +++ b/Frameworks/Account/AccountTests/Feedly/TestGetEntriesService.swift @@ -0,0 +1,26 @@ +// +// TestGetEntriesService.swift +// AccountTests +// +// Created by Kiel Gillard on 11/1/20. +// Copyright © 2020 Ranchero Software, LLC. All rights reserved. +// + +import XCTest +@testable import Account + +final class TestGetEntriesService: FeedlyGetEntriesService { + var mockResult: Result<[FeedlyEntry], Error>? + var getEntriesExpectation: XCTestExpectation? + + func getEntries(for ids: Set, completion: @escaping (Result<[FeedlyEntry], Error>) -> ()) { + guard let result = mockResult else { + XCTFail("Missing mock result. Test may time out because the completion will not be called.") + return + } + DispatchQueue.main.async { + completion(result) + self.getEntriesExpectation?.fulfill() + } + } +} diff --git a/Frameworks/Account/Feedly/FeedlyAPICaller.swift b/Frameworks/Account/Feedly/FeedlyAPICaller.swift index afa4d7747..dfd101325 100644 --- a/Frameworks/Account/Feedly/FeedlyAPICaller.swift +++ b/Frameworks/Account/Feedly/FeedlyAPICaller.swift @@ -637,7 +637,7 @@ extension FeedlyAPICaller: FeedlyGetStreamIdsService { } queryItems.append(contentsOf: [ - URLQueryItem(name: "count", value: "1000"), + URLQueryItem(name: "count", value: "10000"), URLQueryItem(name: "streamId", value: resource.id), ]) diff --git a/Frameworks/Account/Feedly/FeedlyAccountDelegate.swift b/Frameworks/Account/Feedly/FeedlyAccountDelegate.swift index 98af63ca2..4e24cde7d 100644 --- a/Frameworks/Account/Feedly/FeedlyAccountDelegate.swift +++ b/Frameworks/Account/Feedly/FeedlyAccountDelegate.swift @@ -147,10 +147,6 @@ final class FeedlyAccountDelegate: AccountDelegate { /// So if the user is using another client roughly simultaneously with this app, /// this app does its part to ensure the articles have a consistent status between both. /// - /// Feedly has no API that allows the app to fetch the identifiers of unread articles only. - /// The only way to identify unread articles is to pull all of the article data, - /// which is effectively equivalent of a full refresh. - /// /// - Parameter account: The account whose articles have a remote status. /// - Parameter completion: Call on the main queue. func refreshArticleStatus(for account: Account, completion: @escaping ((Result) -> Void)) { @@ -160,18 +156,18 @@ final class FeedlyAccountDelegate: AccountDelegate { let group = DispatchGroup() - let syncUnread = FeedlySyncUnreadStatusesOperation(account: account, credentials: credentials, service: caller, newerThan: nil, log: log) + let ingestUnread = FeedlyIngestUnreadArticleIdsOperation(account: account, credentials: credentials, service: caller, newerThan: nil, log: log) group.enter() - syncUnread.completionBlock = { + ingestUnread.completionBlock = { group.leave() } - let syncStarred = FeedlySyncStarredArticlesOperation(account: account, credentials: credentials, service: caller, log: log) + let ingestStarred = FeedlyIngestStarredArticleIdsOperation(account: account, credentials: credentials, service: caller, newerThan: nil, log: log) group.enter() - syncStarred.completionBlock = { + ingestStarred.completionBlock = { group.leave() } @@ -179,7 +175,7 @@ final class FeedlyAccountDelegate: AccountDelegate { completion(.success(())) } - operationQueue.addOperations([syncUnread, syncStarred], waitUntilFinished: false) + operationQueue.addOperations([ingestUnread, ingestStarred], waitUntilFinished: false) } func importOPML(for account: Account, opmlFile: URL, completion: @escaping (Result) -> Void) { diff --git a/Frameworks/Account/Feedly/Models/FeedlyEntryIdentifierProviding.swift b/Frameworks/Account/Feedly/Models/FeedlyEntryIdentifierProviding.swift new file mode 100644 index 000000000..4ad0054ec --- /dev/null +++ b/Frameworks/Account/Feedly/Models/FeedlyEntryIdentifierProviding.swift @@ -0,0 +1,29 @@ +// +// FeedlyEntryIdentifierProviding.swift +// Account +// +// Created by Kiel Gillard on 9/1/20. +// Copyright © 2020 Ranchero Software, LLC. All rights reserved. +// + +import Foundation + +protocol FeedlyEntryIdentifierProviding: class { + var entryIds: Set { get } +} + +final class FeedlyEntryIdentifierProvider: FeedlyEntryIdentifierProviding { + private (set) var entryIds: Set + + init(entryIds: Set = Set()) { + self.entryIds = entryIds + } + + func addEntryIds(from provider: FeedlyEntryIdentifierProviding) { + entryIds.formUnion(provider.entryIds) + } + + func addEntryIds(in articleIds: [String]) { + entryIds.formUnion(articleIds) + } +} diff --git a/Frameworks/Account/Feedly/Operations/FeedlyAddNewFeedOperation.swift b/Frameworks/Account/Feedly/Operations/FeedlyAddNewFeedOperation.swift index bb98a7ada..4d83e41ad 100644 --- a/Frameworks/Account/Feedly/Operations/FeedlyAddNewFeedOperation.swift +++ b/Frameworks/Account/Feedly/Operations/FeedlyAddNewFeedOperation.swift @@ -92,7 +92,7 @@ class FeedlyAddNewFeedOperation: FeedlyOperation, FeedlyOperationDelegate, Feedl createFeeds.downloadProgress = downloadProgress self.operationQueue.addOperation(createFeeds) - let syncUnread = FeedlySyncUnreadStatusesOperation(account: account, credentials: credentials, service: syncUnreadIdsService, newerThan: nil, log: log) + let syncUnread = FeedlyIngestUnreadArticleIdsOperation(account: account, credentials: credentials, service: syncUnreadIdsService, newerThan: nil, log: log) syncUnread.addDependency(createFeeds) syncUnread.downloadProgress = downloadProgress self.operationQueue.addOperation(syncUnread) diff --git a/Frameworks/Account/Feedly/Operations/FeedlyDownloadArticlesOperation.swift b/Frameworks/Account/Feedly/Operations/FeedlyDownloadArticlesOperation.swift new file mode 100644 index 000000000..fb8c36604 --- /dev/null +++ b/Frameworks/Account/Feedly/Operations/FeedlyDownloadArticlesOperation.swift @@ -0,0 +1,99 @@ +// +// FeedlyDownloadArticlesOperation.swift +// Account +// +// Created by Kiel Gillard on 9/1/20. +// Copyright © 2020 Ranchero Software, LLC. All rights reserved. +// + +import Foundation +import os.log + +class FeedlyDownloadArticlesOperation: FeedlyOperation { + private let account: Account + private let log: OSLog + private let missingArticleEntryIdProvider: FeedlyEntryIdentifierProviding + private let updatedArticleEntryIdProvider: FeedlyEntryIdentifierProviding + private let getEntriesService: FeedlyGetEntriesService + private let operationQueue: OperationQueue + private let finishOperation: FeedlyCheckpointOperation + + init(account: Account, missingArticleEntryIdProvider: FeedlyEntryIdentifierProviding, updatedArticleEntryIdProvider: FeedlyEntryIdentifierProviding, getEntriesService: FeedlyGetEntriesService, log: OSLog) { + self.account = account + self.operationQueue = OperationQueue() + self.operationQueue.isSuspended = true + self.missingArticleEntryIdProvider = missingArticleEntryIdProvider + self.updatedArticleEntryIdProvider = updatedArticleEntryIdProvider + self.getEntriesService = getEntriesService + self.finishOperation = FeedlyCheckpointOperation() + self.log = log + + super.init() + + self.finishOperation.checkpointDelegate = self + self.operationQueue.addOperation(self.finishOperation) + } + + override func cancel() { + os_log(.debug, log: log, "Cancelling %{public}@.", self) + operationQueue.cancelAllOperations() + super.cancel() + didFinish() + } + + override func main() { + guard !isCancelled else { + // override of cancel calls didFinish(). + return + } + + var articleIds = missingArticleEntryIdProvider.entryIds + articleIds.formUnion(updatedArticleEntryIdProvider.entryIds) + + os_log(.debug, log: log, "Requesting %{public}i articles.", articleIds.count) + + let feedlyAPILimitBatchSize = 1000 + for articleIds in Array(articleIds).chunked(into: feedlyAPILimitBatchSize) { + + let provider = FeedlyEntryIdentifierProvider(entryIds: Set(articleIds)) + let getEntries = FeedlyGetEntriesOperation(account: account, service: getEntriesService, provider: provider, log: log) + getEntries.delegate = self + self.operationQueue.addOperation(getEntries) + + let organiseByFeed = FeedlyOrganiseParsedItemsByFeedOperation(account: account, + parsedItemProvider: getEntries, + log: log) + organiseByFeed.delegate = self + organiseByFeed.addDependency(getEntries) + self.operationQueue.addOperation(organiseByFeed) + + let updateAccount = FeedlyUpdateAccountFeedsWithItemsOperation(account: account, + organisedItemsProvider: organiseByFeed, + log: log) + + updateAccount.delegate = self + updateAccount.addDependency(organiseByFeed) + self.operationQueue.addOperation(updateAccount) + + finishOperation.addDependency(updateAccount) + } + + operationQueue.isSuspended = false + } +} + +extension FeedlyDownloadArticlesOperation: FeedlyCheckpointOperationDelegate { + + func feedlyCheckpointOperationDidReachCheckpoint(_ operation: FeedlyCheckpointOperation) { + didFinish() + } +} + +extension FeedlyDownloadArticlesOperation: FeedlyOperationDelegate { + + func feedlyOperation(_ operation: FeedlyOperation, didFailWith error: Error) { + assert(Thread.isMainThread) + os_log(.debug, log: log, "%{public}@ failed with error: %{public}@.", operation, error as NSError) + cancel() + } +} diff --git a/Frameworks/Account/Feedly/Operations/FeedlyFetchIdsForMissingArticlesOperation.swift b/Frameworks/Account/Feedly/Operations/FeedlyFetchIdsForMissingArticlesOperation.swift new file mode 100644 index 000000000..f4ccd7c98 --- /dev/null +++ b/Frameworks/Account/Feedly/Operations/FeedlyFetchIdsForMissingArticlesOperation.swift @@ -0,0 +1,40 @@ +// +// FeedlyFetchIdsForMissingArticlesOperation.swift +// Account +// +// Created by Kiel Gillard on 7/1/20. +// Copyright © 2020 Ranchero Software, LLC. All rights reserved. +// + +import Foundation +import os.log + +final class FeedlyFetchIdsForMissingArticlesOperation: FeedlyOperation, FeedlyEntryIdentifierProviding { + private let account: Account + private let log: OSLog + + private(set) var entryIds = Set() + + init(account: Account, log: OSLog) { + self.account = account + self.log = log + } + + override func main() { + guard !isCancelled else { + didFinish() + return + } + + account.fetchArticleIDsForStatusesWithoutArticlesNewerThanCutoffDate { result in + switch result { + case .success(let articleIds): + self.entryIds.formUnion(articleIds) + self.didFinish() + + case .failure(let error): + self.didFinish(error) + } + } + } +} diff --git a/Frameworks/Account/Feedly/Operations/FeedlyGetEntriesOperation.swift b/Frameworks/Account/Feedly/Operations/FeedlyGetEntriesOperation.swift index e16cca103..ab150dee0 100644 --- a/Frameworks/Account/Feedly/Operations/FeedlyGetEntriesOperation.swift +++ b/Frameworks/Account/Feedly/Operations/FeedlyGetEntriesOperation.swift @@ -8,15 +8,16 @@ import Foundation import os.log +import RSParser /// Single responsibility is to get full entries for the entry identifiers. -final class FeedlyGetEntriesOperation: FeedlyOperation, FeedlyEntryProviding { +final class FeedlyGetEntriesOperation: FeedlyOperation, FeedlyEntryProviding, FeedlyParsedItemProviding { let account: Account let service: FeedlyGetEntriesService - let provider: FeedlyEntryIdenifierProviding + let provider: FeedlyEntryIdentifierProviding let log: OSLog - init(account: Account, service: FeedlyGetEntriesService, provider: FeedlyEntryIdenifierProviding, log: OSLog) { + init(account: Account, service: FeedlyGetEntriesService, provider: FeedlyEntryIdentifierProviding, log: OSLog) { self.account = account self.service = service self.provider = provider @@ -25,6 +26,33 @@ final class FeedlyGetEntriesOperation: FeedlyOperation, FeedlyEntryProviding { private (set) var entries = [FeedlyEntry]() + private var storedParsedEntries: Set? + + var parsedEntries: Set { + if let entries = storedParsedEntries { + return entries + } + + let parsed = Set(entries.compactMap { + FeedlyEntryParser(entry: $0).parsedItemRepresentation + }) + + if parsed.count != entries.count { + let entryIds = Set(entries.map { $0.id }) + let parsedIds = Set(parsed.map { $0.uniqueID }) + let difference = entryIds.subtracting(parsedIds) + os_log(.debug, log: log, "%{public}@ dropping articles with ids: %{public}@.", self, difference) + } + + storedParsedEntries = parsed + + return parsed + } + + var parsedItemProviderName: String { + return name ?? String(describing: Self.self) + } + override func main() { guard !isCancelled else { didFinish() diff --git a/Frameworks/Account/Feedly/Operations/FeedlyGetStreamContentsOperation.swift b/Frameworks/Account/Feedly/Operations/FeedlyGetStreamContentsOperation.swift index db8c1a201..adb0f0059 100644 --- a/Frameworks/Account/Feedly/Operations/FeedlyGetStreamContentsOperation.swift +++ b/Frameworks/Account/Feedly/Operations/FeedlyGetStreamContentsOperation.swift @@ -15,7 +15,7 @@ protocol FeedlyEntryProviding { } protocol FeedlyParsedItemProviding { - var resource: FeedlyResourceId { get } + var parsedItemProviderName: String { get } var parsedEntries: Set { get } } @@ -32,8 +32,8 @@ final class FeedlyGetStreamContentsOperation: FeedlyOperation, FeedlyEntryProvid let resourceProvider: FeedlyResourceProviding - var resource: FeedlyResourceId { - return resourceProvider.resource + var parsedItemProviderName: String { + return resourceProvider.resource.id } var entries: [FeedlyEntry] { diff --git a/Frameworks/Account/Feedly/Operations/FeedlyGetStreamIdsOperation.swift b/Frameworks/Account/Feedly/Operations/FeedlyGetStreamIdsOperation.swift index 6624a792b..940bede54 100644 --- a/Frameworks/Account/Feedly/Operations/FeedlyGetStreamIdsOperation.swift +++ b/Frameworks/Account/Feedly/Operations/FeedlyGetStreamIdsOperation.swift @@ -9,17 +9,12 @@ import Foundation import os.log -protocol FeedlyEntryIdenifierProviding: class { - var resource: FeedlyResourceId { get } - var entryIds: Set { get } -} - protocol FeedlyGetStreamIdsOperationDelegate: class { func feedlyGetStreamIdsOperation(_ operation: FeedlyGetStreamIdsOperation, didGet streamIds: FeedlyStreamIds) } /// Single responsibility is to get the stream ids from Feedly. -final class FeedlyGetStreamIdsOperation: FeedlyOperation, FeedlyEntryIdenifierProviding, FeedlyUnreadEntryIdProviding { +final class FeedlyGetStreamIdsOperation: FeedlyOperation, FeedlyEntryIdentifierProviding { var entryIds: Set { guard let ids = streamIds?.ids else { diff --git a/Frameworks/Account/Feedly/Operations/FeedlyGetUpdatedArticleIdsOperation.swift b/Frameworks/Account/Feedly/Operations/FeedlyGetUpdatedArticleIdsOperation.swift new file mode 100644 index 000000000..8be988176 --- /dev/null +++ b/Frameworks/Account/Feedly/Operations/FeedlyGetUpdatedArticleIdsOperation.swift @@ -0,0 +1,83 @@ +// +// FeedlyGetUpdatedArticleIdsOperation.swift +// Account +// +// Created by Kiel Gillard on 11/1/20. +// Copyright © 2020 Ranchero Software, LLC. All rights reserved. +// + +import Foundation +import os.log + +/// Single responsibility is to identify articles that have changed since a particular date. +/// +/// Typically, it pages through the article ids of the global.all stream. +/// When all the article ids are collected, it is the responsibility of another operation to download them when appropriate. +class FeedlyGetUpdatedArticleIdsOperation: FeedlyOperation, FeedlyEntryIdentifierProviding { + private let account: Account + private let resource: FeedlyResourceId + private let service: FeedlyGetStreamIdsService + private let newerThan: Date? + private let log: OSLog + + init(account: Account, resource: FeedlyResourceId, service: FeedlyGetStreamIdsService, newerThan: Date?, log: OSLog) { + self.account = account + self.resource = resource + self.service = service + self.newerThan = newerThan + self.log = log + } + + convenience init(account: Account, credentials: Credentials, service: FeedlyGetStreamIdsService, newerThan: Date?, log: OSLog) { + let all = FeedlyCategoryResourceId.Global.all(for: credentials.username) + self.init(account: account, resource: all, service: service, newerThan: newerThan, log: log) + } + + var entryIds: Set { + return storedUpdatedArticleIds + } + + private var storedUpdatedArticleIds = Set() + + override func main() { + guard !isCancelled else { + didFinish() + return + } + + getStreamIds(nil) + } + + private func getStreamIds(_ continuation: String?) { + guard let date = newerThan else { + os_log(.debug, log: log, "No date provided so everything must be new (nothing is updated).") + didFinish() + return + } + + service.getStreamIds(for: resource, continuation: continuation, newerThan: date, unreadOnly: nil, completion: didGetStreamIds(_:)) + } + + private func didGetStreamIds(_ result: Result) { + guard !isCancelled else { + didFinish() + return + } + + switch result { + case .success(let streamIds): + storedUpdatedArticleIds.formUnion(streamIds.ids) + + guard let continuation = streamIds.continuation else { + os_log(.debug, log: log, "%{public}i articles updated since last successful sync start date.", storedUpdatedArticleIds.count) + didFinish() + return + } + + getStreamIds(continuation) + + case .failure(let error): + didFinish(error) + } + } +} diff --git a/Frameworks/Account/Feedly/Operations/FeedlyIngestStarredArticleIdsOperation.swift b/Frameworks/Account/Feedly/Operations/FeedlyIngestStarredArticleIdsOperation.swift new file mode 100644 index 000000000..87b9d73b9 --- /dev/null +++ b/Frameworks/Account/Feedly/Operations/FeedlyIngestStarredArticleIdsOperation.swift @@ -0,0 +1,130 @@ +// +// FeedlyIngestStarredArticleIdsOperation.swift +// Account +// +// Created by Kiel Gillard on 15/10/19. +// Copyright © 2019 Ranchero Software, LLC. All rights reserved. +// + +import Foundation +import os.log + +/// Single responsibility is to clone locally the remote starred article state. +/// +/// Typically, it pages through the article ids of the global.saved stream. +/// When all the article ids are collected, a status is created for each. +/// The article ids previously marked as starred but not collected become unstarred. +/// So this operation has side effects *for the entire account* it operates on. +final class FeedlyIngestStarredArticleIdsOperation: FeedlyOperation { + private let account: Account + private let resource: FeedlyResourceId + private let service: FeedlyGetStreamIdsService + private let entryIdsProvider: FeedlyEntryIdentifierProvider + private let log: OSLog + + convenience init(account: Account, credentials: Credentials, service: FeedlyGetStreamIdsService, newerThan: Date?, log: OSLog) { + let resource = FeedlyTagResourceId.Global.saved(for: credentials.username) + self.init(account: account, resource: resource, service: service, newerThan: newerThan, log: log) + } + + init(account: Account, resource: FeedlyResourceId, service: FeedlyGetStreamIdsService, newerThan: Date?, log: OSLog) { + self.account = account + self.resource = resource + self.service = service + self.entryIdsProvider = FeedlyEntryIdentifierProvider() + self.log = log + } + + override func main() { + guard !isCancelled else { + didFinish() + return + } + + getStreamIds(nil) + } + + private func getStreamIds(_ continuation: String?) { + service.getStreamIds(for: resource, continuation: continuation, newerThan: nil, unreadOnly: nil, completion: didGetStreamIds(_:)) + } + + private func didGetStreamIds(_ result: Result) { + guard !isCancelled else { + didFinish() + return + } + + switch result { + case .success(let streamIds): + + entryIdsProvider.addEntryIds(in: streamIds.ids) + + guard let continuation = streamIds.continuation else { + updateStarredStatuses() + return + } + + getStreamIds(continuation) + + case .failure(let error): + didFinish(error) + } + } + + private func updateStarredStatuses() { + guard !isCancelled else { + didFinish() + return + } + + account.fetchStarredArticleIDs { result in + switch result { + case .success(let localStarredArticleIDs): + self.processStarredArticleIDs(localStarredArticleIDs) + + case .failure(let error): + self.didFinish(error) + } + } + } + + func processStarredArticleIDs(_ localStarredArticleIDs: Set) { + guard !isCancelled else { + didFinish() + return + } + + let remoteStarredArticleIDs = entryIdsProvider.entryIds + + let group = DispatchGroup() + + final class StarredStatusResults { + var markAsStarredError: Error? + var markAsUnstarredError: Error? + } + + let results = StarredStatusResults() + + group.enter() + account.markAsStarred(remoteStarredArticleIDs) { error in + results.markAsStarredError = error + group.leave() + } + + let deltaUnstarredArticleIDs = localStarredArticleIDs.subtracting(remoteStarredArticleIDs) + group.enter() + account.markAsUnstarred(deltaUnstarredArticleIDs) { error in + results.markAsUnstarredError = error + group.leave() + } + + group.notify(queue: .main) { + let markingError = results.markAsStarredError ?? results.markAsUnstarredError + guard let error = markingError else { + self.didFinish() + return + } + self.didFinish(error) + } + } +} diff --git a/Frameworks/Account/Feedly/Operations/FeedlyIngestStreamArticleIdsOperation.swift b/Frameworks/Account/Feedly/Operations/FeedlyIngestStreamArticleIdsOperation.swift new file mode 100644 index 000000000..d115cea38 --- /dev/null +++ b/Frameworks/Account/Feedly/Operations/FeedlyIngestStreamArticleIdsOperation.swift @@ -0,0 +1,75 @@ +// +// FeedlyIngestStreamArticleIdsOperation.swift +// Account +// +// Created by Kiel Gillard on 9/1/20. +// Copyright © 2020 Ranchero Software, LLC. All rights reserved. +// + +import Foundation +import os.log + +/// Single responsibility is to ensure a status exists for every article id the user might be interested in. +/// +/// Typically, it pages through the article ids of the global.all stream. +/// As the article ids are collected, a default read status is created for each. +/// So this operation has side effects *for the entire account* it operates on. +class FeedlyIngestStreamArticleIdsOperation: FeedlyOperation { + private let account: Account + private let resource: FeedlyResourceId + private let service: FeedlyGetStreamIdsService + private let log: OSLog + + init(account: Account, resource: FeedlyResourceId, service: FeedlyGetStreamIdsService, log: OSLog) { + self.account = account + self.resource = resource + self.service = service + self.log = log + } + + convenience init(account: Account, credentials: Credentials, service: FeedlyGetStreamIdsService, log: OSLog) { + let all = FeedlyCategoryResourceId.Global.all(for: credentials.username) + self.init(account: account, resource: all, service: service, log: log) + } + + override func main() { + guard !isCancelled else { + didFinish() + return + } + + getStreamIds(nil) + } + + private func getStreamIds(_ continuation: String?) { + service.getStreamIds(for: resource, continuation: continuation, newerThan: nil, unreadOnly: nil, completion: didGetStreamIds(_:)) + } + + private func didGetStreamIds(_ result: Result) { + guard !isCancelled else { + didFinish() + return + } + + switch result { + case .success(let streamIds): + account.createStatusesIfNeeded(articleIDs: Set(streamIds.ids)) { databaseError in + + if let error = databaseError { + self.didFinish(error) + return + } + + guard let continuation = streamIds.continuation else { + os_log(.debug, log: self.log, "Reached end of stream for %@", self.resource.id) + self.didFinish() + return + } + + self.getStreamIds(continuation) + } + case .failure(let error): + didFinish(error) + } + } +} diff --git a/Frameworks/Account/Feedly/Operations/FeedlyIngestUnreadArticleIdsOperation.swift b/Frameworks/Account/Feedly/Operations/FeedlyIngestUnreadArticleIdsOperation.swift new file mode 100644 index 000000000..3f5036138 --- /dev/null +++ b/Frameworks/Account/Feedly/Operations/FeedlyIngestUnreadArticleIdsOperation.swift @@ -0,0 +1,130 @@ +// +// FeedlyIngestUnreadArticleIdsOperation.swift +// Account +// +// Created by Kiel Gillard on 18/10/19. +// Copyright © 2019 Ranchero Software, LLC. All rights reserved. +// + +import Foundation +import os.log +import RSParser + +/// Single responsibility is to clone locally the remote unread article state. +/// +/// Typically, it pages through the unread article ids of the global.all stream. +/// When all the unread article ids are collected, a status is created for each. +/// The article ids previously marked as unread but not collected become read. +/// So this operation has side effects *for the entire account* it operates on. +final class FeedlyIngestUnreadArticleIdsOperation: FeedlyOperation { + private let account: Account + private let resource: FeedlyResourceId + private let service: FeedlyGetStreamIdsService + private let entryIdsProvider: FeedlyEntryIdentifierProvider + private let log: OSLog + + convenience init(account: Account, credentials: Credentials, service: FeedlyGetStreamIdsService, newerThan: Date?, log: OSLog) { + let resource = FeedlyCategoryResourceId.Global.all(for: credentials.username) + self.init(account: account, resource: resource, service: service, newerThan: newerThan, log: log) + } + + init(account: Account, resource: FeedlyResourceId, service: FeedlyGetStreamIdsService, newerThan: Date?, log: OSLog) { + self.account = account + self.resource = resource + self.service = service + self.entryIdsProvider = FeedlyEntryIdentifierProvider() + self.log = log + } + + override func main() { + guard !isCancelled else { + didFinish() + return + } + + getStreamIds(nil) + } + + private func getStreamIds(_ continuation: String?) { + service.getStreamIds(for: resource, continuation: continuation, newerThan: nil, unreadOnly: true, completion: didGetStreamIds(_:)) + } + + private func didGetStreamIds(_ result: Result) { + guard !isCancelled else { + didFinish() + return + } + + switch result { + case .success(let streamIds): + + entryIdsProvider.addEntryIds(in: streamIds.ids) + + guard let continuation = streamIds.continuation else { + updateUnreadStatuses() + return + } + + getStreamIds(continuation) + + case .failure(let error): + didFinish(error) + } + } + + private func updateUnreadStatuses() { + guard !isCancelled else { + didFinish() + return + } + + account.fetchUnreadArticleIDs { result in + switch result { + case .success(let localUnreadArticleIDs): + self.processUnreadArticleIDs(localUnreadArticleIDs) + + case .failure(let error): + self.didFinish(error) + } + } + } + + private func processUnreadArticleIDs(_ localUnreadArticleIDs: Set) { + guard !isCancelled else { + didFinish() + return + } + + let remoteUnreadArticleIDs = entryIdsProvider.entryIds + let group = DispatchGroup() + + final class ReadStatusResults { + var markAsUnreadError: Error? + var markAsReadError: Error? + } + + let results = ReadStatusResults() + + group.enter() + account.markAsUnread(remoteUnreadArticleIDs) { error in + results.markAsUnreadError = error + group.leave() + } + + let articleIDsToMarkRead = localUnreadArticleIDs.subtracting(remoteUnreadArticleIDs) + group.enter() + account.markAsRead(articleIDsToMarkRead) { error in + results.markAsReadError = error + group.leave() + } + + group.notify(queue: .main) { + let markingError = results.markAsReadError ?? results.markAsUnreadError + guard let error = markingError else { + self.didFinish() + return + } + self.didFinish(error) + } + } +} diff --git a/Frameworks/Account/Feedly/Operations/FeedlyOrganiseParsedItemsByFeedOperation.swift b/Frameworks/Account/Feedly/Operations/FeedlyOrganiseParsedItemsByFeedOperation.swift index b535c1b44..441d333f9 100644 --- a/Frameworks/Account/Feedly/Operations/FeedlyOrganiseParsedItemsByFeedOperation.swift +++ b/Frameworks/Account/Feedly/Operations/FeedlyOrganiseParsedItemsByFeedOperation.swift @@ -11,7 +11,7 @@ import RSParser import os.log protocol FeedlyParsedItemsByFeedProviding { - var providerName: String { get } + var parsedItemsByFeedProviderName: String { get } var parsedItemsKeyedByFeedId: [String: Set] { get } } @@ -21,15 +21,15 @@ final class FeedlyOrganiseParsedItemsByFeedOperation: FeedlyOperation, FeedlyPar private let parsedItemProvider: FeedlyParsedItemProviding private let log: OSLog + var parsedItemsByFeedProviderName: String { + return name ?? String(describing: Self.self) + } + var parsedItemsKeyedByFeedId: [String : Set] { assert(Thread.isMainThread) // Needs to be on main thread because Feed is a main-thread-only model type. return itemsKeyedByFeedId } - var providerName: String { - return parsedItemProvider.resource.id - } - private var itemsKeyedByFeedId = [String: Set]() init(account: Account, parsedItemProvider: FeedlyParsedItemProviding, log: OSLog) { @@ -61,7 +61,7 @@ final class FeedlyOrganiseParsedItemsByFeedOperation: FeedlyOperation, FeedlyPar guard !isCancelled else { return } } - os_log(.debug, log: log, "Grouped %i items by %i feeds for %@", items.count, dict.count, parsedItemProvider.resource.id) + os_log(.debug, log: log, "Grouped %i items by %i feeds for %@", items.count, dict.count, parsedItemProvider.parsedItemProviderName) itemsKeyedByFeedId = dict } diff --git a/Frameworks/Account/Feedly/Operations/FeedlySetStarredArticlesOperation.swift b/Frameworks/Account/Feedly/Operations/FeedlySetStarredArticlesOperation.swift deleted file mode 100644 index e3b0623be..000000000 --- a/Frameworks/Account/Feedly/Operations/FeedlySetStarredArticlesOperation.swift +++ /dev/null @@ -1,92 +0,0 @@ -// -// FeedlySetStarredArticlesOperation.swift -// Account -// -// Created by Kiel Gillard on 14/10/19. -// Copyright © 2019 Ranchero Software, LLC. All rights reserved. -// - -import Foundation -import os.log - -protocol FeedlyStarredEntryIdProviding { - var entryIds: Set { get } -} - -/// Single responsibility is to associate a starred status for ingested and remote -/// articles identfied by the provided identifiers *for the entire account.* -final class FeedlySetStarredArticlesOperation: FeedlyOperation { - private let account: Account - private let allStarredEntryIdsProvider: FeedlyStarredEntryIdProviding - private let log: OSLog - - init(account: Account, allStarredEntryIdsProvider: FeedlyStarredEntryIdProviding, log: OSLog) { - self.account = account - self.allStarredEntryIdsProvider = allStarredEntryIdsProvider - self.log = log - } - - override func main() { - guard !isCancelled else { - didFinish() - return - } - - account.fetchStarredArticleIDs { result in - switch result { - case .success(let localStarredArticleIDs): - self.processStarredArticleIDs(localStarredArticleIDs) - - case .failure(let error): - self.didFinish(error) - } - } - } -} - -private extension FeedlySetStarredArticlesOperation { - - func processStarredArticleIDs(_ localStarredArticleIDs: Set) { - guard !isCancelled else { - didFinish() - return - } - - let remoteStarredArticleIDs = allStarredEntryIdsProvider.entryIds - guard !remoteStarredArticleIDs.isEmpty else { - didFinish() - return - } - - let group = DispatchGroup() - - final class StarredStatusResults { - var markAsStarredError: Error? - var markAsUnstarredError: Error? - } - - let results = StarredStatusResults() - - group.enter() - account.markAsStarred(remoteStarredArticleIDs) { error in - results.markAsStarredError = error - group.leave() - } - - let deltaUnstarredArticleIDs = localStarredArticleIDs.subtracting(remoteStarredArticleIDs) - group.enter() - account.markAsUnstarred(deltaUnstarredArticleIDs) { error in - results.markAsUnstarredError = error - group.leave() - } - - group.notify(queue: .main) { - let markingError = results.markAsStarredError ?? results.markAsUnstarredError - guard let error = markingError else { - self.didFinish() - return - } - self.didFinish(error) - } - } -} diff --git a/Frameworks/Account/Feedly/Operations/FeedlySetUnreadArticlesOperation.swift b/Frameworks/Account/Feedly/Operations/FeedlySetUnreadArticlesOperation.swift deleted file mode 100644 index f98e93e81..000000000 --- a/Frameworks/Account/Feedly/Operations/FeedlySetUnreadArticlesOperation.swift +++ /dev/null @@ -1,92 +0,0 @@ -// -// FeedlySetUnreadArticlesOperation.swift -// Account -// -// Created by Kiel Gillard on 25/9/19. -// Copyright © 2019 Ranchero Software, LLC. All rights reserved. -// - -import Foundation -import os.log - -protocol FeedlyUnreadEntryIdProviding { - var entryIds: Set { get } -} - -/// Single responsibility is to associate a read status for ingested and remote articles -/// where the provided article identifers identify the unread articles *for the entire account.* -final class FeedlySetUnreadArticlesOperation: FeedlyOperation { - private let account: Account - private let allUnreadIdsProvider: FeedlyUnreadEntryIdProviding - private let log: OSLog - - init(account: Account, allUnreadIdsProvider: FeedlyUnreadEntryIdProviding, log: OSLog) { - self.account = account - self.allUnreadIdsProvider = allUnreadIdsProvider - self.log = log - } - - override func main() { - guard !isCancelled else { - didFinish() - return - } - - account.fetchUnreadArticleIDs { result in - switch result { - case .success(let localUnreadArticleIDs): - self.processUnreadArticleIDs(localUnreadArticleIDs) - - case .failure(let error): - self.didFinish(error) - } - } - } -} - -private extension FeedlySetUnreadArticlesOperation { - - private func processUnreadArticleIDs(_ localUnreadArticleIDs: Set) { - guard !isCancelled else { - didFinish() - return - } - - let remoteUnreadArticleIDs = allUnreadIdsProvider.entryIds - guard !remoteUnreadArticleIDs.isEmpty else { - didFinish() - return - } - - let group = DispatchGroup() - - final class ReadStatusResults { - var markAsUnreadError: Error? - var markAsReadError: Error? - } - - let results = ReadStatusResults() - - group.enter() - account.markAsUnread(remoteUnreadArticleIDs) { error in - results.markAsUnreadError = error - group.leave() - } - - let articleIDsToMarkRead = localUnreadArticleIDs.subtracting(remoteUnreadArticleIDs) - group.enter() - account.markAsRead(articleIDsToMarkRead) { error in - results.markAsReadError = error - group.leave() - } - - group.notify(queue: .main) { - let markingError = results.markAsReadError ?? results.markAsUnreadError - guard let error = markingError else { - self.didFinish() - return - } - self.didFinish(error) - } - } -} diff --git a/Frameworks/Account/Feedly/Operations/FeedlySyncAllOperation.swift b/Frameworks/Account/Feedly/Operations/FeedlySyncAllOperation.swift index 595ad0555..2218eb501 100644 --- a/Frameworks/Account/Feedly/Operations/FeedlySyncAllOperation.swift +++ b/Frameworks/Account/Feedly/Operations/FeedlySyncAllOperation.swift @@ -19,7 +19,19 @@ final class FeedlySyncAllOperation: FeedlyOperation { var syncCompletionHandler: ((Result) -> ())? - init(account: Account, credentials: Credentials, lastSuccessfulFetchStartDate: Date?, markArticlesService: FeedlyMarkArticlesService, getUnreadService: FeedlyGetStreamIdsService, getCollectionsService: FeedlyGetCollectionsService, getStreamContentsService: FeedlyGetStreamContentsService, getStarredArticlesService: FeedlyGetStreamContentsService, database: SyncDatabase, downloadProgress: DownloadProgress, log: OSLog) { + /// These requests to Feedly determine which articles to download: + /// 1. The set of all article ids we might need or show. + /// 2. The set of all unread article ids we might need or show (a subset of 1). + /// 3. The set of all article ids changed since the last sync (a subset of 1). + /// 4. The set of all starred article ids. + /// + /// On the response for 1, create statuses for each article id. + /// On the response for 2, create unread statuses for each article id and mark as read those no longer in the response. + /// On the response for 4, create starred statuses for each article id and mark as unstarred those no longer in the response. + /// + /// Download articles for statuses at the union of those statuses without its corresponding article and those included in 3 (changed since last successful sync). + /// + init(account: Account, credentials: Credentials, lastSuccessfulFetchStartDate: Date?, markArticlesService: FeedlyMarkArticlesService, getUnreadService: FeedlyGetStreamIdsService, getCollectionsService: FeedlyGetCollectionsService, getStreamContentsService: FeedlyGetStreamContentsService, getStarredService: FeedlyGetStreamIdsService, getStreamIdsService: FeedlyGetStreamIdsService, getEntriesService: FeedlyGetEntriesService, database: SyncDatabase, downloadProgress: DownloadProgress, log: OSLog) { self.syncUUID = UUID() self.log = log self.operationQueue = OperationQueue() @@ -54,48 +66,66 @@ final class FeedlySyncAllOperation: FeedlyOperation { createFeedsOperation.addDependency(mirrorCollectionsAsFolders) self.operationQueue.addOperation(createFeedsOperation) + let getAllArticleIds = FeedlyIngestStreamArticleIdsOperation(account: account, credentials: credentials, service: getStreamIdsService, log: log) + getAllArticleIds.delegate = self + getAllArticleIds.downloadProgress = downloadProgress + getAllArticleIds.addDependency(createFeedsOperation) + self.operationQueue.addOperation(getAllArticleIds) + // Get each page of unread article ids in the global.all stream for the last 31 days (nil = Feedly API default). - let getUnread = FeedlySyncUnreadStatusesOperation(account: account, credentials: credentials, service: getUnreadService, newerThan: nil, log: log) + let getUnread = FeedlyIngestUnreadArticleIdsOperation(account: account, credentials: credentials, service: getUnreadService, newerThan: nil, log: log) getUnread.delegate = self - getUnread.addDependency(createFeedsOperation) + getUnread.addDependency(getAllArticleIds) getUnread.downloadProgress = downloadProgress self.operationQueue.addOperation(getUnread) - // Get each page of the global.all stream until we get either the content from the last sync or the last 31 days. - let getStreamContents = FeedlySyncStreamContentsOperation(account: account, credentials: credentials, service: getStreamContentsService, newerThan: lastSuccessfulFetchStartDate, log: log) - getStreamContents.delegate = self - getStreamContents.downloadProgress = downloadProgress - getStreamContents.addDependency(getUnread) - self.operationQueue.addOperation(getStreamContents) + // Get each page of the article ids which have been update since the last successful fetch start date. + // If the date is nil, this operation provides an empty set (everything is new, nothing is updated). + let getUpdated = FeedlyGetUpdatedArticleIdsOperation(account: account, credentials: credentials, service: getStreamIdsService, newerThan: lastSuccessfulFetchStartDate, log: log) + getUpdated.delegate = self + getUpdated.downloadProgress = downloadProgress + getUpdated.addDependency(createFeedsOperation) + self.operationQueue.addOperation(getUpdated) - // Get each and every starred article. - let syncStarred = FeedlySyncStarredArticlesOperation(account: account, credentials: credentials, service: getStarredArticlesService, log: log) - syncStarred.delegate = self - syncStarred.downloadProgress = downloadProgress - syncStarred.addDependency(createFeedsOperation) - self.operationQueue.addOperation(syncStarred) + // Get each page of the article ids for starred articles. + let getStarred = FeedlyIngestStarredArticleIdsOperation(account: account, credentials: credentials, service: getStarredService, newerThan: nil, log: log) + getStarred.delegate = self + getStarred.downloadProgress = downloadProgress + getStarred.addDependency(createFeedsOperation) + self.operationQueue.addOperation(getStarred) + + // Now all the possible article ids we need have a status, fetch the article ids for missing articles. + let getMissingIds = FeedlyFetchIdsForMissingArticlesOperation(account: account, log: log) + getMissingIds.delegate = self + getMissingIds.downloadProgress = downloadProgress + getMissingIds.addDependency(getAllArticleIds) + getMissingIds.addDependency(getUnread) + getMissingIds.addDependency(getStarred) + getMissingIds.addDependency(getUpdated) + self.operationQueue.addOperation(getMissingIds) + + // Download all the missing and updated articles + let downloadMissingArticles = FeedlyDownloadArticlesOperation(account: account, + missingArticleEntryIdProvider: getMissingIds, + updatedArticleEntryIdProvider: getUpdated, + getEntriesService: getEntriesService, + log: log) + downloadMissingArticles.delegate = self + downloadMissingArticles.downloadProgress = downloadProgress + downloadMissingArticles.addDependency(getMissingIds) + downloadMissingArticles.addDependency(getUpdated) + self.operationQueue.addOperation(downloadMissingArticles) // Once this operation's dependencies, their dependencies etc finish, we can finish. let finishOperation = FeedlyCheckpointOperation() finishOperation.checkpointDelegate = self finishOperation.downloadProgress = downloadProgress - finishOperation.addDependency(getStreamContents) - finishOperation.addDependency(syncStarred) - + finishOperation.addDependency(downloadMissingArticles) self.operationQueue.addOperation(finishOperation) } convenience init(account: Account, credentials: Credentials, caller: FeedlyAPICaller, database: SyncDatabase, lastSuccessfulFetchStartDate: Date?, downloadProgress: DownloadProgress, log: OSLog) { - - let newerThan: Date? = { - if let date = lastSuccessfulFetchStartDate { - return date - } else { - return Calendar.current.date(byAdding: .day, value: -31, to: Date()) - } - }() - - self.init(account: account, credentials: credentials, lastSuccessfulFetchStartDate: newerThan, markArticlesService: caller, getUnreadService: caller, getCollectionsService: caller, getStreamContentsService: caller, getStarredArticlesService: caller, database: database, downloadProgress: downloadProgress, log: log) + self.init(account: account, credentials: credentials, lastSuccessfulFetchStartDate: lastSuccessfulFetchStartDate, markArticlesService: caller, getUnreadService: caller, getCollectionsService: caller, getStreamContentsService: caller, getStarredService: caller, getStreamIdsService: caller, getEntriesService: caller, database: database, downloadProgress: downloadProgress, log: log) } override func cancel() { diff --git a/Frameworks/Account/Feedly/Operations/FeedlySyncStarredArticlesOperation.swift b/Frameworks/Account/Feedly/Operations/FeedlySyncStarredArticlesOperation.swift deleted file mode 100644 index b8da7d64e..000000000 --- a/Frameworks/Account/Feedly/Operations/FeedlySyncStarredArticlesOperation.swift +++ /dev/null @@ -1,150 +0,0 @@ -// -// FeedlySyncStarredArticlesOperation.swift -// Account -// -// Created by Kiel Gillard on 15/10/19. -// Copyright © 2019 Ranchero Software, LLC. All rights reserved. -// - -import Foundation -import os.log -import RSParser - -final class FeedlySyncStarredArticlesOperation: FeedlyOperation, FeedlyOperationDelegate, FeedlyGetStreamContentsOperationDelegate, FeedlyCheckpointOperationDelegate { - private let account: Account - private let operationQueue: OperationQueue - private let service: FeedlyGetStreamContentsService - private let log: OSLog - - private let setStatuses: FeedlySetStarredArticlesOperation - private let finishOperation: FeedlyCheckpointOperation - - /// Buffers every starred/saved entry from every page. - private class StarredEntryProvider: FeedlyEntryProviding, FeedlyStarredEntryIdProviding, FeedlyParsedItemProviding { - var resource: FeedlyResourceId - - private(set) var parsedEntries = Set() - private(set) var entries = [FeedlyEntry]() - - init(resource: FeedlyResourceId) { - self.resource = resource - } - - func addEntries(from provider: FeedlyEntryProviding & FeedlyParsedItemProviding) { - entries.append(contentsOf: provider.entries) - parsedEntries.formUnion(provider.parsedEntries) - } - - var entryIds: Set { - return Set(entries.map { $0.id }) - } - } - - private let entryProvider: StarredEntryProvider - - convenience init(account: Account, credentials: Credentials, service: FeedlyGetStreamContentsService, log: OSLog) { - let saved = FeedlyTagResourceId.Global.saved(for: credentials.username) - self.init(account: account, resource: saved, service: service, log: log) - } - - init(account: Account, resource: FeedlyResourceId, service: FeedlyGetStreamContentsService, log: OSLog) { - self.account = account - self.service = service - self.operationQueue = OperationQueue() - self.operationQueue.isSuspended = true - self.finishOperation = FeedlyCheckpointOperation() - self.log = log - - let provider = StarredEntryProvider(resource: resource) - self.entryProvider = provider - self.setStatuses = FeedlySetStarredArticlesOperation(account: account, - allStarredEntryIdsProvider: provider, - log: log) - - super.init() - - let getFirstPage = FeedlyGetStreamContentsOperation(account: account, - resource: resource, - service: service, - newerThan: nil, - log: log) - - let organiseByFeed = FeedlyOrganiseParsedItemsByFeedOperation(account: account, - parsedItemProvider: provider, - log: log) - - let updateAccount = FeedlyUpdateAccountFeedsWithItemsOperation(account: account, - organisedItemsProvider: organiseByFeed, - log: log) - - getFirstPage.delegate = self - getFirstPage.streamDelegate = self - - setStatuses.addDependency(getFirstPage) - setStatuses.delegate = self - - organiseByFeed.addDependency(setStatuses) - organiseByFeed.delegate = self - - updateAccount.addDependency(organiseByFeed) - updateAccount.delegate = self - - finishOperation.checkpointDelegate = self - finishOperation.addDependency(updateAccount) - - let operations = [getFirstPage, setStatuses, organiseByFeed, updateAccount, finishOperation] - operationQueue.addOperations(operations, waitUntilFinished: false) - } - - override func cancel() { - os_log(.debug, log: log, "Canceling sync starred articles") - operationQueue.cancelAllOperations() - super.cancel() - didFinish() - } - - override func main() { - guard !isCancelled else { - // override of cancel calls didFinish(). - return - } - - operationQueue.isSuspended = false - } - - func feedlyGetStreamContentsOperation(_ operation: FeedlyGetStreamContentsOperation, didGetContentsOf stream: FeedlyStream) { - guard !isCancelled else { - os_log(.debug, log: log, "Cancelled starred stream contents for %@", stream.id) - return - } - - entryProvider.addEntries(from: operation) - os_log(.debug, log: log, "Collecting %i items from %@", stream.items.count, stream.id) - - guard let continuation = stream.continuation else { - return - } - - let nextPageOperation = FeedlyGetStreamContentsOperation(account: operation.account, - resource: operation.resource, - service: operation.service, - continuation: continuation, - newerThan: operation.newerThan, - log: log) - nextPageOperation.delegate = self - nextPageOperation.streamDelegate = self - - setStatuses.addDependency(nextPageOperation) - operationQueue.addOperation(nextPageOperation) - } - - func feedlyCheckpointOperationDidReachCheckpoint(_ operation: FeedlyCheckpointOperation) { - didFinish() - } - - func feedlyOperation(_ operation: FeedlyOperation, didFailWith error: Error) { - os_log(.debug, log: log, "%{public}@ failing and cancelling other operations because %{public}@.", operation, error.localizedDescription) - operationQueue.cancelAllOperations() - didFinish(error) - } -} diff --git a/Frameworks/Account/Feedly/Operations/FeedlySyncUnreadStatusesOperation.swift b/Frameworks/Account/Feedly/Operations/FeedlySyncUnreadStatusesOperation.swift deleted file mode 100644 index 9c015fd86..000000000 --- a/Frameworks/Account/Feedly/Operations/FeedlySyncUnreadStatusesOperation.swift +++ /dev/null @@ -1,131 +0,0 @@ -// -// FeedlySyncUnreadStatusesOperation.swift -// Account -// -// Created by Kiel Gillard on 18/10/19. -// Copyright © 2019 Ranchero Software, LLC. All rights reserved. -// - -import Foundation -import os.log -import RSParser - -/// Makes one or more requests to get the complete set of unread article ids to update the status of those articles *for the entire account.* -final class FeedlySyncUnreadStatusesOperation: FeedlyOperation, FeedlyOperationDelegate, FeedlyGetStreamIdsOperationDelegate, FeedlyCheckpointOperationDelegate { - private let account: Account - private let resource: FeedlyResourceId - private let operationQueue: OperationQueue - private let service: FeedlyGetStreamIdsService - private let log: OSLog - - /// Buffers every unread article id from every page of the resource's stream. - private class UnreadEntryIdsProvider: FeedlyUnreadEntryIdProviding { - let resource: FeedlyResourceId - private(set) var entryIds = Set() - - init(resource: FeedlyResourceId) { - self.resource = resource - } - - func addEntryIds(from provider: FeedlyEntryIdenifierProviding) { - entryIds.formUnion(provider.entryIds) - } - } - - private let unreadEntryIdsProvider: UnreadEntryIdsProvider - private let setStatuses: FeedlySetUnreadArticlesOperation - - convenience init(account: Account, credentials: Credentials, service: FeedlyGetStreamIdsService, newerThan: Date?, log: OSLog) { - let resource = FeedlyCategoryResourceId.Global.all(for: credentials.username) - self.init(account: account, resource: resource, service: service, newerThan: newerThan, log: log) - } - - init(account: Account, resource: FeedlyResourceId, service: FeedlyGetStreamIdsService, newerThan: Date?, log: OSLog) { - self.account = account - self.resource = resource - self.service = service - self.operationQueue = OperationQueue() - self.operationQueue.isSuspended = true - self.log = log - - let provider = UnreadEntryIdsProvider(resource: resource) - self.unreadEntryIdsProvider = provider - self.setStatuses = FeedlySetUnreadArticlesOperation(account: account, - allUnreadIdsProvider: unreadEntryIdsProvider, - log: log) - - super.init() - - let getFirstPageOfUnreadIds = FeedlyGetStreamIdsOperation(account: account, - resource: resource, - service: service, - newerThan: nil, - unreadOnly: true, - log: log) - - getFirstPageOfUnreadIds.delegate = self - getFirstPageOfUnreadIds.streamIdsDelegate = self - - setStatuses.addDependency(getFirstPageOfUnreadIds) - setStatuses.delegate = self - - let finishOperation = FeedlyCheckpointOperation() - finishOperation.checkpointDelegate = self - finishOperation.addDependency(setStatuses) - - let operations = [getFirstPageOfUnreadIds, setStatuses, finishOperation] - operationQueue.addOperations(operations, waitUntilFinished: false) - } - - override func cancel() { - os_log(.debug, log: log, "Canceling sync unread statuses") - operationQueue.cancelAllOperations() - super.cancel() - didFinish() - } - - override func main() { - guard !isCancelled else { - // override of cancel calls didFinish(). - return - } - - operationQueue.isSuspended = false - } - - func feedlyGetStreamIdsOperation(_ operation: FeedlyGetStreamIdsOperation, didGet streamIds: FeedlyStreamIds) { - guard !isCancelled else { - os_log(.debug, log: log, "Cancelled unread stream ids.") - return - } - - os_log(.debug, log: log, "Collecting %i unread article ids from %@", streamIds.ids.count, resource.id) - unreadEntryIdsProvider.addEntryIds(from: operation) - - guard let continuation = streamIds.continuation else { - return - } - - let nextPageOperation = FeedlyGetStreamIdsOperation(account: operation.account, - resource: operation.resource, - service: operation.service, - continuation: continuation, - newerThan: operation.newerThan, - unreadOnly: operation.unreadOnly, - log: log) - nextPageOperation.delegate = self - nextPageOperation.streamIdsDelegate = self - - setStatuses.addDependency(nextPageOperation) - operationQueue.addOperation(nextPageOperation) - } - - func feedlyCheckpointOperationDidReachCheckpoint(_ operation: FeedlyCheckpointOperation) { - didFinish() - } - - func feedlyOperation(_ operation: FeedlyOperation, didFailWith error: Error) { - operationQueue.cancelAllOperations() - didFinish(error) - } -} diff --git a/Frameworks/Account/Feedly/Operations/FeedlyUpdateAccountFeedsWithItemsOperation.swift b/Frameworks/Account/Feedly/Operations/FeedlyUpdateAccountFeedsWithItemsOperation.swift index b9820981e..2483321ba 100644 --- a/Frameworks/Account/Feedly/Operations/FeedlyUpdateAccountFeedsWithItemsOperation.swift +++ b/Frameworks/Account/Feedly/Operations/FeedlyUpdateAccountFeedsWithItemsOperation.swift @@ -37,7 +37,7 @@ final class FeedlyUpdateAccountFeedsWithItemsOperation: FeedlyOperation { return } - os_log(.debug, log: self.log, "Updated %i feeds for \"%@\"", webFeedIDsAndItems.count, self.organisedItemsProvider.providerName) + os_log(.debug, log: self.log, "Updated %i feeds for \"%@\"", webFeedIDsAndItems.count, self.organisedItemsProvider.parsedItemsByFeedProviderName) self.didFinish() } } diff --git a/Frameworks/ArticlesDatabase/ArticlesDatabase.swift b/Frameworks/ArticlesDatabase/ArticlesDatabase.swift index 97121dd6d..86b2b2210 100644 --- a/Frameworks/ArticlesDatabase/ArticlesDatabase.swift +++ b/Frameworks/ArticlesDatabase/ArticlesDatabase.swift @@ -176,7 +176,7 @@ public final class ArticlesDatabase { articlesTable.fetchStarredArticleIDsAsync(webFeedIDs, completion) } - /// Fetch articleIDs for articles that we should have, but don’t. These articles are not userDeleted, and they are either (starred) or (unread and newer than the article cutoff date). + /// Fetch articleIDs for articles that we should have, but don’t. These articles are not userDeleted, and they are either (starred) or (newer than the article cutoff date). public func fetchArticleIDsForStatusesWithoutArticlesNewerThanCutoffDate(_ completion: @escaping ArticleIDsCompletionBlock) { articlesTable.fetchArticleIDsForStatusesWithoutArticlesNewerThanCutoffDate(completion) }