Skip to content

Example of Combine usage#306

Closed
Kharchevskyi wants to merge 2 commits intomasterfrom
feature/test-combine
Closed

Example of Combine usage#306
Kharchevskyi wants to merge 2 commits intomasterfrom
feature/test-combine

Conversation

@Kharchevskyi
Copy link
Contributor

This is an example for #290 of how we can use Combine without huge changing of our codebase.
Please take a look on some improvements which can be added with using Combine

  • operation will be canceled if user close controller (just an example)
  • retry operation

@Kharchevskyi Kharchevskyi requested a review from tomholub May 6, 2021 20:52
@tomholub
Copy link
Collaborator

Thank you - I'll have a closer look next week

@tomholub
Copy link
Collaborator

tomholub commented Jul 9, 2021

Could you please produce an alternative PR that uses Combine promises? I think that could be the best compromise.

Copy link
Collaborator

@tomholub tomholub left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! more questions

private func encryptAndSendMessage() -> Promise<Bool> {
Promise<Bool> { [weak self] () -> Bool in
guard let self = self else { return false }
private func prepareMessage() -> Result<Data, SendMessageError> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it, the futures are similar to promises. Now I understand it it a bit better.

What is this particular method - it returns Result<Data, SendMessageError>. How is that different from Future<...>?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is return synchronously.
In this case it's just a "helper function"

Comment on lines +236 to +247
case .success(let data):
self.messageSender.sendMail(mime: data)
.retry(3)
.sink{ [weak self] result in
guard case .failure(let error) = result else {
return
}
self?.showAlert(error: error, message: "compose_error".localized)
} receiveValue: { [weak self] _ in
self?.handleSuccessfullySentMessage()
}
.store(in: &cancellable)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For this demo, please replicate original behavior - without retry or other features not present in the original.

Is the switch prepareMessage() { the only way to handle a Result? Is there some other possible pattern like:

let preparedMessage = prepareMessage()
if preparedMessage.isSuccess {

} else {

}

?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please check latest commit. Implemented similar to what we have in original code.

}.catch(on: .main) { [weak self] error in
self?.showAlert(error: error, message: "compose_error".localized)
case .success(let data):
self.messageSender.sendMail(mime: data)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does self.messageSender.sendMail have to be in the result handling code? Could it be more similar to original code, where encryptAndSendMessage took care of sending as well?

Copy link
Collaborator

@tomholub tomholub left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can accept using Combine futures this way, it looks good. Thank you for the demo.

We can clean up this PR to settle on a pattern, then merge it.

When converging to Combine, please don't editorialize the code at the same time. Please try to match the code structure 1:1 else it's very difficult to see if original functionality was preserved. If you notice things to refactor, if possible or unless it's really tiny, please file issues with examples for later instead of doing it in the same step.

Please do self-review of the code after you commit it, you yourself will discover a lot of the things that I'm discovering and can fix it instead of having me raise the concern (like SendMessageError which is unused and errors dropped silently - a self review on GitHub would make this very obvious).

Going forward, whenever you'd like me to look at code, please go to Files changed first yourself and go through your changes as a reviewer would. See possible problems when comparing original and new code, and adjust them if needed. Then if you made changes, do another such review until you're satisfied with the changes as they show on GitHub. Then click Finish your review, choose the Comment type and say LGTM. I also do this when I submit my own PRs, it's a way to make sure to not unnecessarily burden the reviewer with pointing out small fixes that are obvious, and instead allow the reviewer to focus on the bigger picture. Thanks!

Comment on lines +230 to +232
guard recipients.isNotEmpty else {
return
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should add back error handling, not just return and ignore. If it's input validation error from user, then we render an alert and return. If it's a nilcheck that was already supposed to be checked earlier and we're just sanity-checking our code, than that's a coding error and should result in a crash, not be ignored.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would suggest to use implementation with Result<SendableMessage, SendMessageError> as it was in previous commit for all this checks instead of returning true/false, and use asynchronous Future only for send message operation.

Comment on lines +234 to +235
guard let text = self.contextToSend.message else {
return
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should add back error handling, not just return and ignore. If it's input validation error from user, then we render an alert and return. If it's a nilcheck that was already supposed to be checked earlier and we're just sanity-checking our code, than that's a coding error and should result in a crash, not be ignored.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skip this part for this example, will return back alert and proper handling

Comment on lines +242 to 244
guard let myPubKey = self.dataService.publicKey else {
return
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also here bring back alert?

Comment on lines +246 to +248
guard let allRecipientPubs = getPubKeys(for: recipients) else {
return
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unsure how was this handled before, looks like we should improve it, but that would be for another PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will use same logic as it was before. This method will return true or false

Comment on lines +259 to +269
.sink(
receiveCompletion: { [weak self] result in
guard case .failure(let error) = result else {
return
}
self?.showAlert(error: error, message: "compose_error".localized)
},
receiveValue: { [weak self] in
self?.handleSuccessfullySentMessage()
})
.store(in: &cancellable)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR looks good, and we could actually clean up and merge it.

What does the .sink represent?

I figured receiveCompletion triggers on both error and success, and you are filtering for error only? Isn't there a more convenient method like receiveFailure?

What does the .store(in: &cancellable) do?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sink is used to "subscribe" to stream of events.
receive completion will be triggered in case of error and in case when stream is finished

 .sink(
   receiveCompletion: { completion in
      switch completion {
      case .failure(let error): print("Error \(error)")
      case .finished: print("Publisher is finished")
      }
   }
   ...

In this example with Future(promise), there will be only 1 value, which can be accessed in receiveValue: closure and 1 receiveCompletion.
But in cases with other publishers, receiveValue closure can be called multiple times but receiveCompletion - will be called only once when publisher finished or failed

There is sink(receiveValue:) function, but there is no just receiveError.

.store(in: &cancellable).
In case we started some long going request and left the screen cancellable will frees up any allocated resources and also will stop side effects.
For example user start fetching message with bad internet connection and was waiting for few seconds. Then taped back and left the screen, cancelable will terminate this execution and free up resources.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All subscriptions should be stored in cancellable to prevent their execution if view controller/service is deallocated

// temporary disable search contacts - https://github.com/FlowCrypt/flowcrypt-ios/issues/217
// showScopeAlertIfNeeded()

cancellable.forEach { $0.cancel() }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this do?

@Kharchevskyi
Copy link
Contributor Author

This PR, as title says is just an example of Combine usage. It doesn't meant to be reviewed at all.

@tomholub
Copy link
Collaborator

This PR, as title says is just an example of Combine usage. It doesn't meant to be reviewed at all.

I get it, it was only meant as a demonstration.

Demonstration succeeded - can we bring the PR from its current state to a mergeable one, so that it's not wasted effort? To begin work to get rid of google Promise library.

Please also help me by answering the questions above.

@Kharchevskyi
Copy link
Contributor Author

Sure, thanks. On my way with answers
Are we going to use only Future from Combine? or we are free to use Combine itself?

@tomholub
Copy link
Collaborator

tomholub commented Jul 14, 2021 via email

@Kharchevskyi
Copy link
Contributor Author

@tomholub can you please close this PR if favour of #400
(was hard to resolve all conflicts after pulling master)

@tomholub
Copy link
Collaborator

closing in favor of #400

@tomholub tomholub closed this Jul 21, 2021
@tomholub tomholub deleted the feature/test-combine branch November 9, 2021 16:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants