From 59bd4af8181180b4cd5246eec41042359bd729b6 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 3 Jul 2026 00:05:58 -0700 Subject: [PATCH] Remove `ConcurrencyExtras` --- Package.swift | 2 - Package@swift-5.9.swift | 2 - Package@swift-6.0.swift | 2 - Sources/Sharing/Internal/LockIsolated.swift | 16 +++++++ .../Sharing/SharedKeys/AppStorageKey.swift | 1 - .../Sharing/SharedKeys/FileStorageKey.swift | 19 ++++---- Tests/SharingTests/FileStorageTests.swift | 47 +++++++++---------- 7 files changed, 48 insertions(+), 41 deletions(-) create mode 100644 Sources/Sharing/Internal/LockIsolated.swift diff --git a/Package.swift b/Package.swift index c2b8cc7..5d7db20 100644 --- a/Package.swift +++ b/Package.swift @@ -34,7 +34,6 @@ let package = Package( dependencies: [ .package(url: "https://github.com/pointfreeco/combine-schedulers", from: "1.0.0"), .package(url: "https://github.com/pointfreeco/swift-case-paths", from: "1.7.3"), - .package(url: "https://github.com/pointfreeco/swift-concurrency-extras", from: "1.3.0"), .package(url: "https://github.com/pointfreeco/swift-custom-dump", from: "1.0.0"), .package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.5.1"), .package(url: "https://github.com/pointfreeco/swift-identified-collections", from: "1.0.0"), @@ -55,7 +54,6 @@ let package = Package( traits: ["CasePaths"] ) ), - .product(name: "ConcurrencyExtras", package: "swift-concurrency-extras"), .product( name: "CustomDump", package: "swift-custom-dump", diff --git a/Package@swift-5.9.swift b/Package@swift-5.9.swift index e8139b5..3a00e79 100644 --- a/Package@swift-5.9.swift +++ b/Package@swift-5.9.swift @@ -17,7 +17,6 @@ let package = Package( ) ], dependencies: [ - .package(url: "https://github.com/pointfreeco/swift-concurrency-extras", from: "1.3.0"), .package(url: "https://github.com/pointfreeco/swift-custom-dump", from: "1.0.0"), .package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.5.1"), .package(url: "https://github.com/pointfreeco/swift-identified-collections", from: "1.0.0"), @@ -31,7 +30,6 @@ let package = Package( dependencies: [ "Sharing1", "Sharing2", - .product(name: "ConcurrencyExtras", package: "swift-concurrency-extras"), .product(name: "CustomDump", package: "swift-custom-dump"), .product(name: "Dependencies", package: "swift-dependencies"), .product(name: "IdentifiedCollections", package: "swift-identified-collections"), diff --git a/Package@swift-6.0.swift b/Package@swift-6.0.swift index a2c79b0..e74e418 100644 --- a/Package@swift-6.0.swift +++ b/Package@swift-6.0.swift @@ -18,7 +18,6 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/pointfreeco/combine-schedulers", from: "1.0.0"), - .package(url: "https://github.com/pointfreeco/swift-concurrency-extras", from: "1.3.0"), .package(url: "https://github.com/pointfreeco/swift-custom-dump", from: "1.0.0"), .package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.5.1"), .package(url: "https://github.com/pointfreeco/swift-identified-collections", from: "1.0.0"), @@ -32,7 +31,6 @@ let package = Package( dependencies: [ "Sharing1", "Sharing2", - .product(name: "ConcurrencyExtras", package: "swift-concurrency-extras"), .product(name: "CustomDump", package: "swift-custom-dump"), .product(name: "Dependencies", package: "swift-dependencies"), .product(name: "IdentifiedCollections", package: "swift-identified-collections"), diff --git a/Sources/Sharing/Internal/LockIsolated.swift b/Sources/Sharing/Internal/LockIsolated.swift new file mode 100644 index 0000000..ed8213d --- /dev/null +++ b/Sources/Sharing/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/Sources/Sharing/SharedKeys/AppStorageKey.swift b/Sources/Sharing/SharedKeys/AppStorageKey.swift index 23f815e..317a2c5 100644 --- a/Sources/Sharing/SharedKeys/AppStorageKey.swift +++ b/Sources/Sharing/SharedKeys/AppStorageKey.swift @@ -1,5 +1,4 @@ #if canImport(AppKit) || canImport(UIKit) || canImport(WatchKit) - import ConcurrencyExtras public import Dependencies @preconcurrency public import Foundation import IssueReporting diff --git a/Sources/Sharing/SharedKeys/FileStorageKey.swift b/Sources/Sharing/SharedKeys/FileStorageKey.swift index 44f3bf3..64411f8 100644 --- a/Sources/Sharing/SharedKeys/FileStorageKey.swift +++ b/Sources/Sharing/SharedKeys/FileStorageKey.swift @@ -1,6 +1,5 @@ #if canImport(AppKit) || canImport(UIKit) || canImport(WatchKit) import Combine - public import ConcurrencyExtras public import Dependencies @preconcurrency import Dispatch @@ -143,7 +142,7 @@ setUpSources() } } - if state.withValue({ $0.workItem == nil }) { + if state.withLock({ $0.workItem == nil }) { if fileExists { subscriber.yield(with: Result { try self.decode(self.storage.load(self.url)) }) } else { @@ -161,7 +160,7 @@ ? (try? self.storage.attributesOfItemAtPath(self.url.path)[.modificationDate] as? Date) : nil - let shouldYield = state.withValue { state in + let shouldYield = state.withLock { state in guard fileExists else { state.cancelWorkItem() @@ -210,7 +209,7 @@ public func save(_ value: Value, context: SaveContext, continuation: SaveContinuation) { do { - let workItem: DispatchWorkItem? = try state.withValue { state in + let workItem: DispatchWorkItem? = try state.withLock { state in let data = try encode(value) switch context { case .didSet: @@ -224,7 +223,7 @@ continuation.resume() let workItem = DispatchWorkItem { [weak self] in guard let self else { return } - self.state.withValue { state in + self.state.withLock { state in defer { state.value = nil state.workItem = nil @@ -334,7 +333,7 @@ @Sendable (URL, DispatchSource.FileSystemEvent, @escaping @Sendable () -> Void) throws -> SharedSubscription let load: @Sendable (URL) throws -> Data - @_spi(Internals) public let save: @Sendable (Data, URL) throws -> Void + public let save: @Sendable (Data, URL) throws -> Void /// File storage that interacts directly with the file system for saving, loading and listening /// for file changes. @@ -391,7 +390,7 @@ inMemory(fileSystem: LockIsolated([:])) } - @_spi(Internals) public static func inMemory( + package static func inMemory( fileSystem: LockIsolated<[URL: Data]>, async: @escaping @Sendable (DispatchWorkItem) -> Void = { $0.perform() }, asyncAfter: @escaping @Sendable (DispatchTimeInterval, DispatchWorkItem) -> Void = { @@ -405,14 +404,14 @@ asyncAfter: asyncAfter, attributesOfItemAtPath: { _ in [:] }, createDirectory: { _, _ in }, - fileExists: { fileSystem.keys.contains($0) }, + fileExists: { url in fileSystem.withLock { $0.keys.contains(url) } }, fileSystemSource: { url, event, handler in guard event.contains(.write) else { return SharedSubscription {} } return SharedSubscription {} }, load: { - guard let data = fileSystem[$0] + guard let data = fileSystem.withLock(\.[$0]) else { struct LoadError: Error {} throw LoadError() @@ -420,7 +419,7 @@ return data }, save: { data, url in - fileSystem.withValue { $0[url] = data } + fileSystem.withLock { $0[url] = data } } ) } diff --git a/Tests/SharingTests/FileStorageTests.swift b/Tests/SharingTests/FileStorageTests.swift index d1e2adc..3b3b978 100644 --- a/Tests/SharingTests/FileStorageTests.swift +++ b/Tests/SharingTests/FileStorageTests.swift @@ -1,16 +1,15 @@ #if canImport(AppKit) || canImport(UIKit) || canImport(WatchKit) import Combine import CombineSchedulers - import ConcurrencyExtras import CustomDump import Dependencies import DependenciesTestSupport import Foundation - @_spi(Internals) import Sharing + import Sharing import Testing @Suite struct FileStorageTests { - let fileSystem = LockIsolated<[URL: Data]>([:]) + let fileSystem = Sharing.LockIsolated<[URL: Data]>([:]) let testScheduler = DispatchQueue.test @Test func basics() throws { @@ -20,11 +19,11 @@ @Shared(.fileStorage(.fileURL)) var users = [User]() #expect($users.loadError == nil) expectNoDifference( - fileSystem.value, + fileSystem.withLock(\.self), [.fileURL: Data()] ) $users.withLock { $0.append(.blob) } - try expectNoDifference(fileSystem.value.users(for: .fileURL), [.blob]) + try expectNoDifference(fileSystem.withLock(\.self).users(for: .fileURL), [.blob]) } } @@ -35,12 +34,12 @@ @Shared(.utf8String) var string = "" #expect($string.loadError == nil) expectNoDifference( - fileSystem.value, + fileSystem.withLock(\.self), [.utf8StringURL: Data()] ) $string.withLock { $0 = "hello" } expectNoDifference( - fileSystem.value[.utf8StringURL].map { String(decoding: $0, as: UTF8.self) }, + fileSystem.withLock(\.self)[.utf8StringURL].map { String(decoding: $0, as: UTF8.self) }, "hello" ) } @@ -51,26 +50,26 @@ $0.defaultFileStorage = .inMemory(fileSystem: fileSystem, scheduler: testScheduler) } operation: { @Shared(.fileStorage(.fileURL)) var users = [User]() - try expectNoDifference(fileSystem.value.users(for: .fileURL), nil) + try expectNoDifference(fileSystem.withLock(\.self).users(for: .fileURL), nil) $users.withLock { $0.append(.blob) } - try expectNoDifference(fileSystem.value.users(for: .fileURL), [.blob]) + try expectNoDifference(fileSystem.withLock(\.self).users(for: .fileURL), [.blob]) $users.withLock { $0.append(.blobJr) } testScheduler.advance(by: .seconds(1) - .milliseconds(1)) - try expectNoDifference(fileSystem.value.users(for: .fileURL), [.blob]) + try expectNoDifference(fileSystem.withLock(\.self).users(for: .fileURL), [.blob]) $users.withLock { $0.append(.blobSr) } testScheduler.advance(by: .milliseconds(1)) - try expectNoDifference(fileSystem.value.users(for: .fileURL), [.blob, .blobJr, .blobSr]) + try expectNoDifference(fileSystem.withLock(\.self).users(for: .fileURL), [.blob, .blobJr, .blobSr]) testScheduler.advance(by: .seconds(1)) - try expectNoDifference(fileSystem.value.users(for: .fileURL), [.blob, .blobJr, .blobSr]) + try expectNoDifference(fileSystem.withLock(\.self).users(for: .fileURL), [.blob, .blobJr, .blobSr]) testScheduler.advance(by: .seconds(0.5)) $users.withLock { $0.append(.blobEsq) } try expectNoDifference( - fileSystem.value.users(for: .fileURL), + fileSystem.withLock(\.self).users(for: .fileURL), [ .blob, .blobJr, @@ -86,14 +85,14 @@ $0.defaultFileStorage = .inMemory(fileSystem: fileSystem, scheduler: testScheduler) } operation: { @Shared(.fileStorage(.fileURL)) var users = [User]() - try expectNoDifference(fileSystem.value.users(for: .fileURL), nil) + try expectNoDifference(fileSystem.withLock(\.self).users(for: .fileURL), nil) $users.withLock { $0.append(.blob) } - try expectNoDifference(fileSystem.value.users(for: .fileURL), [.blob]) + try expectNoDifference(fileSystem.withLock(\.self).users(for: .fileURL), [.blob]) testScheduler.advance(by: .seconds(2)) $users.withLock { $0.append(.blobJr) } - try expectNoDifference(fileSystem.value.users(for: .fileURL), [.blob, .blobJr]) + try expectNoDifference(fileSystem.withLock(\.self).users(for: .fileURL), [.blob, .blobJr]) } } @@ -114,17 +113,17 @@ @Shared(.fileStorage(.anotherFileURL)) var otherUsers = [User]() $users.withLock { $0.append(.blob) } - try expectNoDifference(fileSystem.value.users(for: .fileURL), [.blob]) - try expectNoDifference(fileSystem.value.users(for: .anotherFileURL), nil) + try expectNoDifference(fileSystem.withLock(\.self).users(for: .fileURL), [.blob]) + try expectNoDifference(fileSystem.withLock(\.self).users(for: .anotherFileURL), nil) $otherUsers.withLock { $0.append(.blobJr) } - try expectNoDifference(fileSystem.value.users(for: .fileURL), [.blob]) - try expectNoDifference(fileSystem.value.users(for: .anotherFileURL), [.blobJr]) + try expectNoDifference(fileSystem.withLock(\.self).users(for: .fileURL), [.blob]) + try expectNoDifference(fileSystem.withLock(\.self).users(for: .anotherFileURL), [.blobJr]) } } @Test func initialValue() async throws { - let fileSystem = try LockIsolated<[URL: Data]>( + let fileSystem = Sharing.LockIsolated<[URL: Data]>( [.fileURL: try JSONEncoder().encode([User.blob])] ) try await withDependencies { @@ -133,12 +132,12 @@ @Shared(.fileStorage(.fileURL)) var users = [User]() _ = users await Task.yield() - try expectNoDifference(fileSystem.value.users(for: .fileURL), [.blob]) + try expectNoDifference(fileSystem.withLock(\.self).users(for: .fileURL), [.blob]) } } @Test func decodeFailure() async throws { - let fileSystem = LockIsolated<[URL: Data]>( + let fileSystem = Sharing.LockIsolated<[URL: Data]>( [.fileURL: Data("corrupt".utf8)] ) try withDependencies { @@ -488,7 +487,7 @@ extension FileStorage { fileprivate static func inMemory( - fileSystem: LockIsolated<[URL: Data]>, + fileSystem: Sharing.LockIsolated<[URL: Data]>, scheduler: S ) -> Self where S.SchedulerTimeType == DispatchQueue.SchedulerTimeType { .inMemory(