|
4 | 4 |
|
5 | 5 | @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) |
6 | 6 | package final class MockCloudDatabase: CloudDatabase { |
7 | | - package let storage = LockIsolated<[CKRecordZone.ID: [CKRecord.ID: CKRecord]]>([:]) |
| 7 | + package let storage = LockIsolated<[CKRecordZone.ID: Zone]>([:]) |
8 | 8 | let assets = LockIsolated<[AssetID: Data]>([:]) |
9 | 9 | package let databaseScope: CKDatabase.Scope |
10 | 10 | let _container = IsolatedWeakVar<MockCloudContainer>() |
11 | | - |
12 | 11 | let dataManager = Dependency(\.dataManager) |
13 | 12 |
|
14 | 13 | struct AssetID: Hashable { |
15 | 14 | let recordID: CKRecord.ID |
16 | 15 | let key: String |
17 | 16 | } |
18 | 17 |
|
| 18 | + package struct Zone { |
| 19 | + package var zone: CKRecordZone |
| 20 | + package var records: [CKRecord.ID: CKRecord] = [:] |
| 21 | + } |
| 22 | + |
19 | 23 | package init(databaseScope: CKDatabase.Scope) { |
20 | 24 | self.databaseScope = databaseScope |
21 | 25 | } |
|
34 | 38 | else { throw ckError(forAccountStatus: accountStatus) } |
35 | 39 | guard let zone = storage[recordID.zoneID] |
36 | 40 | else { throw CKError(.zoneNotFound) } |
37 | | - guard let record = zone[recordID] |
| 41 | + guard let record = zone.records[recordID] |
38 | 42 | else { throw CKError(.unknownItem) } |
39 | 43 | guard let record = record.copy() as? CKRecord |
40 | 44 | else { fatalError("Could not copy CKRecord.") } |
|
81 | 85 | else { throw ckError(forAccountStatus: accountStatus) } |
82 | 86 |
|
83 | 87 | return storage.withValue { storage in |
| 88 | + let previousStorage = storage |
84 | 89 | var saveResults: [CKRecord.ID: Result<CKRecord, any Error>] = [:] |
85 | 90 | var deleteResults: [CKRecord.ID: Result<Void, any Error>] = [:] |
86 | 91 |
|
|
91 | 96 | let isSavingRootRecord = recordsToSave.contains(where: { |
92 | 97 | $0.share?.recordID == share.recordID |
93 | 98 | }) |
94 | | - let shareWasPreviouslySaved = storage[share.recordID.zoneID]?[share.recordID] != nil |
| 99 | + let shareWasPreviouslySaved = |
| 100 | + storage[share.recordID.zoneID]?.records[share.recordID] != nil |
95 | 101 | guard shareWasPreviouslySaved || isSavingRootRecord |
96 | 102 | else { |
97 | 103 | saveResults[recordToSave.recordID] = .failure(CKError(.invalidArguments)) |
|
114 | 120 | continue |
115 | 121 | } |
116 | 122 |
|
117 | | - let existingRecord = storage[recordToSave.recordID.zoneID]?[recordToSave.recordID] |
| 123 | + let existingRecord = storage[recordToSave.recordID.zoneID]?.records[ |
| 124 | + recordToSave.recordID |
| 125 | + ] |
118 | 126 |
|
119 | 127 | func saveRecordToDatabase() { |
120 | 128 | let hasReferenceViolation = |
121 | 129 | recordToSave.parent.map { parent in |
122 | | - storage[parent.recordID.zoneID]?[parent.recordID] == nil |
| 130 | + storage[parent.recordID.zoneID]?.records[parent.recordID] == nil |
123 | 131 | && !recordsToSave.contains { $0.recordID == parent.recordID } |
124 | 132 | } |
125 | 133 | ?? false |
|
132 | 140 | func root(of record: CKRecord) -> CKRecord { |
133 | 141 | guard let parent = record.parent |
134 | 142 | else { return record } |
135 | | - return (storage[parent.recordID.zoneID]?[parent.recordID]).map(root) ?? record |
| 143 | + return (storage[parent.recordID.zoneID]?.records[parent.recordID]).map( |
| 144 | + root |
| 145 | + ) ?? record |
136 | 146 | } |
137 | 147 | func share(for rootRecord: CKRecord) -> CKShare? { |
138 | | - for (_, record) in storage[rootRecord.recordID.zoneID] ?? [:] { |
| 148 | + for (_, record) in storage[rootRecord.recordID.zoneID]?.records ?? [:] { |
139 | 149 | guard record.recordID == rootRecord.share?.recordID |
140 | 150 | else { continue } |
141 | 151 | return record as? CKShare |
|
169 | 179 | } |
170 | 180 |
|
171 | 181 | // TODO: This should merge copy's values to more accurately reflect reality |
172 | | - storage[recordToSave.recordID.zoneID]?[recordToSave.recordID] = copy |
| 182 | + storage[recordToSave.recordID.zoneID]?.records[recordToSave.recordID] = copy |
173 | 183 | saveResults[recordToSave.recordID] = .success(copy) |
174 | 184 | } |
175 | 185 |
|
|
228 | 238 | continue |
229 | 239 | } |
230 | 240 | let hasReferenceViolation = !Set( |
231 | | - storage[recordIDToDelete.zoneID]?.values |
| 241 | + storage[recordIDToDelete.zoneID]?.records.values |
232 | 242 | .compactMap { $0.parent?.recordID == recordIDToDelete ? $0.recordID : nil } |
233 | 243 | ?? [] |
234 | 244 | ) |
|
240 | 250 | deleteResults[recordIDToDelete] = .failure(CKError(.referenceViolation)) |
241 | 251 | continue |
242 | 252 | } |
243 | | - let recordToDelete = storage[recordIDToDelete.zoneID]?[recordIDToDelete] |
244 | | - storage[recordIDToDelete.zoneID]?[recordIDToDelete] = nil |
| 253 | + let recordToDelete = storage[recordIDToDelete.zoneID]?.records[recordIDToDelete] |
| 254 | + storage[recordIDToDelete.zoneID]?.records[recordIDToDelete] = nil |
245 | 255 | deleteResults[recordIDToDelete] = .success(()) |
246 | 256 |
|
247 | 257 | // NB: If deleting a share that the current user owns, delete the shared records and all |
|
251 | 261 | shareToDelete.recordID.zoneID.ownerName == CKCurrentUserDefaultName |
252 | 262 | { |
253 | 263 | func deleteRecords(referencing recordID: CKRecord.ID) { |
254 | | - for recordToDelete in (storage[recordIDToDelete.zoneID] ?? [:]).values { |
| 264 | + for recordToDelete in (storage[recordIDToDelete.zoneID]?.records ?? [:]).values { |
255 | 265 | guard |
256 | 266 | recordToDelete.share?.recordID == recordID |
257 | 267 | || recordToDelete.parent?.recordID == recordID |
258 | 268 | else { |
259 | 269 | continue |
260 | 270 | } |
261 | | - storage[recordIDToDelete.zoneID]?[recordToDelete.recordID] = nil |
| 271 | + storage[recordIDToDelete.zoneID]?.records[recordToDelete.recordID] = nil |
262 | 272 | deleteRecords(referencing: recordToDelete.recordID) |
263 | 273 | } |
264 | 274 | } |
265 | 275 | deleteRecords(referencing: shareToDelete.recordID) |
266 | 276 | } |
267 | 277 | } |
268 | 278 |
|
| 279 | + guard atomically |
| 280 | + else { |
| 281 | + return (saveResults: saveResults, deleteResults: deleteResults) |
| 282 | + } |
| 283 | + |
| 284 | + let affectedZones = Set( |
| 285 | + recordsToSave.map(\.recordID.zoneID) + recordIDsToDelete.map(\.zoneID) |
| 286 | + ) |
| 287 | + for zoneID in affectedZones { |
| 288 | + let saveResultsInZone = saveResults.filter { recordID, _ in recordID.zoneID == zoneID } |
| 289 | + let deleteResultsInZone = deleteResults.filter { recordID, _ in |
| 290 | + recordID.zoneID == zoneID |
| 291 | + } |
| 292 | + let saveSuccessRecordIDs = saveResultsInZone.compactMap { recordID, result in |
| 293 | + (try? result.get()) == nil ? nil : recordID |
| 294 | + } |
| 295 | + let deleteSuccessRecordIDs = deleteResultsInZone.compactMap { recordID, result in |
| 296 | + (try? result.get()) == nil ? nil : recordID |
| 297 | + } |
| 298 | + guard |
| 299 | + saveSuccessRecordIDs.count != saveResultsInZone.count |
| 300 | + || deleteSuccessRecordIDs.count != deleteResultsInZone.count |
| 301 | + else { |
| 302 | + continue |
| 303 | + } |
| 304 | + // Every successful save and deletion becomes a '.batchRequestFailed'. |
| 305 | + for saveSuccessRecordID in saveSuccessRecordIDs { |
| 306 | + saveResults[saveSuccessRecordID] = .failure(CKError(.batchRequestFailed)) |
| 307 | + } |
| 308 | + for deleteSuccessRecordID in deleteSuccessRecordIDs { |
| 309 | + deleteResults[deleteSuccessRecordID] = .failure(CKError(.batchRequestFailed)) |
| 310 | + } |
| 311 | + // All storage changes are reverted in zone. |
| 312 | + storage[zoneID]?.records = previousStorage[zoneID]?.records ?? [:] |
| 313 | + } |
269 | 314 | return (saveResults: saveResults, deleteResults: deleteResults) |
270 | 315 | } |
271 | 316 | } |
|
286 | 331 | var deleteResults: [CKRecordZone.ID: Result<Void, any Error>] = [:] |
287 | 332 |
|
288 | 333 | for recordZoneToSave in recordZonesToSave { |
289 | | - storage[recordZoneToSave.zoneID] = storage[recordZoneToSave.zoneID] ?? [:] |
| 334 | + storage[recordZoneToSave.zoneID] = |
| 335 | + storage[recordZoneToSave.zoneID] ?? Zone(zone: recordZoneToSave) |
290 | 336 | saveResults[recordZoneToSave.zoneID] = .success(recordZoneToSave) |
291 | 337 | } |
292 | 338 |
|
|
0 commit comments