Самый короткий код для кэширования HTTP-запроса Rxjs пока не завершен?

Я пытаюсь создать наблюдаемый поток, который удовлетворяет следующим требованиям:

  • Загрузка данных из хранилища во время подписки
  • Если данные еще не истекли, верните наблюдаемое значение
  • Если данные истекли, верните HTTP-запрос, наблюдаемый, который использует токен обновления, чтобы получить новое значение и сохранить его
    • Если этот код достигнут снова до завершения запроса, верните тот же запрос, который можно наблюдать
    • Если этот код достигнут после завершения предыдущего запроса или с другим токеном обновления, запустите новый запрос

Я знаю, что есть много разных ответов о том, как выполнить шаг (3), но поскольку я пытаюсь выполнить эти шаги вместе, я ищу руководство относительно того, было ли решение, с которым я столкнулся, наиболее кратким это может быть (что я сомневаюсь!).

Здесь образец, демонстрирующий мой текущий подход:

var cachedRequestToken;
var cachedRequest;

function getOrUpdateValue() {
    return loadFromStorage()
      .flatMap(data => {
        // data doesn't exist, shortcut out
        if (!data || !data.refreshtoken) 
            return Rx.Observable.empty();

        // data still valid, return the existing value
        if (data.expires > new Date().getTime())
            return Rx.Observable.return(data.value);

        // if the refresh token is different or the previous request is 
        // complete, start a new request, otherwise return the cached request
        if (!cachedRequest || cachedRequestToken !== data.refreshtoken) { 
            cachedRequestToken = data.refreshtoken;

            var pretendHttpBody = {
                value: Math.random(),
                refreshToken: Math.random(),
                expires: new Date().getTime() + (10 * 60 * 1000) // set by server, expires in ten minutes
            };

            cachedRequest = Rx.Observable.create(ob => { 
                // this would really be a http request that exchanges
                // the one use refreshtoken for new data, then saves it
                // to storage for later use before passing on the value

                window.setTimeout(() => { // emulate slow response   
                    saveToStorage(pretendHttpBody);
                    ob.next(pretendHttpBody.value);                 
                    ob.completed(); 
                    cachedRequest = null; // clear the request now we're complete
                }, 2500);
          });
        }

        return cachedRequest;
    });
}

function loadFromStorage() {
    return Rx.Observable.create(ob => {
        var storedData = {  // loading from storage goes here
            value: 15,      // wrapped in observable to delay loading until subscribed
            refreshtoken: 63, // other process may have updated this between requests
            expires: new Date().getTime() - (60 * 1000) // pretend to have already expired
        };

        ob.next(storedData);
        ob.completed();
    })
}

function saveToStorage(data) {
    // save goes here
}

// first request
getOrUpdateValue().subscribe(function(v) { console.log('sub1: ' + v); }); 

// second request, can occur before or after first request finishes
window.setTimeout(
    () => getOrUpdateValue().subscribe(function(v) { console.log('sub2: ' + v); }),
    1500); 

Ответ 1

Сначала рассмотрим рабочий пример jsbin.

Решение немного отличается от исходного кода, и я хотел бы объяснить, почему. Необходимость продолжать возвращаться в локальное хранилище, сохранять его, сохранять флаги (кеш и токен) не подходит для меня с реактивным, функциональным подходом. Сердце решения, которое я дал, это:

var data$ = new Rx.BehaviorSubject(storageMock);
var request$ = new Rx.Subject();
request$.flatMapFirst(loadFromServer).share().startWith(storageMock).subscribe(data$);
data$.subscribe(saveToStorage);

function getOrUpdateValue() {
    return data$.take(1)
      .filter(data => (data && data.refreshtoken))
      .switchMap(data => (data.expires > new Date().getTime() 
                    ? data$.take(1)
                    : (console.log('expired ...'), request$.onNext(true) ,data$.skip(1).take(1))));
}

Ключ в том, что data$ содержит ваши последние данные и всегда в актуальном состоянии, его легко получить, выполнив data$.take(1). take(1) важно убедиться, что ваша подписка получает отдельные значения и заканчивается (потому что вы пытаетесь работать в процедурном, а не в функциональном порядке). Без take(1) ваша подписка останется активной, и у вас будет несколько обработчиков, то есть вы будете обрабатывать будущие обновления, а также в коде, предназначенном только для текущего обновления.

Кроме того, у меня есть объект request$, который является вашим способом начать получать новые данные с сервера. Функция работает так:

  • Фильтр гарантирует, что если ваши данные пустые или не имеют токена, ничего не проходит, как у return Rx.Observable.empty().
  • Если данные обновлены, он возвращает data$.take(1), который представляет собой единую последовательность элементов, на которую вы можете подписаться.
  • Если нет, он нуждается в обновлении. Для этого он запускает request$.onNext(true) и возвращает data$.skip(1).take(1). skip(1) заключается в том, чтобы избежать текущего, исходящего значения.

Для краткости я использовал (console.log('expired ...'), request$.onNext(true) ,data$.skip(1).take(1))). Это может показаться немного загадочным. Он использует синтаксис, разделенный запятыми js, который является общим в minifiers/uglifiers. Он выполняет все утверждения и возвращает результат последнего оператора. Если вам нужен более читаемый код, вы можете переписать его так:

.switchMap(data => {
  if(data.expires > new Date().getTime()){ 
    return data$.take(1);
  } else {
    console.log('expired ...');
    request$.onNext(true);
    return data$.skip(1).take(1);
  }
});

Последняя часть - это использование flatMapFirst. Это гарантирует, что после выполнения запроса все последующие запросы будут удалены. Вы можете видеть, что он работает в распечатке консоли. "Нагрузка с сервера" печатается несколько раз, но фактическая последовательность вызывается только один раз, и вы получаете одну распечатку "загрузка с сервера". Это более реактивное ориентированное решение для проверки исходного флештокера.

Хотя мне не нужны сохраненные данные, он сохраняется, потому что вы упомянули, что можете читать его на будущих сеансах.

Несколько советов по rxjs:

  • Вместо использования setTimeout, который может вызвать множество проблем, вы можете просто сделать Rx.Observable.timer(time_out_value).subscribe(...).

  • Создание наблюдаемого является громоздким (вам даже приходилось звонить next(...) и complete()). У вас есть более чистый способ сделать это, используя Rx.Subject. Обратите внимание, что у вас есть спецификации этого класса, BehaviorSubject и ReplaySubject. Эти классы заслуживают внимания и могут многое помочь.

Одна последняя заметка. Это было довольно сложно:-) Я не знаком с вашим кодом на стороне сервера и соображениями дизайна, но необходимость подавлять звонки мне неудобно. Если не существует очень веской причины, связанной с вашим бэкэнд, моим естественным подходом было бы использовать flatMap и позволить последнему запросу "выиграть", т.е. Отменить предыдущие незавершенные вызовы и установить значение.

Код основан на rxjs 4 (поэтому он может работать в jsbin), если вы используете angular2 (следовательно, rxjs 5), вам нужно будет его адаптировать. Посмотрите руководство по миграции.

==================================================================================================================================================================================================================

Существует одна статья Я могу порекомендовать. Это название говорит все: -)

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

let token$ = data$.pluck('refreshtoken');

а затем потребляйте его при необходимости.

Мой общий подход состоит в том, чтобы сначала отобразить мои потоки данных и отношения, а затем, как хороший "водопроводчик клавиатуры" (как и все мы), постройте трубопровод. Мой проект верхнего уровня для службы будет (пропустить формальности angular2 и поставщика для краткости):

class UserService {
  data$: <as above>;
  token$: data$.pluck('refreshtoken');
  private request$: <as above>;

  refresh(){
    request.onNext(true);
  }
}

Возможно, вам потребуется выполнить некоторую проверку, чтобы pluck не выходил из строя.

Затем каждый компонент, которому нужны данные или токен, может получить к нему доступ напрямую.

Теперь давайте предположим, что у вас есть служба, которая должна действовать при изменении данных или токена:

class SomeService {
  constructor(private userSvc: UserService){
    this.userSvc.token$.subscribe(() => this.doMyUpdates());
  }
}

Если вам нужно синтезировать данные, то есть использовать данные/токен и некоторые локальные данные:

Rx.Observable.combineLatest(this.userSvc.data$, this.myRelevantData$)
  .subscribe(([data, myData] => this.doMyUpdates(data.someField, myData.someField));

Опять же, философия состоит в том, что вы создаете поток данных и каналы, подключаете их, а затем все, что вам нужно сделать, это вызвать материал.

"Мини-шаблон", который я придумал, - это перейти к службе после моей последовательности триггеров и зарегистрировать результат. Возьмем, например, автозаполнение:

class ACService {
   fetch(text: string): Observable<Array<string>> {
     return http.get(text).map(response => response.json().data;
   }
}

Затем вы должны вызывать его каждый раз, когда ваш текст изменяется и присваивает результат вашему компоненту:

<div class="suggestions" *ngFor="let suggestion; of suggestions | async;">
  <div>{{suggestion}}</div>
</div>

и в вашем компоненте:

onTextChange(text) {
  this.suggestions = acSVC.fetch(text);
}

но это можно сделать так:

class ACService {
   createFetcher(textStream: Observable<string>): Observable<Array<string>> {
     return textStream.flatMap(text => http.get(text))
       .map(response => response.json().data;
   }
}

И затем в вашем компоненте:

textStream: Subject<string> = new Subject<string>();
suggestions: Observable<string>;

constructor(private acSVC: ACService){
  this.suggestions = acSVC.createFetcher(textStream);
}

onTextChange(text) {
  this.textStream.next(text);
}

код шаблона остается прежним.

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

onTextChange(text) {
  this.suggestions = acSVC.fetch(text);
  this.suggestionsCount = suggestions.pluck('length'); // in a sequence
  // or
  this.suggestions.subscribe(suggestions => this.suggestionsCount = suggestions.length); // in a numeric variable.
}

Теперь во втором методе вы просто определяете:

constructor(private acSVC: ACService){
  this.suggestions = acSVC.createFetcher(textStream);
  this.suggestionsCount = this.suggestions.pluck('length');
}

Надеюсь, что это поможет: -)

Во время написания я попытался задуматься о том, как я привык к тому, чтобы использовать такой реактивный способ. Излишне говорить о том, что при проведении экспериментов многие jsbins и странные неудачи - большая часть этого. Еще одна вещь, которая, по моему мнению, помогла сформировать мой подход (хотя в настоящее время я его не использую), учит сокращению и чтению/попытке немного ngrx (angular redux port). Философия и подход не позволяют вам даже думать о процедуре, поэтому вам нужно настроиться на функциональные, данные, отношения и потоки, основанные на мышлении.