Сигнал с побочными эффектами, полученный из другого сигнала, несколько подписчиков

Хорошо, это меня немного озадачило.

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

Двухэтапный процесс:

  1. Получить токен сеанса
  2. Убедитесь, что токен сеанса работает, вызвав конечную точку API.

Я попытаюсь все упростить, но у меня есть объект, назовем его UserSession, который имеет простое свойство isLoggedIn, которое возвращает YES, если у пользователя есть токен сеанса, и NO, если его нет. Это значение изменяется и выдает обычные уведомления KVO, когда токен сеанса извлекается и устанавливается для объекта UserSession. Я могу наблюдать за этим свойством, используя RACObserve, если я просто хочу знать, когда у меня есть токен.

Что я действительно хочу сделать, так это иметь свойство UserSession с именем authenticated, которое возвращает RACSignal. Этот сигнал должен:

  • Выдать НЕТ, если isLoggedIn изменится на НЕТ
  • Выдать YES, если isLoggedIn изменится на YES и запрос на проверку выполнен успешно.
  • Выдайте NO, если isLoggedIn изменится на YES и запрос на проверку завершится неудачно.

Простая наивная реализация выглядит так:

- (RACSignal *)authenticated
{
  if (_authenticated == nil) {
    _authenticated = [RACObserve(self, isLoggedIn) flattenMap:^id(NSNumber *isLoggedIn) {
      if (isLoggedIn.boolValue) {
        // does the async HTTP request, wrapped up in a signal that emits YES/NO, or error
        // then completes.
        return [self verifySessionToken];
      }
      return [RACSignal return:@NO];
    }];
  }
  return _authenticated;
}

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

Я пытался использовать многоадресное соединение, заключая [self doVerificationRequest] в блок defer, выполняя многоадресную рассылку, а затем возвращая многоадресный сигнал внутри блока flattenMap. Такой способ работает — он предотвращает множественные запросы проверки — но последующие изменения свойства isLoggedIn не вызывают новый запрос проверки.

Чтобы было ясно, следующая последовательность работает, как и ожидалось:

  1. Нет токена сеанса, isLoggedIn начинается с NO
  2. authenticated излучает NO
  3. Пользователь входит в систему, получает токен сеанса
  4. isLoggedIn меняется на YES, инициирует запрос подтверждения
  5. Запрос проверки выполнен успешно, authenticated выдает YES

Следующая последовательность не работает:

  1. Присутствует токен сеанса с истекшим сроком действия, isLoggedIn начинается с YES
  2. Запрос проверки запущен, он не работает
  3. authenticated излучает NO
  4. В ответ на это отображается экран входа в систему, пользователь входит в систему, получает новый токен сеанса.
  5. isLoggedIn должен передать еще один YES своему RACObserve и вызвать еще один запрос на проверку, но этого никогда не происходит.

Есть ли способ добиться того, чего я хочу здесь?

Изменить: это была моя попытка многоадресной рассылки:

- (RACSignal *)authenticated
{
    if (_authenticated == nil) {
        RACSignal *deferredVerification = [RACSignal defer:^RACSignal *{
            return [self verifySessionToken];
        }];

        self.tokenVerificationConnection = [deferredVerification publish];

        _authenticated = [RACObserve(self, isLoggedIn) flattenMap:^id(NSNumber *isLoggedIn) {
            if (isLoggedIn.boolValue) {
                return [self.tokenVerificationConnection autoconnect];
            }
            return [RACSignal return:@NO];
        }];
    }
    return _authenticated;
}

Это также, по-видимому, ведет себя в основном так же, с меньшим количеством кода, но с тем же поведением, что и выше. Я добавил блоки do, чтобы попытаться визуализировать происходящее:

- (RACSignal *)authenticated
{
    if (_authenticated == nil) {
        @weakify(self);

        _authenticated = [[[[RACObserve(self, isLoggedIn) doNext:^(id x) {
            NSLog(@"LOGGED IN %@", x);
        }] flattenMap:^id(NSNumber *isLoggedIn) {
            @strongify(self);

            if (isLoggedIn.boolValue) {
                return [self verifySessionToken];
            }
            return [RACSignal return:@NO];
        }] doNext:^(id x) {
            NSLog(@"AUTH: %@", x);
        }] replay];
    }
    return _authenticated;
}

В приведенном выше сценарии 2 я никогда не вижу вызовов журнала LOGGED IN или AUTH, когда токен сеанса устанавливается после входа в систему.


person Luke Redpath    schedule 17.09.2014    source источник


Ответы (1)


Ну, похоже, я нашел свой собственный ответ. Я был недалек ни с одним из моих решений для многоадресной передачи/воспроизведения. Проблема заключалась в том, что сигнал, возвращаемый [self verifySessionToken], отправлял ошибку, если соединение каким-либо образом терпело неудачу, что нарушало все дело.

Я мог бы исправить это, отправив @NO вместо ошибки, но я решил оставить все как есть и сделать обработку ошибок явной.

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

Это было мое окончательное рабочее решение:

- (RACSignal *)authenticated
{
    if (_authenticated == nil) {
        @weakify(self);

        _authenticated = [[RACObserve(self, isLoggedIn) flattenMap:^id(NSNumber *isLoggedIn) {
            @strongify(self);

            if (isLoggedIn.boolValue) {
                return [[self verifySessionToken] catch:^RACSignal *(NSError *error) {
                    DDLogError(@"Error verifying session token");
                    return [RACSignal return:@NO];
                }];
            }
            return [RACSignal return:@NO];
        }] replay];
    }
    return _authenticated;
}
person Luke Redpath    schedule 17.09.2014