Fix some concurrency warnings.

This commit is contained in:
Brent Simmons 2024-04-21 19:59:45 -07:00
parent 261c3136d2
commit 6d9d1762aa
2 changed files with 78 additions and 72 deletions

View File

@ -36,11 +36,11 @@ public typealias CloudKitRecordKey = (recordType: CKRecord.RecordType, recordID:
public protocol CloudKitZone: AnyObject {
@MainActor static var qualityOfService: QualityOfService { get }
static var qualityOfService: QualityOfService { get }
var zoneID: CKRecordZone.ID { get }
@MainActor var log: OSLog { get }
var log: OSLog { get }
@MainActor var container: CKContainer? { get }
@MainActor var database: CKDatabase? { get }
@ -64,7 +64,7 @@ public protocol CloudKitZone: AnyObject {
// 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.
// .default (or lower) on macOS will sometimes hang for extended periods of time and appear to hang.
static var qualityOfService: QualityOfService {
nonisolated static var qualityOfService: QualityOfService {
#if os(macOS) || targetEnvironment(macCatalyst)
return .userInitiated
#else
@ -121,7 +121,7 @@ public protocol CloudKitZone: AnyObject {
}
}
func retryIfPossible(after: Double, block: @escaping @MainActor () -> ()) {
nonisolated func retryIfPossible(after: Double, block: @escaping @Sendable @MainActor () -> ()) {
let delayTime = DispatchTime.now() + after
DispatchQueue.main.asyncAfter(deadline: delayTime, execute: {
Task { @MainActor in
@ -846,7 +846,7 @@ public protocol CloudKitZone: AnyObject {
}
/// Modify and delete the supplied CKRecords and CKRecord.IDs
func modify(recordsToSave: [CKRecord], recordIDsToDelete: [CKRecord.ID], completion: @escaping (Result<Void, Error>) -> Void) {
func modify(recordsToSave: [CKRecord], recordIDsToDelete: [CKRecord.ID], completion: @escaping @Sendable (Result<Void, Error>) -> Void) {
guard !(recordsToSave.isEmpty && recordIDsToDelete.isEmpty) else {
DispatchQueue.main.async {
@ -905,7 +905,7 @@ public protocol CloudKitZone: AnyObject {
var recordToSaveChunks = recordsToSave.chunked(into: 200)
var recordIDsToDeleteChunks = recordIDsToDelete.chunked(into: 200)
@MainActor func saveChunks(completion: @escaping (Result<Void, Error>) -> Void) {
func saveChunks(completion: @escaping @Sendable (Result<Void, Error>) -> Void) {
if !recordToSaveChunks.isEmpty {
let records = recordToSaveChunks.removeFirst()

View File

@ -95,52 +95,45 @@ public protocol CloudKitFeedInfoDelegate {
try await delete(ckQuery: ckQuery)
}
@MainActor public func modifyArticles(_ statusUpdates: [CloudKitArticleStatusUpdate], completion: @escaping ((Result<Void, Error>) -> Void)) {
public func modifyArticles(_ statusUpdates: [CloudKitArticleStatusUpdate], completion: @escaping ((Result<Void, Error>) -> Void)) {
guard !statusUpdates.isEmpty else {
completion(.success(()))
return
}
var modifyRecords = [CKRecord]()
var newRecords = [CKRecord]()
var deleteRecordIDs = [CKRecord.ID]()
for statusUpdate in statusUpdates {
switch statusUpdate.record {
case .all:
modifyRecords.append(self.makeStatusRecord(statusUpdate))
modifyRecords.append(self.makeArticleRecord(statusUpdate.article!))
case .new:
newRecords.append(self.makeStatusRecord(statusUpdate))
newRecords.append(self.makeArticleRecord(statusUpdate.article!))
case .delete:
deleteRecordIDs.append(CKRecord.ID(recordName: self.statusID(statusUpdate.articleID), zoneID: zoneID))
case .statusOnly:
modifyRecords.append(self.makeStatusRecord(statusUpdate))
deleteRecordIDs.append(CKRecord.ID(recordName: self.articleID(statusUpdate.articleID), zoneID: zoneID))
}
}
compressionQueue.async { [newRecords] in
let compressedModifyRecords = self.compressArticleRecords(modifyRecords)
self.modify(recordsToSave: compressedModifyRecords, recordIDsToDelete: deleteRecordIDs) { result in
switch result {
case .success:
let compressedNewRecords = self.compressArticleRecords(newRecords)
self.saveIfNew(compressedNewRecords) { result in
switch result {
case .success:
completion(.success(()))
case .failure(let error):
completion(.failure(error))
}
}
case .failure(let error):
Task { @MainActor in
self.handleModifyArticlesError(error, statusUpdates: statusUpdates, completion: completion)
}
Task { @MainActor in
var modifyRecords = [CKRecord]()
var newRecords = [CKRecord]()
var deleteRecordIDs = [CKRecord.ID]()
for statusUpdate in statusUpdates {
switch statusUpdate.record {
case .all:
modifyRecords.append(self.makeStatusRecord(statusUpdate))
modifyRecords.append(self.makeArticleRecord(statusUpdate.article!))
case .new:
newRecords.append(self.makeStatusRecord(statusUpdate))
newRecords.append(self.makeArticleRecord(statusUpdate.article!))
case .delete:
deleteRecordIDs.append(CKRecord.ID(recordName: self.statusID(statusUpdate.articleID), zoneID: zoneID))
case .statusOnly:
modifyRecords.append(self.makeStatusRecord(statusUpdate))
deleteRecordIDs.append(CKRecord.ID(recordName: self.articleID(statusUpdate.articleID), zoneID: zoneID))
}
}
let compressedModifyRecords = await compressedArticleRecords(modifyRecords)
do {
try await self.modify(recordsToSave: compressedModifyRecords, recordIDsToDelete: deleteRecordIDs)
let compressedNewRecords = await compressedArticleRecords(newRecords)
try await self.saveIfNew(compressedNewRecords)
} catch {
self.handleModifyArticlesError(error, statusUpdates: statusUpdates, completion: completion)
}
}
}
}
@ -236,34 +229,47 @@ private extension CloudKitArticlesZone {
return record
}
nonisolated func compressArticleRecords(_ records: [CKRecord]) -> [CKRecord] {
var result = [CKRecord]()
for record in records {
if record.recordType == CloudKitArticle.recordType {
if let contentHTML = record[CloudKitArticle.Fields.contentHTML] as? String {
let data = Data(contentHTML.utf8) as NSData
if let compressedData = try? data.compressed(using: .lzfse) {
record[CloudKitArticle.Fields.contentHTMLData] = compressedData as Data
record[CloudKitArticle.Fields.contentHTML] = nil
}
}
if let contentText = record[CloudKitArticle.Fields.contentText] as? String {
let data = Data(contentText.utf8) as NSData
if let compressedData = try? data.compressed(using: .lzfse) {
record[CloudKitArticle.Fields.contentTextData] = compressedData as Data
record[CloudKitArticle.Fields.contentText] = nil
}
}
func compressedArticleRecords(_ records: [CKRecord]) async -> [CKRecord] {
await withCheckedContinuation { continuation in
self._compressedArticleRecords(records) { records in
continuation.resume(returning: records)
}
result.append(record)
}
return result
}
func _compressedArticleRecords(_ records: [CKRecord], completion: @escaping @Sendable ([CKRecord]) -> Void) {
compressionQueue.async {
var result = [CKRecord]()
for record in records {
if record.recordType == CloudKitArticle.recordType {
if let contentHTML = record[CloudKitArticle.Fields.contentHTML] as? String {
let data = Data(contentHTML.utf8) as NSData
if let compressedData = try? data.compressed(using: .lzfse) {
record[CloudKitArticle.Fields.contentHTMLData] = compressedData as Data
record[CloudKitArticle.Fields.contentHTML] = nil
}
}
if let contentText = record[CloudKitArticle.Fields.contentText] as? String {
let data = Data(contentText.utf8) as NSData
if let compressedData = try? data.compressed(using: .lzfse) {
record[CloudKitArticle.Fields.contentTextData] = compressedData as Data
record[CloudKitArticle.Fields.contentText] = nil
}
}
}
result.append(record)
}
completion(result)
}
}
}