PromiseKit no callback / deallocates? (Alamofire) - ios

My promise chain is broken (maybe deallocated) before it's resolved.
This happens (so far ONLY) when I make Alamofire request fail due to host trust evaluation -> forcing evaluation to fail which results in -999 cancelled etc).
Setup is rather simple:
APIRequest:
func start(_ onSuccess:#escaping SuccessBlock, onError:#escaping ErrorBlock) {
do {
try executeRequest { dataResponse in
self.onSuccess(dataResponse)
}
} catch {
self.onError(error)
}
}
where executeRequest() is:
self.manager.request(request).responseJSON(queue: self.queue) { (response) in
completion(response)
}
Then, there is PromiseKit wrapper defined as APIRequest extension:
(This wrapper callbacks are called correctly in either case)
func start() -> Promise<APIResponse> {
return Promise<APIResponse> { resolver in
self.start({ response in
resolver.fulfill(response)
}) { error in
resolver.reject(error)
}
}
}
And finally, unit test calling the start promise (extension):
( flow never reaches this place in case of Alamofire failing )
request.start().done { response in
}.catch { error in
// not called if request failed
}
Outcome: if request fails -> the extension promise wrapper (catch) block is called, but it's not propagated back to UnitTest promise block.
Simply replacing Alamofire request with mock implementation (which triggers some other error( makes all setup work as expected (Unit Test completes with catch block being called etc) ex:
DispatchQueue.global(qos: .default).asyncAfter(deadline: .now() + 2) {
let result = Alamofire.Result { () -> Any in
return try JSONSerialization.data(withJSONObject: [:], options: .fragmentsAllowed)
}
completion(DataResponse<Any>(request: nil, response: nil, data: nil, result: result))
}
Is this something to do with Alamofire? I don't really see how else to handle the promises there ( they shouldn't deallocate anyways... Or is this bug in PromiseKit? Alamofire? I yet have to test this in the app itself ( maybe it's Unit test issue ... )
Looking at related issues -> i'm definitely resolving promises (fulfilling / rejecting) for any flow path.
I don't see how Alamofire request is different from DispatchAsync (where the latter works as expected).

I was only 10 mins short of finding the answer... Problem is also described here:
https://github.com/mxcl/PromiseKit/issues/686
Issue is that '-999 cancelled' error is not treated as 'Error' by PromiseKit. Solution is to use "catch(policy: .allErrors)" - then catch block is called as expected.

func start(_ onSuccess:#escaping SuccessBlock, onError:#escaping ErrorBlock) {
do {
try executeRequest { dataResponse in
onSuccess(dataResponse)
}
} catch {
onError(error)
}
}
You are using self.onSuccess that means its not function parameter block but instance block thats why its not going back to block from you are calling start.

Related

Multi Thread API calls with Socket Swift IOS

I'm using sockets to communicate with server API's. I have to call multiple API calls at the same time but when response comes my method loosing completion handler reference. for example if i hit 4 Api's at the same time, Sockets reply back with 4 responses as expected but my method returning back only one callback which is last API call.
So my question is how can i divide these calls into different threads so i got exact callbacks?
My Socket Call which is return back with completion Handler
if self.socketManager.isConnected {
self.socketManager.perform(request: request) { (result) in
completion?(result)
self.cache(request: request, result: result)
}
}
My Socket call
// MARK: - Methods
func perform(request: Request, completion: #escaping ResultAnyCompletion) {
do {
print("requst##### \(request.correlationId ?? "")")
let identityStatus = checkForIdentityStatus(request: request)
let paramData = try request.createBody(excludeIdentity: identityStatus)
webSocket?.write(stringData: paramData, completion: nil)
resultCompletion = completion
} catch {
completion(.failure(NSError(domain: "Params Not Found", code: 404, userInfo: nil)))
}
}

How to schedule a synchronous sequence of asynchronous calls in Combine?

I'd like to handle a series of network calls in my app. Each call is asynchronous and flatMap() seems like the right call. However, flatMap processes all arguments at the same time and I need the calls to be sequential -- the next network call starts only after the previous one is finished. I looked up an RxSwift answer but it requires concatMap operator that Combine does not have. Here is rough outline of what I'm trying to do, but flatMap fires all myCalls at the same time.
Publishers.Sequence(sequence: urls)
.flatMap { url in
Publishers.Future<Result, Error> { callback in
myCall { data, error in
if let data = data {
callback(.success(data))
} else if let error = error {
callback(.failure(error))
}
}
}
}
After experimenting for a while in a playground, I believe I found a solution, but if you have a better idea, please share. The solution is to add maxPublishers parameter to flatMap and set the value to max(1)
Publishers.Sequence(sequence: urls)
.flatMap(maxPublishers: .max(1)) // <<<<--- here
{ url in
Publishers.Future<Result, Error> { callback in
myCall { data, error in
if let data = data {
callback(.success(data))
} else if let error = error {
callback(.failure(error))
}
}
}
}
You can also use prepend(_:) method on observable which creates concatenated sequence which, I suppose is similar to Observable.concat(:) in RxSwift.
Here is a simple example that I tried to simulate your use case, where I have few different sequences which are followed by one another.
func dataTaskPublisher(_ urlString: String) -> AnyPublisher<(data: Data, response: URLResponse), Never> {
let interceptedError = (Data(), URLResponse())
return Publishers.Just(URL(string: urlString)!)
.flatMap {
URLSession.shared
.dataTaskPublisher(for: $0)
.replaceError(with: interceptedError)
}
.eraseToAnyPublisher()
}
let publisher: AnyPublisher<(data: Data, response: URLResponse), Never> = Publishers.Empty().eraseToAnyPublisher()
for urlString in [
"http://ipv4.download.thinkbroadband.com/1MB.zip",
"http://ipv4.download.thinkbroadband.com/50MB.zip",
"http://ipv4.download.thinkbroadband.com/10MB.zip"
] {
publisher = publisher.prepend(dataTaskPublisher(urlString)).eraseToAnyPublisher()
}
publisher.sink(receiveCompletion: { completion in
print("Completed")
}) { response in
print("Data: \(response)")
}
Here, prepend(_:) operator prefixes the sequence and so, prepended sequences starts first, completes and next sequence start.
If you run the code below, you should see that firstly 10 MB file is download, then 50 MB and at last 1 MB, since the last prepended starts first and so on.
There is other variant of prepend(_:) operator which takes array, but that does not seem to work sequentially.

How to not freeze the UI, and wait for response?

I've been trying since the morning but I didnt achieve what I wanted.
I tried DispatchQueue.main.async and completion block but my "Submit" button in the UI still freezes waiting for the data to be returned from the server. This is my code:
func createData(request:Crudpb_CreateRequest, with completion: #escaping (String) -> Void) throws {
DispatchQueue.main.async {
self.response = try! self.client.create(request) // <---- How to handle error for this server call when the server is not available or is down?
completion(self.response.result)
}
}
I just noticed Im calling the 1st method from the following which is a Synchronous Unary which might be the reason behind the problem. But again I dont know how to call the second function in the fallowing:
/// Synchronous. Unary.
internal func create(_ request: Crudpb_CreateRequest, metadata customMetadata: Metadata) throws -> Crudpb_CreateResponse {
return try Crudpb_CrudServiceCreateCallBase(channel)
.run(request: request, metadata: customMetadata)
}
/// Asynchronous. Unary.
#discardableResult
internal func create(_ request: Crudpb_CreateRequest, metadata customMetadata: Metadata, completion: #escaping (Crudpb_CreateResponse?, CallResult) -> Void) throws -> Crudpb_CrudServiceCreateCall {
return try Crudpb_CrudServiceCreateCallBase(channel)
.start(request: request, metadata: customMetadata, completion: completion)
}
Server Side Code:
func (*server) Create(ctx context.Context, req *crudpb.CreateRequest) (*crudpb.CreateResponse, error) {
var result string
firstName := req.GetAccount().GetFirstName()
lastName := req.GetAccount().GetLastName()
// id := gocql.TimeUUID()
fmt.Println("Triggered CREATE function on Go Server " + firstName + " " + lastName + "! Yayy!")
result = fmt.Sprintf("id for %s %s : %s", firstName, lastName, strconv.Itoa(rand.Intn(100)))
return &crudpb.CreateResponse{
Result: result,
}, nil
I need this app / submit button not to freeze while it fetches result from server.
Please help.
You are still performing work on the main thread.. async only makes the createData() method to return before the task is completed.. but the task will still be processed at some time in the main thread and during this time your application will become unresponsive.. try using a global queue instead.. to keep your main thread clean..
Dont forget to perform all your UI work on the main thread after getting your response.
Use the asynchronous function instead and call the completion block inside create function's completion.
func createData(request:Crudpb_CreateRequest, with completion: #escaping (String) -> Void) throws {
try! self.client.create(request) { (response: Crudpb_CreateResponse?, result: CallResult) in
DispatchQueue.main.async {
// This is assuming your completion involves UI operations. Otherwise there is no need for this async call.
let stringOutput = String(data: result.resultData!, encoding: String.Encoding.utf8))
completion(stringOutput)
}
}
}
Remove DispatchQueue.main.async block from the createData method
func createData(request:Crudpb_CreateRequest, with completion: #escaping (String) -> Void) throws {
self.response = try! self.client.create(request)
completion(self.response.result)
}
Use main queue only where you update the UI from the api response
myobj.createData(request: request, with: { string in
print(string)//background thread
DispatchQueue.main.async {
self.label.text = sting//main thread
}
})
The UI freeze because you are doing too much work on the main thread. You should find out what function blocks the main thread.
The instruments time profiler is an easy way to see which function is spending too much time.

What is the best practice to deal with RxSwift retry and error handling

I read some post says that the best practice to deal with RxSwift is to only pass fatal error to the onError and pass Result to the onNext.
It makes sense to me until I realise that I can't deal with retry anymore since it only happen on onError.
How do I deal with this issue?
Another question is, how do I handle global and local retry mixes together?
A example would be, the iOS receipt validation flow.
1, try to fetch receipt locally
2, if failed, ask Apple server for the latest receipt.
3, send the receipt to our backend to validate.
4, if success, then whole flow complete
5, if failed, check the error code if it's retryable, then go back to 1.
and in the new 1, it will force to ask for new receipt from apple server. then when it reaches 5 again, the whole flow will stop since this is the second attempt already. meaning only retry once.
So in this example, if using state machine and without using rx, I will end up using state machine and shares some global state like isSecondAttempt: Bool, shouldForceFetchReceipt: Bool, etc.
How do I design this flow in rx? with these global shared state designed in the flow.
I read some post says that the best practice to deal with RxSwift is to only pass fatal error to the onError and pass Result to the onNext.
I don't agree with that sentiment. It is basically saying that you should only use onError if the programmer made a mistake. You should use errors for un-happy paths or to abort a procedure. They are just like throwing except in an async way.
Here's your algorithm as an Rx chain.
enum ReceiptError: Error {
case noReceipt
case tooManyAttempts
}
struct Response {
// the server response info
}
func getReceiptResonse() -> Observable<Response> {
return fetchReceiptLocally()
.catchError { _ in askAppleForReceipt() }
.flatMapLatest { data in
sendReceiptToServer(data)
}
.retryWhen { error in
error
.scan(0) { attempts, error in
let max = 1
guard attempts < max else { throw ReceiptError.tooManyAttempts }
guard isRetryable(error) else { throw error }
return attempts + 1
}
}
}
Here are the support functions that the above uses:
func fetchReceiptLocally() -> Observable<Data> {
// return the local receipt data or call `onError`
}
func sendReceiptToServer(_ data: Data) -> Observable<Response> {
// send the receipt data or `onError` if the server failed to receive or process it correctly.
}
func isRetryable(_ error: Error) -> Bool {
// is this error the kind that can be retried?
}
func askAppleForReceipt() -> Observable<Data> {
return Observable.just(Bundle.main.appStoreReceiptURL)
.map { (url) -> URL in
guard let url = url else { throw ReceiptError.noReceipt }
return url
}
.observeOn(ConcurrentDispatchQueueScheduler(qos: .userInitiated))
.map { try Data(contentsOf: $0) }
}

Performing an asynchronous task within a synchronous call

I would like to register a user which is performed asynchronous. However, the calling function behaves synchronous since the program should only continue when a user is created successfully.
The current implementation is:
class SignUp: NSObject {
// ...
func signUpUser() throws -> Bool {
guard hasEmptyFields() else {
throw CustomErrorCodes.EmptyField
}
guard isValidEmail() else {
throw CustomErrorCodes.InvalidEmail
}
createUser( { (result) in
guard result else {
throw CustomErrorCodes.UserNameTaken
}
return true // Error: cannot throw....
})
}
func createUser( succeeded: (result: Bool) -> () ) -> Void {
let newUser = User()
newUser.username = username!
newUser.password = password!
// User is created asynchronously
createUserInBackground(newUser, onCompletion: {(succeed, error) -> Void in
if (error != nil) {
// Show error alert
} else {
succeeded(result: succeed)
}
})
}
}
and in a ViewController the signup is initiated as follows:
do {
try signup.signUpUser()
} catch let error as CustomErrorCodes {
// Process error
}
However, this does not work since createUser is not a throwing function. How could I ensure that signUpUser() only returns true when an new user is created successfully?
You say:
and in a ViewController the signup is initiated as follows:
do {
try signup.signUpUser()
} catch let error as CustomErrorCodes {
// Process error
}
But don't. That's not how asynchronous works. The whole idea is that you do not wait. If you're waiting, it's not asynchronous. That means you're blocking, and that's just what you mustn't do.
Instead, arrange to be called back at the end of your asynchronous process. That's when you'll hear that things have succeeded or not. Look at how a download task delegate is structured:
https://developer.apple.com/library/ios/documentation/Foundation/Reference/NSURLSessionDownloadTask_class/
The download task calls back into the delegate to let it know whether we completed successfully or not. That is the relationship you want to have with your asynchronous task. You want to be like that delegate.
You need to adjust your thinking. Instead of trying to write a synchronous method that we need to wait for an asynchronous event, write a method that takes a completion closure. The method will return immediately, but once the asynchronous process is complete it wild invoke the completion closure. When you call such a method you pass in code in the incompletion closure that gets called once the job is done.

Resources