@@ -6,37 +6,25 @@ public final class FeedbackLoop<State, Event> {
6
6
private let token : Lifetime . Token
7
7
8
8
public var producer : SignalProducer < State , Never > {
9
- SignalProducer { observer, lifetime in
10
- self . floodgate. withValue { initial, hasStarted -> Void in
11
- if hasStarted {
12
- // The feedback loop has started already, so the initial value has to be manually delivered.
13
- // Uninitialized feedback loop that does not start immediately will emit the initial state
14
- // when `start()` is called.
15
- observer. send ( value: initial)
16
- }
17
-
18
- lifetime += self . floodgate. stateDidChange. observe ( observer)
19
- }
20
- }
9
+ floodgate. producer
21
10
}
22
11
12
+ private let feedbacks : [ Feedback ]
13
+
23
14
public init (
24
15
initial: State ,
25
16
reduce: @escaping ( inout State , Event ) -> Void ,
26
17
feedbacks: [ Feedback ]
27
18
) {
28
19
( lifetime, token) = Lifetime . make ( )
29
- floodgate = Floodgate < State , Event > ( state: initial, reducer: reduce)
30
- lifetime . observeEnded ( floodgate . dispose )
20
+ self . floodgate = Floodgate < State , Event > ( state: initial, reducer: reduce)
21
+ self . feedbacks = feedbacks
31
22
32
- for feedback in feedbacks {
33
- lifetime += feedback
34
- . events ( floodgate. stateDidChange. producer, floodgate)
35
- }
23
+ lifetime. observeEnded ( floodgate. dispose)
36
24
}
37
25
38
26
public func start( ) {
39
- floodgate. bootstrap ( )
27
+ floodgate. bootstrap ( with : feedbacks )
40
28
}
41
29
42
30
public func stop( ) {
@@ -71,7 +59,58 @@ extension FeedbackLoop {
71
59
/// `Feedback(skippingRepeated:effects:)` enqueues events inside `flatMap(.latest)`, so that unprocessed events
72
60
/// are automatically removed when the inner producer has switched.
73
61
///
74
- /// - important: The `state` producer provided to the setup closure **does not** replay the current state.
62
+ /// ## State producer in the `setup` closure
63
+ /// The setup closure provides you a `state` producer — it replays the latest state at starting time, and then
64
+ /// publishes all state changes.
65
+ ///
66
+ /// Loop guarantees only that this `state` producer is **eventually consistent** with events emitted by your
67
+ /// feedback. This means you should not make any strong assumptions on events you enqueued being immediately
68
+ /// reflected by `state`.
69
+ ///
70
+ /// For example, if you start the `state` producer again, synchronously after enqueuing an event, the event
71
+ /// may not have been processed yet, and therefore the assertion would fail:
72
+ /// ```swift
73
+ /// Feedback { state, output in
74
+ /// state
75
+ /// .filter { $0.apples.isEmpty == false }
76
+ /// .map(value: Event.eatAllApples)
77
+ /// .take(first: 1)
78
+ /// .concat(
79
+ /// state
80
+ /// .take(first: 1)
81
+ /// .on(value: { state in
82
+ /// guard state.apples.isEmpty else { return }
83
+ ///
84
+ /// // ❌🙅♀️ No guarantee that this is true.
85
+ /// fatalError("It should have eaten all the apples!")
86
+ /// })
87
+ /// )
88
+ /// .enqueue(to: output)
89
+ /// }
90
+ /// ```
91
+ ///
92
+ /// You can however expect it to be eventually consistent:
93
+ /// ```swift
94
+ /// Feedback { state, output in
95
+ /// state
96
+ /// .filter { $0.apples.isEmpty == false }
97
+ /// .map(value: Event.eatAllApples)
98
+ /// .take(first: 1)
99
+ /// .concat(
100
+ /// state
101
+ /// .filter { $0.apples.isEmpty }
102
+ /// .take(first: 1)
103
+ /// .on(value: { state in
104
+ /// guard state.apples.isEmpty else { return }
105
+ ///
106
+ /// // ✅👍 We would eventually observe this, when the loop event queue
107
+ /// // has caught up with `.eatAppleApples` we enqueued earlier.
108
+ /// fatalError("It should have eaten all the apples!")
109
+ /// })
110
+ /// )
111
+ /// .enqueue(to: output)
112
+ /// }
113
+ /// ```
75
114
///
76
115
/// - parameters:
77
116
/// - setup: The setup closure to construct a data flow producing events in respond to changes from `state`,
0 commit comments