Loop icon indicating copy to clipboard operation
Loop copied to clipboard

Add `pause` and `resume` API's to `Loop`

Open p4checo opened this issue 5 years ago • 2 comments

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 plugFeedbacks and unplugFeedbacks API's in Floodgate, which establish and dispose feedbacks observations, respectively. Floodgate now retains the feedbacks passed in on bootstrap to use them on plugFeedbacks.

  • Add pause and resume API's to LoopBoxBase.

  • Implement pause and resume API's in RootLoopBox, which unplug and replug the feedbacks on the Floodgate, respectively.

  • Implement pause and resume API's in ScopedLoopBox, which forward the calls to their root, respectively.

  • Fixed .podspec to include swift files from all folders.

p4checo avatar Jul 06 '20 15:07 p4checo

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.

andersio avatar Jul 13 '20 10:07 andersio

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.

p4checo avatar Jul 21 '20 12:07 p4checo