7

I'm using ReactiveCocoa signals to represent calls to RESTful backend in our system. Each RESTful invocation should receive a token as one of the parameters. The token itself is received from authentication API call.

All works fine and we're now introduced token expiration, so the backend access class may need to reauthorize itself if the API call fails with HTTP code 403. I want to make this operation completely transparent for the callers, this is the best I came up with:

- (RACSignal *)apiCallWithSession:(Session *)session base:(NSString *)base params:(NSDictionary *)params get:(BOOL)get {
    NSMutableDictionary* p = [params mutableCopy];
    p[@"token"] = session.token;

    RACSubject *subject = [RACReplaySubject subject];

    RACSignal *first = [self apiCall:base params:p get:get];  // this returns the signal representing the asynchronous HTTP operation

    @weakify(self);
    [first subscribeNext:^(id x) {
        [subject sendNext:x];   // if it works, all is fine
    } error:^(NSError *error) {
        @strongify(self);

        // if it doesn't work, try re-requesting a token
        RACSignal *f = [[self action:@"logon" email:session.user.email password:session.user.password]
                         flattenMap:^RACStream *(NSDictionary *json) {  // and map it to the other instance of the original signal to proceed with new token
            NSString *token = json[@"token"];

            p[@"token"] = token;
            session.token = token;

            return [self apiCall:base params:p get:get];
        }];

        // all signal updates are forwarded, we're only re-requesting token once            
        [f subscribeNext:^(id x) {
            [subject sendNext:x];
        } error:^(NSError *error) {
            [subject sendError:error];
        } completed:^{
            [subject sendCompleted];
        }];
    } completed:^{
        [subject sendCompleted];
    }];

    return subject;
}

Is this the right way to do it?

Sergey Mikhanov
  • 8,880
  • 9
  • 44
  • 54

1 Answers1

12

First of all, subscriptions and subjects should generally be avoided as much as possible. Nested subscriptions, in particular, are quite an anti-pattern—usually there are signal operators that can replace them.

In this case, we need to take advantage of the fact that signals can represent deferred work, and create only one signal to perform the actual request:

// This was originally the `first` signal.
RACSignal *apiCall = [RACSignal defer:^{
    return [self apiCall:base params:p get:get];
}];

The use of +defer: here ensures that no work will begin until subscription. An important corollary is that the work can be repeated by subscribing multiple times.

For example, if we catch an error, we can try fetching a token, then return the same deferred signal to indicate that it should be attempted again:

return [[apiCall
    catch:^(NSError *error) {
        // If an error occurs, try requesting a token.
        return [[self
            action:@"logon" email:session.user.email password:session.user.password]
            flattenMap:^(NSDictionary *json) {
                NSString *token = json[@"token"];

                p[@"token"] = token;
                session.token = token;

                // Now that we have a token, try the original API call again.
                return apiCall;
            }];
    }]
    replay];

The use of -replay replaces the RACReplaySubject that was there before, and makes the request start immediately; however, it could also be -replayLazily or even eliminated completely (to redo the call once per subscription).

That's it! It's important to point out that no explicit subscription was needed just to set up the work that will be performed. Subscription should generally only occur at the "leaves" of the program—where the caller actually requests that work be performed.

Justin Spahr-Summers
  • 16,893
  • 2
  • 61
  • 79
  • Thanks for the answer! All looks fine to me except for the fact that `p` variable is not updated. In my code snippet, `[self apiCall:params:get:]` is called twice with *different* values of `p` (the second invocation happens with an updated token). In your snippet, this may or may not be the case (depends on whether different parts of the code are sharing the object reference). What is a good way to make the parameter update explicit? – Sergey Mikhanov Jan 22 '14 at 14:57
  • `p` is updated the same way in the above sample. As long as it's a mutable dictionary, the block will see the update. If you'd like to make it more explicit, you could turn `apiCall` into a block which _accepts_ the `p` dictionary, then _returns_ the `RACSignal` for the API call. – Justin Spahr-Summers Jan 22 '14 at 19:18
  • What if you want to try getting the token either a fixed number of times or repeatedly until it's correct? I'm thinking of the case where you're get credentials from the user and so want to either give the user a chance or two to correct things or halt going forward until they get it right. – Ayal Apr 09 '14 at 14:42
  • @Ayal Can you ask that in a separate question? A comment here wouldn't do it justice. – Justin Spahr-Summers Apr 09 '14 at 15:26
  • done - http://stackoverflow.com/questions/22969368/authenticating-with-reactivecocoa – Ayal Apr 10 '14 at 17:56
  • @JustinSpahr-Summers It seems, that your catch block is also required `@weakify(self);` before and `@strongify(self);` inside, right? – skywinder Aug 13 '14 at 07:22
  • 2
    @skywinder My example assumes the signal is finite, meaning it'll complete or error in some reasonable amount of time. When a signal terminates, and all references to it are lost, its blocks will be deallocated, breaking any retain cycle. I avoid the weak/strong dance in cases like this, because it can be unsafe if variables suddenly change to `nil` in the middle of a signal chain. – Justin Spahr-Summers Aug 13 '14 at 16:53