Andreas Aarsland
Andreas Aarsland

Reputation: 955

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:

Request B:

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
    ]];
}

Upvotes: 8

Views: 3116

Answers (1)

Justin Spahr-Summers
Justin Spahr-Summers

Reputation: 16973

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

Upvotes: 12

Related Questions