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
24 changes: 12 additions & 12 deletions Sources/ComposableArchitecture/Internal/DefaultSubscript.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
final class DefaultSubscript<Value>: Hashable {
var value: Value
final class DefaultSubscript<Value: Sendable>: Hashable, Sendable {
let wrappedValue: LockIsolated<Value>
Copy link
Member

Choose a reason for hiding this comment

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

We know we want to remove these helpers in the future, but since they're use by store.scope let's split off a non-sendable version for that.

init(_ value: Value) {
self.value = value
self.wrappedValue = LockIsolated(value)
}
static func == (lhs: DefaultSubscript, rhs: DefaultSubscript) -> Bool {
lhs === rhs
Expand All @@ -11,37 +11,37 @@ final class DefaultSubscript<Value>: Hashable {
}
}

extension Optional {
extension Optional where Wrapped: Sendable {
subscript(default defaultSubscript: DefaultSubscript<Wrapped>) -> Wrapped {
get { self ?? defaultSubscript.value }
get { self ?? defaultSubscript.wrappedValue.value }
set {
defaultSubscript.value = newValue
defaultSubscript.wrappedValue.withValue { $0 = newValue }
if self != nil { self = newValue }
}
}
}

extension RandomAccessCollection where Self: MutableCollection {
extension RandomAccessCollection where Self: MutableCollection, Element: Sendable {
subscript(
position: Index,
default defaultSubscript: DefaultSubscript<Element>
) -> Element {
get { self.indices.contains(position) ? self[position] : defaultSubscript.value }
get { self.indices.contains(position) ? self[position] : defaultSubscript.wrappedValue.value }
set {
defaultSubscript.value = newValue
defaultSubscript.wrappedValue.withValue { $0 = newValue }
if self.indices.contains(position) { self[position] = newValue }
}
}
}

extension _MutableIdentifiedCollection {
extension _MutableIdentifiedCollection where Element: Sendable {
subscript(
id id: ID,
default defaultSubscript: DefaultSubscript<Element>
) -> Element {
get { self[id: id] ?? defaultSubscript.value }
get { self[id: id] ?? defaultSubscript.wrappedValue.value }
set {
defaultSubscript.value = newValue
defaultSubscript.wrappedValue.withValue { $0 = newValue }
self[id: id] = newValue
}
}
Expand Down
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,135 +154,135 @@ 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
private let store: UncheckedSendable<UserDefaults>

public var id: AnyHashable {
AppStorageKeyID(key: self.key, store: self.store)
AppStorageKeyID(key: self.key, store: self.store.wrappedValue)
}

fileprivate init(_ key: String) where Value == Bool {
@Dependency(\.defaultAppStorage) var store
self.lookup = CastableLookup()
self.key = key
self.store = store
self.store = UncheckedSendable(store)
}

fileprivate init(_ key: String) where Value == Int {
@Dependency(\.defaultAppStorage) var store
self.lookup = CastableLookup()
self.key = key
self.store = store
self.store = UncheckedSendable(store)
}

fileprivate init(_ key: String) where Value == Double {
@Dependency(\.defaultAppStorage) var store
self.lookup = CastableLookup()
self.key = key
self.store = store
self.store = UncheckedSendable(store)
}

fileprivate init(_ key: String) where Value == String {
@Dependency(\.defaultAppStorage) var store
self.lookup = CastableLookup()
self.key = key
self.store = store
self.store = UncheckedSendable(store)
}

fileprivate init(_ key: String) where Value == URL {
@Dependency(\.defaultAppStorage) var store
self.lookup = URLLookup()
self.key = key
self.store = store
self.store = UncheckedSendable(store)
}

fileprivate init(_ key: String) where Value == Data {
@Dependency(\.defaultAppStorage) var store
self.lookup = CastableLookup()
self.key = key
self.store = store
self.store = UncheckedSendable(store)
}

fileprivate init(_ key: String) where Value: RawRepresentable<Int> {
@Dependency(\.defaultAppStorage) var store
self.lookup = RawRepresentableLookup(base: CastableLookup())
self.key = key
self.store = store
self.store = UncheckedSendable(store)
}

fileprivate init(_ key: String) where Value: RawRepresentable<String> {
@Dependency(\.defaultAppStorage) var store
self.lookup = RawRepresentableLookup(base: CastableLookup())
self.key = key
self.store = store
self.store = UncheckedSendable(store)
}

fileprivate init(_ key: String) where Value == Bool? {
@Dependency(\.defaultAppStorage) var store
self.lookup = OptionalLookup(base: CastableLookup())
self.key = key
self.store = store
self.store = UncheckedSendable(store)
}

fileprivate init(_ key: String) where Value == Int? {
@Dependency(\.defaultAppStorage) var store
self.lookup = OptionalLookup(base: CastableLookup())
self.key = key
self.store = store
self.store = UncheckedSendable(store)
}

fileprivate init(_ key: String) where Value == Double? {
@Dependency(\.defaultAppStorage) var store
self.lookup = OptionalLookup(base: CastableLookup())
self.key = key
self.store = store
self.store = UncheckedSendable(store)
}

fileprivate init(_ key: String) where Value == String? {
@Dependency(\.defaultAppStorage) var store
self.lookup = OptionalLookup(base: CastableLookup())
self.key = key
self.store = store
self.store = UncheckedSendable(store)
}

fileprivate init(_ key: String) where Value == URL? {
@Dependency(\.defaultAppStorage) var store
self.lookup = OptionalLookup(base: URLLookup())
self.key = key
self.store = store
self.store = UncheckedSendable(store)
}

fileprivate init(_ key: String) where Value == Data? {
@Dependency(\.defaultAppStorage) var store
self.lookup = OptionalLookup(base: CastableLookup())
self.key = key
self.store = store
self.store = UncheckedSendable(store)
}

fileprivate init<R: RawRepresentable<Int>>(_ key: String) where Value == R? {
@Dependency(\.defaultAppStorage) var store
self.lookup = OptionalLookup(base: RawRepresentableLookup(base: CastableLookup()))
self.key = key
self.store = store
self.store = UncheckedSendable(store)
}

fileprivate init<R: RawRepresentable<String>>(_ key: String) where Value == R? {
@Dependency(\.defaultAppStorage) var store
self.lookup = OptionalLookup(base: RawRepresentableLookup(base: CastableLookup()))
self.key = key
self.store = store
self.store = UncheckedSendable(store)
}
}

extension AppStorageKey: PersistenceKey {
public func load(initialValue: Value?) -> Value? {
self.lookup.loadValue(from: self.store, at: self.key, default: initialValue)
self.lookup.loadValue(from: self.store.wrappedValue, at: self.key, default: initialValue)
}

public func save(_ value: Value) {
self.lookup.saveValue(value, to: self.store, at: self.key)
self.lookup.saveValue(value, to: self.store.wrappedValue, at: self.key)
}

public func subscribe(
Expand All @@ -292,7 +292,7 @@ extension AppStorageKey: PersistenceKey {
let previousValue = LockIsolated(initialValue)
let userDefaultsDidChange = NotificationCenter.default.addObserver(
forName: UserDefaults.didChangeNotification,
object: self.store,
object: self.store.wrappedValue,
queue: nil
) { _ in
let newValue = load(initialValue: initialValue)
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,33 @@ 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
Loading
Loading