Как к использованию ReactiveCocoa, чтобы прозрачно подтвердить подлинность прежде, чем сделать вызовы API?

Я использую ReactiveCocoa в приложении, которое делает звонки отдаленной веб-ПЧЕЛЕ. Но прежде чем любая вещь может быть восстановлена от данного хозяина API, приложение должно обеспечить верительные грамоты пользователя и восстановить символ API, который тогда используется, чтобы подписать последующие запросы.

Я хочу резюмировать далеко этот процесс аутентификации так, чтобы это произошло автоматически каждый раз, когда я делаю вызов API. Предположите, что у меня есть класс клиента API, который содержит верительные грамоты пользователя.

// getThing returns RACSignal yielding the data returned by GET /thing.
// if the apiClient instance doesn't already have a token, it must
// retrieve one before calling GET /thing 
RAC(self.thing) = [apiClient getThing]; 

Как я могу использовать ReactiveCocoa, чтобы прозрачно вызвать первое (и только первое) просят к API восстановить и, как побочный эффект, безопасно сохранить символ API, прежде чем с какими-либо последующими просьбами обратятся?

Это - также требование, чтобы я мог использовать combineLatest: (или подобный), чтобы начать многократные одновременные запросы и что они будут все неявно ждать символа, который будет восстановлен.

RAC(self.tupleOfThisAndThat) = [RACSignal combineLatest:@[ [apiClient getThis], [apiClient getThat]]];

Далее, если запрос восстанавливать-символа уже находится в полете, когда вызов API сделан, тот вызов API должен ждать, пока запрос восстанавливать-символа не закончил.

Мое частичное решение следует:

Основной образец будет, чтобы использовать flattenMap: , чтобы нанести на карту сигнал, который приводит к символу сигналу, который, учитывая символ, выполняет желаемый запрос и приводит к результату вызова API.

Принятие, что некоторые удобные расширения NSURLRequest :

- (RACSignal *)requestSignalWithURLRequest:(NSURLRequest *)urlRequest {
    if ([urlRequest isSignedWithAToken])
        return [self performURLRequest:urlRequest];

    return [[self getToken] flattenMap:^ RACSignal * (id token) {
        NSURLRequest *signedRequest = [urlRequest signedRequestWithToken:token];
        assert([urlRequest isSignedWithAToken]);
        return [self requestSignalWithURLRequest:signedRequest];
    }
}

Теперь рассмотрите подписное внедрение -getToken .

  • В тривиальном случае, когда символ был уже восстановлен, подписка, немедленно приводит к символу.
  • , Если символ не был восстановлен, подписка, подчиняется вызову API идентификации, который возвращает символ.
  • , Если вызов API идентификации находится в полете, должно быть безопасно добавить другого наблюдателя, не заставляя вызов API идентификации быть повторенным по проводу.

Однако, я не уверен, как сделать это. Кроме того, как и где безопасно сохранить символ? Некоторый постоянный/повторяемый сигнал?

25
nl ja de

3 ответы

Так, есть две главных вещи, продолжающиеся здесь:

  1. Вы хотите разделить некоторые побочные эффекты (в этом случае, принося символ), не повторно вызывая их каждый раз, есть новый подписчик.
  2. Вы хотите, чтобы любой подписывающийся <�закодировал>-getToken , чтобы получить те же самые ценности несмотря ни на что.

Чтобы разделить побочные эффекты (#1 выше), мы будем использовать RACMulticastConnection. Как документация говорит:

Связь передачи заключает в капсулу идею разделить одну подписку на сигнал многим подписчикам. Это чаще всего необходимо, если подписка на основной сигнал включает побочные эффекты или не должна быть названа несколько раз.

Давайте добавим одного из тех как частная собственность на классе клиента API:

@interface APIClient ()
@property (nonatomic, strong, readonly) RACMulticastConnection *tokenConnection;
@end

Теперь, это решит случай нынешних подписчиков N, которые весь нуждаются тот же самый будущий результат (Вызовы API, ждущие на символе запроса, являющемся в полете), но нам все еще нужно что-то еще, чтобы гарантировать, чтобы будущее подписчики получили тот же самый результат (уже принесенный символ), неважно когда они подписываются.

Это что RACReplaySubject для:

Предмет переигровки экономит ценности, его посылают (до его определенной способности) и отправляет тех новым подписчикам. Это также переиграет ошибку или завершение.

Чтобы связать эти два понятия, мы можем использовать RACSignal's - передача: метод, который превращает нормальный сигнал в связь при помощи определенный вид предмета.

Мы можем соединить большинство поведений во время инициализации:

- (id)init {
    self = [super init];
    if (self == nil) return nil;

   //Defer the invocation of -reallyGetToken until it's actually needed.
   //The -defer: is only necessary if -reallyGetToken might kick off
   //a request immediately.
    RACSignal *deferredToken = [RACSignal defer:^{
        return [self reallyGetToken];
    }];

   //Create a connection which only kicks off -reallyGetToken when
   //-connect is invoked, shares the result with all subscribers, and
   //pushes all results to a replay subject (so new subscribers get the
   //retrieved value too).
    _tokenConnection = [deferredToken multicast:[RACReplaySubject subject]];

    return self;
}

Затем мы осуществляем -getToken , чтобы вызвать усилие лениво:

- (RACSignal *)getToken {
   //Performs the actual fetch if it hasn't started yet.
    [self.tokenConnection connect];

    return self.tokenConnection.signal;
}

Впоследствии, что-либо, что подписывается на результат -getToken (как -requestSignalWithURLRequest: ), получит символ, если это еще не было принесено, начните приносить его при необходимости или ждите запроса в полете, если есть тот.

45
добавлено
@JustinSpahr-Summers я думаю, используя ReactiveCocoa, чтобы обработать сетевые запросы, такой общий вариант использования, что там мог бы существовать возможность для некоторого универсального класса клиента API, который (главным образом) прозрачно обращается с вещами как логин, выход из системы, сетевое наблюдение доступности, повторите на ошибке, и т.д. Мысли?
добавлено автор Tony, источник
@ColinBarrett Каждый из тех потребовал бы подробного ответа самостоятельно – это было просто простым решением проблемы, изложенной выше. Поддержка выхода из системы могла бы включить помещение tokenSignal в RACReplaySubject способности 1, так, чтобы можно было выдвинуть новый сигнал на него по желанию. Многократные счета были бы намного большим изменением, потому что assumably API запроса должен будет быть обновлен также. I' d быть рад ответить или более подробно в новом вопросе на ТАК или проблеме о GitHub.
добавлено автор Justin Spahr-Summers, источник
@Tony It' s трудно, чтобы знать, что могло на самом деле резюмироваться из этого, так как у RAC уже есть примитивы, которые сделают тяжелый подъем для вас. Если у вас есть идеи, не стесняйтесь регистрировать проблему о RAC repo, и мы можем говорить об этом более подробно: github.com/ReactiveCocoa/ReactiveCocoa/issues
добавлено автор Justin Spahr-Summers, источник
это - яркий пример того, как задача, которая обычно была бы довольно сложным использующим "стандартным" какао, может стать ПУТЕМ, более простым при помощи потрясающего RAC.!
добавлено автор jere, источник
Как был бы вы обращаться выйти из системы? Или многократные счета?
добавлено автор Colin Barrett, источник
@JustinSpahr-Summers я был более справедлив любопытный, don' t на самом деле имеют потребность самостоятельно.
добавлено автор Colin Barrett, источник
Потрясающее объяснение.Thank you!
добавлено автор bvanderveen, источник
Выбросьте случай APIClient, чтобы выйти из системы и создать новый с различными верительными грамотами, чтобы зарегистрироваться, въезжают задним ходом. Используйте многократные случаи APIClient, чтобы поддержать многократные счета.
добавлено автор bvanderveen, источник

Как насчет

...

@property (nonatomic, strong) RACSignal *getToken;

...

- (id)init {
    self = [super init];
    if (self == nil) return nil;

    self.getToken = [[RACSignal defer:^{
        return [self reallyGetToken];
    }] replayLazily];
    return self;
}

Безусловно, это решение функционально идентичный ответу Джастина выше. В основном мы используем в своих интересах то, что удобный метод уже существует в RACSignal общественный API:)

3
добавлено

Взгляды о символе истекут позже, и мы должны освежить его.

Я храню символ в MutableProperty и использовал замок, чтобы предотвратить многократную просьбу с истекшим сроком освежить символ, когда-то символ получен или освежен, просто просите снова с новым символом.

Для первых нескольких запросов с тех пор нет никакого символа, сигнал запроса будет flatMap к ошибке, и таким образом вызывать refreshAT, между тем мы не имеем refreshToken, таким образом вызываем refreshRT и устанавливаем и в и rt в заключительном шаге.

вот полный код

static var headers = MutableProperty(["TICKET":""])
static let atLock = NSLock()
static let manager = Manager(
    configuration: NSURLSessionConfiguration.defaultSessionConfiguration()
)

internal static func GET(path:String!, params:[String: String]) -> SignalProducer<[String: AnyObject], NSError> {
    let reqSignal = SignalProducer<[String: AnyObject], NSError> {
        sink, dispose in
        manager.request(Router.GET(path: path, params: params))
        .validate()
        .responseJSON({ (response) -> Void in
            if let error = response.result.error {
                sink.sendFailed(error)
            } else {
                sink.sendNext(response.result.value!)
                sink.sendCompleted()
            }
        })
    }

    return reqSignal.flatMapError { (error) -> SignalProducer<[String: AnyObject], NSError> in
            return HHHttp.refreshAT()
        }.flatMapError({ (error) -> SignalProducer<[String : AnyObject], NSError> in
            return HHHttp.refreshRT()
        }).then(reqSignal)
}

private static func refreshAT() -> SignalProducer<[String: AnyObject], NSError> {
    return SignalProducer<[String: AnyObject], NSError> {
        sink, dispose in
        if atLock.tryLock() {
            Alamofire.Manager.sharedInstance.request(.POST, "http://example.com/auth/refresh")
                .validate()
                .responseJSON({ (response) -> Void in
                    if let error = response.result.error {
                        sink.sendFailed(error)
                    } else {
                        let v = response.result.value!["data"]
                        headers.value.updateValue(v!["at"] as! String, forKey: "TICKET")
                        sink.sendCompleted()
                    }
                    atLock.unlock()
                })
        } else {
            headers.signal.observe(Observer(next: { value in
                print("get headers from local: \(value)")
                sink.sendCompleted()
            }))
        }
    }
}

private static func refreshRT() -> SignalProducer<[String: AnyObject], NSError> {
    return SignalProducer<[String: AnyObject], NSError> {
        sink, dispose in
        Alamofire.Manager.sharedInstance.request(.POST, "http://example.com/auth/refresh")
        .responseJSON({ (response) -> Void in
            let v = response.result.value!["data"]                
            headers.value.updateValue(v!["at"] as! String, forKey: "TICKET")                
            sink.sendCompleted()
        })
    }
}
0
добавлено