Skip to content

Commit 7b053fa

Browse files
stephencelismluisbrown
authored andcommitted
Rename Effect<Output, _> to Effect<Action, _> (#1362)
* Rename Effect<Output, _> -> Effect<Action, _> We'll keep the typealias for the `Publisher` conformance, but given the changes made to TCA for concurrency, `Effect` should typically only be used these days in reducers to feed actions back into the store, and not more generally as publishers of any output. * wip (cherry picked from commit 26f9ed286d4b65aee1a949f09a38650eaa61ab8e) # Conflicts: # Sources/ComposableArchitecture/Debugging/ReducerInstrumentation.swift # Sources/ComposableArchitecture/Effect.swift # Sources/ComposableArchitecture/Effects/Cancellation.swift # Sources/ComposableArchitecture/Effects/SignalProducer.swift # Sources/ComposableArchitecture/Effects/Throttling.swift # Sources/ComposableArchitecture/Effects/Timer.swift # Sources/ComposableArchitecture/Internal/Create.swift # Sources/ComposableArchitecture/Internal/Deprecations.swift
1 parent a414901 commit 7b053fa

File tree

10 files changed

+387
-388
lines changed

10 files changed

+387
-388
lines changed

Sources/ComposableArchitecture/Debugging/ReducerInstrumentation.swift

Lines changed: 84 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,120 +1,120 @@
11
#if canImport(os)
2-
import os.signpost
2+
import os.signpost
33

4-
extension Reducer {
5-
/// Instruments the reducer with
6-
/// [signposts](https://developer.apple.com/documentation/os/logging/recording_performance_data).
7-
/// Each invocation of the reducer will be measured by an interval, and the lifecycle of its
8-
/// effects will be measured with interval and event signposts.
9-
///
10-
/// To use, build your app for Instruments (⌘I), create a blank instrument, and then use the "+"
11-
/// icon at top right to add the signpost instrument. Start recording your app (red button at top
12-
/// left) and then you should see timing information for every action sent to the store and every
13-
/// effect executed.
14-
///
15-
/// Effect instrumentation can be particularly useful for inspecting the lifecycle of long-living
16-
/// effects. For example, if you start an effect (e.g. a location manager) in `onAppear` and
17-
/// forget to tear down the effect in `onDisappear`, it will clearly show in Instruments that the
18-
/// effect never completed.
19-
///
20-
/// - Parameters:
21-
/// - prefix: A string to print at the beginning of the formatted message for the signpost.
22-
/// - log: An `OSLog` to use for signposts.
23-
/// - Returns: A reducer that has been enhanced with instrumentation.
24-
public func signpost(
25-
_ prefix: String = "",
26-
log: OSLog = OSLog(
27-
subsystem: "co.pointfree.composable-architecture",
28-
category: "Reducer Instrumentation"
29-
)
30-
) -> Self {
31-
guard log.signpostsEnabled else { return self }
4+
extension Reducer {
5+
/// Instruments the reducer with
6+
/// [signposts](https://developer.apple.com/documentation/os/logging/recording_performance_data).
7+
/// Each invocation of the reducer will be measured by an interval, and the lifecycle of its
8+
/// effects will be measured with interval and event signposts.
9+
///
10+
/// To use, build your app for Instruments (⌘I), create a blank instrument, and then use the "+"
11+
/// icon at top right to add the signpost instrument. Start recording your app (red button at top
12+
/// left) and then you should see timing information for every action sent to the store and every
13+
/// effect executed.
14+
///
15+
/// Effect instrumentation can be particularly useful for inspecting the lifecycle of long-living
16+
/// effects. For example, if you start an effect (e.g. a location manager) in `onAppear` and
17+
/// forget to tear down the effect in `onDisappear`, it will clearly show in Instruments that the
18+
/// effect never completed.
19+
///
20+
/// - Parameters:
21+
/// - prefix: A string to print at the beginning of the formatted message for the signpost.
22+
/// - log: An `OSLog` to use for signposts.
23+
/// - Returns: A reducer that has been enhanced with instrumentation.
24+
public func signpost(
25+
_ prefix: String = "",
26+
log: OSLog = OSLog(
27+
subsystem: "co.pointfree.composable-architecture",
28+
category: "Reducer Instrumentation"
29+
)
30+
) -> Self {
31+
guard log.signpostsEnabled else { return self }
3232

33-
// NB: Prevent rendering as "N/A" in Instruments
34-
let zeroWidthSpace = "\u{200B}"
33+
// NB: Prevent rendering as "N/A" in Instruments
34+
let zeroWidthSpace = "\u{200B}"
3535

36-
let prefix = prefix.isEmpty ? zeroWidthSpace : "[\(prefix)] "
36+
let prefix = prefix.isEmpty ? zeroWidthSpace : "[\(prefix)] "
3737

38-
return Self { state, action, environment in
39-
var actionOutput: String!
40-
if log.signpostsEnabled {
41-
actionOutput = debugCaseOutput(action)
42-
os_signpost(.begin, log: log, name: "Action", "%s%s", prefix, actionOutput)
43-
}
44-
let effects = self.run(&state, action, environment)
45-
if log.signpostsEnabled {
46-
os_signpost(.end, log: log, name: "Action")
47-
return
48-
effects
49-
.effectSignpost(prefix, log: log, actionOutput: actionOutput)
50-
}
51-
return effects
38+
return Self { state, action, environment in
39+
var actionOutput: String!
40+
if log.signpostsEnabled {
41+
actionOutput = debugCaseOutput(action)
42+
os_signpost(.begin, log: log, name: "Action", "%s%s", prefix, actionOutput)
5243
}
44+
let effects = self.run(&state, action, environment)
45+
if log.signpostsEnabled {
46+
os_signpost(.end, log: log, name: "Action")
47+
return
48+
effects
49+
.effectSignpost(prefix, log: log, actionOutput: actionOutput)
50+
}
51+
return effects
5352
}
5453
}
54+
}
5555

56-
extension Effect where Failure == Never {
57-
func effectSignpost(
58-
_ prefix: String,
59-
log: OSLog,
60-
actionOutput: String
61-
) -> Self {
62-
let sid = OSSignpostID(log: log)
56+
extension Effect where Failure == Never {
57+
func effectSignpost(
58+
_ prefix: String,
59+
log: OSLog,
60+
actionOutput: String
61+
) -> Self {
62+
let sid = OSSignpostID(log: log)
6363

64-
switch self.operation {
65-
case .none:
66-
return self
64+
switch self.operation {
65+
case .none:
66+
return self
6767
case let .producer(producer):
68-
return .init(
68+
return .init(
6969
operation: .producer(
7070
producer
7171
.on(
7272
starting: {
73-
os_signpost(
74-
.begin, log: log, name: "Effect", signpostID: sid, "%sStarted from %s", prefix,
73+
os_signpost(
74+
.begin, log: log, name: "Effect", signpostID: sid, "%sStarted from %s", prefix,
7575
actionOutput
7676
)
7777
},
7878
completed: {
7979
os_signpost(.end, log: log, name: "Effect", signpostID: sid, "%sFinished", prefix)
80-
},
80+
},
8181
disposed: {
82-
os_signpost(
82+
os_signpost(
8383
.end, log: log, name: "Effect", signpostID: sid, "%sCancelled", prefix)
84-
},
84+
},
8585
value: { value in
8686
os_signpost(
8787
.event, log: log, name: "Effect Output", "%sOutput from %s", prefix,
8888
actionOutput
8989
)
90-
}
91-
)
90+
}
9291
)
9392
)
94-
case let .run(priority, operation):
95-
return .init(
96-
operation: .run(priority) { send in
97-
os_signpost(
98-
.begin, log: log, name: "Effect", signpostID: sid, "%sStarted from %s", prefix,
99-
actionOutput
100-
)
101-
await operation(
102-
Send { output in
103-
os_signpost(
104-
.event, log: log, name: "Effect Output", "%sOutput from %s", prefix, actionOutput
105-
)
106-
send(output)
107-
}
108-
)
109-
if Task.isCancelled {
110-
os_signpost(.end, log: log, name: "Effect", signpostID: sid, "%sCancelled", prefix)
93+
)
94+
case let .run(priority, operation):
95+
return .init(
96+
operation: .run(priority) { send in
97+
os_signpost(
98+
.begin, log: log, name: "Effect", signpostID: sid, "%sStarted from %s", prefix,
99+
actionOutput
100+
)
101+
await operation(
102+
Send { action in
103+
os_signpost(
104+
.event, log: log, name: "Effect Output", "%sOutput from %s", prefix, actionOutput
105+
)
106+
send(action)
111107
}
112-
os_signpost(.end, log: log, name: "Effect", signpostID: sid, "%sFinished", prefix)
108+
)
109+
if Task.isCancelled {
110+
os_signpost(.end, log: log, name: "Effect", signpostID: sid, "%sCancelled", prefix)
113111
}
114-
)
115-
}
112+
os_signpost(.end, log: log, name: "Effect", signpostID: sid, "%sFinished", prefix)
113+
}
114+
)
116115
}
117116
}
117+
}
118118
#endif
119119

120120
func debugCaseOutput(_ value: Any) -> String {

Sources/ComposableArchitecture/Documentation.docc/Articles/Performance.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -205,10 +205,10 @@ store.
205205

206206
Reducers are run on the main thread and so they are not appropriate for performing intense CPU
207207
work. If you need to perform lots of CPU-bound work, then it is more appropriate to use an
208-
``Effect``, which will operate in the cooperative thread pool, and then send it's output back into
209-
the system via an action. You should also make sure to perform your CPU intensive work in a
210-
cooperative manner by periodically suspending with `Task.yield()` so that you do not block a thread
211-
in the cooperative pool for too long.
208+
``Effect``, which will operate in the cooperative thread pool, and then send actions back into the
209+
system. You should also make sure to perform your CPU intensive work in a cooperative manner by
210+
periodically suspending with `Task.yield()` so that you do not block a thread in the cooperative
211+
pool for too long.
212212

213213
So, instead of performing intense work like this in your reducer:
214214

Sources/ComposableArchitecture/Documentation.docc/Extensions/EffectDeprecations.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ Avoid using deprecated APIs in your app. Select a method to see the replacement
2828

2929
### Combine Integration
3030

31+
- ``Effect/Output``
3132
- ``Effect/init(_:)``
3233
- ``Effect/init(value:)``
3334
- ``Effect/init(error:)``

Sources/ComposableArchitecture/Effect.swift

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import XCTestDynamicOverlay
77
#endif
88

99
/// The ``Effect`` type encapsulates a unit of work that can be run in the outside world, and can
10-
/// feed data back to the ``Store``. It is the perfect place to do side effects, such as network
10+
/// feed actions back to the ``Store``. It is the perfect place to do side effects, such as network
1111
/// requests, saving/loading from disk, creating timers, interacting with dependencies, and more.
1212
///
1313
/// Effects are returned from reducers so that the ``Store`` can perform the effects after the
@@ -34,12 +34,12 @@ import XCTestDynamicOverlay
3434
/// > This is only an issue if using the ReactiveSwift interface of ``Effect`` as mentioned above.
3535
/// If you you are using Swift's concurrency tools and the `.task`, `.run` and `.fireAndForget`
3636
/// functions on ``Effect``, then threading is automatically handled for you.
37-
public struct Effect<Output, Failure: Error> {
37+
public struct Effect<Action, Failure: Error> {
3838
@usableFromInline
3939
enum Operation {
4040
case none
41-
case producer(SignalProducer<Output, Failure>)
42-
case run(TaskPriority? = nil, @Sendable (Send<Output>) async -> Void)
41+
case producer(SignalProducer<Action, Failure>)
42+
case run(TaskPriority? = nil, @Sendable (Send<Action>) async -> Void)
4343
}
4444

4545
@usableFromInline
@@ -117,8 +117,8 @@ extension Effect where Failure == Never {
117117
/// - Returns: An effect wrapping the given asynchronous work.
118118
public static func task(
119119
priority: TaskPriority? = nil,
120-
operation: @escaping @Sendable () async throws -> Output,
121-
catch handler: (@Sendable (Error) async -> Output)? = nil,
120+
operation: @escaping @Sendable () async throws -> Action,
121+
catch handler: (@Sendable (Error) async -> Action)? = nil,
122122
file: StaticString = #file,
123123
fileID: StaticString = #fileID,
124124
line: UInt = #line
@@ -173,7 +173,7 @@ extension Effect where Failure == Never {
173173
/// }
174174
/// ```
175175
///
176-
/// Then you could attach to it in a `run` effect by using `for await` and sending each output of
176+
/// Then you could attach to it in a `run` effect by using `for await` and sending each action of
177177
/// the stream back into the system:
178178
///
179179
/// ```swift
@@ -201,8 +201,8 @@ extension Effect where Failure == Never {
201201
/// - Returns: An effect wrapping the given asynchronous work.
202202
public static func run(
203203
priority: TaskPriority? = nil,
204-
operation: @escaping @Sendable (Send<Output>) async throws -> Void,
205-
catch handler: (@Sendable (Error, Send<Output>) async -> Void)? = nil,
204+
operation: @escaping @Sendable (Send<Action>) async throws -> Void,
205+
catch handler: (@Sendable (Error, Send<Action>) async -> Void)? = nil,
206206
file: StaticString = #file,
207207
fileID: StaticString = #fileID,
208208
line: UInt = #line
@@ -445,11 +445,11 @@ extension Effect {
445445

446446
/// Transforms all elements from the upstream effect with a provided closure.
447447
///
448-
/// - Parameter transform: A closure that transforms the upstream effect's output to a new output.
448+
/// - Parameter transform: A closure that transforms the upstream effect's action to a new action.
449449
/// - Returns: An effect that uses the provided closure to map elements from the upstream effect
450450
/// to new elements that it then publishes.
451451
@inlinable
452-
public func map<T>(_ transform: @escaping (Output) -> T) -> Effect<T, Failure> {
452+
public func map<T>(_ transform: @escaping (Action) -> T) -> Effect<T, Failure> {
453453
switch self.operation {
454454
case .none:
455455
return .none
@@ -459,12 +459,10 @@ extension Effect {
459459
return .init(
460460
operation: .run(priority) { send in
461461
await operation(
462-
.init(
463-
send: { output in
464-
send(transform(output))
462+
Send { action in
463+
send(transform(action))
465464
}
466465
)
467-
)
468466
}
469467
)
470468
}

Sources/ComposableArchitecture/Effects/Animation.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
case let .producer(producer):
2323
return Self(
2424
operation: .producer(
25-
SignalProducer<Output, Failure> { observer, _ in
25+
SignalProducer<Action, Failure> { observer, _ in
2626
producer.start { action in
2727
switch action {
2828
case let .value(value):

Sources/ComposableArchitecture/Effects/Cancellation.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ extension Effect {
4545
case let .producer(producer):
4646
return Self(
4747
operation: .producer(
48-
SignalProducer.deferred { () -> SignalProducer<Output, Failure> in
48+
SignalProducer.deferred { () -> SignalProducer<Action, Failure> in
4949
cancellablesLock.lock()
5050
defer { cancellablesLock.unlock() }
5151

@@ -54,8 +54,8 @@ extension Effect {
5454
cancellationCancellables[id]?.forEach { $0.dispose() }
5555
}
5656

57-
let subject = Signal<Output, Failure>.pipe()
58-
let values = Atomic<[Output]>([])
57+
let subject = Signal<Action, Failure>.pipe()
58+
let values = Atomic<[Action]>([])
5959
var isCaching = true
6060
let disposable =
6161
producer
@@ -76,9 +76,9 @@ extension Effect {
7676
}
7777
}
7878

79-
cancellationCancellables[id, default: []].insert(
79+
cancellationCancellables[id, default: []].insert(
8080
cancellationDisposable
81-
)
81+
)
8282

8383
return SignalProducer(values.value)
8484
.concat(subject.output.producer)

0 commit comments

Comments
 (0)