Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Address @Shared sendability. #3329

Merged
merged 16 commits into from
Sep 6, 2024
2 changes: 1 addition & 1 deletion Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@
{
"identity" : "swift-docc-symbolkit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-docc-symbolkit",
"location" : "https://github.com/swiftlang/swift-docc-symbolkit",
"state" : {
"revision" : "b45d1f2ed151d057b54504d653e0da5552844e34",
"version" : "1.0.0"
Expand Down
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ let package = Package(
)

#if compiler(>=6)
for target in package.targets where target.type != .system {
for target in package.targets where target.type != .system && target.type != .test {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added this temporarily so that we don't get flooded with warnings in test targets, but maybe we should keep this around until we've addressed all/most warnings in TCA proper, and then we can later concentrate on tests?

target.swiftSettings = target.swiftSettings ?? []
target.swiftSettings?.append(contentsOf: [
.enableExperimentalFeature("StrictConcurrency"),
Expand Down
7 changes: 7 additions & 0 deletions Sources/ComposableArchitecture/Internal/AnySendable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
struct AnySendable: @unchecked Sendable {
let base: Any
@inlinable
init<Base: Sendable>(_ base: Base) {
self.base = base
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
///
/// See the article <doc:SharingState> for more information, in particular the
/// <doc:SharingState#Custom-persistence> section.
public protocol PersistenceReaderKey<Value> {
public protocol PersistenceReaderKey<Value>: Sendable {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Persistence keys need to be sendable because they are captured in some LockIsolated's, which currently require Sendable values and @Sendable transaction closures. If we weaken that, then perhaps this can be weakened too?

/// A type that can be loaded or subscribed to in an external system.
associatedtype Value
associatedtype Value: Sendable
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Value is captured in a variety of sendable contexts, such as in the subscription endpoint for persistence strategies. So I think this does really need to be full blown Sendable.


/// A type representing the hashable identity of a persistence key.
associatedtype ID: Hashable = Self
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ extension PersistenceReaderKey {
/// A type defining a user defaults persistence strategy.
///
/// See ``PersistenceReaderKey/appStorage(_:)-4l5b`` to create values of this type.
public struct AppStorageKey<Value> {
public struct AppStorageKey<Value: Sendable>: Sendable {
private let lookup: any Lookup<Value>
private let key: String
private let store: UserDefaults
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically UserDefaults is not Sendable even though the docs specifically say "User defaults is thread-safe". So, it should be ok to make this UncheckedSendable, but it was strangely causing some test failures. So I need to look at this again.

Expand Down Expand Up @@ -362,13 +362,13 @@ private enum SharedAppStorageLocals {
@TaskLocal static var isSetting = false
}

private protocol Lookup<Value> {
associatedtype Value
private protocol Lookup<Value>: Sendable {
associatedtype Value: Sendable
func loadValue(from store: UserDefaults, at key: String, default defaultValue: Value?) -> Value?
func saveValue(_ newValue: Value, to store: UserDefaults, at key: String)
}

private struct CastableLookup<Value>: Lookup {
private struct CastableLookup<Value: Sendable>: Lookup, Sendable {
func loadValue(
from store: UserDefaults,
at key: String,
Expand Down Expand Up @@ -421,7 +421,7 @@ private struct URLLookup: Lookup {
}
}

private struct RawRepresentableLookup<Value: RawRepresentable, Base: Lookup>: Lookup
private struct RawRepresentableLookup<Value: RawRepresentable & Sendable, Base: Lookup>: Lookup, Sendable
where Value.RawValue == Base.Value {
let base: Base
func loadValue(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ extension PersistenceReaderKey {
/// - Parameter key: A string key identifying a value to share in memory.
/// - Returns: A persistence key.
public static func appStorage<Value>(
_ keyPath: ReferenceWritableKeyPath<UserDefaults, Value>
_ keyPath: _ReferenceWritableKeyPath<UserDefaults, Value>
) -> Self where Self == AppStorageKeyPathKey<Value> {
AppStorageKeyPathKey(keyPath)
}
Expand All @@ -24,33 +24,34 @@ extension PersistenceReaderKey {
/// A type defining a user defaults persistence strategy via key path.
///
/// See ``PersistenceReaderKey/appStorage(_:)-5jsie`` to create values of this type.
public struct AppStorageKeyPathKey<Value> {
private let keyPath: ReferenceWritableKeyPath<UserDefaults, Value>
private let store: UserDefaults
public struct AppStorageKeyPathKey<Value: Sendable>: Sendable {

private let keyPath: _ReferenceWritableKeyPath<UserDefaults, Value>
private let store: UncheckedSendable<UserDefaults>

public init(_ keyPath: ReferenceWritableKeyPath<UserDefaults, Value>) {
public init(_ keyPath: _ReferenceWritableKeyPath<UserDefaults, Value>) {
@Dependency(\.defaultAppStorage) var store
self.keyPath = keyPath
self.store = store
self.store = UncheckedSendable(store)
}
}

extension AppStorageKeyPathKey: PersistenceKey, Hashable {
public func load(initialValue _: Value?) -> Value? {
self.store[keyPath: self.keyPath]
self.store.wrappedValue[keyPath: self.keyPath]
}

public func save(_ newValue: Value) {
SharedAppStorageLocals.$isSetting.withValue(true) {
self.store[keyPath: self.keyPath] = newValue
self.store.wrappedValue[keyPath: self.keyPath] = newValue
}
}

public func subscribe(
initialValue: Value?,
didSet: @escaping @Sendable (_ newValue: Value?) -> Void
) -> Shared<Value>.Subscription {
let observer = self.store.observe(self.keyPath, options: .new) { _, change in
let observer = self.store.wrappedValue.observe(self.keyPath, options: .new) { _, change in
guard
!SharedAppStorageLocals.isSetting
else { return }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,6 @@ public final class FileStorageKey<Value: Sendable>: PersistenceKey, Sendable {
private let decode: @Sendable (Data) throws -> Value
private let encode: @Sendable (Value) throws -> Data
fileprivate let state = LockIsolated(State())
// private let value = LockIsolated<Value?>(nil)
// private let workItem = LockIsolated<DispatchWorkItem?>(nil)

fileprivate struct State {
var value: Value?
Expand Down Expand Up @@ -262,7 +260,7 @@ public struct FileStorage: Hashable, Sendable {
let createDirectory: @Sendable (URL, Bool) throws -> Void
let fileExists: @Sendable (URL) -> Bool
let fileSystemSource:
@Sendable (URL, DispatchSource.FileSystemEvent, @escaping () -> Void) -> AnyCancellable
@Sendable (URL, DispatchSource.FileSystemEvent, @escaping @Sendable () -> Void) -> AnyCancellable
let load: @Sendable (URL) throws -> Data
@_spi(Internals) public let save: @Sendable (Data, URL) throws -> Void

Expand All @@ -271,7 +269,7 @@ public struct FileStorage: Hashable, Sendable {
///
/// This is the version of the ``Dependencies/DependencyValues/defaultFileStorage`` dependency
/// that is used by default when running your app in the simulator or on device.
public static var fileSystem = fileSystem(
public static let fileSystem = fileSystem(
queue: DispatchQueue(label: "co.pointfree.ComposableArchitecture.FileStorage")
)

Expand Down Expand Up @@ -345,9 +343,9 @@ public struct FileStorage: Hashable, Sendable {
)
}

fileprivate struct Handler: Hashable {
fileprivate struct Handler: Hashable, Sendable {
let id = UUID()
let operation: () -> Void
let operation: @Sendable () -> Void
static func == (lhs: Self, rhs: Self) -> Bool {
lhs.id == rhs.id
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ extension PersistenceReaderKey {
/// A type defining an in-memory persistence strategy
///
/// See ``PersistenceReaderKey/inMemory(_:)`` to create values of this type.
public struct InMemoryKey<Value>: PersistenceKey, Sendable {
public struct InMemoryKey<Value: Sendable>: PersistenceKey, Sendable {
private let key: String
private let store: InMemoryStorage
fileprivate init(_ key: String) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@
/// ```
public struct PersistenceKeyDefault<Base: PersistenceReaderKey>: PersistenceReaderKey {
let base: Base
let defaultValue: () -> Base.Value
let defaultValue: @Sendable () -> Base.Value

public init(_ key: Base, _ value: @autoclosure @escaping () -> Base.Value) {
public init(_ key: Base, _ value: @autoclosure @escaping @Sendable () -> Base.Value) {
self.base = key
self.defaultValue = value
}
Expand Down
4 changes: 2 additions & 2 deletions Sources/ComposableArchitecture/SharedState/Reference.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
import Combine
#endif

protocol Reference<Value>: AnyObject, CustomStringConvertible {
associatedtype Value
protocol Reference<Value>: AnyObject, CustomStringConvertible, Sendable {
associatedtype Value: Sendable
var value: Value { get set }

func access()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ extension Shared {
/// - persistenceKey: A persistence key associated with the shared reference. It is responsible
/// for loading and saving the shared reference's value from some external source.
public init(
wrappedValue value: @autoclosure @escaping () -> Value,
wrappedValue value: @autoclosure @escaping @Sendable () -> Value,
_ persistenceKey: some PersistenceKey<Value>,
fileID: StaticString = #fileID,
line: UInt = #line
Expand Down Expand Up @@ -93,7 +93,7 @@ extension Shared {
}

private init(
throwingValue value: @autoclosure @escaping () throws -> Value,
throwingValue value: @autoclosure @escaping @Sendable () throws -> Value,
_ persistenceKey: some PersistenceKey<Value>,
fileID: StaticString = #fileID,
line: UInt = #line
Expand Down Expand Up @@ -154,7 +154,7 @@ extension Shared {
/// - persistenceKey: A persistence key associated with the shared reference. It is responsible
/// for loading and saving the shared reference's value from some external source.
public init<Key: PersistenceKey<Value>>(
wrappedValue value: @autoclosure @escaping () -> Value,
wrappedValue value: @autoclosure @escaping @Sendable () -> Value,
_ persistenceKey: PersistenceKeyDefault<Key>,
fileID: StaticString = #fileID,
line: UInt = #line
Expand All @@ -177,7 +177,7 @@ extension SharedReader {
/// - persistenceKey: A persistence key associated with the shared reference. It is responsible
/// for loading the shared reference's value from some external source.
public init(
wrappedValue value: @autoclosure @escaping () -> Value,
wrappedValue value: @autoclosure @escaping @Sendable () -> Value,
_ persistenceKey: some PersistenceReaderKey<Value>,
fileID: StaticString = #fileID,
line: UInt = #line
Expand Down Expand Up @@ -252,7 +252,7 @@ extension SharedReader {
}

private init(
throwingValue value: @autoclosure @escaping () throws -> Value,
throwingValue value: @autoclosure @escaping @Sendable () throws -> Value,
_ persistenceKey: some PersistenceReaderKey<Value>,
fileID: StaticString = #fileID,
line: UInt = #line
Expand Down Expand Up @@ -312,7 +312,7 @@ extension SharedReader {
/// - persistenceKey: A persistence key associated with the shared reference. It is responsible
/// for loading the shared reference's value from some external source.
public init<Key: PersistenceReaderKey<Value>>(
wrappedValue value: @autoclosure @escaping () -> Value,
wrappedValue value: @autoclosure @escaping @Sendable () -> Value,
_ persistenceKey: PersistenceKeyDefault<Key>,
fileID: StaticString = #fileID,
line: UInt = #line
Expand All @@ -328,15 +328,15 @@ extension SharedReader {

private struct LoadError: Error {}

final class ValueReference<Value, Persistence: PersistenceReaderKey<Value>>: Reference, @unchecked
Sendable
final class ValueReference<Value, Persistence: PersistenceReaderKey<Value>>: Reference,
@unchecked Sendable
{
private let lock = NSRecursiveLock()
private let persistenceKey: Persistence?
#if canImport(Combine)
private let subject: CurrentValueRelay<Value>
#endif
private var subscription: Shared<Value>.Subscription?
private var subscription: Shared<Value>.Subscription!
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we'd prefer for this to be a let so that we don't have to do @unchecked Sendable above, but we need lazy initialization. And to make that clear I went ahead and made it an implicitly unwrapped optional.

private var _value: Value {
willSet {
self.subject.send(newValue)
Expand Down
16 changes: 9 additions & 7 deletions Sources/ComposableArchitecture/SharedState/Shared.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import IssueReporting
/// wrapper.
@dynamicMemberLookup
@propertyWrapper
public struct Shared<Value> {
public struct Shared<Value: Sendable> {
private let reference: any Reference
private let keyPath: AnyKeyPath
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically this should now be:

public struct Shared<Value: Sendable> {

…and we should drop the conditional conformance below. But there are some weird interactions with _AnyKeyPath. Let's chat about this today.


Expand Down Expand Up @@ -275,7 +275,7 @@ public struct Shared<Value> {

private var snapshot: Value? {
get {
func open<Root>(_ reference: some Reference<Root>) -> Value? {
func open<Root: Sendable>(_ reference: some Reference<Root>) -> Value? {
@Dependency(\.sharedChangeTracker) var changeTracker
return changeTracker?[reference]?.snapshot[
keyPath: unsafeDowncast(self.keyPath, to: WritableKeyPath<Root, Value>.self)
Expand All @@ -284,7 +284,7 @@ public struct Shared<Value> {
return open(self.reference)
}
nonmutating set {
func open<Root>(_ reference: some Reference<Root>) {
func open<Root: Sendable>(_ reference: some Reference<Root>) {
@Dependency(\.sharedChangeTracker) var changeTracker
guard let newValue else {
changeTracker?[reference] = nil
Expand Down Expand Up @@ -344,7 +344,10 @@ extension Shared: _CustomDiffObject {
}

extension Shared
where Value: _MutableIdentifiedCollection {
where
Value: _MutableIdentifiedCollection,
Value.Element: Sendable
{
/// Allows a `ForEach` view to transform a shared collection into shared elements.
///
/// ```swift
Expand Down Expand Up @@ -408,11 +411,10 @@ extension Shared: MutableCollection
where Value: MutableCollection & RandomAccessCollection, Value.Index: Hashable {
public subscript(position: Value.Index) -> Shared<Value.Element> {
get {
assertionFailure("Conformance of 'Shared<Value>' to 'MutableCollection' is unavailable.")
return self[position, default: DefaultSubscript(self.wrappedValue[position])]
fatalError("Conformance of 'Shared<Value>' to 'MutableCollection' is unavailable.")
}
set {
self._wrappedValue[position] = newValue.wrappedValue
fatalError("Conformance of 'Shared<Value>' to 'MutableCollection' is unavailable.")
Comment on lines +426 to +429
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This subscript is unavailable so it should be ok to fatalError. If we don't fatal error, then we get sendability warnings that can't be fixed because you can't conditionally conform to a protocol with a constraint against a marker protocol, like Sendable.

}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ extension Change {
}
}

struct AnyChange<Value>: Change {
struct AnyChange<Value: Sendable>: Change, Sendable {
let reference: any Reference<Value>
var snapshot: Value

Expand All @@ -58,7 +58,7 @@ struct AnyChange<Value>: Change {

@_spi(Internals)
public final class SharedChangeTracker: Sendable {
let changes: LockIsolated<[ObjectIdentifier: Any]> = LockIsolated([:])
let changes: LockIsolated<[ObjectIdentifier: AnySendable]> = LockIsolated([:])
var hasChanges: Bool { !self.changes.isEmpty }
@_spi(Internals) public init() {}
func resetChanges() { self.changes.withValue { $0.removeAll() } }
Expand All @@ -70,17 +70,17 @@ public final class SharedChangeTracker: Sendable {
}
self.resetChanges()
}
func track<Value>(_ reference: some Reference<Value>) {
func track<Value: Sendable>(_ reference: some Reference<Value>) {
if !self.changes.keys.contains(ObjectIdentifier(reference)) {
self.changes.withValue { $0[ObjectIdentifier(reference)] = AnyChange(reference) }
self.changes.withValue { $0[ObjectIdentifier(reference)] = AnySendable(AnyChange(reference)) }
}
}
subscript<Value>(_ reference: some Reference<Value>) -> AnyChange<Value>? {
_read { yield self.changes[ObjectIdentifier(reference)] as? AnyChange<Value> }
_read { yield self.changes[ObjectIdentifier(reference)]?.base as? AnyChange<Value> }
_modify {
var change = self.changes[ObjectIdentifier(reference)] as? AnyChange<Value>
var change = self.changes[ObjectIdentifier(reference)]?.base as? AnyChange<Value>
yield &change
self.changes.withValue { [change] in $0[ObjectIdentifier(reference)] = change }
self.changes.withValue { [change] in $0[ObjectIdentifier(reference)] = AnySendable(change) }
}
}
func track<R>(_ operation: () throws -> R) rethrows -> R {
Expand Down
10 changes: 7 additions & 3 deletions Sources/ComposableArchitecture/SharedState/SharedReader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
/// wrapper, in particular <doc:SharingState#Read-only-shared-state>.
@dynamicMemberLookup
@propertyWrapper
public struct SharedReader<Value> {
public struct SharedReader<Value: Sendable> {
fileprivate let reference: any Reference
fileprivate let keyPath: AnyKeyPath

Expand Down Expand Up @@ -47,7 +47,7 @@ public struct SharedReader<Value> {
else { return nil }
self.init(
reference: base.reference,
keyPath: base.keyPath.appending(path: \Value?.[default:DefaultSubscript(initialValue)])!
keyPath: base.keyPath.appending(path: \Value?.[default: DefaultSubscript(initialValue)])!
)
}

Expand Down Expand Up @@ -183,7 +183,11 @@ extension SharedReader: CustomDumpRepresentable {
}

extension SharedReader
where Value: RandomAccessCollection & MutableCollection, Value.Index: Hashable & Sendable {
where
Value: RandomAccessCollection & MutableCollection,
Value.Index: Hashable & Sendable,
Value.Element: Sendable
{
/// Derives a collection of read-only shared elements from a read-only shared collection of
/// elements.
///
Expand Down
Loading