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 37 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
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ final class WebSocketTests: XCTestCase {
$0.continuousClock = clock
$0.webSocket.open = { @Sendable _, _, _ in actions.stream }
$0.webSocket.receive = { @Sendable _ in try await Task.never() }
$0.webSocket.sendPing = { @Sendable @MainActor _ in pingsCount += 1 }
$0.webSocket.sendPing = { @MainActor @Sendable _ in pingsCount += 1 }
}

// Connect to the socket
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ final class RecursionTests: XCTestCase {
$0.rows.append(Nested.State(id: UUID(0)))
}

await store.send(\.rows[id:UUID(0)].addRowButtonTapped) {
await store.send(\.rows[id: UUID(0)].addRowButtonTapped) {
$0.rows[id: UUID(0)]?.rows.append(Nested.State(id: UUID(1)))
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,20 +33,20 @@ final class ReusableComponentsFavoritingTests: XCTestCase {
)
}

await store.send(\.episodes[id:UUID(0)].favorite.buttonTapped) {
await store.send(\.episodes[id: UUID(0)].favorite.buttonTapped) {
$0.episodes[id: UUID(0)]?.isFavorite = true
}
await clock.advance(by: .seconds(1))
await store.receive(\.episodes[id:episodes[0].id].favorite.response.success)
await store.receive(\.episodes[id: episodes[0].id].favorite.response.success)

await store.send(\.episodes[id:episodes[1].id].favorite.buttonTapped) {
await store.send(\.episodes[id: episodes[1].id].favorite.buttonTapped) {
$0.episodes[id: UUID(1)]?.isFavorite = true
}
await store.send(\.episodes[id:episodes[1].id].favorite.buttonTapped) {
await store.send(\.episodes[id: episodes[1].id].favorite.buttonTapped) {
$0.episodes[id: UUID(1)]?.isFavorite = false
}
await clock.advance(by: .seconds(1))
await store.receive(\.episodes[id:episodes[1].id].favorite.response.success)
await store.receive(\.episodes[id: episodes[1].id].favorite.response.success)
}

func testUnhappyPath() async {
Expand All @@ -61,17 +61,17 @@ final class ReusableComponentsFavoritingTests: XCTestCase {
Episodes(favorite: { _, _ in throw FavoriteError() })
}

await store.send(\.episodes[id:UUID(0)].favorite.buttonTapped) {
await store.send(\.episodes[id: UUID(0)].favorite.buttonTapped) {
$0.episodes[id: UUID(0)]?.isFavorite = true
}

await store.receive(\.episodes[id:episodes[0].id].favorite.response.failure) {
await store.receive(\.episodes[id: episodes[0].id].favorite.response.failure) {
$0.episodes[id: UUID(0)]?.alert = AlertState {
TextState("Favoriting failed.")
}
}

await store.send(\.episodes[id:UUID(0)].favorite.alert.dismiss) {
await store.send(\.episodes[id: UUID(0)].favorite.alert.dismiss) {
$0.episodes[id: UUID(0)]?.alert = nil
$0.episodes[id: UUID(0)]?.isFavorite = false
}
Expand Down
2 changes: 1 addition & 1 deletion Examples/CaseStudies/UIKitCaseStudies/ListsOfState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ final class CountersTableViewController: UITableViewController {

override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let id = store.counters[indexPath.row].id
if let store = store.scope(state: \.counters[id:id], action: \.counters[id:id]) {
if let store = store.scope(state: \.counters[id: id], action: \.counters[id: id]) {
navigationController?.pushViewController(CounterViewController(store: store), animated: true)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,24 +30,24 @@ final class UIKitCaseStudiesTests: XCTestCase {
CounterList()
}

await store.send(\.counters[id:firstState.id].incrementButtonTapped) {
await store.send(\.counters[id: firstState.id].incrementButtonTapped) {
$0.counters[id: firstState.id]?.count = 1
}
await store.send(\.counters[id:firstState.id].decrementButtonTapped) {
await store.send(\.counters[id: firstState.id].decrementButtonTapped) {
$0.counters[id: firstState.id]?.count = 0
}

await store.send(\.counters[id:secondState.id].incrementButtonTapped) {
await store.send(\.counters[id: secondState.id].incrementButtonTapped) {
$0.counters[id: secondState.id]?.count = 1
}
await store.send(\.counters[id:secondState.id].decrementButtonTapped) {
await store.send(\.counters[id: secondState.id].decrementButtonTapped) {
$0.counters[id: secondState.id]?.count = 0
}

await store.send(\.counters[id:thirdState.id].incrementButtonTapped) {
await store.send(\.counters[id: thirdState.id].incrementButtonTapped) {
$0.counters[id: thirdState.id]?.count = 1
}
await store.send(\.counters[id:thirdState.id].decrementButtonTapped) {
await store.send(\.counters[id: thirdState.id].decrementButtonTapped) {
$0.counters[id: thirdState.id]?.count = 0
}
}
Expand Down
12 changes: 6 additions & 6 deletions Examples/SyncUps/SyncUpsTests/AppFeatureTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,18 @@ final class AppFeatureTests: XCTestCase {
$0.path[id: 0] = .detail(SyncUpDetail.State(syncUp: sharedSyncUp))
}

await store.send(\.path[id:0].detail.editButtonTapped) {
await store.send(\.path[id: 0].detail.editButtonTapped) {
$0.path[id: 0]?.modify(\.detail) { $0.destination = .edit(SyncUpForm.State(syncUp: syncUp)) }
}

syncUp.title = "Blob"
await store.send(\.path[id:0].detail.destination.edit.binding.syncUp, syncUp) {
await store.send(\.path[id: 0].detail.destination.edit.binding.syncUp, syncUp) {
$0.path[id: 0]?.modify(\.detail) {
$0.destination?.modify(\.edit) { $0.syncUp.title = "Blob" }
}
}

await store.send(\.path[id:0].detail.doneEditingButtonTapped) {
await store.send(\.path[id: 0].detail.doneEditingButtonTapped) {
$0.path[id: 0]?.modify(\.detail) {
$0.destination = nil
$0.syncUp.title = "Blob"
Expand All @@ -50,11 +50,11 @@ final class AppFeatureTests: XCTestCase {
$0.path[id: 0] = .detail(SyncUpDetail.State(syncUp: sharedSyncUp))
}

await store.send(\.path[id:0].detail.deleteButtonTapped) {
await store.send(\.path[id: 0].detail.deleteButtonTapped) {
$0.path[id: 0]?.modify(\.detail) { $0.destination = .alert(.deleteSyncUp) }
}

await store.send(\.path[id:0].detail.destination.alert.confirmDeletion) {
await store.send(\.path[id: 0].detail.destination.alert.confirmDeletion) {
$0.path[id: 0]?.modify(\.detail) { $0.destination = nil }
$0.syncUpsList.syncUps = []
}
Expand Down Expand Up @@ -104,7 +104,7 @@ final class AppFeatureTests: XCTestCase {
}

await store.withExhaustivity(.off) {
await store.send(\.path[id:1].record.onTask)
await store.send(\.path[id: 1].record.onTask)
await store.receive(\.path.popFrom) {
XCTAssertEqual($0.path.count, 1)
}
Expand Down
10 changes: 5 additions & 5 deletions Examples/Todos/TodosTests/TodosTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ final class TodosTests: XCTestCase {
Todos()
}

await store.send(\.todos[id:UUID(0)].binding.description, "Learn Composable Architecture") {
await store.send(\.todos[id: UUID(0)].binding.description, "Learn Composable Architecture") {
$0.todos[id: UUID(0)]?.description = "Learn Composable Architecture"
}
}
Expand All @@ -82,7 +82,7 @@ final class TodosTests: XCTestCase {
$0.continuousClock = clock
}

await store.send(\.todos[id:UUID(0)].binding.isComplete, true) {
await store.send(\.todos[id: UUID(0)].binding.isComplete, true) {
$0.todos[id: UUID(0)]?.isComplete = true
}
await clock.advance(by: .seconds(1))
Expand Down Expand Up @@ -116,11 +116,11 @@ final class TodosTests: XCTestCase {
$0.continuousClock = clock
}

await store.send(\.todos[id:UUID(0)].binding.isComplete, true) {
await store.send(\.todos[id: UUID(0)].binding.isComplete, true) {
$0.todos[id: UUID(0)]?.isComplete = true
}
await clock.advance(by: .milliseconds(500))
await store.send(\.todos[id:UUID(0)].binding.isComplete, false) {
await store.send(\.todos[id: UUID(0)].binding.isComplete, false) {
$0.todos[id: UUID(0)]?.isComplete = false
}
await clock.advance(by: .seconds(1))
Expand Down Expand Up @@ -336,7 +336,7 @@ final class TodosTests: XCTestCase {
await store.send(\.binding.filter, .completed) {
$0.filter = .completed
}
await store.send(\.todos[id:UUID(1)].binding.description, "Did this already") {
await store.send(\.todos[id: UUID(1)].binding.description, "Did this already") {
$0.todos[id: UUID(1)]?.description = "Did this already"
}
}
Expand Down
38 changes: 19 additions & 19 deletions Examples/VoiceMemos/VoiceMemosTests/VoiceMemosTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,25 +61,25 @@ final class VoiceMemosTests: XCTestCase {
)
]
}
await store.send(\.voiceMemos[id:deadbeefURL].playButtonTapped) {
await store.send(\.voiceMemos[id: deadbeefURL].playButtonTapped) {
$0.voiceMemos[id: deadbeefURL]?.mode = .playing(progress: 0)
}
await store.receive(\.voiceMemos[id:deadbeefURL].delegate.playbackStarted)
await store.receive(\.voiceMemos[id: deadbeefURL].delegate.playbackStarted)
await clock.run()

await store.receive(\.voiceMemos[id:deadbeefURL].timerUpdated) {
await store.receive(\.voiceMemos[id: deadbeefURL].timerUpdated) {
$0.voiceMemos[id: deadbeefURL]?.mode = .playing(progress: 0.2)
}
await store.receive(\.voiceMemos[id:deadbeefURL].timerUpdated) {
await store.receive(\.voiceMemos[id: deadbeefURL].timerUpdated) {
$0.voiceMemos[id: deadbeefURL]?.mode = .playing(progress: 0.4)
}
await store.receive(\.voiceMemos[id:deadbeefURL].timerUpdated) {
await store.receive(\.voiceMemos[id: deadbeefURL].timerUpdated) {
$0.voiceMemos[id: deadbeefURL]?.mode = .playing(progress: 0.6)
}
await store.receive(\.voiceMemos[id:deadbeefURL].timerUpdated) {
await store.receive(\.voiceMemos[id: deadbeefURL].timerUpdated) {
$0.voiceMemos[id: deadbeefURL]?.mode = .playing(progress: 0.8)
}
await store.receive(\.voiceMemos[id:deadbeefURL].audioPlayerClient.success) {
await store.receive(\.voiceMemos[id: deadbeefURL].audioPlayerClient.success) {
$0.voiceMemos[id: deadbeefURL]?.mode = .notPlaying
}
}
Expand Down Expand Up @@ -260,20 +260,20 @@ final class VoiceMemosTests: XCTestCase {
$0.continuousClock = clock
}

await store.send(\.voiceMemos[id:url].playButtonTapped) {
await store.send(\.voiceMemos[id: url].playButtonTapped) {
$0.voiceMemos[id: url]?.mode = .playing(progress: 0)
}
await store.receive(\.voiceMemos[id:url].delegate.playbackStarted)
await store.receive(\.voiceMemos[id: url].delegate.playbackStarted)
await clock.advance(by: .milliseconds(500))
await store.receive(\.voiceMemos[id:url].timerUpdated) {
await store.receive(\.voiceMemos[id: url].timerUpdated) {
$0.voiceMemos[id: url]?.mode = .playing(progress: 0.4)
}
await clock.advance(by: .milliseconds(500))
await store.receive(\.voiceMemos[id:url].timerUpdated) {
await store.receive(\.voiceMemos[id: url].timerUpdated) {
$0.voiceMemos[id: url]?.mode = .playing(progress: 0.8)
}
await clock.advance(by: .milliseconds(250))
await store.receive(\.voiceMemos[id:url].audioPlayerClient.success) {
await store.receive(\.voiceMemos[id: url].audioPlayerClient.success) {
$0.voiceMemos[id: url]?.mode = .notPlaying
}
}
Expand Down Expand Up @@ -302,14 +302,14 @@ final class VoiceMemosTests: XCTestCase {
$0.continuousClock = clock
}

let task = await store.send(\.voiceMemos[id:url].playButtonTapped) {
let task = await store.send(\.voiceMemos[id: url].playButtonTapped) {
$0.voiceMemos[id: url]?.mode = .playing(progress: 0)
}
await store.receive(\.voiceMemos[id:url].delegate.playbackStarted)
await store.receive(\.voiceMemos[id:url].audioPlayerClient.failure) {
await store.receive(\.voiceMemos[id: url].delegate.playbackStarted)
await store.receive(\.voiceMemos[id: url].audioPlayerClient.failure) {
$0.voiceMemos[id: url]?.mode = .notPlaying
}
await store.receive(\.voiceMemos[id:url].delegate.playbackFailed) {
await store.receive(\.voiceMemos[id: url].delegate.playbackFailed) {
$0.alert = AlertState { TextState("Voice memo playback failed.") }
}
await task.cancel()
Expand All @@ -333,7 +333,7 @@ final class VoiceMemosTests: XCTestCase {
VoiceMemos()
}

await store.send(\.voiceMemos[id:url].playButtonTapped) {
await store.send(\.voiceMemos[id: url].playButtonTapped) {
$0.voiceMemos[id: url]?.mode = .notPlaying
}
}
Expand Down Expand Up @@ -435,10 +435,10 @@ final class VoiceMemosTests: XCTestCase {
$0.continuousClock = clock
}

await store.send(\.voiceMemos[id:url].playButtonTapped) {
await store.send(\.voiceMemos[id: url].playButtonTapped) {
$0.voiceMemos[id: url]?.mode = .playing(progress: 0)
}
await store.receive(\.voiceMemos[id:url].delegate.playbackStarted)
await store.receive(\.voiceMemos[id: url].delegate.playbackStarted)
await store.send(.onDelete([0])) {
$0.voiceMemos = []
}
Expand Down
3 changes: 2 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,8 @@ let package = Package(
for target in package.targets where target.type != .system {
target.swiftSettings = target.swiftSettings ?? []
target.swiftSettings?.append(contentsOf: [
.enableExperimentalFeature("StrictConcurrency")
.enableExperimentalFeature("StrictConcurrency"),
.enableUpcomingFeature("InferSendableFromCaptures"),
])
}
#endif
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
Loading