Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to combine two async network calls with ReactiveCocoa

I have two network signals that I want to merge, but with some restrictions.

Let us call the network signals A and B. A does use AFNetworking to look up a resource in the cache and return any response for that request immediately. B also considers the cache, but can go to the remote server for revalidation of the response.

Ok, so what I want to do:

Request A:

  • should do sendNext as soon as possible.
  • if B already has done a sendNext, we will just ignore A.
  • if something goes wrong, and A creates an error, we should just ignore it.

Request B:

  • should do sendNext as soon as possible, even if A already has done a sendNext.
  • if something goes wrong, I am interessed in the error from B, but it should not stop A.

My current solution is this:

- (RACSignal *)issueById:(NSString *)issueId {

    RACSignal *filterSignal = [RACSignal createSignal:^RACDisposable *(id <RACSubscriber> subscriber) {
        RACSignal *cacheSignal = [[IssueWSRequest instance] issueWithId:issueId cachePolicy:NSURLRequestReturnCacheDataDontLoad];

        return [cacheSignal subscribeNext:^(id x) {
            [subscriber sendNext:x];
        } error:^(NSError *error) {
            NSLog(@"Ignore error");
            [subscriber sendCompleted];
        } completed:^{
            [subscriber sendCompleted];
        }];
    }];

    RACSignal *remoteSignal = [[IssueWSRequest instance] issueWithId:issueId cachePolicy:NSURLRequestUseProtocolCachePolicy];

    RACSignal *combined = [RACSignal merge:@[newSign, remoteSignal]];
    return combined;
}

I know that this solution does not fulfill my requirements, so I wonder if anyone could help me with a better solution.

My solution (derived from @JustinSpahr-Summers answer):

- (RACSignal *)issueById:(NSString *)issueId {

    RACSubject *localErrors = [RACSubject subject];

    RACSignal *remoteSignal = [[IssueWSRequest instance] issueWithId:issueId cachePolicy:NSURLRequestUseProtocolCachePolicy];

    RACSignal *cacheSignal = [[[[[[IssueWSRequest instance] issueWithId:issueId cachePolicy:NSURLRequestReturnCacheDataDontLoad] 
            takeUntil:remoteSignal] doError:^(NSError *error) {
                [localErrors sendNext:error];
            }] finally:^{
                // Make sure to complete the subject, since infinite signals are
                // difficult to use.
                [localErrors sendCompleted];
            }]
            replayLazily];

    return [RACSignal merge:@[
            [cacheSignal catchTo:[RACSignal empty]],
            remoteSignal
    ]];
}
like image 655
Andreas Aarsland Avatar asked Dec 04 '13 12:12

Andreas Aarsland


1 Answers

This is a difficult question to answer, because your desired error handling is fundamentally incompatible with the RACSignal API contract, which states that errors have exception semantics.

The only way to ignore but still care about errors is to redirect them elsewhere. In this example, I'll use a subject:

RACSubject *remoteErrors = [RACSubject subject];

… but you could also use a property or some other notification mechanism.

I'll continue to use the remoteSignal and cacheSignal you've given above, with some modifications. Here's the behavior we want from them:

  1. remoteSignal should send its errors to remoteErrors
  2. cacheSignal should be canceled as soon as remoteSignal sends a value
  3. Errors from either signal should not terminate the other
  4. We want to merge the values from cacheSignal and remoteSignal, so that we still get the remote value after the cache is read

With this in mind, let's take a look at remoteSignal:

RACSignal *remoteSignal = [[[[[IssueWSRequest
    instance]
    issueWithId:issueId cachePolicy:NSURLRequestUseProtocolCachePolicy]
    doError:^(NSError *error) {
        [remoteErrors sendNext:error];
    }]
    finally:^{
        // Make sure to complete the subject, since infinite signals are
        // difficult to use.
        [remoteErrors sendCompleted];
    }]
    replayLazily];

The -doError: and -finally: control the remoteErrors subject, fulfilling our first requirement above. Because we need to use remoteSignal in more than one place (as you can sorta see in the list above), we use -replayLazily to ensure that its side effects only occur once.

cacheSignal is almost unchanged. We just need to use -takeUntil: to ensure that it terminates when remoteSignal sends a value (but not if it sends an error):

RACSignal *cacheSignal = [[[IssueWSRequest
    instance]
    issueWithId:issueId cachePolicy:NSURLRequestReturnCacheDataDontLoad]
    takeUntil:remoteSignal];

Finally, we want to merge their values, so that both signals are started at the same time and their values can arrive in any order:

return [RACSignal merge:@[
    [cacheSignal catchTo:[RACSignal empty]],
    [remoteSignal catchTo:[RACSignal empty]]
];

We're ignoring errors here, because an error from either would terminate both (since they've been combined now). Our error handling behavior is already taken care of above.

And, despite the merge, using -takeUntil: on the cacheSignal ensures that it's impossible for it to send a value after remoteSignal does.

Taking another look at the list of requirements, you can see the operators used to fulfill each one:

  1. [-doError:] remoteSignal should send its errors to remoteErrors
  2. [-takeUntil:] cacheSignal should be canceled as soon as remoteSignal sends a value
  3. [-catchTo:] Errors from either signal should not terminate the other
  4. [+merge:] We want to merge the values from cacheSignal and remoteSignal, so that we still get the remote value after the cache is read
like image 183
Justin Spahr-Summers Avatar answered Oct 18 '22 07:10

Justin Spahr-Summers