Fix numerous concurrency warnings in CloudKit code.

This commit is contained in:
Brent Simmons 2024-04-17 22:14:36 -07:00
parent 88ec8b20c2
commit 254a02cd8e
4 changed files with 268 additions and 335 deletions

View File

@ -24,9 +24,9 @@ enum CloudKitAccountZoneError: LocalizedError {
@MainActor final class CloudKitAccountZone: CloudKitZone { @MainActor final class CloudKitAccountZone: CloudKitZone {
var zoneID: CKRecordZone.ID let zoneID: CKRecordZone.ID
var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "CloudKit") let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "CloudKit")
weak var container: CKContainer? weak var container: CKContainer?
weak var database: CKDatabase? weak var database: CKDatabase?
@ -240,7 +240,7 @@ enum CloudKitAccountZoneError: LocalizedError {
return try await findOrCreateAccount() return try await findOrCreateAccount()
case .zoneNotFound, .userDeletedZone: case .zoneNotFound, .userDeletedZone:
try await createZoneRecord() _ = try await createZoneRecord()
return try await findOrCreateAccount() return try await findOrCreateAccount()
default: default:

View File

@ -59,7 +59,7 @@ final class CloudKitArticlesZone: CloudKitZone {
} }
} }
init(container: CKContainer) { @MainActor init(container: CKContainer) {
self.container = container self.container = container
self.database = container.privateCloudDatabase self.database = container.privateCloudDatabase
self.zoneID = CKRecordZone.ID(zoneName: "Articles", ownerName: CKCurrentUserDefaultName) self.zoneID = CKRecordZone.ID(zoneName: "Articles", ownerName: CKCurrentUserDefaultName)
@ -73,16 +73,16 @@ final class CloudKitArticlesZone: CloudKitZone {
completion(.success(())) completion(.success(()))
case .failure(let error): case .failure(let error):
if case CloudKitZoneError.userDeletedZone = error { if case CloudKitZoneError.userDeletedZone = error {
self.createZoneRecord() { result in
switch result { Task { @MainActor in
case .success: do {
Task { @MainActor in _ = try await self.createZoneRecord()
self.refreshArticles(completion: completion) self.refreshArticles(completion: completion)
} } catch {
case .failure(let error):
completion(.failure(error)) completion(.failure(error))
} }
} }
} else { } else {
completion(.failure(error)) completion(.failure(error))
} }
@ -174,16 +174,16 @@ private extension CloudKitArticlesZone {
@MainActor func handleModifyArticlesError(_ error: Error, statusUpdates: [CloudKitArticleStatusUpdate], completion: @escaping ((Result<Void, Error>) -> Void)) { @MainActor func handleModifyArticlesError(_ error: Error, statusUpdates: [CloudKitArticleStatusUpdate], completion: @escaping ((Result<Void, Error>) -> Void)) {
if case CloudKitZoneError.userDeletedZone = error { if case CloudKitZoneError.userDeletedZone = error {
self.createZoneRecord() { result in
switch result { Task { @MainActor in
case .success: do {
MainActor.assumeIsolated { _ = try await self.createZoneRecord()
self.modifyArticles(statusUpdates, completion: completion) self.modifyArticles(statusUpdates, completion: completion)
} } catch {
case .failure(let error):
completion(.failure(error)) completion(.failure(error))
} }
} }
} else { } else {
completion(.failure(error)) completion(.failure(error))
} }

View File

@ -94,5 +94,4 @@ public final class CloudKitError: LocalizedError {
return NSLocalizedString("Unhandled Error.", comment: "Unknown iCloud Error") return NSLocalizedString("Unhandled Error.", comment: "Unknown iCloud Error")
} }
} }
} }

View File

@ -42,9 +42,9 @@ public protocol CloudKitZone: AnyObject {
var log: OSLog { get } var log: OSLog { get }
var container: CKContainer? { get } @MainActor var container: CKContainer? { get }
var database: CKDatabase? { get } @MainActor var database: CKDatabase? { get }
var delegate: CloudKitZoneDelegate? { get set } @MainActor var delegate: CloudKitZoneDelegate? { get set }
/// Reset the change token used to determine what point in time we are doing changes fetches /// Reset the change token used to determine what point in time we are doing changes fetches
func resetChangeToken() func resetChangeToken()
@ -59,7 +59,7 @@ public protocol CloudKitZone: AnyObject {
func receiveRemoteNotification(userInfo: [AnyHashable : Any]) async func receiveRemoteNotification(userInfo: [AnyHashable : Any]) async
} }
public extension CloudKitZone { @MainActor public extension CloudKitZone {
// My observation has been that QoS is treated differently for CloudKit operations on macOS vs iOS. // My observation has been that QoS is treated differently for CloudKit operations on macOS vs iOS.
// .userInitiated is too aggressive on iOS and can lead the UI slowing down and appearing to block. // .userInitiated is too aggressive on iOS and can lead the UI slowing down and appearing to block.
@ -172,7 +172,7 @@ public extension CloudKitZone {
} }
/// Retrieves the zone record for this zone only. If the record isn't found it will be created. /// Retrieves the zone record for this zone only. If the record isn't found it will be created.
func fetchZoneRecord(completion: @escaping @Sendable (Result<CKRecordZone?, Error>) -> Void) { @MainActor func fetchZoneRecord(completion: @escaping @Sendable (Result<CKRecordZone?, Error>) -> Void) {
let op = CKFetchRecordZonesOperation(recordZoneIDs: [zoneID]) let op = CKFetchRecordZonesOperation(recordZoneIDs: [zoneID])
op.qualityOfService = Self.qualityOfService op.qualityOfService = Self.qualityOfService
@ -193,38 +193,38 @@ public extension CloudKitZone {
op.fetchRecordZonesResultBlock = { [weak self] result in op.fetchRecordZonesResultBlock = { [weak self] result in
guard let self else { Task { @MainActor in
completion(.failure(CloudKitZoneError.unknown))
return
}
switch result { guard let self else {
completion(.failure(CloudKitZoneError.unknown))
return
}
case .success: switch result {
completion(.success(zoneRecords[self.zoneID]))
case .failure(let error): case .success:
completion(.success(zoneRecords[self.zoneID]))
switch CloudKitZoneResult.resolve(error) { case .failure(let error):
case .zoneNotFound, .userDeletedZone:
self.createZoneRecord() { result in switch CloudKitZoneResult.resolve(error) {
switch result { case .zoneNotFound, .userDeletedZone:
case .success:
self.fetchZoneRecord(completion: completion) do {
case .failure(let error): let recordZone = try await self.createZoneRecord()
DispatchQueue.main.async { completion(.success(recordZone))
completion(.failure(error)) } catch {
} completion(.failure(error))
} }
}
case .retry(let timeToWait): case .retry(let timeToWait):
os_log(.error, log: self.log, "%@ zone fetch changes retry in %f seconds.", self.zoneID.zoneName, timeToWait) os_log(.error, log: self.log, "%@ zone fetch changes retry in %f seconds.", self.zoneID.zoneName, timeToWait)
self.retryIfPossible(after: timeToWait) { await self.delay(for: timeToWait)
self.fetchZoneRecord(completion: completion) self.fetchZoneRecord(completion: completion)
} default:
default: DispatchQueue.main.async {
DispatchQueue.main.async { completion(.failure(CloudKitError(error)))
completion(.failure(CloudKitError(error))) }
} }
} }
} }
@ -234,54 +234,34 @@ public extension CloudKitZone {
} }
/// Creates the zone record /// Creates the zone record
func createZoneRecord() async throws { func createZoneRecord() async throws -> CKRecordZone {
try await withCheckedThrowingContinuation { continuation in guard let database else { throw CloudKitZoneError.unknown }
self.createZoneRecord { result in
switch result {
case .success:
continuation.resume()
case .failure(let error):
continuation.resume(throwing: error)
}
}
}
}
/// Creates the zone record do {
func createZoneRecord(completion: @escaping @Sendable (Result<Void, Error>) -> Void) { return try await database.save(CKRecordZone(zoneID: zoneID))
guard let database = database else { } catch {
completion(.failure(CloudKitZoneError.unknown)) throw CloudKitError(error)
return
}
database.save(CKRecordZone(zoneID: zoneID)) { (recordZone, error) in
if let error = error {
DispatchQueue.main.async {
completion(.failure(CloudKitError(error)))
}
} else {
DispatchQueue.main.async {
completion(.success(()))
}
}
} }
} }
/// Subscribes to zone changes /// Subscribes to zone changes
func subscribeToZoneChanges() { func subscribeToZoneChanges() {
let subscription = CKRecordZoneSubscription(zoneID: zoneID, subscriptionID: zoneID.zoneName)
let info = CKSubscription.NotificationInfo() Task { @MainActor in
info.shouldSendContentAvailable = true let subscription = CKRecordZoneSubscription(zoneID: zoneID, subscriptionID: zoneID.zoneName)
subscription.notificationInfo = info
save(subscription) { result in let info = CKSubscription.NotificationInfo()
if case .failure(let error) = result { info.shouldSendContentAvailable = true
subscription.notificationInfo = info
do {
_ = try await save(subscription)
} catch {
os_log(.error, log: self.log, "%@ zone subscribe to changes error: %@", self.zoneID.zoneName, error.localizedDescription) os_log(.error, log: self.log, "%@ zone subscribe to changes error: %@", self.zoneID.zoneName, error.localizedDescription)
} }
} }
} }
/// Issue a CKQuery and return the resulting CKRecords. /// Issue a CKQuery and return the resulting CKRecords.
func query(_ ckQuery: CKQuery, desiredKeys: [String]? = nil) async throws -> [CKRecord] { func query(_ ckQuery: CKQuery, desiredKeys: [String]? = nil) async throws -> [CKRecord] {
@ -322,55 +302,43 @@ public extension CloudKitZone {
op.queryResultBlock = { [weak self] result in op.queryResultBlock = { [weak self] result in
guard let self else { Task { @MainActor in
completion(.failure(CloudKitZoneError.unknown))
return
}
switch result { guard let self else {
completion(.failure(CloudKitZoneError.unknown))
case .success(let cursor): return
if let cursor {
Task { @MainActor in
self.query(cursor: cursor, desiredKeys: desiredKeys, carriedRecords: records, completion: completion)
}
} else {
completion(.success(records))
} }
case .failure(let error): switch result {
switch CloudKitZoneResult.resolve(error) { case .success(let cursor):
if let cursor {
case .zoneNotFound: self.query(cursor: cursor, desiredKeys: desiredKeys, carriedRecords: records, completion: completion)
self.createZoneRecord() { result in } else {
switch result { completion(.success(records))
case .success:
Task { @MainActor in
self.query(ckQuery, desiredKeys: desiredKeys, completion: completion)
}
case .failure(let error):
DispatchQueue.main.async {
completion(.failure(error))
}
}
} }
case .retry(let timeToWait): case .failure(let error):
os_log(.error, log: self.log, "%@ zone query retry in %f seconds.", self.zoneID.zoneName, timeToWait)
self.retryIfPossible(after: timeToWait) { switch CloudKitZoneResult.resolve(error) {
Task { @MainActor in
case .zoneNotFound:
do {
_ = try await self.createZoneRecord()
self.query(ckQuery, desiredKeys: desiredKeys, completion: completion) self.query(ckQuery, desiredKeys: desiredKeys, completion: completion)
} catch {
completion(.failure(error))
} }
}
case .userDeletedZone: case .retry(let timeToWait):
DispatchQueue.main.async { os_log(.error, log: self.log, "%@ zone query retry in %f seconds.", self.zoneID.zoneName, timeToWait)
await self.delay(for: timeToWait)
self.query(ckQuery, desiredKeys: desiredKeys, completion: completion)
case .userDeletedZone:
completion(.failure(CloudKitZoneError.userDeletedZone)) completion(.failure(CloudKitZoneError.userDeletedZone))
}
default: default:
DispatchQueue.main.async {
completion(.failure(CloudKitError(error))) completion(.failure(CloudKitError(error)))
} }
} }
@ -419,51 +387,44 @@ public extension CloudKitZone {
op.queryResultBlock = { [weak self] result in op.queryResultBlock = { [weak self] result in
guard let self else { Task { @MainActor in
completion(.failure(CloudKitZoneError.unknown))
return
}
switch result { guard let self else {
completion(.failure(CloudKitZoneError.unknown))
return
}
case .success(let newCursor): switch result {
Task { @MainActor in
case .success(let newCursor):
if let newCursor = newCursor { if let newCursor = newCursor {
self.query(cursor: newCursor, desiredKeys: desiredKeys, carriedRecords: records, completion: completion) self.query(cursor: newCursor, desiredKeys: desiredKeys, carriedRecords: records, completion: completion)
} else { } else {
completion(.success(records)) completion(.success(records))
} }
}
case .failure(let error): case .failure(let error):
switch CloudKitZoneResult.resolve(error) { switch CloudKitZoneResult.resolve(error) {
case .zoneNotFound:
self.createZoneRecord() { result in case .zoneNotFound:
switch result {
case .success: do {
Task { @MainActor in _ = try await self.createZoneRecord()
self.query(cursor: cursor, desiredKeys: desiredKeys, carriedRecords: records, completion: completion)
}
case .failure(let error):
DispatchQueue.main.async {
completion(.failure(error))
}
}
}
case .retry(let timeToWait):
os_log(.error, log: self.log, "%@ zone query retry in %f seconds.", self.zoneID.zoneName, timeToWait)
self.retryIfPossible(after: timeToWait) {
Task { @MainActor in
self.query(cursor: cursor, desiredKeys: desiredKeys, carriedRecords: records, completion: completion) self.query(cursor: cursor, desiredKeys: desiredKeys, carriedRecords: records, completion: completion)
} catch {
completion(.failure(error))
} }
}
case .userDeletedZone: case .retry(let timeToWait):
DispatchQueue.main.async { os_log(.error, log: self.log, "%@ zone query retry in %f seconds.", self.zoneID.zoneName, timeToWait)
await self.delay(for: timeToWait)
self.query(cursor: cursor, desiredKeys: desiredKeys, carriedRecords: records, completion: completion)
case .userDeletedZone:
completion(.failure(CloudKitZoneError.userDeletedZone)) completion(.failure(CloudKitZoneError.userDeletedZone))
}
default: default:
DispatchQueue.main.async {
completion(.failure(CloudKitError(error))) completion(.failure(CloudKitError(error)))
} }
} }
@ -498,42 +459,41 @@ public extension CloudKitZone {
let recordID = CKRecord.ID(recordName: externalID, zoneID: zoneID) let recordID = CKRecord.ID(recordName: externalID, zoneID: zoneID)
database?.fetch(withRecordID: recordID) { [weak self] record, error in database?.fetch(withRecordID: recordID) { [weak self] record, error in
guard let self = self else {
guard let self else {
completion(.failure(CloudKitZoneError.unknown)) completion(.failure(CloudKitZoneError.unknown))
return return
} }
switch CloudKitZoneResult.resolve(error) { Task { @MainActor in
case .success:
DispatchQueue.main.async { switch CloudKitZoneResult.resolve(error) {
if let record = record {
case .success:
if let record {
completion(.success(record)) completion(.success(record))
} else { } else {
completion(.failure(CloudKitZoneError.unknown)) completion(.failure(CloudKitZoneError.unknown))
} }
}
case .zoneNotFound: case .zoneNotFound:
self.createZoneRecord() { result in
switch result { do {
case .success: _ = try await self.createZoneRecord()
self.fetch(externalID: externalID, completion: completion) self.fetch(externalID: externalID, completion: completion)
case .failure(let error): } catch {
DispatchQueue.main.async { completion(.failure(error))
completion(.failure(error))
}
} }
}
case .retry(let timeToWait): case .retry(let timeToWait):
os_log(.error, log: self.log, "%@ zone fetch retry in %f seconds.", self.zoneID.zoneName, timeToWait) os_log(.error, log: self.log, "%@ zone fetch retry in %f seconds.", self.zoneID.zoneName, timeToWait)
self.retryIfPossible(after: timeToWait) { self.retryIfPossible(after: timeToWait) {
self.fetch(externalID: externalID, completion: completion) self.fetch(externalID: externalID, completion: completion)
} }
case .userDeletedZone: case .userDeletedZone:
DispatchQueue.main.async {
completion(.failure(CloudKitZoneError.userDeletedZone)) completion(.failure(CloudKitZoneError.userDeletedZone))
}
default: default:
DispatchQueue.main.async {
completion(.failure(CloudKitError(error!))) completion(.failure(CloudKitError(error!)))
} }
} }
@ -610,65 +570,59 @@ public extension CloudKitZone {
return return
} }
switch result { Task { @MainActor in
case .success: switch result {
completion(.success(()))
case .failure(let error): case .success:
completion(.success(()))
switch CloudKitZoneResult.resolve(error) { case .failure(let error):
case .success, .partialFailure:
DispatchQueue.main.async { switch CloudKitZoneResult.resolve(error) {
case .success, .partialFailure:
completion(.success(())) completion(.success(()))
}
case .zoneNotFound: case .zoneNotFound:
self.createZoneRecord() { result in
switch result { do {
case .success: _ = try await self.createZoneRecord()
self.saveIfNew(records, completion: completion) self.saveIfNew(records, completion: completion)
case .failure(let error): } catch {
DispatchQueue.main.async { completion(.failure(error))
completion(.failure(error))
}
} }
}
case .userDeletedZone: case .userDeletedZone:
DispatchQueue.main.async {
completion(.failure(CloudKitZoneError.userDeletedZone)) completion(.failure(CloudKitZoneError.userDeletedZone))
}
case .retry(let timeToWait): case .retry(let timeToWait):
self.retryIfPossible(after: timeToWait) { self.retryIfPossible(after: timeToWait) {
self.saveIfNew(records, completion: completion) self.saveIfNew(records, completion: completion)
}
case .limitExceeded:
var chunkedRecords = records.chunked(into: 200)
func saveChunksIfNew() {
if let records = chunkedRecords.popLast() {
self.saveIfNew(records) { result in
switch result {
case .success:
os_log(.info, log: self.log, "Saved %d chunked new records.", records.count)
saveChunksIfNew()
case .failure(let error):
completion(.failure(error))
}
}
} else {
completion(.success(()))
} }
}
saveChunksIfNew() case .limitExceeded:
default: var chunkedRecords = records.chunked(into: 200)
DispatchQueue.main.async {
@MainActor func saveChunksIfNew() {
if let records = chunkedRecords.popLast() {
self.saveIfNew(records) { result in
switch result {
case .success:
os_log(.info, log: self.log, "Saved %d chunked new records.", records.count)
saveChunksIfNew()
case .failure(let error):
completion(.failure(error))
}
}
} else {
completion(.success(()))
}
}
saveChunksIfNew()
default:
completion(.failure(CloudKitError(error))) completion(.failure(CloudKitError(error)))
} }
} }
@ -681,52 +635,26 @@ public extension CloudKitZone {
/// Save the CKSubscription /// Save the CKSubscription
func save(_ subscription: CKSubscription) async throws -> CKSubscription { func save(_ subscription: CKSubscription) async throws -> CKSubscription {
try await withCheckedThrowingContinuation { continuation in guard let database else { throw CloudKitZoneError.unknown }
self.save(subscription) { result in
switch result {
case .success(let subscription):
continuation.resume(returning: subscription)
case .failure(let error):
continuation.resume(throwing: error)
}
}
}
}
/// Save the CKSubscription do {
func save(_ subscription: CKSubscription, completion: @escaping @Sendable (Result<CKSubscription, Error>) -> Void) { return try await database.save(subscription)
database?.save(subscription) { [weak self] savedSubscription, error in
guard let self else { } catch {
completion(.failure(CloudKitZoneError.unknown))
return
}
switch CloudKitZoneResult.resolve(error) { switch CloudKitZoneResult.resolve(error) {
case .success:
DispatchQueue.main.async {
completion(.success((savedSubscription!)))
}
case .zoneNotFound: case .zoneNotFound:
self.createZoneRecord() { result in _ = try await createZoneRecord()
switch result { return try await save(subscription)
case .success:
self.save(subscription, completion: completion)
case .failure(let error):
DispatchQueue.main.async {
completion(.failure(error))
}
}
}
case .retry(let timeToWait): case .retry(let timeToWait):
os_log(.error, log: self.log, "%@ zone save subscription retry in %f seconds.", self.zoneID.zoneName, timeToWait) os_log(.error, log: self.log, "%@ zone save subscription retry in %f seconds.", self.zoneID.zoneName, timeToWait)
self.retryIfPossible(after: timeToWait) { await delay(for: timeToWait)
self.save(subscription, completion: completion) return try await save(subscription)
}
default: default:
DispatchQueue.main.async { throw CloudKitError(error)
completion(.failure(CloudKitError(error!)))
}
} }
} }
} }
@ -938,24 +866,32 @@ public extension CloudKitZone {
/// Delete a CKSubscription /// Delete a CKSubscription
func delete(subscriptionID: String, completion: @escaping @Sendable (Result<Void, Error>) -> Void) { func delete(subscriptionID: String, completion: @escaping @Sendable (Result<Void, Error>) -> Void) {
database?.delete(withSubscriptionID: subscriptionID) { [weak self] _, error in
guard let self = self else { guard let database else {
completion(.failure(CloudKitZoneError.unknown))
return
}
database.delete(withSubscriptionID: subscriptionID) { [weak self] _, error in
guard let self else {
completion(.failure(CloudKitZoneError.unknown)) completion(.failure(CloudKitZoneError.unknown))
return return
} }
switch CloudKitZoneResult.resolve(error) { Task { @MainActor in
case .success:
DispatchQueue.main.async { switch CloudKitZoneResult.resolve(error) {
case .success:
completion(.success(())) completion(.success(()))
}
case .retry(let timeToWait): case .retry(let timeToWait):
os_log(.error, log: self.log, "%@ zone delete subscription retry in %f seconds.", self.zoneID.zoneName, timeToWait) os_log(.error, log: self.log, "%@ zone delete subscription retry in %f seconds.", self.zoneID.zoneName, timeToWait)
self.retryIfPossible(after: timeToWait) { await self.delay(for: timeToWait)
self.delete(subscriptionID: subscriptionID, completion: completion) self.delete(subscriptionID: subscriptionID, completion: completion)
}
default: default:
DispatchQueue.main.async {
completion(.failure(CloudKitError(error!))) completion(.failure(CloudKitError(error!)))
} }
} }
@ -1014,16 +950,16 @@ public extension CloudKitZone {
completion(.success(())) completion(.success(()))
} }
case .zoneNotFound: case .zoneNotFound:
self.createZoneRecord() { result in
switch result { Task { @MainActor in
case .success: do {
_ = try await self.createZoneRecord()
self.modify(recordsToSave: recordsToSave, recordIDsToDelete: recordIDsToDelete, completion: completion) self.modify(recordsToSave: recordsToSave, recordIDsToDelete: recordIDsToDelete, completion: completion)
case .failure(let error): } catch {
DispatchQueue.main.async { completion(.failure(error))
completion(.failure(error))
}
} }
} }
case .userDeletedZone: case .userDeletedZone:
DispatchQueue.main.async { DispatchQueue.main.async {
completion(.failure(CloudKitZoneError.userDeletedZone)) completion(.failure(CloudKitZoneError.userDeletedZone))
@ -1040,12 +976,13 @@ public extension CloudKitZone {
func saveChunks(completion: @escaping (Result<Void, Error>) -> Void) { func saveChunks(completion: @escaping (Result<Void, Error>) -> Void) {
if !recordToSaveChunks.isEmpty { if !recordToSaveChunks.isEmpty {
let records = recordToSaveChunks.removeFirst() let records = recordToSaveChunks.removeFirst()
self.modify(recordsToSave: records, recordIDsToDelete: []) { result in
switch result { Task { @MainActor in
case .success: do {
try await self.modify(recordsToSave: records, recordIDsToDelete: [])
os_log(.info, log: self.log, "Saved %d chunked records.", records.count) os_log(.info, log: self.log, "Saved %d chunked records.", records.count)
saveChunks(completion: completion) saveChunks(completion: completion)
case .failure(let error): } catch {
completion(.failure(error)) completion(.failure(error))
} }
} }
@ -1057,17 +994,18 @@ public extension CloudKitZone {
func deleteChunks() { func deleteChunks() {
if !recordIDsToDeleteChunks.isEmpty { if !recordIDsToDeleteChunks.isEmpty {
let records = recordIDsToDeleteChunks.removeFirst() let records = recordIDsToDeleteChunks.removeFirst()
self.modify(recordsToSave: [], recordIDsToDelete: records) { result in
switch result { Task { @MainActor in
case .success:
do {
try await self.modify(recordsToSave: [], recordIDsToDelete: records)
os_log(.info, log: self.log, "Deleted %d chunked records.", records.count) os_log(.info, log: self.log, "Deleted %d chunked records.", records.count)
deleteChunks() deleteChunks()
case .failure(let error): } catch {
DispatchQueue.main.async { completion(.failure(error))
completion(.failure(error))
}
} }
} }
} else { } else {
DispatchQueue.main.async { DispatchQueue.main.async {
completion(.success(())) completion(.success(()))
@ -1115,6 +1053,11 @@ public extension CloudKitZone {
/// Fetch all the changes in the CKZone since the last time we checked /// Fetch all the changes in the CKZone since the last time we checked
@MainActor func fetchChangesInZone(completion: @escaping (Result<Void, Error>) -> Void) { @MainActor func fetchChangesInZone(completion: @escaping (Result<Void, Error>) -> Void) {
guard let database else {
completion(.failure(CloudKitZoneError.unknown))
return
}
var changedRecords = [CKRecord]() var changedRecords = [CKRecord]()
var deletedRecordKeys = [CloudKitRecordKey]() var deletedRecordKeys = [CloudKitRecordKey]()
@ -1154,58 +1097,49 @@ public extension CloudKitZone {
return return
} }
switch result { Task { @MainActor in
case .success: switch result {
Task { @MainActor in
case .success:
do { do {
try await self.delegate?.cloudKitDidModify(changed: changedRecords, deleted: deletedRecordKeys) try await self.delegate?.cloudKitDidModify(changed: changedRecords, deleted: deletedRecordKeys)
completion(.success(())) completion(.success(()))
} catch { } catch {
completion(.failure(error)) completion(.failure(error))
} }
}
case .failure(let error): case .failure(let error):
switch CloudKitZoneResult.resolve(error) { switch CloudKitZoneResult.resolve(error) {
case .zoneNotFound: case .zoneNotFound:
self.createZoneRecord() { result in
switch result { do {
case .success: _ = try await self.createZoneRecord()
Task { @MainActor in
self.fetchChangesInZone(completion: completion)
}
case .failure(let error):
DispatchQueue.main.async {
completion(.failure(error))
}
}
}
case .userDeletedZone:
DispatchQueue.main.async {
completion(.failure(CloudKitZoneError.userDeletedZone))
}
case .retry(let timeToWait):
os_log(.error, log: self.log, "%@ zone fetch changes retry in %f seconds.", self.zoneID.zoneName, timeToWait)
self.retryIfPossible(after: timeToWait) {
Task { @MainActor in
self.fetchChangesInZone(completion: completion) self.fetchChangesInZone(completion: completion)
} catch {
completion(.failure(error))
} }
}
case .changeTokenExpired: case .userDeletedZone:
DispatchQueue.main.async { completion(.failure(CloudKitZoneError.userDeletedZone))
case .retry(let timeToWait):
os_log(.error, log: self.log, "%@ zone fetch changes retry in %f seconds.", self.zoneID.zoneName, timeToWait)
await self.delay(for: timeToWait)
self.fetchChangesInZone(completion: completion)
case .changeTokenExpired:
self.changeToken = nil self.changeToken = nil
self.fetchChangesInZone(completion: completion) self.fetchChangesInZone(completion: completion)
}
default: default:
DispatchQueue.main.async {
completion(.failure(CloudKitError(error))) completion(.failure(CloudKitError(error)))
} }
} }
} }
} }
database?.add(op) database.add(op)
} }
} }