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

Sendable miscellany: effects, publishers, etc. #3317

Merged
merged 38 commits into from
Aug 29, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
709eaf8
`@preconcurrency @MainActor` isolation of `Store`
stephencelis Aug 10, 2024
9add3a1
Remove unneeded `@MainActor`s
stephencelis Aug 10, 2024
1b053fe
Remove thread checking code
stephencelis Aug 10, 2024
fcd9c8c
Remove unneeded `@MainActor`s
stephencelis Aug 10, 2024
d3440f3
Swift 5.10 compatibility fixes
stephencelis Aug 10, 2024
67772a2
wip
stephencelis Aug 10, 2024
1ab386d
More 5.10 fixes
stephencelis Aug 10, 2024
1e23bca
wip
mbrandonw Aug 10, 2024
fe7bd98
fixes
mbrandonw Aug 10, 2024
9972512
wip
stephencelis Aug 11, 2024
f2d710b
wip
stephencelis Aug 11, 2024
2033a26
up the timeout
mbrandonw Aug 11, 2024
24312e5
wip
stephencelis Aug 12, 2024
dab61ef
Merge remote-tracking branch 'origin/main' into main-actor-preconcurr…
stephencelis Aug 12, 2024
73e12ca
Merge remote-tracking branch 'origin/main' into main-actor-preconcurr…
stephencelis Aug 13, 2024
02622e5
Fixes
stephencelis Aug 13, 2024
a9e8ebd
wip
stephencelis Aug 12, 2024
1cad10b
wip
stephencelis Aug 12, 2024
cd3a014
wip
stephencelis Aug 13, 2024
b25b608
wip
stephencelis Aug 27, 2024
f487678
Merge remote-tracking branch 'origin/main' into key-path-sendability
stephencelis Aug 27, 2024
273fd57
wip
stephencelis Aug 27, 2024
aefc73a
Merge branch 'main' into key-path-sendability
stephencelis Aug 28, 2024
7fd9436
Fix binding action sendability
stephencelis Aug 28, 2024
a4a00e4
Address more binding action sendability
stephencelis Aug 28, 2024
b85004b
more bindable action sendability
stephencelis Aug 28, 2024
c07b02d
more bindable action warnings
stephencelis Aug 28, 2024
de6b901
fix
stephencelis Aug 28, 2024
a25b469
Make `Effect.map` sendable
stephencelis Aug 28, 2024
5268ca1
Make `Effect.actions` sendable
stephencelis Aug 28, 2024
bafeab2
Make `AnyPublisher.create` sendable
stephencelis Aug 28, 2024
6234b4f
Make `_SynthesizedConformance` sendable
stephencelis Aug 28, 2024
ec4183f
Avoid non-sendable captures of `self` in reducers
stephencelis Aug 28, 2024
7eae6be
Make `ViewStore.yield` sendable
stephencelis Aug 28, 2024
a25ed28
Address internal sendability warning
stephencelis Aug 28, 2024
2faac88
fix
stephencelis Aug 28, 2024
1606c50
Another small warning
stephencelis Aug 28, 2024
6e5fa33
Merge branch 'main' into sendable-misc
stephencelis Aug 29, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
@preconcurrency @MainActor isolation of Store
  • Loading branch information
stephencelis committed Aug 10, 2024
commit 709eaf8f4a6ffd51fc308c9120e2619ce0172997
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-dependencies",
"state" : {
"revision" : "cc26d06125dbc913c6d9e8a905a5db0b994509e0",
"version" : "1.3.5"
"revision" : "d7472be6b3c89251ce4c0db07d32405b43426781",
"version" : "1.3.7"
}
},
{
Expand Down Expand Up @@ -113,8 +113,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-macro-testing",
"state" : {
"revision" : "a35257b7e9ce44e92636447003a8eeefb77b145c",
"version" : "0.5.1"
"revision" : "20c1a8f3b624fb5d1503eadcaa84743050c350f4",
"version" : "0.5.2"
}
},
{
Expand All @@ -131,8 +131,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-snapshot-testing.git",
"state" : {
"revision" : "c097f955b4e724690f0fc8ffb7a6d4b881c9c4e3",
"version" : "1.17.2"
"revision" : "6d932a79e7173b275b96c600c86c603cf84f153c",
"version" : "1.17.4"
}
},
{
Expand Down
18 changes: 17 additions & 1 deletion Sources/ComposableArchitecture/Internal/DispatchQueue.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,18 @@ func mainActorASAP(execute block: @escaping @MainActor @Sendable () -> Void) {
}
}

func mainActorNow(execute block: @escaping @MainActor @Sendable () -> Void) {
if DispatchQueue.getSpecific(key: key) == value {
assumeMainActorIsolated {
block()
}
} else {
DispatchQueue.main.sync {
block()
}
}
}

private let key: DispatchSpecificKey<UInt8> = {
let key = DispatchSpecificKey<UInt8>()
DispatchQueue.main.setSpecific(key: key, value: value)
Expand All @@ -21,7 +33,11 @@ private let value: UInt8 = 0

// NB: Currently we can't use 'MainActor.assumeIsolated' on CI, but we can approximate this in
// the meantime.
@MainActor(unsafe)
#if swift(<5.10)
@MainActor(unsafe)
#else
@preconcurrency @MainActor
#endif
private func assumeMainActorIsolated(_ block: @escaping @MainActor @Sendable () -> Void) {
block()
}
11 changes: 6 additions & 5 deletions Sources/ComposableArchitecture/Observation/ViewAction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@ public protocol ViewAction<ViewAction> {
}

/// A type that represents a view with a ``Store`` that can send ``ViewAction``s.
#if swift(<5.10)
@MainActor(unsafe)
#else
@preconcurrency @MainActor
#endif
public protocol ViewActionSending<StoreState, StoreAction> {
associatedtype StoreState
associatedtype StoreAction: ViewAction
#if swift(>=5.10)
@MainActor @preconcurrency var store: Store<StoreState, StoreAction> { get }
#else
@MainActor(unsafe) var store: Store<StoreState, StoreAction> { get }
#endif
var store: Store<StoreState, StoreAction> { get }
}

extension ViewActionSending {
Expand Down
1 change: 1 addition & 0 deletions Sources/ComposableArchitecture/RootStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import Combine
import Foundation

@_spi(Internals)
@MainActor
public final class RootStore {
private var bufferedActions: [Any] = []
let didSet = CurrentValueRelay(())
Expand Down
7 changes: 6 additions & 1 deletion Sources/ComposableArchitecture/Store.swift
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,11 @@ import SwiftUI
/// case. Further, all actions sent to the store and all scopes (see ``scope(state:action:)-90255``)
/// of the store are also checked to make sure that work is performed on the main thread.
@dynamicMemberLookup
#if swift(<5.10)
@MainActor(unsafe)
#else
@preconcurrency @MainActor
#endif
public final class Store<State, Action> {
var canCacheChildren = true
private var children: [ScopeID<State, Action>: AnyObject] = [:]
Expand Down Expand Up @@ -462,7 +467,7 @@ public final class Store<State, Action> {
}

extension Store: CustomDebugStringConvertible {
public var debugDescription: String {
public nonisolated var debugDescription: String {
storeTypeName(of: self)
}
}
Expand Down
5 changes: 5 additions & 0 deletions Sources/ComposableArchitecture/SwiftUI/Binding.swift
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,11 @@ where Value: CustomDebugStringConvertible {
/// Read <doc:Bindings> for more information.
@dynamicMemberLookup
@propertyWrapper
#if swift(<5.10)
@MainActor(unsafe)
#else
@preconcurrency @MainActor
#endif
public struct BindingViewStore<State> {
let store: Store<State, BindingAction<State>>
#if DEBUG
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,11 @@ public struct AnyIdentifiable: Identifiable {
}
}

#if swift(<5.10)
@MainActor(unsafe)
#else
@preconcurrency @MainActor
#endif
@_spi(Presentation)
public struct DestinationContent<State, Action> {
let store: Store<State?, Action>
Expand Down
49 changes: 15 additions & 34 deletions Sources/ComposableArchitecture/TestStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,9 @@ import IssueReporting
/// One can assert against its behavior over time:
///
/// ```swift
/// @MainActor
/// class CounterTests: XCTestCase {
/// func testCounter() async {
/// let store = TestStore(
/// let store = await TestStore(
/// // Given: a counter state of 0
/// initialState: Counter.State(count: 0),
/// ) {
Expand Down Expand Up @@ -423,6 +422,11 @@ import IssueReporting
/// [merowing.info]: https://www.merowing.info
/// [exhaustive-testing-in-tca]: https://www.merowing.info/exhaustive-testing-in-tca/
/// [Composable-Architecture-at-Scale]: https://vimeo.com/751173570
#if swift(<5.10)
@MainActor(unsafe)
#else
@preconcurrency @MainActor
#endif
public final class TestStore<State, Action> {

/// The current dependencies of the test store.
Expand Down Expand Up @@ -553,7 +557,6 @@ public final class TestStore<State, Action> {
///
/// - Parameter duration: The amount of time to wait before asserting.
@available(iOS 16, macOS 13, tvOS 16, watchOS 9, *)
@MainActor
public func finish(
timeout duration: Duration,
fileID: StaticString = #fileID,
Expand All @@ -574,7 +577,6 @@ public final class TestStore<State, Action> {
///
/// - Parameter nanoseconds: The amount of time to wait before asserting.
@_disfavoredOverload
@MainActor
public func finish(
timeout nanoseconds: UInt64? = nil,
fileID: StaticString = #fileID,
Expand Down Expand Up @@ -624,8 +626,8 @@ public final class TestStore<State, Action> {
}

deinit {
self.completed()
uncheckedUseMainSerialExecutor = self.originalUseMainSerialExecutor
mainActorNow { self.completed() }
}

func completed() {
Expand Down Expand Up @@ -759,14 +761,13 @@ public final class TestStore<State, Action> {
/// - updateValuesForOperation: A closure for updating the store's dependency values for the
/// duration of the operation.
/// - operation: The operation.
@MainActor
public func withDependencies<R>(
_ updateValuesForOperation: (_ dependencies: inout DependencyValues) async throws -> Void,
operation: @MainActor () async throws -> R
_ updateValuesForOperation: (_ dependencies: inout DependencyValues) throws -> Void,
operation: () async throws -> R
) async rethrows -> R {
let previous = self.dependencies
defer { self.dependencies = previous }
try await updateValuesForOperation(&self.dependencies)
try updateValuesForOperation(&self.dependencies)
return try await operation()
}

Expand All @@ -790,10 +791,9 @@ public final class TestStore<State, Action> {
/// - Parameters:
/// - exhaustivity: The exhaustivity.
/// - operation: The operation.
@MainActor
public func withExhaustivity<R>(
_ exhaustivity: Exhaustivity,
operation: @MainActor () async throws -> R
operation: () async throws -> R
) async rethrows -> R {
let previous = self.exhaustivity
defer { self.exhaustivity = previous }
Expand Down Expand Up @@ -838,7 +838,6 @@ extension TestStore where State: Equatable {
/// immediately after awaiting `store.send`:
///
/// ```swift
/// @MainActor
/// func testAnalytics() async {
/// let events = LockIsolated<[String]>([])
/// let analytics = AnalyticsClient(
Expand All @@ -847,7 +846,7 @@ extension TestStore where State: Equatable {
/// }
/// )
///
/// let store = TestStore(initialState: Feature.State()) {
/// let store = await TestStore(initialState: Feature.State()) {
/// Feature()
/// } withDependencies {
/// $0.analytics = analytics
Expand Down Expand Up @@ -893,7 +892,6 @@ extension TestStore where State: Equatable {
/// expected.
/// - Returns: A ``TestStoreTask`` that represents the lifecycle of the effect executed when
/// sending the action.
@MainActor
@discardableResult
public func send(
_ action: Action,
Expand Down Expand Up @@ -1024,7 +1022,6 @@ extension TestStore where State: Equatable {
/// - Parameters:
/// - updateStateToExpectedResult: A closure that asserts against the current state of the test
/// store.
@MainActor
public func assert(
_ updateStateToExpectedResult: @escaping (_ state: inout State) throws -> Void,
fileID: StaticString = #fileID,
Expand Down Expand Up @@ -1293,7 +1290,6 @@ extension TestStore where State: Equatable, Action: Equatable {
/// of the store after processing the given action. Do not provide a closure if no change
/// is expected.
@available(iOS 16, macOS 13, tvOS 16, watchOS 9, *)
@MainActor
public func receive(
_ expectedAction: Action,
timeout duration: Duration,
Expand Down Expand Up @@ -1342,7 +1338,6 @@ extension TestStore where State: Equatable, Action: Equatable {
/// the store. The mutable state sent to this closure must be modified to match the state of
/// the store after processing the given action. Do not provide a closure if no change is
/// expected.
@MainActor
@_disfavoredOverload
public func receive(
_ expectedAction: Action,
Expand Down Expand Up @@ -1508,9 +1503,8 @@ extension TestStore where State: Equatable {
/// to the store. The mutable state sent to this closure must be modified to match the state
/// of the store after processing the given action. Do not provide a closure if no change is
/// expected.
@available(iOS 16, macOS 13, tvOS 16, watchOS 9, *)
@MainActor
@_disfavoredOverload
@available(iOS 16, macOS 13, tvOS 16, watchOS 9, *)
public func receive(
_ isMatching: (_ action: Action) -> Bool,
timeout duration: Duration,
Expand Down Expand Up @@ -1563,7 +1557,6 @@ extension TestStore where State: Equatable {
/// the store. The mutable state sent to this closure must be modified to match the state of
/// the store after processing the given action. Do not provide a closure if no change is
/// expected.
@MainActor
@_disfavoredOverload
public func receive(
_ isMatching: (_ action: Action) -> Bool,
Expand Down Expand Up @@ -1641,7 +1634,6 @@ extension TestStore where State: Equatable {
/// the store. The mutable state sent to this closure must be modified to match the state of
/// the store after processing the given action. Do not provide a closure if no change is
/// expected.
@MainActor
@_disfavoredOverload
public func receive<Value>(
_ actionCase: CaseKeyPath<Action, Value>,
Expand Down Expand Up @@ -1687,7 +1679,6 @@ extension TestStore where State: Equatable {
/// to the store. The mutable state sent to this closure must be modified to match the state
/// of the store after processing the given action. Do not provide a closure if no change is
/// expected.
@MainActor
@_disfavoredOverload
public func receive<Value: Equatable>(
_ actionCase: CaseKeyPath<Action, Value>,
Expand Down Expand Up @@ -1764,7 +1755,6 @@ extension TestStore where State: Equatable {
message:
"Use the version of this operator with case key paths, instead. See the following migration guide for more information: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.4#Using-case-key-paths"
)
@MainActor
@_disfavoredOverload
public func receive<Value>(
_ actionCase: AnyCasePath<Action, Value>,
Expand Down Expand Up @@ -1842,7 +1832,6 @@ extension TestStore where State: Equatable {
/// to the store. The mutable state sent to this closure must be modified to match the state
/// of the store after processing the given action. Do not provide a closure if no change is
/// expected.
@MainActor
@_disfavoredOverload
@available(iOS 16, macOS 13, tvOS 16, watchOS 9, *)
public func receive<Value>(
Expand Down Expand Up @@ -1889,7 +1878,6 @@ extension TestStore where State: Equatable {
/// to the store. The mutable state sent to this closure must be modified to match the state
/// of the store after processing the given action. Do not provide a closure if no change is
/// expected.
@MainActor
@_disfavoredOverload
@available(iOS 16, macOS 13, tvOS 16, watchOS 9, *)
public func receive<Value: Equatable>(
Expand Down Expand Up @@ -1919,7 +1907,6 @@ extension TestStore where State: Equatable {
)
}

@MainActor
@_disfavoredOverload
@available(
iOS,
Expand Down Expand Up @@ -2103,7 +2090,6 @@ extension TestStore where State: Equatable {
self.reducer.state = state
}

@MainActor
private func receiveAction(
matching predicate: (Action) -> Bool,
timeout nanoseconds: UInt64?,
Expand Down Expand Up @@ -2196,9 +2182,8 @@ extension TestStore where State: Equatable {
/// expected.
/// - Returns: A ``TestStoreTask`` that represents the lifecycle of the effect executed when
/// sending the action.
@MainActor
@discardableResult
@_disfavoredOverload
@discardableResult
public func send(
_ action: CaseKeyPath<Action, Void>,
assert updateStateToExpectedResult: ((_ state: inout State) throws -> Void)? = nil,
Expand Down Expand Up @@ -2243,9 +2228,8 @@ extension TestStore where State: Equatable {
/// expected.
/// - Returns: A ``TestStoreTask`` that represents the lifecycle of the effect executed when
/// sending the action.
@MainActor
@discardableResult
@_disfavoredOverload
@discardableResult
public func send<Value>(
_ action: CaseKeyPath<Action, Value>,
_ value: Value,
Expand Down Expand Up @@ -2288,7 +2272,6 @@ extension TestStore {
///
/// - Parameter strict: When `true` and there are no in-flight actions to cancel, a test failure
/// will be reported.
@MainActor
public func skipReceivedActions(
strict: Bool = true,
fileID: StaticString = #fileID,
Expand Down Expand Up @@ -2369,7 +2352,6 @@ extension TestStore {
///
/// - Parameter strict: When `true` and there are no in-flight actions to cancel, a test failure
/// will be reported.
@MainActor
public func skipInFlightEffects(
strict: Bool = true,
fileID: StaticString = #fileID,
Expand Down Expand Up @@ -2872,7 +2854,6 @@ public enum Exhaustivity: Equatable, Sendable {
}

extension TestStore {
@MainActor
@available(
*,
unavailable,
Expand Down
Loading