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 all commits
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
2 changes: 1 addition & 1 deletion Sources/ComposableArchitecture/Effect.swift
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,7 @@ extension Effect {
/// - Returns: A publisher that uses the provided closure to map elements from the upstream effect
/// to new elements that it then publishes.
@inlinable
public func map<T>(_ transform: @escaping (Action) -> T) -> Effect<T> {
public func map<T>(_ transform: @escaping @Sendable (Action) -> T) -> Effect<T> {
switch self.operation {
case .none:
return .none
Expand Down
42 changes: 21 additions & 21 deletions Sources/ComposableArchitecture/Internal/Create.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

import Combine
@preconcurrency import Combine
import Darwin

final class DemandBuffer<S: Subscriber>: @unchecked Sendable {
Expand Down Expand Up @@ -105,25 +105,25 @@ final class DemandBuffer<S: Subscriber>: @unchecked Sendable {

extension AnyPublisher where Failure == Never {
private init(
_ callback: @escaping (Effect<Output>.Subscriber) -> Cancellable
_ callback: @escaping @Sendable (Effect<Output>.Subscriber) -> Cancellable
) {
self = Publishers.Create(callback: callback).eraseToAnyPublisher()
}

static func create(
_ factory: @escaping (Effect<Output>.Subscriber) -> Cancellable
_ factory: @escaping @Sendable (Effect<Output>.Subscriber) -> Cancellable
) -> AnyPublisher<Output, Failure> {
AnyPublisher(factory)
}
}

extension Publishers {
fileprivate class Create<Output>: Publisher {
fileprivate final class Create<Output>: Publisher, Sendable {
typealias Failure = Never

private let callback: (Effect<Output>.Subscriber) -> Cancellable
private let callback: @Sendable (Effect<Output>.Subscriber) -> Cancellable

init(callback: @escaping (Effect<Output>.Subscriber) -> Cancellable) {
init(callback: @escaping @Sendable (Effect<Output>.Subscriber) -> Cancellable) {
self.callback = callback
}

Expand All @@ -134,33 +134,33 @@ extension Publishers {
}

extension Publishers.Create {
fileprivate final class Subscription<Downstream: Subscriber>: Combine.Subscription
fileprivate final class Subscription<Downstream: Subscriber>: Combine.Subscription, Sendable
where Downstream.Input == Output, Downstream.Failure == Never {
private let buffer: DemandBuffer<Downstream>
private var cancellable: Cancellable?
private let cancellable = LockIsolated<Cancellable?>(nil)

init(
callback: @escaping (Effect<Output>.Subscriber) -> Cancellable,
callback: @escaping @Sendable (Effect<Output>.Subscriber) -> Cancellable,
downstream: Downstream
) {
self.buffer = DemandBuffer(subscriber: downstream)

let cancellable = callback(
.init(
send: { [weak self] in _ = self?.buffer.buffer(value: $0) },
complete: { [weak self] in self?.buffer.complete(completion: $0) }
self.cancellable.setValue(
callback(
.init(
send: { [weak self] in _ = self?.buffer.buffer(value: $0) },
complete: { [weak self] in self?.buffer.complete(completion: $0) }
)
)
)

self.cancellable = cancellable
}

func request(_ demand: Subscribers.Demand) {
_ = self.buffer.demand(demand)
}

func cancel() {
self.cancellable?.cancel()
self.cancellable.value?.cancel()
}
}
}
Expand All @@ -172,13 +172,13 @@ extension Publishers.Create.Subscription: CustomStringConvertible {
}

extension Effect {
struct Subscriber {
private let _send: (Action) -> Void
private let _complete: (Subscribers.Completion<Never>) -> Void
struct Subscriber: Sendable {
private let _send: @Sendable (Action) -> Void
private let _complete: @Sendable (Subscribers.Completion<Never>) -> Void

init(
send: @escaping (Action) -> Void,
complete: @escaping (Subscribers.Completion<Never>) -> Void
send: @escaping @Sendable (Action) -> Void,
complete: @escaping @Sendable (Subscribers.Completion<Never>) -> Void
) {
self._send = send
self._complete = complete
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
extension Effect {
@_spi(Internals)
public var actions: AsyncStream<Action> {
@preconcurrency import Combine

extension Effect where Action: Sendable {
@_spi(Internals) public var actions: AsyncStream<Action> {
switch self.operation {
case .none:
return .finished
Expand Down
2 changes: 1 addition & 1 deletion Sources/ComposableArchitecture/Internal/NavigationID.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ struct NavigationID: Hashable, @unchecked Sendable {
self.identifier = AnyHashableSendable(id)
}

init<Value, Root, ID: Hashable>(
init<Value, Root, ID: Hashable & Sendable>(
id: ID,
keyPath: KeyPath<Root, IdentifiedArray<ID, Value>>
) {
Expand Down
2 changes: 1 addition & 1 deletion Sources/ComposableArchitecture/Macros.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public macro Reducer(state: _SynthesizedConformance..., action: _SynthesizedConf
///
/// See <doc:Reducers#Synthesizing-protocol-conformances-on-State-and-Action> for more information.
@_documentation(visibility: public)
public struct _SynthesizedConformance {}
public struct _SynthesizedConformance: Sendable {}

extension _SynthesizedConformance {
/// Extends the `State` or `Action` types that ``Reducer()`` creates with the `Codable`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,7 @@ public struct _ForEachReducer<
return self.element
.dependency(\.navigationIDPath, elementNavigationID)
.reduce(into: &state[keyPath: self.toElementsState][id: id]!, action: elementAction)
.map { self.toElementAction.embed((id, $0)) }
.map { [toElementAction] in toElementAction.embed((id, $0)) }
._cancellable(id: navigationID, navigationIDPath: self.navigationIDPath)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ public struct _IfCaseLetReducer<Parent: Reducer, Child: Reducer>: Reducer {
return self.child
.dependency(\.navigationIDPath, newNavigationID)
.reduce(into: &childState, action: childAction)
.map { self.toChildAction.embed($0) }
.map { [toChildAction] in toChildAction.embed($0) }
.cancellable(id: childID)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,7 @@ public struct _IfLetReducer<Parent: Reducer, Child: Reducer>: Reducer {
return self.child
.dependency(\.navigationIDPath, self.navigationIDPath.appending(navigationID))
.reduce(into: &state[keyPath: self.toChildState]!, action: childAction)
.map { self.toChildAction.embed($0) }
.map { [toChildAction] in toChildAction.embed($0) }
._cancellable(id: navigationID, navigationIDPath: self.navigationIDPath)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -607,7 +607,7 @@ public struct _PresentationReducer<Base: Reducer, Destination: Reducer>: Reducer
.reduce(
into: &state[keyPath: self.toPresentationState].wrappedValue!, action: destinationAction
)
.map { self.toPresentationAction.embed(.presented($0)) }
.map { [toPresentationAction] in toPresentationAction.embed(.presented($0)) }
._cancellable(navigationIDPath: destinationNavigationIDPath)
baseEffects = self.base.reduce(into: &state, action: action)
if let ephemeralType = ephemeralType(of: destinationState),
Expand Down
4 changes: 2 additions & 2 deletions Sources/ComposableArchitecture/Reducer/Reducers/Scope.swift
Original file line number Diff line number Diff line change
Expand Up @@ -379,12 +379,12 @@ public struct Scope<ParentState, ParentAction, Child: Reducer>: Reducer {

return self.child
.reduce(into: &childState, action: childAction)
.map { self.toChildAction.embed($0) }
.map { [toChildAction] in toChildAction.embed($0) }

case let .keyPath(toChildState):
return self.child
.reduce(into: &state[keyPath: toChildState], action: childAction)
.map { self.toChildAction.embed($0) }
.map { [toChildAction] in toChildAction.embed($0) }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -517,7 +517,7 @@ public struct _StackReducer<Base: Reducer, Destination: Reducer>: Reducer {
into: &state[keyPath: self.toStackState][id: elementID]!,
action: destinationAction
)
.map { toStackAction.embed(.element(id: elementID, action: $0)) }
.map { [toStackAction] in toStackAction.embed(.element(id: elementID, action: $0)) }
._cancellable(navigationIDPath: elementNavigationIDPath)
} else {
reportIssue(
Expand Down
2 changes: 1 addition & 1 deletion Sources/ComposableArchitecture/TestStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1917,7 +1917,7 @@ extension TestStore where State: Equatable {
/// expected.
@_disfavoredOverload
@available(iOS 16, macOS 13, tvOS 16, watchOS 9, *)
public func receive<Value: Equatable>(
public func receive<Value: Equatable & Sendable>(
_ actionCase: _CaseKeyPath<Action, Value>,
_ value: Value,
timeout duration: Duration,
Expand Down
41 changes: 18 additions & 23 deletions Sources/ComposableArchitecture/ViewStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -399,31 +399,26 @@ public final class ViewStore<ViewState, ViewAction>: ObservableObject {
/// - Parameter predicate: A predicate on `ViewState` that determines for how long this method
/// should suspend.
public func yield(while predicate: @escaping (_ state: ViewState) -> Bool) async {
if #available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) {
_ = await self.publisher
.values
.first(where: { !predicate($0) })
} else {
let cancellable = Box<AnyCancellable?>(wrappedValue: nil)
try? await withTaskCancellationHandler {
try Task.checkCancellation()
try await withUnsafeThrowingContinuation {
(continuation: UnsafeContinuation<Void, Error>) in
guard !Task.isCancelled else {
continuation.resume(throwing: CancellationError())
return
}
cancellable.wrappedValue = self.publisher
.filter { !predicate($0) }
.prefix(1)
.sink { _ in
continuation.resume()
_ = cancellable
}
let isolatedCancellable = LockIsolated<AnyCancellable?>(nil)
try? await withTaskCancellationHandler {
try Task.checkCancellation()
try await withUnsafeThrowingContinuation {
(continuation: UnsafeContinuation<Void, Error>) in
guard !Task.isCancelled else {
continuation.resume(throwing: CancellationError())
return
}
} onCancel: {
cancellable.wrappedValue?.cancel()
let cancellable = self.publisher
.filter { !predicate($0) }
.prefix(1)
.sink { _ in
continuation.resume()
_ = isolatedCancellable
}
isolatedCancellable.setValue(cancellable)
}
} onCancel: {
isolatedCancellable.value?.cancel()
}
}

Expand Down
Loading