Skip to content

Commit 8b2f333

Browse files
authored
Merge pull request #2 from musicspot24/operation
✨ Use `Sendable` wrapper for handling side effect
2 parents 8dde505 + fb6869f commit 8b2f333

5 files changed

Lines changed: 74 additions & 16 deletions

File tree

Examples/DripperDemo/DripperDemo/Counter.swift

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,17 +40,18 @@ struct Counter: Dripper {
4040
case .resetCounter:
4141
state.counter = .zero
4242
case .randomNumber:
43-
return .init { _ in
43+
return .run { pour in
4444
func randomNumber() async throws -> Int {
45-
try await Task.sleep(for: .seconds(2))
45+
try await Task.sleep(for: .seconds(1))
4646
return Int.random(in: 0...10)
4747
}
4848
let randomNumber = try await randomNumber()
49+
await pour(.decreaseCounter)
4950
state.counter = randomNumber
5051
}
5152
}
5253

53-
return .init { _ in }
54+
return .none
5455
}
5556
}
5657
}

Sources/Dripper/Drip.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ public struct Drip<State: Observable, Action>: Dripper {
1111

1212
// MARK: Properties
1313

14-
@usableFromInline let drip: (State, Action) -> Effect<Action>
14+
@usableFromInline let drip: (State, Action) -> Effect<Action>?
1515

1616
// MARK: Lifecycle
1717

@@ -25,14 +25,14 @@ public struct Drip<State: Observable, Action>: Dripper {
2525
}
2626

2727
@usableFromInline
28-
init(internal drip: @escaping (_ state: State, _ action: Action) -> Effect<Action>) {
28+
init(internal drip: @escaping (_ state: State, _ action: Action) -> Effect<Action>?) {
2929
self.drip = drip
3030
}
3131

3232
// MARK: Functions
3333

3434
@inlinable
35-
public func drip(_ state: State, _ action: Action) -> Effect<Action> {
35+
public func drip(_ state: State, _ action: Action) -> Effect<Action>? {
3636
drip(state, action)
3737
}
3838
}

Sources/Dripper/Dripper.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public protocol Dripper<State, Action> {
1616
associatedtype Action
1717
associatedtype Body
1818

19-
func drip(_ state: State, _ action: Action) -> Effect<Action>
19+
func drip(_ state: State, _ action: Action) -> Effect<Action>?
2020

2121
@DripperBuilder<State, Action>
2222
var body: Body { get }
@@ -30,7 +30,7 @@ extension Dripper where Body == Never {
3030

3131
extension Dripper where Body: Dripper<State, Action> {
3232
@inlinable
33-
public func drip(_ state: Body.State, _ action: Body.Action) -> Effect<Action> {
33+
public func drip(_ state: Body.State, _ action: Body.Action) -> Effect<Action>? {
3434
body.drip(state, action)
3535
}
3636
}

Sources/Dripper/Effect.swift

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,68 @@
66
//
77

88
import Foundation
9+
import OSLog
10+
11+
// MARK: - Effect
912

1013
public struct Effect<Action> {
11-
public typealias ActionHandler = @MainActor @Sendable (Action) -> Void
14+
public typealias Kettle = @Sendable (_ blend: Pour<Action>) async -> Void
15+
16+
@usableFromInline let kettle: Kettle
17+
18+
@usableFromInline
19+
init(kettle: @escaping Kettle) {
20+
self.kettle = kettle
21+
}
22+
}
23+
24+
// MARK: - Pour
25+
26+
@MainActor
27+
public struct Pour<Action>: Sendable {
28+
let pour: @MainActor @Sendable (Action) -> Void
29+
30+
init(pour: @escaping @MainActor @Sendable (Action) -> Void) {
31+
self.pour = pour
32+
}
33+
34+
public func callAsFunction(_ action: Action) {
35+
pour(action)
36+
}
37+
}
38+
39+
extension Effect {
40+
41+
// MARK: Static Computed Properties
42+
43+
public static var none: Self {
44+
Self { _ in }
45+
}
1246

13-
public let run: (_ action: ActionHandler) async throws -> Void
47+
// MARK: Static Functions
1448

15-
public init(run: @escaping (_ action: ActionHandler) async throws -> Void) {
16-
self.run = run
49+
public static func run(
50+
kettle: @escaping @Sendable (_ pour: Pour<Action>) async throws -> Void,
51+
catch errorHandler: (@Sendable (_ error: any Error, _ pour: Pour<Action>) async -> Void)? = nil,
52+
fileID: StaticString = #fileID,
53+
line: UInt = #line
54+
) -> Self {
55+
Self { pour in
56+
do {
57+
try await kettle(pour)
58+
} catch {
59+
guard let errorHandler else {
60+
os_log(
61+
.fault,
62+
"""
63+
An "Effect.run" returned from "\(fileID):\(line)" threw an unhandled error.
64+
This error must be handled via the `catch` parameter.
65+
"""
66+
)
67+
return
68+
}
69+
await errorHandler(error, pour)
70+
}
71+
}
1772
}
1873
}

Sources/Dripper/Station.swift

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,12 @@ public final class Station<State: Observable, Action> {
4949

5050
public func pour(_ action: Action) {
5151
let effect = dripper.drip(state, action)
52-
// FIXME: Currently, side effect is called no matter it's empty or not.
53-
Task {
54-
try await effect.run { action in
55-
pour(action)
52+
53+
if let effect {
54+
Task {
55+
await effect.kettle(
56+
Pour { self.pour($0) }
57+
)
5658
}
5759
}
5860
}

0 commit comments

Comments
 (0)