【问题标题】:Retrying an asynchronous operation using ReactiveCocoa使用 ReactiveCocoa 重试异步操作
【发布时间】:2014-01-20 19:32:52
【问题描述】:

我正在使用 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;
}

这是正确的做法吗?

【问题讨论】:

    标签: ios frp reactive-cocoa


    【解决方案1】:

    首先,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 甚至完全消除(每次订阅重做一次调用)。

    就是这样!需要指出的是,仅仅设置将要执行的工作不需要显式订阅。订阅通常应该只发生在程序的“叶子”——调用者实际请求执行工作的地方。

    【讨论】:

    • 感谢您的回答!除了p 变量没有更新之外,对我来说一切都很好。在我的代码 sn-p 中,[self apiCall:params:get:] 被调用两次,不同的 值为p(第二次调用发生在更新的令牌中)。在您的 sn-p 中,这可能是也可能不是(取决于代码的不同部分是否共享对象引用)。什么是明确参数更新的好方法?
    • p 在上述示例中的更新方式相同。只要它是一个可变字典,块就会看到更新。如果您想让它更明确,您可以将apiCall 变成一个接受 p 字典的块,然后返回 RACSignal API 调用。
    • 如果您想尝试获取令牌固定次数或重复直到正确为止怎么办?我正在考虑这样一种情况,您从用户那里获得了凭据,因此希望给用户一两个机会来纠正问题,或者在他们做对之前停止前进。
    • @Ayal 你能在一个单独的问题中问这个吗?此处的评论不公平。
    • @skywinder 我的示例假设信号是有限的,这意味着它会在合理的时间内完成或出错。当一个信号终止并且对它的所有引用都丢失时,它的块将被释放,从而中断任何保留周期。在这种情况下,我会避免弱/强舞蹈,因为如果变量在信号链中间突然变为nil,这可能是不安全的。
    猜你喜欢
    • 2017-03-03
    • 2018-11-23
    • 1970-01-01
    • 1970-01-01
    • 2023-03-16
    • 1970-01-01
    • 2017-01-26
    • 1970-01-01
    • 2012-05-20
    相关资源
    最近更新 更多