diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cbe67147..26a544c4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,7 @@ jobs: name: macOS strategy: matrix: - xcode: ["26.2"] + xcode: ["26.6"] config: ["debug", "release"] runs-on: macos-26 steps: @@ -32,7 +32,7 @@ jobs: name: Examples strategy: matrix: - xcode: ["26.2"] + xcode: ["26.6"] config: ["debug"] scheme: ["Reminders", "CaseStudies", "SyncUps"] runs-on: macos-26 diff --git a/Examples/Examples.xcodeproj/project.pbxproj b/Examples/Examples.xcodeproj/project.pbxproj index 0040127d..5dc5bdb5 100644 --- a/Examples/Examples.xcodeproj/project.pbxproj +++ b/Examples/Examples.xcodeproj/project.pbxproj @@ -1100,6 +1100,7 @@ isa = XCLocalSwiftPackageReference; relativePath = ..; traits = ( + CasePaths, LazyInitializableByDefault, ); }; @@ -1121,9 +1122,6 @@ kind = upToNextMajorVersion; minimumVersion = 1.7.0; }; - traits = ( - Clocks, - ); }; DCBE8A122D4842BF0071F499 /* XCRemoteSwiftPackageReference "swift-case-paths" */ = { isa = XCRemoteSwiftPackageReference; @@ -1140,8 +1138,6 @@ kind = upToNextMajorVersion; minimumVersion = 2.2.3; }; - traits = ( - ); }; /* End XCRemoteSwiftPackageReference section */ diff --git a/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index ce3dfb78..d2dfd2ae 100644 --- a/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "c56e7b70de4fe8bcc798354797a8405c0eeb2e9bc68bc0f202c14a8b11a0a97f", + "originHash" : "c133bf7d10c8ce1e5d6506c3d2f080eac8b4c8c2827044d53a9b925e903564fd", "pins" : [ { "identity" : "combine-schedulers", @@ -15,8 +15,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/groue/GRDB.swift", "state" : { - "revision" : "9ed8c8457e00ff9c7aedb3bf213f20a2cfdf509e", - "version" : "7.11.0" + "revision" : "b83108d10f42680d78f23fe4d4d80fc88dab3212", + "version" : "7.11.1" } }, { @@ -24,8 +24,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-case-paths", "state" : { - "revision" : "206cbce3882b4de9aee19ce62ac5b7306cadd45b", - "version" : "1.7.3" + "revision" : "1197e80bc7e4b177051b6869ef93d8ac3ad677da", + "version" : "1.8.0" } }, { @@ -60,8 +60,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-custom-dump", "state" : { - "revision" : "b9b59eb58c946236d6f16305c576ad194c36444e", - "version" : "1.6.0" + "revision" : "a8cd6c976f335ed361dcecddb0dc39ebda51bc3e", + "version" : "1.6.1" } }, { @@ -69,8 +69,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-dependencies", "state" : { - "revision" : "16f7dd14ee28d04617090f2a73198b8b316ffa12", - "version" : "1.13.1" + "revision" : "8dc1fbf2f6255a73dec53b4648164884898db4c5", + "version" : "1.14.1" } }, { @@ -87,8 +87,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-navigation", "state" : { - "revision" : "32f35241b8be0719c4c7f00eb27713b1cadb6248", - "version" : "2.8.0" + "revision" : "fad75807c596fecd724b0fc81cd61c94008faad4", + "version" : "2.10.3" } }, { @@ -96,8 +96,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-perception", "state" : { - "revision" : "25ac73741c3436605d61eceb5207e896973918e7", - "version" : "2.0.10" + "revision" : "de219a1cf34e958134e75a9ebb134cf09bf52fc6", + "version" : "2.0.11" } }, { @@ -105,8 +105,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-sharing", "state" : { - "revision" : "e47a2f545bafa3c0c702600f3e6ce02b3d566b6f", - "version" : "2.8.2" + "revision" : "8244fe63bf43e58188ab13851ad693eecf6a9e90", + "version" : "2.9.1" } }, { @@ -114,8 +114,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-snapshot-testing.git", "state" : { - "revision" : "ad5e3190cc63dc288f28546f9c6827efc1e9d495", - "version" : "1.19.2" + "revision" : "1bc16f430d8410e7f087d4c787767b26fd32fe30", + "version" : "1.19.3" } }, { @@ -123,8 +123,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-structured-queries", "state" : { - "revision" : "50a429884d7a6c0613df2a31a24009cc436bfb18", - "version" : "0.33.2" + "revision" : "e61b3713460507ce93ed4ca4d8f9e4423bf342ca", + "version" : "0.33.0" } }, { @@ -141,8 +141,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", "state" : { - "revision" : "cb281f343fd953280336dcbd3822cdf47c182f5b", - "version" : "1.10.0" + "revision" : "8f6abcf4c8950e2679d5b2fee4ca284fd7c34886", + "version" : "1.11.0" } } ], diff --git a/Package.swift b/Package.swift index eb33cad5..317db099 100644 --- a/Package.swift +++ b/Package.swift @@ -68,7 +68,6 @@ let package = Package( .target( name: "SQLiteData", dependencies: [ - .product(name: "ConcurrencyExtras", package: "swift-concurrency-extras"), .product(name: "Dependencies", package: "swift-dependencies"), .product(name: "GRDB", package: "GRDB.swift"), .product(name: "IssueReporting", package: "xctest-dynamic-overlay"), @@ -87,7 +86,6 @@ let package = Package( name: "SQLiteDataTestSupport", dependencies: [ "SQLiteData", - .product(name: "ConcurrencyExtras", package: "swift-concurrency-extras"), .product(name: "ConcurrencyExtrasTestSupport", package: "swift-concurrency-extras"), .product(name: "CustomDump", package: "swift-custom-dump"), .product(name: "Dependencies", package: "swift-dependencies"), diff --git a/Package@swift-6.0.swift b/Package@swift-6.0.swift index 66138923..19a896ec 100644 --- a/Package@swift-6.0.swift +++ b/Package@swift-6.0.swift @@ -35,7 +35,6 @@ let package = Package( .target( name: "SQLiteData", dependencies: [ - .product(name: "ConcurrencyExtras", package: "swift-concurrency-extras"), .product(name: "Dependencies", package: "swift-dependencies"), .product(name: "GRDB", package: "GRDB.swift"), .product(name: "IssueReporting", package: "xctest-dynamic-overlay"), @@ -48,7 +47,6 @@ let package = Package( name: "SQLiteDataTestSupport", dependencies: [ "SQLiteData", - .product(name: "ConcurrencyExtras", package: "swift-concurrency-extras"), .product(name: "CustomDump", package: "swift-custom-dump"), .product(name: "Dependencies", package: "swift-dependencies"), .product(name: "InlineSnapshotTesting", package: "swift-snapshot-testing"), diff --git a/Sources/SQLiteData/CloudKit/CloudKitSharing.swift b/Sources/SQLiteData/CloudKit/CloudKitSharing.swift index 8451fa7e..a6c497dd 100644 --- a/Sources/SQLiteData/CloudKit/CloudKitSharing.swift +++ b/Sources/SQLiteData/CloudKit/CloudKitSharing.swift @@ -1,6 +1,5 @@ #if canImport(CloudKit) public import CloudKit - import ConcurrencyExtras import GRDB import IssueReporting public import StructuredQueries @@ -241,7 +240,7 @@ } func unshare(share: CKShare) async throws { - let result = try await syncEngines.private?.database.modifyRecords( + let result = try await syncEngines.withLock(\.private)?.database.modifyRecords( saving: [], deleting: [share.recordID] ) diff --git a/Sources/SQLiteData/CloudKit/Internal/DataManager.swift b/Sources/SQLiteData/CloudKit/Internal/DataManager.swift index 9a67b42e..78c6b6b0 100644 --- a/Sources/SQLiteData/CloudKit/Internal/DataManager.swift +++ b/Sources/SQLiteData/CloudKit/Internal/DataManager.swift @@ -1,5 +1,4 @@ #if canImport(CloudKit) && canImport(CryptoKit) - package import ConcurrencyExtras import CryptoKit import Dependencies package import Foundation @@ -55,7 +54,7 @@ package init() {} package func load(_ url: URL) throws -> Data { - try storage.withValue { storage in + try storage.withLock { storage in guard let data = storage[url] else { struct FileNotFound: Error {} @@ -66,11 +65,11 @@ } package func save(_ data: Data, to url: URL) throws { - storage.withValue { $0[url] = data } + storage.withLock { $0[url] = data } } package func sha256(of fileURL: URL) -> Data? { - storage.withValue { + storage.withLock { $0[fileURL].map { Data(SHA256.hash(data: $0)) } diff --git a/Sources/SQLiteData/CloudKit/Internal/MockCloudContainer.swift b/Sources/SQLiteData/CloudKit/Internal/MockCloudContainer.swift index 3af32acf..1ca58c0c 100644 --- a/Sources/SQLiteData/CloudKit/Internal/MockCloudContainer.swift +++ b/Sources/SQLiteData/CloudKit/Internal/MockCloudContainer.swift @@ -1,5 +1,4 @@ #if canImport(CloudKit) - package import ConcurrencyExtras package import CloudKit import Dependencies @@ -23,13 +22,13 @@ guard let containerIdentifier else { return } @Dependency(\.mockCloudContainers) var mockCloudContainers - mockCloudContainers.withValue { storage in + mockCloudContainers.withLock { storage in storage[containerIdentifier] = self } } package func accountStatus() -> CKAccountStatus { - _accountStatus.withValue(\.self) + _accountStatus.withLock(\.self) } package var rawValue: CKContainer { @@ -37,7 +36,7 @@ } package func accountStatus() async throws -> CKAccountStatus { - _accountStatus.withValue { $0 } + _accountStatus.withLock { $0 } } package func shareMetadata( @@ -49,7 +48,7 @@ ? privateCloudDatabase : sharedCloudDatabase - let rootRecord: CKRecord? = database.state.withValue { + let rootRecord: CKRecord? = database.state.withLock { $0.storage[share.recordID.zoneID]?.records.values.first { record in record.share?.recordID == share.recordID } @@ -80,7 +79,7 @@ -> MockCloudContainer { @Dependency(\.mockCloudContainers) var mockCloudContainers - return mockCloudContainers.withValue { storage in + return mockCloudContainers.withLock { storage in let container: MockCloudContainer if let existingContainer = storage[containerIdentifier] { return existingContainer diff --git a/Sources/SQLiteData/CloudKit/Internal/MockCloudDatabase.swift b/Sources/SQLiteData/CloudKit/Internal/MockCloudDatabase.swift index 510d1f5c..51daf55f 100644 --- a/Sources/SQLiteData/CloudKit/Internal/MockCloudDatabase.swift +++ b/Sources/SQLiteData/CloudKit/Internal/MockCloudDatabase.swift @@ -1,5 +1,4 @@ #if canImport(CloudKit) - package import ConcurrencyExtras package import CloudKit import Dependencies import IssueReporting @@ -48,7 +47,7 @@ let accountStatus = container.accountStatus() guard accountStatus == .available else { throw ckError(forAccountStatus: accountStatus) } - let record = try state.withValue { state in + let record = try state.withLock { state in guard let zone = state.storage[recordID.zoneID] else { throw CKError(.zoneNotFound) } guard let record = zone.records[recordID] @@ -58,7 +57,7 @@ return record } - try state.withValue { state in + try state.withLock { state in for key in record.allKeys() { guard let assetData = state.assets[AssetID(recordID: record.recordID, key: key)] else { continue } @@ -107,7 +106,7 @@ throw CKError(.limitExceeded) } - return state.withValue { state in + return state.withLock { state in let previousStorage = state.storage var saveResults: [CKRecord.ID: Result] = [:] var deleteResults: [CKRecord.ID: Result] = [:] @@ -367,7 +366,7 @@ guard accountStatus == .available else { throw ckError(forAccountStatus: accountStatus) } - return state.withValue { state in + return state.withLock { state in var saveResults: [CKRecordZone.ID: Result] = [:] var deleteResults: [CKRecordZone.ID: Result] = [:] diff --git a/Sources/SQLiteData/CloudKit/Internal/MockSyncEngine.swift b/Sources/SQLiteData/CloudKit/Internal/MockSyncEngine.swift index c8ef0113..82c9c476 100644 --- a/Sources/SQLiteData/CloudKit/Internal/MockSyncEngine.swift +++ b/Sources/SQLiteData/CloudKit/Internal/MockSyncEngine.swift @@ -1,5 +1,4 @@ #if canImport(CloudKit) - package import ConcurrencyExtras package import CloudKit import IssueReporting package import OrderedCollections @@ -27,7 +26,7 @@ } package func acceptShare(metadata: ShareMetadata) { - _ = _acceptedShareMetadata.withValue { $0.insert(metadata) } + _ = _acceptedShareMetadata.withLock { $0.insert(metadata) } } package func fetchChanges(_ options: CKSyncEngine.FetchChangesOptions) async throws { @@ -35,16 +34,16 @@ let zoneIDs: [CKRecordZone.ID] switch options.scope { case .all: - zoneIDs = Array(database.state.storage.keys) + zoneIDs = Array(database.state.withLock(\.storage.keys)) case .allExcluding(let excludedZoneIDs): - zoneIDs = Array(Set(database.state.storage.keys).subtracting(excludedZoneIDs)) + zoneIDs = Array(Set(database.state.withLock(\.storage.keys)).subtracting(excludedZoneIDs)) case .zoneIDs(let includedZoneIDs): zoneIDs = includedZoneIDs @unknown default: fatalError() } - modifications = database.state.withValue { state in + modifications = database.state.withLock { state in zoneIDs.reduce(into: [CKRecord]()) { accum, zoneID in @@ -54,12 +53,12 @@ $0._recordChangeTag != nil, "Records stored in database should have their 'recordChangeTag' assigned." ) - return $0._recordChangeTag! > self.state.changeTag.value + return $0._recordChangeTag! > self.state.changeTag.withLock(\.self) } } } - let deletions = database.state.withValue { + let deletions = database.state.withLock { let records = $0.deletedRecords.filter { recordID, _ in zoneIDs.contains(recordID.zoneID) } @@ -72,7 +71,7 @@ guard !modifications.isEmpty || !deletions.isEmpty else { return } - state.changeTag.withValue { changeTag in + state.changeTag.withLock { changeTag in changeTag = modifications.compactMap(\._recordChangeTag).max() ?? changeTag } @@ -161,38 +160,38 @@ } package var pendingRecordZoneChanges: [CKSyncEngine.PendingRecordZoneChange] { - _pendingRecordZoneChanges.withValue { Array($0) } + _pendingRecordZoneChanges.withLock { Array($0) } } package var pendingDatabaseChanges: [CKSyncEngine.PendingDatabaseChange] { - _pendingDatabaseChanges.withValue { Array($0) } + _pendingDatabaseChanges.withLock { Array($0) } } package func removePendingChanges() { - _pendingDatabaseChanges.withValue { $0.removeAll() } - _pendingRecordZoneChanges.withValue { $0.removeAll() } + _pendingDatabaseChanges.withLock { $0.removeAll() } + _pendingRecordZoneChanges.withLock { $0.removeAll() } } package func add(pendingRecordZoneChanges: [CKSyncEngine.PendingRecordZoneChange]) { - self._pendingRecordZoneChanges.withValue { + self._pendingRecordZoneChanges.withLock { $0.append(contentsOf: pendingRecordZoneChanges) } } package func remove(pendingRecordZoneChanges: [CKSyncEngine.PendingRecordZoneChange]) { - self._pendingRecordZoneChanges.withValue { + self._pendingRecordZoneChanges.withLock { $0.subtract(pendingRecordZoneChanges) } } package func add(pendingDatabaseChanges: [CKSyncEngine.PendingDatabaseChange]) { - self._pendingDatabaseChanges.withValue { + self._pendingDatabaseChanges.withLock { $0.append(contentsOf: pendingDatabaseChanges) } } package func remove(pendingDatabaseChanges: [CKSyncEngine.PendingDatabaseChange]) { - self._pendingDatabaseChanges.withValue { + self._pendingDatabaseChanges.withLock { $0.subtract(pendingDatabaseChanges) } } @@ -439,10 +438,10 @@ } package var `private`: MockSyncEngine { - syncEngines.private as! MockSyncEngine + syncEngines.withLock(\.private) as! MockSyncEngine } package var shared: MockSyncEngine { - syncEngines.shared as! MockSyncEngine + syncEngines.withLock(\.shared) as! MockSyncEngine } package func syncEngine(for scope: CKDatabase.Scope) -> MockSyncEngine { diff --git a/Sources/SQLiteData/CloudKit/SyncEngine.swift b/Sources/SQLiteData/CloudKit/SyncEngine.swift index ddcb3f32..42b115d7 100644 --- a/Sources/SQLiteData/CloudKit/SyncEngine.swift +++ b/Sources/SQLiteData/CloudKit/SyncEngine.swift @@ -1,6 +1,5 @@ #if canImport(CloudKit) public import CloudKit - package import ConcurrencyExtras import Dependencies public import GRDB public import IssueReporting @@ -282,7 +281,7 @@ ) #if os(iOS) @Dependency(\.defaultNotificationCenter) var defaultNotificationCenter - notificationsObserver.withValue { + notificationsObserver.withLock { $0 = defaultNotificationCenter.addObserver( forName: UIApplication.willResignActiveNotification, object: nil, @@ -291,7 +290,7 @@ _ = Task { @MainActor in let taskIdentifier = UIApplication.shared.beginBackgroundTask() defer { UIApplication.shared.endBackgroundTask(taskIdentifier) } - let (privateSyncEngine, sharedSyncEngine) = syncEngines.withValue { + let (privateSyncEngine, sharedSyncEngine) = syncEngines.withLock { ($0.private, $0.shared) } try await privateSyncEngine?.sendChanges(CKSyncEngine.SendChangesOptions()) @@ -304,7 +303,7 @@ } deinit { - notificationsObserver.withValue { + notificationsObserver.withLock { guard let observer = $0 else { return } NotificationCenter.default.removeObserver(observer) @@ -430,13 +429,13 @@ public func stop() { guard isRunning else { return } #if DEBUG && canImport(DeveloperToolsSupport) - previewTimerTask.withValue { + previewTimerTask.withLock { $0?.cancel() $0 = nil } #endif observationRegistrar.withMutation(of: self, keyPath: \.isRunning) { - syncEngines.withValue { + syncEngines.withLock { $0 = SyncEngines() } } @@ -445,7 +444,7 @@ /// Determines if the sync engine is currently running or not. public var isRunning: Bool { observationRegistrar.access(self, keyPath: \.isRunning) - return syncEngines.withValue { + return syncEngines.withLock { $0.isRunning } } @@ -453,7 +452,7 @@ private func start() throws -> Task { guard !isRunning else { return Task {} } observationRegistrar.withMutation(of: self, keyPath: \.isRunning) { - syncEngines.withValue { + syncEngines.withLock { let (privateSyncEngine, sharedSyncEngine) = defaultSyncEngines(metadatabase, self) $0 = SyncEngines( private: privateSyncEngine, @@ -513,7 +512,7 @@ @Dependency(\.context) var context @Dependency(\.continuousClock) var clock if context == .preview { - previewTimerTask.withValue { + previewTimerTask.withLock { $0?.cancel() $0 = Task { @Sendable [weak self] in await withErrorReporting { @@ -531,7 +530,7 @@ await withErrorReporting(.sqliteDataCloudKitFailure) { guard try await container.accountStatus() == .available else { return } - syncEngines.withValue { + syncEngines.withLock { $0.private?.state.add(pendingDatabaseChanges: [.saveZone(defaultZone)]) } try await uploadRecordsToCloudKit( @@ -545,7 +544,7 @@ try await cacheUserTables(recordTypes: currentRecordTypes) } } - self.startTask.withValue { + self.startTask.withLock { $0?.cancel() $0 = startTask } @@ -563,8 +562,8 @@ public func fetchChanges( _ options: CKSyncEngine.FetchChangesOptions = CKSyncEngine.FetchChangesOptions() ) async throws { - await startTask.withValue(\.self)?.value - let (privateSyncEngine, sharedSyncEngine) = syncEngines.withValue { + await startTask.withLock(\.self)?.value + let (privateSyncEngine, sharedSyncEngine) = syncEngines.withLock { ($0.private, $0.shared) } guard let privateSyncEngine, let sharedSyncEngine @@ -585,8 +584,8 @@ public func sendChanges( _ options: CKSyncEngine.SendChangesOptions = CKSyncEngine.SendChangesOptions() ) async throws { - await startTask.withValue(\.self)?.value - let (privateSyncEngine, sharedSyncEngine) = syncEngines.withValue { + await startTask.withLock(\.self)?.value + let (privateSyncEngine, sharedSyncEngine) = syncEngines.withLock { ($0.private, $0.shared) } guard let privateSyncEngine, let sharedSyncEngine @@ -656,7 +655,7 @@ false } } - syncEngines.withValue { + syncEngines.withLock { $0.private?.state.add(pendingRecordZoneChanges: changesByIsPrivate[true] ?? []) $0.shared?.state.add(pendingRecordZoneChanges: changesByIsPrivate[false] ?? []) } @@ -837,10 +836,10 @@ } return } - let oldSyncEngine = self.syncEngines.withValue { + let oldSyncEngine = self.syncEngines.withLock { oldZoneID.ownerName == CKCurrentUserDefaultName ? $0.private : $0.shared } - let syncEngine = self.syncEngines.withValue { + let syncEngine = self.syncEngines.withLock { zoneID.ownerName == CKCurrentUserDefaultName ? $0.private : $0.shared } oldSyncEngine?.state.add(pendingRecordZoneChanges: oldChanges) @@ -878,7 +877,7 @@ return } - let syncEngine = self.syncEngines.withValue { + let syncEngine = self.syncEngines.withLock { zoneID.ownerName == CKCurrentUserDefaultName ? $0.private : $0.shared } syncEngine?.state.add(pendingRecordZoneChanges: changes) @@ -892,7 +891,7 @@ } let container = type(of: container).createContainer(identifier: metadata.containerIdentifier) _ = try await container.accept(metadata) - try await syncEngines.shared?.fetchChanges( + try await syncEngines.withLock(\.shared)?.fetchChanges( CKSyncEngine.FetchChangesOptions( scope: .zoneIDs([rootRecordID.zoneID]), operationGroup: nil @@ -924,22 +923,22 @@ private var sendingChangesCount: Int { get { observationRegistrar.access(self, keyPath: \.isSendingChanges) - return activityCounts.withValue(\.sendingChangesCount) + return activityCounts.withLock(\.sendingChangesCount) } set { observationRegistrar.withMutation(of: self, keyPath: \.isSendingChanges) { - activityCounts.withValue { $0.sendingChangesCount = newValue } + activityCounts.withLock { $0.sendingChangesCount = newValue } } } } private var fetchingChangesCount: Int { get { observationRegistrar.access(self, keyPath: \.isFetchingChanges) - return activityCounts.withValue(\.fetchingChangesCount) + return activityCounts.withLock(\.fetchingChangesCount) } set { observationRegistrar.withMutation(of: self, keyPath: \.isFetchingChanges) { - activityCounts.withValue { $0.fetchingChangesCount = newValue } + activityCounts.withLock { $0.fetchingChangesCount = newValue } } } } @@ -1116,7 +1115,7 @@ #if DEBUG let state = LockIsolated(NextRecordZoneChangeBatchLoggingState()) defer { - let state = state.withValue(\.self) + let state = state.withLock(\.self) if let tabularDescription = state.tabularDescription { logger.debug( """ @@ -1153,7 +1152,7 @@ var sentRecord: CKRecord.ID? #if DEBUG defer { - state.withValue { [missingTable, missingRecord, sentRecord] in + state.withLock { [missingTable, missingRecord, sentRecord] in if let missingTable { $0.events.append("⚠️ Missing table") $0.recordTypes.append(metadata.recordType) @@ -1345,7 +1344,7 @@ changeType: CKSyncEngine.Event.AccountChange.ChangeType, syncEngine: any SyncEngineProtocol ) async { - guard syncEngine === syncEngines.private + guard syncEngine === syncEngines.withLock(\.private) else { return } switch changeType { diff --git a/Sources/SQLiteData/Fetch.swift b/Sources/SQLiteData/Fetch.swift index 50eb9fe6..2a7c5516 100644 --- a/Sources/SQLiteData/Fetch.swift +++ b/Sources/SQLiteData/Fetch.swift @@ -206,7 +206,7 @@ extension Fetch: Equatable where Value: Equatable { public func update() { let persisted = state.wrappedValue if persisted !== box { - persisted.reconcile(from: box, propertyName: "@Fetch") + persisted.update(from: box) } persisted.subscribe(generation: generation) } diff --git a/Sources/SQLiteData/FetchAll.swift b/Sources/SQLiteData/FetchAll.swift index e073c42c..e2a9fd3f 100644 --- a/Sources/SQLiteData/FetchAll.swift +++ b/Sources/SQLiteData/FetchAll.swift @@ -438,7 +438,7 @@ extension FetchAll: Equatable where Element: Equatable { public func update() { let persisted = state.wrappedValue if persisted !== box { - persisted.reconcile(from: box, propertyName: "@FetchAll") + persisted.update(from: box) } persisted.subscribe(generation: generation) } diff --git a/Sources/SQLiteData/FetchOne.swift b/Sources/SQLiteData/FetchOne.swift index 07f7e4e9..39f3ba88 100644 --- a/Sources/SQLiteData/FetchOne.swift +++ b/Sources/SQLiteData/FetchOne.swift @@ -950,7 +950,7 @@ extension FetchOne: Equatable where Value: Equatable { public func update() { let persisted = state.wrappedValue if persisted !== box { - persisted.reconcile(from: box, propertyName: "@FetchOne") + persisted.update(from: box) } persisted.subscribe(generation: generation) } diff --git a/Sources/SQLiteData/FetchSubscription.swift b/Sources/SQLiteData/FetchSubscription.swift index de794216..00644b50 100644 --- a/Sources/SQLiteData/FetchSubscription.swift +++ b/Sources/SQLiteData/FetchSubscription.swift @@ -1,4 +1,3 @@ -import ConcurrencyExtras import Perception import Sharing @@ -15,7 +14,7 @@ import Sharing /// } /// ``` public struct FetchSubscription: Sendable { - let cancellable = LockIsolated?>(nil) + let cancellable = LockIsolated.Continuation?>(nil) let onCancel: @Sendable () -> Void init(sharedReader: SharedReader) { @@ -28,20 +27,17 @@ public struct FetchSubscription: Sendable { /// the observation of the associated ``FetchAll``, ``FetchOne``, or ``Fetch``. public var task: Void { get async throws { - let task = Task { - try await withTaskCancellationHandler { - try await Task.never() - } onCancel: { - onCancel() - } + let never = AsyncStream { continuation in + cancellable.withLock { $0 = continuation } } - cancellable.withValue { $0 = task } - try await task.cancellableValue + for await _ in never {} + onCancel() + throw CancellationError() } } /// Cancels the database observation of the associated ``FetchAll``, ``FetchOne``, or ``Fetch``. public func cancel() { - cancellable.value?.cancel() + cancellable.withLock { $0?.finish() } } } diff --git a/Sources/SQLiteData/Internal/FetchBox.swift b/Sources/SQLiteData/Internal/FetchBox.swift index 7203efc7..7368147d 100644 --- a/Sources/SQLiteData/Internal/FetchBox.swift +++ b/Sources/SQLiteData/Internal/FetchBox.swift @@ -1,75 +1,51 @@ #if canImport(SwiftUI) import Combine import Foundation - import IssueReporting import Sharing import SwiftUI final class FetchBox: @unchecked Sendable { - private let lock = NSLock() - private var storage: Storage + private let storage: LockIsolated init(sharedReader: SharedReader) { - storage = Storage(sharedReader: sharedReader) + storage = LockIsolated(Storage(sharedReader: sharedReader)) } var sharedReader: SharedReader { - get { lock.withLock { storage.sharedReader } } - set { lock.withLock { storage.sharedReader = newValue } } + get { storage.withLock { $0.sharedReader } } + set { storage.withLock { $0.sharedReader = newValue } } } var fetchKeyID: FetchKeyID? { - get { lock.withLock { storage.fetchKeyID } } - set { lock.withLock { storage.fetchKeyID = newValue } } + get { storage.withLock(\.fetchKeyID) } + set { storage.withLock { $0.fetchKeyID = newValue } } } - func reconcile(from fresh: FetchBox, propertyName: String) { - let freshSnapshot = fresh.lock.withLock { fresh.storage } - if let freshFetchKeyID = freshSnapshot.fetchKeyID { - if freshFetchKeyID != fetchKeyID { - update(from: freshSnapshot) - } - } else if fetchKeyID != nil { - #if DEBUG - let hasReported = lock.withLock { - defer { storage.hasReportedIgnoredReinitialization = true } - return storage.hasReportedIgnoredReinitialization - } - guard !hasReported else { return } - reportIssue( - """ - A '\(propertyName)' property was re-initialized without a query, but was previously \ - initialized with one; this re-initialization will be ignored, and the property \ - will continue to observe the existing query - """ - ) - #endif - } - } - - private func update(from other: Storage) { - lock.withLock { - storage.sharedReader = other.sharedReader - storage.fetchKeyID = other.fetchKeyID + func update(from other: FetchBox) { + guard + let otherFetchKeyID = other.storage.withLock(\.fetchKeyID), + otherFetchKeyID != fetchKeyID + else { return } + storage.withLock { + $0.sharedReader = other.sharedReader + $0.fetchKeyID = other.fetchKeyID } } func subscribe(generation: SwiftUI.State) { guard #unavailable(iOS 17, macOS 14, tvOS 17, watchOS 10) else { return } _ = generation.wrappedValue - let cancellable = sharedReader.publisher - .dropFirst() - .sink { _ in generation.wrappedValue &+= 1 } - lock.withLock { storage.swiftUICancellable = cancellable } + storage.withLock { + $0.swiftUICancellable = sharedReader.publisher + .dropFirst() + .sink { _ in generation.wrappedValue &+= 1 } + } } private struct Storage { var sharedReader: SharedReader var fetchKeyID: FetchKeyID? var swiftUICancellable: AnyCancellable? - #if DEBUG - var hasReportedIgnoredReinitialization = false - #endif } } #endif diff --git a/Sources/SQLiteData/Internal/FetchKey.swift b/Sources/SQLiteData/Internal/FetchKey.swift index 8bab73bf..1ed414cd 100644 --- a/Sources/SQLiteData/Internal/FetchKey.swift +++ b/Sources/SQLiteData/Internal/FetchKey.swift @@ -1,4 +1,3 @@ -import ConcurrencyExtras import Dependencies import Dispatch import Foundation diff --git a/Sources/SQLiteData/Internal/LockIsolated.swift b/Sources/SQLiteData/Internal/LockIsolated.swift new file mode 100644 index 00000000..ed411ac4 --- /dev/null +++ b/Sources/SQLiteData/Internal/LockIsolated.swift @@ -0,0 +1,16 @@ +import class Foundation.NSLock + +package final class LockIsolated: @unchecked Sendable { + private var _value: Value + private let lock = NSLock() + package init(_ value: sending Value) { + self._value = value + } + package func withLock( + _ operation: (inout sending Value) throws(F) -> sending R + ) throws(F) -> sending R { + lock.lock() + defer { lock.unlock() } + return try operation(&_value) + } +} diff --git a/Tests/SQLiteDataTests/CloudKitTests/AccountLifecycleTests.swift b/Tests/SQLiteDataTests/CloudKitTests/AccountLifecycleTests.swift index dd8f0ef9..b93173cc 100644 --- a/Tests/SQLiteDataTests/CloudKitTests/AccountLifecycleTests.swift +++ b/Tests/SQLiteDataTests/CloudKitTests/AccountLifecycleTests.swift @@ -1,5 +1,4 @@ #if canImport(CloudKit) - import ConcurrencyExtrasTestSupport import CloudKit import CustomDump import Foundation diff --git a/Tests/SQLiteDataTests/CloudKitTests/AssetsTests.swift b/Tests/SQLiteDataTests/CloudKitTests/AssetsTests.swift index 467e85f3..6951da7b 100644 --- a/Tests/SQLiteDataTests/CloudKitTests/AssetsTests.swift +++ b/Tests/SQLiteDataTests/CloudKitTests/AssetsTests.swift @@ -1,6 +1,5 @@ #if canImport(CloudKit) import CloudKit - import ConcurrencyExtras import CustomDump import InlineSnapshotTesting import OrderedCollections @@ -59,7 +58,7 @@ """ } - inMemoryDataManager.storage.withValue { storage in + inMemoryDataManager.storage.withLock { storage in let url = URL( string: "file:///tmp/6105d6cc76af400325e94d588ce511be5bfdbb73b437dc51eca43917d7a43e3d" )! @@ -115,7 +114,7 @@ """ } - inMemoryDataManager.storage.withValue { storage in + inMemoryDataManager.storage.withLock { storage in let url = URL( string: "file:///tmp/97e67a5645969953f1a4cfe2ea75649864ff99789189cdd3f6db03e59f8a8ebf" )! diff --git a/Tests/SQLiteDataTests/CloudKitTests/AtomicTests.swift b/Tests/SQLiteDataTests/CloudKitTests/AtomicTests.swift index 4b17da1d..1dc55b3a 100644 --- a/Tests/SQLiteDataTests/CloudKitTests/AtomicTests.swift +++ b/Tests/SQLiteDataTests/CloudKitTests/AtomicTests.swift @@ -1,6 +1,5 @@ #if canImport(CloudKit) import CloudKit - import ConcurrencyExtras import CustomDump import InlineSnapshotTesting import OrderedCollections diff --git a/Tests/SQLiteDataTests/CloudKitTests/AttachedMetadatabaseTests.swift b/Tests/SQLiteDataTests/CloudKitTests/AttachedMetadatabaseTests.swift index dbec8bb2..df8a98fa 100644 --- a/Tests/SQLiteDataTests/CloudKitTests/AttachedMetadatabaseTests.swift +++ b/Tests/SQLiteDataTests/CloudKitTests/AttachedMetadatabaseTests.swift @@ -1,7 +1,5 @@ #if canImport(CloudKit) import CloudKit - import ConcurrencyExtras - import ConcurrencyExtrasTestSupport import CustomDump import InlineSnapshotTesting import OrderedCollections diff --git a/Tests/SQLiteDataTests/CloudKitTests/CloudKitTests.swift b/Tests/SQLiteDataTests/CloudKitTests/CloudKitTests.swift index fa989369..75cb5c6f 100644 --- a/Tests/SQLiteDataTests/CloudKitTests/CloudKitTests.swift +++ b/Tests/SQLiteDataTests/CloudKitTests/CloudKitTests.swift @@ -1,6 +1,5 @@ #if canImport(CloudKit) import CloudKit - import ConcurrencyExtras import CustomDump import InlineSnapshotTesting import OrderedCollections diff --git a/Tests/SQLiteDataTests/CloudKitTests/ForeignKeyConstraintTests.swift b/Tests/SQLiteDataTests/CloudKitTests/ForeignKeyConstraintTests.swift index 071688d1..d20b52de 100644 --- a/Tests/SQLiteDataTests/CloudKitTests/ForeignKeyConstraintTests.swift +++ b/Tests/SQLiteDataTests/CloudKitTests/ForeignKeyConstraintTests.swift @@ -672,9 +672,9 @@ """ } assertInlineSnapshot( - of: syncEngine.private.database.state.storage[syncEngine.defaultZone.zoneID]?.records[ - Reminder.recordID(for: 1) - ], + of: syncEngine.private.database.state.withLock { + $0.storage[syncEngine.defaultZone.zoneID]?.records[Reminder.recordID(for: 1)] + }, as: .customDump ) { """ @@ -767,9 +767,9 @@ """ } assertInlineSnapshot( - of: syncEngine.private.database.state.storage[syncEngine.defaultZone.zoneID]?.records[ - Reminder.recordID(for: 1) - ], + of: syncEngine.private.database.state.withLock { + $0.storage[syncEngine.defaultZone.zoneID]?.records[Reminder.recordID(for: 1)] + }, as: .customDump ) { """ diff --git a/Tests/SQLiteDataTests/CloudKitTests/MergeConflictTests.swift b/Tests/SQLiteDataTests/CloudKitTests/MergeConflictTests.swift index b9840148..ceb1a3f2 100644 --- a/Tests/SQLiteDataTests/CloudKitTests/MergeConflictTests.swift +++ b/Tests/SQLiteDataTests/CloudKitTests/MergeConflictTests.swift @@ -1,6 +1,6 @@ #if canImport(CloudKit) - import CloudKit import ConcurrencyExtrasTestSupport + import CloudKit import CustomDump import Foundation import InlineSnapshotTesting diff --git a/Tests/SQLiteDataTests/CloudKitTests/MetadataTests.swift b/Tests/SQLiteDataTests/CloudKitTests/MetadataTests.swift index bd81326d..945b33da 100644 --- a/Tests/SQLiteDataTests/CloudKitTests/MetadataTests.swift +++ b/Tests/SQLiteDataTests/CloudKitTests/MetadataTests.swift @@ -1,6 +1,5 @@ #if canImport(CloudKit) import CloudKit - import ConcurrencyExtrasTestSupport import CustomDump import SQLiteDataTestSupport import Foundation diff --git a/Tests/SQLiteDataTests/CloudKitTests/MockCloudDatabaseTests.swift b/Tests/SQLiteDataTests/CloudKitTests/MockCloudDatabaseTests.swift index d019ae9e..fca888db 100644 --- a/Tests/SQLiteDataTests/CloudKitTests/MockCloudDatabaseTests.swift +++ b/Tests/SQLiteDataTests/CloudKitTests/MockCloudDatabaseTests.swift @@ -1,6 +1,5 @@ #if canImport(CloudKit) import CloudKit - import ConcurrencyExtras import CustomDump import InlineSnapshotTesting import OrderedCollections @@ -321,7 +320,7 @@ @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) @Test func accountTemporarilyAvailable() async throws { - container._accountStatus.withValue { $0 = .temporarilyUnavailable } + container._accountStatus.withLock { $0 = .temporarilyUnavailable } var error = #expect(throws: CKError.self) { _ = try self.syncEngine.private.database.modifyRecordZones() } @@ -344,7 +343,7 @@ @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) @Test func noAccount() async throws { - container._accountStatus.withValue { $0 = .noAccount } + container._accountStatus.withLock { $0 = .noAccount } var error = #expect(throws: CKError.self) { _ = try self.syncEngine.private.database.modifyRecordZones() } @@ -367,7 +366,7 @@ @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) @Test func accountNotDetermined() async throws { - container._accountStatus.withValue { $0 = .couldNotDetermine } + container._accountStatus.withLock { $0 = .couldNotDetermine } var error = #expect(throws: CKError.self) { _ = try self.syncEngine.private.database.modifyRecordZones() } @@ -390,7 +389,7 @@ @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) @Test func restrictedAccount() async throws { - container._accountStatus.withValue { $0 = .restricted } + container._accountStatus.withLock { $0 = .restricted } var error = #expect(throws: CKError.self) { _ = try self.syncEngine.private.database.modifyRecordZones() } diff --git a/Tests/SQLiteDataTests/CloudKitTests/NewTableSyncTests.swift b/Tests/SQLiteDataTests/CloudKitTests/NewTableSyncTests.swift index 568184a5..0bdb99df 100644 --- a/Tests/SQLiteDataTests/CloudKitTests/NewTableSyncTests.swift +++ b/Tests/SQLiteDataTests/CloudKitTests/NewTableSyncTests.swift @@ -1,6 +1,5 @@ #if canImport(CloudKit) import CloudKit - import ConcurrencyExtrasTestSupport import CustomDump import SQLiteDataTestSupport import Foundation diff --git a/Tests/SQLiteDataTests/CloudKitTests/NextRecordZoneChangeBatchTests.swift b/Tests/SQLiteDataTests/CloudKitTests/NextRecordZoneChangeBatchTests.swift index 8a3c2066..d8fbbed2 100644 --- a/Tests/SQLiteDataTests/CloudKitTests/NextRecordZoneChangeBatchTests.swift +++ b/Tests/SQLiteDataTests/CloudKitTests/NextRecordZoneChangeBatchTests.swift @@ -1,6 +1,5 @@ #if canImport(CloudKit) import CloudKit - import ConcurrencyExtrasTestSupport import CustomDump import Foundation import InlineSnapshotTesting diff --git a/Tests/SQLiteDataTests/CloudKitTests/RecordTypeTests.swift b/Tests/SQLiteDataTests/CloudKitTests/RecordTypeTests.swift index 2891b3e2..a9b494c6 100644 --- a/Tests/SQLiteDataTests/CloudKitTests/RecordTypeTests.swift +++ b/Tests/SQLiteDataTests/CloudKitTests/RecordTypeTests.swift @@ -1,6 +1,5 @@ #if canImport(CloudKit) import CloudKit - import ConcurrencyExtras import CustomDump import InlineSnapshotTesting import SQLiteData diff --git a/Tests/SQLiteDataTests/CloudKitTests/ReferenceViolationTests.swift b/Tests/SQLiteDataTests/CloudKitTests/ReferenceViolationTests.swift index eaabf060..c3871cbe 100644 --- a/Tests/SQLiteDataTests/CloudKitTests/ReferenceViolationTests.swift +++ b/Tests/SQLiteDataTests/CloudKitTests/ReferenceViolationTests.swift @@ -1,6 +1,5 @@ #if canImport(CloudKit) import CloudKit - import ConcurrencyExtras import CustomDump import InlineSnapshotTesting import SQLiteData diff --git a/Tests/SQLiteDataTests/CloudKitTests/SchemaChangeTests.swift b/Tests/SQLiteDataTests/CloudKitTests/SchemaChangeTests.swift index 98601572..e93e5c35 100644 --- a/Tests/SQLiteDataTests/CloudKitTests/SchemaChangeTests.swift +++ b/Tests/SQLiteDataTests/CloudKitTests/SchemaChangeTests.swift @@ -791,7 +791,7 @@ ) .notify() - inMemoryDataManager.storage.withValue { $0.removeAll() } + inMemoryDataManager.storage.withLock { $0.removeAll() } try await userDatabase.userWrite { db in try #sql( @@ -848,7 +848,7 @@ .notify() syncEngine.stop() - inMemoryDataManager.storage.withValue { $0.removeAll() } + inMemoryDataManager.storage.withLock { $0.removeAll() } try await userDatabase.userWrite { db in try #sql( diff --git a/Tests/SQLiteDataTests/CloudKitTests/SyncEngineDelegateTests.swift b/Tests/SQLiteDataTests/CloudKitTests/SyncEngineDelegateTests.swift index 395be01e..ba97ea2b 100644 --- a/Tests/SQLiteDataTests/CloudKitTests/SyncEngineDelegateTests.swift +++ b/Tests/SQLiteDataTests/CloudKitTests/SyncEngineDelegateTests.swift @@ -1,6 +1,5 @@ #if canImport(CloudKit) import CloudKit - import ConcurrencyExtrasTestSupport import CustomDump import DependenciesTestSupport import Foundation @@ -229,10 +228,10 @@ _ syncEngine: SQLiteData.SyncEngine, accountChanged changeType: CKSyncEngine.Event.AccountChange.ChangeType ) async { - wasCalled.withValue { $0 = true } + wasCalled.withLock { $0 = true } } deinit { - guard wasCalled.withValue(\.self) + guard wasCalled.withLock(\.self) else { Issue.record("Delegate method 'syncEngine(_:accountChanged:)' was not called.") return diff --git a/Tests/SQLiteDataTests/CloudKitTests/SyncEngineLifecycleTests.swift b/Tests/SQLiteDataTests/CloudKitTests/SyncEngineLifecycleTests.swift index 229d6b02..e651ba27 100644 --- a/Tests/SQLiteDataTests/CloudKitTests/SyncEngineLifecycleTests.swift +++ b/Tests/SQLiteDataTests/CloudKitTests/SyncEngineLifecycleTests.swift @@ -1,6 +1,5 @@ #if canImport(CloudKit) import CloudKit - import ConcurrencyExtrasTestSupport import DependenciesTestSupport import InlineSnapshotTesting import SQLiteDataTestSupport diff --git a/Tests/SQLiteDataTests/FetchBoxTests.swift b/Tests/SQLiteDataTests/FetchBoxTests.swift index 54bcdbac..c4016cfd 100644 --- a/Tests/SQLiteDataTests/FetchBoxTests.swift +++ b/Tests/SQLiteDataTests/FetchBoxTests.swift @@ -17,7 +17,7 @@ persisted.fetchKeyID = fetchKeyID(TestRequest(id: 1)) let fresh = FetchBox(sharedReader: SharedReader(value: 2)) fresh.fetchKeyID = fetchKeyID(TestRequest(id: 2)) - persisted.reconcile(from: fresh, propertyName: "@Fetch") + persisted.update(from: fresh) #expect(persisted.sharedReader.wrappedValue == 2) #expect(persisted.fetchKeyID == fresh.fetchKeyID) } @@ -27,14 +27,23 @@ persisted.fetchKeyID = fetchKeyID(TestRequest(id: 1)) let fresh = FetchBox(sharedReader: SharedReader(value: 2)) fresh.fetchKeyID = fetchKeyID(TestRequest(id: 1)) - persisted.reconcile(from: fresh, propertyName: "@Fetch") + persisted.update(from: fresh) #expect(persisted.sharedReader.wrappedValue == 1) } + @Test func keyedToKeylessReinitializationIsIgnored() { + let persisted = FetchBox(sharedReader: SharedReader(value: 1)) + persisted.fetchKeyID = fetchKeyID(TestRequest(id: 1)) + let fresh = FetchBox(sharedReader: SharedReader(value: 2)) + persisted.update(from: fresh) + #expect(persisted.sharedReader.wrappedValue == 1) + #expect(persisted.fetchKeyID != nil) + } + @Test func keylessReinitializationIsIgnored() { let persisted = FetchBox(sharedReader: SharedReader(value: 1)) let fresh = FetchBox(sharedReader: SharedReader(value: 2)) - persisted.reconcile(from: fresh, propertyName: "@Fetch") + persisted.update(from: fresh) #expect(persisted.sharedReader.wrappedValue == 1) } @@ -42,7 +51,7 @@ let persisted = FetchBox(sharedReader: SharedReader(value: 1)) let fresh = FetchBox(sharedReader: SharedReader(value: 2)) fresh.fetchKeyID = fetchKeyID(TestRequest(id: 2)) - persisted.reconcile(from: fresh, propertyName: "@Fetch") + persisted.update(from: fresh) #expect(persisted.sharedReader.wrappedValue == 2) #expect(persisted.fetchKeyID == fresh.fetchKeyID) } @@ -51,22 +60,10 @@ let persisted = FetchBox(sharedReader: SharedReader(value: [Int]())) persisted.sharedReader = SharedReader(value: [1, 2, 3]) let fresh = FetchBox(sharedReader: SharedReader(value: [Int]())) - persisted.reconcile(from: fresh, propertyName: "@FetchAll") + persisted.update(from: fresh) #expect(persisted.sharedReader.wrappedValue == [1, 2, 3]) } - @Test func keyedToKeylessReinitializationReportsIssue() { - let persisted = FetchBox(sharedReader: SharedReader(value: 1)) - persisted.fetchKeyID = fetchKeyID(TestRequest(id: 1)) - let fresh = FetchBox(sharedReader: SharedReader(value: 1)) - withKnownIssue(isIntermittent: true) { - persisted.reconcile(from: fresh, propertyName: "@Fetch") - } - #expect(persisted.sharedReader.wrappedValue == 1) - #expect(persisted.fetchKeyID != nil) - persisted.reconcile(from: fresh, propertyName: "@Fetch") - } - private func fetchKeyID(_ request: some FetchKeyRequest) -> FetchKeyID { FetchKey(request: request, database: database, scheduler: nil).id } diff --git a/Tests/SQLiteDataTests/FetchSubscriptionTests.swift b/Tests/SQLiteDataTests/FetchSubscriptionTests.swift index 155dcdc2..25c3702d 100644 --- a/Tests/SQLiteDataTests/FetchSubscriptionTests.swift +++ b/Tests/SQLiteDataTests/FetchSubscriptionTests.swift @@ -31,7 +31,7 @@ import Testing @Test func completeWhenTaskExplicitlyCancelled() async throws { @FetchAll var records: [Record] #expect(records.count == 0) - let didComplete = LockIsolated(false) + nonisolated(unsafe) var didComplete = false try await database.write { db in try Record.insert { Record.Draft() }.execute(db) @@ -43,14 +43,14 @@ import Testing let task = Task { try? await subscription.task - didComplete.withValue { $0 = true } + didComplete = true } try await Task.sleep(for: .seconds(1)) subscription.cancel() await task.value - #expect(didComplete.value) + #expect(didComplete) } @Test func cancellingOneFetchDoesNotCancelAnother() async throws { diff --git a/Tests/SQLiteDataTests/Internal/BaseCloudKitTests.swift b/Tests/SQLiteDataTests/Internal/BaseCloudKitTests.swift index f55e5a64..ea6163f4 100644 --- a/Tests/SQLiteDataTests/Internal/BaseCloudKitTests.swift +++ b/Tests/SQLiteDataTests/Internal/BaseCloudKitTests.swift @@ -1,7 +1,6 @@ #if canImport(CloudKit) import Clocks import CloudKit - import ConcurrencyExtrasTestSupport import DependenciesTestSupport import OrderedCollections import SQLiteData @@ -100,7 +99,7 @@ import TestLocals @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) func signOut() async { - container._accountStatus.withValue { $0 = .noAccount } + container._accountStatus.withLock { $0 = .noAccount } await syncEngine.handleEvent( .accountChange(changeType: .signOut(previousUser: previousUserRecordID)), syncEngine: syncEngine.private @@ -113,12 +112,12 @@ import TestLocals @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) func softSignOut() async { - container._accountStatus.withValue { $0 = .temporarilyUnavailable } + container._accountStatus.withLock { $0 = .temporarilyUnavailable } } @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) func signIn() async { - container._accountStatus.withValue { $0 = .available } + container._accountStatus.withLock { $0 = .available } // NB: Emulates what CKSyncEngine does when signing in syncEngine.private.state.removePendingChanges() syncEngine.shared.state.removePendingChanges() @@ -158,10 +157,10 @@ import TestLocals @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) extension SyncEngine { var `private`: MockSyncEngine { - syncEngines.private as! MockSyncEngine + syncEngines.withLock(\.private) as! MockSyncEngine } var shared: MockSyncEngine { - syncEngines.shared as! MockSyncEngine + syncEngines.withLock(\.shared) as! MockSyncEngine } static nonisolated let defaultTestZone = CKRecordZone( zoneName: "zone" diff --git a/Tests/SQLiteDataTests/Internal/CloudKit+CustomDump.swift b/Tests/SQLiteDataTests/Internal/CloudKit+CustomDump.swift index 16d1903b..e4aab1e4 100644 --- a/Tests/SQLiteDataTests/Internal/CloudKit+CustomDump.swift +++ b/Tests/SQLiteDataTests/Internal/CloudKit+CustomDump.swift @@ -175,13 +175,13 @@ children: [ ( "pendingRecordZoneChanges", - _pendingRecordZoneChanges.withValue(\.self) + _pendingRecordZoneChanges.withLock(\.self) .sorted(by: comparePendingRecordZoneChange) as Any ), ( "pendingDatabaseChanges", - _pendingDatabaseChanges.withValue(\.self) + _pendingDatabaseChanges.withLock(\.self) .sorted(by: comparePendingDatabaseChange) as Any ), ], @@ -249,11 +249,12 @@ children: [ "databaseScope": databaseScope, "storage": state - .value - .storage - .flatMap { _, value in value.records.values } - .sorted { - ($0.recordType, $0.recordID.recordName) < ($1.recordType, $1.recordID.recordName) + .withLock { + $0.storage + .flatMap { _, value in value.records.values } + .sorted { + ($0.recordType, $0.recordID.recordName) < ($1.recordType, $1.recordID.recordName) + } }, ], displayStyle: .struct diff --git a/Tests/SQLiteDataTests/Internal/CloudKitTestHelpers.swift b/Tests/SQLiteDataTests/Internal/CloudKitTestHelpers.swift index 4716be7b..580f512f 100644 --- a/Tests/SQLiteDataTests/Internal/CloudKitTestHelpers.swift +++ b/Tests/SQLiteDataTests/Internal/CloudKitTestHelpers.swift @@ -1,5 +1,4 @@ import CloudKit -import ConcurrencyExtras import CustomDump import OrderedCollections import SQLiteData @@ -75,7 +74,7 @@ extension SyncEngine { > { let syncEngine = syncEngine(for: scope) let recordsToDeleteByID = Dictionary( - grouping: syncEngine.database.state.withValue { state in + grouping: syncEngine.database.state.withLock { state in recordIDsToDelete.compactMap { recordID in state.storage[recordID.zoneID]?.records[recordID] } @@ -118,7 +117,7 @@ extension MockSyncEngine { line: UInt = #line, column: UInt = #column ) { - _fetchChangesScopes.withValue { + _fetchChangesScopes.withLock { expectNoDifference( scopes, $0, @@ -138,7 +137,7 @@ extension MockSyncEngine { line: UInt = #line, column: UInt = #column ) { - _acceptedShareMetadata.withValue { + _acceptedShareMetadata.withLock { expectNoDifference( sharedMetadata, $0, @@ -161,7 +160,7 @@ extension MockSyncEngineState { line: UInt = #line, column: UInt = #column ) { - _pendingRecordZoneChanges.withValue { + _pendingRecordZoneChanges.withLock { expectNoDifference( Set(changes), Set($0), @@ -181,7 +180,7 @@ extension MockSyncEngineState { line: UInt = #line, column: UInt = #column ) { - _pendingDatabaseChanges.withValue { + _pendingDatabaseChanges.withLock { expectNoDifference( Set(changes), Set($0),