使用ReactiveCocoa重试异步操作

时间:2014-01-20 19:32:52

标签: ios frp reactive-cocoa

我正在使用ReactiveCocoa信号来表示对我们系统中RESTful后端的调用。每个RESTful调用都应该接收一个令牌作为参数之一。令牌本身是从身份验证API调用接收的。

一切正常,我们现在引入了令牌过期,因此如果API调用因HTTP代码403失败,后端访问类可能需要重新授权。我想让这个操作对调用者完全透明,这就是最好的我想出来了:

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

这是正确的方法吗?

1 个答案:

答案 0 :(得分:12)

首先,通常应尽可能避免使用subscriptionssubjects。特别是嵌套订阅是一种反模式 - 通常有信号运算符可以替代它们。

在这种情况下,我们需要利用信号可以表示延迟工作的事实,并且只创建一个信号来执行实际请求:

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

在此使用+defer:可确保no work will begin until subscription。一个重要的推论是,通过多次订阅可以重复

例如,如果我们发现错误,我们可以尝试获取令牌,然后返回相同的延迟信号以指示应该再次尝试:

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

使用-replay替换之前的RACReplaySubject,并立即启动请求;但是,它也可以-replayLazily甚至完全消除(每次订阅重拨一次)。

就是这样!重要的是要指出,仅仅设置将要执行的工作不需要显式订阅。订阅通常应该只发生在程序的“离开”处 - 调用者实际上要求执行工作。