Implement CloudKit feed add.

This commit is contained in:
Maurice Parker 2020-03-29 03:43:20 -05:00
parent ecc20ad9e3
commit 6ce82fc28b
7 changed files with 138 additions and 44 deletions

View File

@ -545,7 +545,6 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
let feed = WebFeed(account: self, url: url, metadata: metadata)
feed.name = name
feed.homePageURL = homePageURL
return feed
}
@ -683,7 +682,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
}
func update(_ webFeed: WebFeed, with parsedFeed: ParsedFeed, _ completion: @escaping DatabaseCompletionBlock) {
// Used only by an On My Mac account.
// Used only by an On My Mac and iCloud accounts.
webFeed.takeSettings(from: parsedFeed)
let webFeedIDsAndItems = [webFeed.webFeedID: parsedFeed.items]
update(webFeedIDsAndItems: webFeedIDsAndItems, defaultRead: false, completion: completion)

View File

@ -56,7 +56,7 @@
51BB7B84233531BC008E8144 /* AccountBehaviors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51BB7B83233531BC008E8144 /* AccountBehaviors.swift */; };
51BC8FCC237EC055004F8B56 /* Feed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51BC8FCB237EC055004F8B56 /* Feed.swift */; };
51BFDECE238B508D00216323 /* ContainerIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51BFDECD238B508D00216323 /* ContainerIdentifier.swift */; };
51C034DF242D65D20014DC71 /* CloudKitResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C034DE242D65D20014DC71 /* CloudKitResult.swift */; };
51C034DF242D65D20014DC71 /* CloudKitZoneResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C034DE242D65D20014DC71 /* CloudKitZoneResult.swift */; };
51C034E1242D660D0014DC71 /* CKError+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C034E0242D660D0014DC71 /* CKError+Extensions.swift */; };
51D58755227F53BE00900287 /* FeedbinTag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51D58754227F53BE00900287 /* FeedbinTag.swift */; };
51D5875A227F630B00900287 /* tags_delete.json in Resources */ = {isa = PBXBuildFile; fileRef = 51D58757227F630B00900287 /* tags_delete.json */; };
@ -287,7 +287,7 @@
51BB7B83233531BC008E8144 /* AccountBehaviors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountBehaviors.swift; sourceTree = "<group>"; };
51BC8FCB237EC055004F8B56 /* Feed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Feed.swift; sourceTree = "<group>"; };
51BFDECD238B508D00216323 /* ContainerIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContainerIdentifier.swift; sourceTree = "<group>"; };
51C034DE242D65D20014DC71 /* CloudKitResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudKitResult.swift; sourceTree = "<group>"; };
51C034DE242D65D20014DC71 /* CloudKitZoneResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudKitZoneResult.swift; sourceTree = "<group>"; };
51C034E0242D660D0014DC71 /* CKError+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CKError+Extensions.swift"; sourceTree = "<group>"; };
51D58754227F53BE00900287 /* FeedbinTag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinTag.swift; sourceTree = "<group>"; };
51D58757227F630B00900287 /* tags_delete.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = tags_delete.json; sourceTree = "<group>"; };
@ -514,8 +514,8 @@
51C034E0242D660D0014DC71 /* CKError+Extensions.swift */,
5103A9D82422546800410853 /* CloudKitAccountDelegate.swift */,
51E4DB2F2426353D0091EB5B /* CloudKitAccountZone.swift */,
51C034DE242D65D20014DC71 /* CloudKitResult.swift */,
51E4DB2D242633ED0091EB5B /* CloudKitZone.swift */,
51C034DE242D65D20014DC71 /* CloudKitZoneResult.swift */,
);
path = CloudKit;
sourceTree = "<group>";
@ -1184,7 +1184,7 @@
3B826DAC2385C81C00FC1ADB /* FeedWranglerAccountDelegate.swift in Sources */,
769F295938E5A30D03DFF88F /* NewsBlurAccountDelegate.swift in Sources */,
769F2BA02EF5F329CDE45F5A /* NewsBlurAPICaller.swift in Sources */,
51C034DF242D65D20014DC71 /* CloudKitResult.swift in Sources */,
51C034DF242D65D20014DC71 /* CloudKitZoneResult.swift in Sources */,
179DB28CF49F73A945EBF5DB /* NewsBlurLoginResponse.swift in Sources */,
179DBF4DE2562D4C532F6008 /* NewsBlurFeed.swift in Sources */,
179DB02FFBC17AC9798F0EBC /* NewsBlurStory.swift in Sources */,

View File

@ -45,14 +45,6 @@ final class CloudKitAccountDelegate: AccountDelegate {
return refresher.progress
}
// init() {
// accountZone.startUp() { result in
// if case .failure(let error) = result {
// os_log(.error, log: self.log, "Account zone startup error: %@.", error.localizedDescription)
// }
// }
// }
init(dataFolder: String) {
accountZone = CloudKitAccountZone(container: container)
let databaseFilePath = (dataFolder as NSString).appendingPathComponent("Sync.sqlite3")
@ -140,22 +132,35 @@ final class CloudKitAccountDelegate: AccountDelegate {
return
}
let feed = account.createWebFeed(with: nil, url: url.absoluteString, webFeedID: url.absoluteString, homePageURL: nil)
InitialFeedDownloader.download(url) { parsedFeed in
self.refreshProgress.completeTask()
self.accountZone.createFeed(url: urlString, editedName: name) { result in
switch result {
case .success(let externalID):
let feed = account.createWebFeed(with: nil, url: url.absoluteString, webFeedID: url.absoluteString, homePageURL: nil)
InitialFeedDownloader.download(url) { parsedFeed in
self.refreshProgress.completeTask()
if let parsedFeed = parsedFeed {
account.update(feed, with: parsedFeed, {_ in})
if let parsedFeed = parsedFeed {
account.update(feed, with: parsedFeed, {_ in
feed.editedName = name
feed.externalID = externalID
container.addWebFeed(feed)
completion(.success(feed))
})
}
}
case .failure(let error):
self.refreshProgress.completeTask()
completion(.failure(error)) // TODO: need to handle userDeletedZone
}
feed.editedName = name
container.addWebFeed(feed)
completion(.success(feed))
}
case .failure:
self.refreshProgress.completeTask()
completion(.failure(AccountError.createErrorNotFound))

View File

@ -9,17 +9,17 @@
import Foundation
import CloudKit
enum CloudKitResult {
enum CloudKitZoneResult {
case success
case retry(afterSeconds: Double)
case chunk
case limitExceeded
case changeTokenExpired
case partialFailure
case partialFailure(errors: [CKRecord.ID: CKError])
case serverRecordChanged
case noZone
case failure(error: Error)
static func resolve(_ error: Error?) -> CloudKitResult {
static func resolve(_ error: Error?) -> CloudKitZoneResult {
guard error != nil else { return .success }
@ -39,11 +39,17 @@ enum CloudKitResult {
case .serverRecordChanged:
return .serverRecordChanged
case .partialFailure:
return .partialFailure
if let partialErrors = ckError.userInfo[CKPartialErrorsByItemIDKey] as? [CKRecord.ID: CKError] {
if anyZoneErrors(partialErrors) {
return .noZone
} else {
return .partialFailure(errors: partialErrors)
}
} else {
return .failure(error: error!)
}
case .limitExceeded:
return .chunk
case .zoneNotFound, .userDeletedZone:
return .noZone
return .limitExceeded
default:
return .failure(error: error!)
}
@ -51,3 +57,11 @@ enum CloudKitResult {
}
}
private extension CloudKitZoneResult {
static func anyZoneErrors(_ errors: [CKRecord.ID: CKError]) -> Bool {
return errors.values.contains(where: { $0.code == .zoneNotFound || $0.code == .userDeletedZone } )
}
}

View File

@ -9,6 +9,7 @@
import CloudKit
public enum CloudKitZoneError: Error {
case userDeletedZone
case unknown
}
@ -146,14 +147,13 @@ extension CloudKitZone {
guard let self = self else { return }
switch CloudKitResult.resolve(error) {
switch CloudKitZoneResult.resolve(error) {
case .success:
DispatchQueue.main.async {
completion(.success(()))
}
case .noZone:
case .zoneNotFound:
self.createZoneRecord() { result in
// TODO: Need to rebuild (push) zone data here...
switch result {
case .success:
self.modify(recordsToStore: recordsToStore, recordIDsToDelete: recordIDsToDelete, completion: completion)
@ -161,11 +161,15 @@ extension CloudKitZone {
completion(.failure(error))
}
}
case .userDeletedZone:
DispatchQueue.main.async {
completion(.failure(CloudKitZoneError.userDeletedZone))
}
case .retry(let timeToWait):
self.retryOperationIfPossible(retryAfter: timeToWait) {
self.modify(recordsToStore: recordsToStore, recordIDsToDelete: recordIDsToDelete, completion: completion)
}
case .chunk:
case .limitExceeded:
/// CloudKit says maximum number of items in a single request is 400.
/// So I think 300 should be fine by them.
let chunkedRecords = recordsToStore.chunked(into: 300)
@ -173,7 +177,9 @@ extension CloudKitZone {
self.modify(recordsToStore: chunk, recordIDsToDelete: recordIDsToDelete, completion: completion)
}
default:
return
DispatchQueue.main.async {
completion(.failure(error!))
}
}
}

View File

@ -0,0 +1,74 @@
//
// CloudKitResult.swift
// Account
//
// Created by Maurice Parker on 3/26/20.
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
//
import Foundation
import CloudKit
enum CloudKitZoneResult {
case success
case retry(afterSeconds: Double)
case limitExceeded
case changeTokenExpired
case partialFailure(errors: [CKRecord.ID: CKError])
case serverRecordChanged
case zoneNotFound
case userDeletedZone
case failure(error: Error)
static func resolve(_ error: Error?) -> CloudKitZoneResult {
guard error != nil else { return .success }
guard let ckError = error as? CKError else {
return .failure(error: error!)
}
switch ckError.code {
case .serviceUnavailable, .requestRateLimited, .zoneBusy:
if let retry = ckError.userInfo[CKErrorRetryAfterKey] as? Double {
return .retry(afterSeconds: retry)
} else {
return .failure(error: error!)
}
case .changeTokenExpired:
return .changeTokenExpired
case .serverRecordChanged:
return .serverRecordChanged
case .partialFailure:
if let partialErrors = ckError.userInfo[CKPartialErrorsByItemIDKey] as? [CKRecord.ID: CKError] {
if let zoneResult = anyZoneErrors(partialErrors) {
return zoneResult
} else {
return .partialFailure(errors: partialErrors)
}
} else {
return .failure(error: error!)
}
case .limitExceeded:
return .limitExceeded
default:
return .failure(error: error!)
}
}
}
private extension CloudKitZoneResult {
static func anyZoneErrors(_ errors: [CKRecord.ID: CKError]) -> CloudKitZoneResult? {
if errors.values.contains(where: { $0.code == .zoneNotFound } ) {
return .zoneNotFound
}
if errors.values.contains(where: { $0.code == .userDeletedZone } ) {
return .userDeletedZone
}
return nil
}
}

View File

@ -58,16 +58,12 @@ class AddFeedController: AddFeedWindowControllerDelegate {
return
}
BatchUpdate.shared.start()
account.createWebFeed(url: url.absoluteString, name: title, container: container) { result in
DispatchQueue.main.async {
self.endShowingProgress()
}
BatchUpdate.shared.end()
switch result {
case .success(let feed):
NotificationCenter.default.post(name: .UserDidAddFeed, object: self, userInfo: [UserInfoKey.webFeed: feed])