Loop
Loop copied to clipboard
Add `pause` and `resume` API's to `Loop`
Motivation
On some scenarios, it's useful to "pause" the Loop so that we stop processing events for some reason (e.g. to stop a Loop backed Service). Following the same reasoning, it becomes necessary to have a "resume" mechanism so that the Loop starts processing events again.
Given Loop now starts automatically and stop is designed as a tear down mechanism to be used on dealloc and dispose all observations, some new API's are required so that we can unplug/replug feedbacks to achieve the above mentioned pause/resume behavior.
⚠️ Note: Tests missing, as I wanted to validate the approach first 🙏
Changes
-
Create new
plugFeedbacksandunplugFeedbacksAPI's inFloodgate, which establish and dispose feedbacks observations, respectively. Floodgate now retains the feedbacks passed in onbootstrapto use them onplugFeedbacks. -
Add
pauseandresumeAPI's toLoopBoxBase. -
Implement
pauseandresumeAPI's inRootLoopBox, which unplug and replug the feedbacks on theFloodgate, respectively. -
Implement
pauseandresumeAPI's inScopedLoopBox, which forward the calls to their root, respectively. -
Fixed .podspec to include swift files from all folders.
Having given some thoughts about this over the weekend, I think making resume() and pause() available on Loop can make it potentially confusing when it comes to semantics around scoped stores (as you briefly mentioned).
I wonder if we would venture a state-driven approach instead. Say given this state definition:
struct State {
var messages: [Message] = []
var isPaused: Bool = false
}
Then we can define pausable feedbacks through a feedback modifier, that instructs the feedback to listen to a particular predicate. So Loop users also get to control and program the scope and granularity of cancellation.
let loop = Loop(
initial: State(),
reducer: ...,
feedbacks: [
Loop.Feedback
.combine(
Self.feedback_pausable_whenLoading(),
Self.feedback_pausable_whenX(),
Self.feedback_pausable_whenY(),
Self.feedback_pausable_whenZ()
)
.autoconnect(condition: { $0.isPaused == false })
]
)
(... while Loop.send remains always available.)
P.S. I have a bit of hesitation about pause as the term too, since it usually implies progress preservation which would not be the case for Loop (unless it being explicitly programmed to do so). So I chose the term autoconnect in the snippet above.
Sorry for the delay, I was off last week 😇
I agree that the resume() and pause() APIs can be a bit confusing for users. I went with it because on initial conversations with @RuiAAPeres he suggested that it would be the preferred direction for this. My initial approach was to simply use start() and stop() APIs, however they have been deprecated. Out of curiosity: what was the reasoning for it?
On our particular case, the need for this arose because we are using Loop to model Services on our app (e.g. responsible for performing network requests and managing a local state) which we want to start/stop. Even though we currently think of the start/stop mechanics as external to the state machine, I believe that they can (and probably should) be an integral part of it. Following this state-driven approach we keep this behavior nicely packed in the state which makes everything more explicit while also being more extensible and composable. ✨
Regarding the term, I agree that pause is not ideal and I think that autoconnect is definitely an improvement, despite personally not being fully convinced by it either 😅. Since I couldn't come up with a better one, I'll go with it for now.