RxJS缓存和使用shareReplay刷新

时间:2020-06-29 07:33:39

标签: angular rxjs rxjs-pipeable-operators

我正在对从API检索到的某些数据使用缓存,出于逻辑原因,存储的数据仅在有限的时间内有效,因此我使用了类似的东西:

someApiData$ = this.getData()
    .pipe(shareReplay(1, 3000))

对我来说似乎很明显,但显然shareReplay运算符的创建者并不知道,如果不再缓存数据,则应重新获取它,或者至少我应该拥有另一个参数,它将为我提供此选项,例如:

someApiData$ = this.getData()
    .pipe(shareReplay(1, 3000, shouldRefresh))

相反,下一个订阅者将获得的结果为null。 因此,我正在寻找解决此问题的理想方法。

5 个答案:

答案 0 :(得分:1)

在此线程上的答案和网络上的其他一些方法中进行了一些跟踪后,这就是我的最终结果。它提供了以下能力:

  1. 缓存值
  2. 如果不再缓存数据,则自动刷新值
  3. 直接与Observable
  4. 合作
  5. 如果需要,指定缓存生命周期的持续时间
  6. 整理我的服务并提供可重复使用的解决方案

我的缓存工具:

export class SharedReplayRefresh {

    private sharedReplay$: Observable<T>;
    private subscriptionTime: number;


   sharedReplayTimerRefresh(
       source: Observable<T>, bufferSize: number = 1,
       windowTime: number = 3000000,  scheduler?: SchedulerLike): Observable<T> {

        const currentTime = new Date().getTime();
        if (!this.sharedReplay$ || 
            currentTime - this.subscriptionTime > windowTime) {
            this.sharedReplay$ = source.pipe(shareReplay(
                bufferSize, windowTime, scheduler));
            this.subscriptionTime = currentTime;
        }

        return this.sharedReplay$;
    }
}

我的数据服务:

export class DataService {
    
    constructor(private httpClient: HttpClient) { }

    private dataSource = 
        new SharedReplayRefresh<Data>();
    private source = this.httpClient.get<Data>(url);
    
    get data$(): Observable<Data> {
        return this.dataSource .sharedReplayTimerRefresh(this.source, 1, 1500);
    }
}

答案 1 :(得分:0)

根据documentationwindow运算符的shareReplay参数不能这样工作:

该缓冲区中的项目可能会被丢弃而不会发送给后续观察者的时间(以毫秒为单位)

在您的代码示例中,这意味着3秒钟后新订阅者将一无所获。

我认为处理此问题的最佳方法是使用外部计数器进行处理:

  private cache$: Observable<any>;
  private lastTime: number;

  public getCachedData() {
    if (!this.cache$ || new Date().getTime() - this.lastTime > 3000) {
      this.cache$ = this.getData().pipe(shareReplay(1));
      this.lastTime = new Date().getTime();
    }

    return this.cache$;
  }

每次新订户调用getCachedData()时,此代码将“重新创建”可观察对象。

但是,较旧的订户不会获得新创建的新Observable的更新。为了使所有它们保持同步,您可能需要使用BehaviorSubject来存储数据:

  // Everybody subscribe to this Subject
  private data$ = new BehaviorSubject(null);

  public getCachedData() {
    // TODO check time expiration here and call this.refreshData();
    if(timeExpired) {
      return this.refreshData().pipe(
        mergeMap(data => {
          return this.data$.asObservable();
        })
      );
    } else {
      return this.data$.asObservable();
    }
  }
  
  private refreshData() {
    return this.getData().pipe(
      tap(data => {
        this.data$.next(data);
      })
    );
  }

上述解决方案只是一个想法,应该加以改进和测试。

答案 2 :(得分:0)

这是一种方法:

const URL = 'https://jsonplaceholder.typicode.com/todos/1';

const notifier = new Subject();
const pending = new BehaviorSubject(false);

const cacheEmpty = Symbol('cache empty')

const shared$ = notifier.pipe(
  withLatestFrom(pending),
  filter(([_, isPending]) => isPending === false),

  switchMap(() => (
    console.warn('[FETCHING DATA]'),
    pending.next(true),
    fetch(URL).then(r => r.json())
  )),

  tap(() => pending.next(false)),
  shareReplay(1, 1000),
);

const src$ = shared$.pipe(
  mergeWith(of(cacheEmpty).pipe(delay(0), takeUntil(shared$))),
  tap(v => v === cacheEmpty && notifier.next()),
  filter(v => v !== cacheEmpty)
)

src$.subscribe(v => console.log('[1]', v));

setTimeout(() => {
  src$.subscribe(v => console.log('[2]', v));
}, 500);

setTimeout(() => {
  src$.subscribe(v => console.log('[3]', v));
}, 1200);

StackBlitz

mergeWithimport { merge as mergeWith } from 'rxjs/operators'(我认为从RxJs 7开始,它可以直接以mergeWith的身份访问。)

我的理由是,我需要找到一种方法来确定正在使用的ReplaySubject的缓存是否为空。众所周知,如果缓存不为空并且有新的订阅者到达,它将同步发送缓存的值。

所以

mergeWith(of(cacheEmpty).pipe(delay(0), takeUntil(shared$))),

本质上与

相同
merge(
  shared$,
  of(cacheEmpty).pipe(delay(0), takeUntil(shared$)) // #2
)

如果缓存中有值,则将发出shared$,而#2将被取消订阅。

如果没有值,#2将发出然后完成(它完成的事实不会影响外部的可观察对象)。

接下来,我们看到如果发出了cacheEmpty,那么我们知道该刷新数据了。

tap(v => v === cacheEmpty && notifier.next()), // `notifier.next()` -> time to refresh
filter(v => v !== cacheEmpty)

现在,让我们看一下notifier的工作方式

const shared$ = notifier.pipe(
  
  // These 2 operators + `pending` make sure that if 2 subscribers register one after the other, thus synchronously
  // the source won't be subscribed more than needed
  withLatestFrom(pending),
  filter(([_, isPending]) => isPending === false),

  switchMap(() => (
    console.warn('[FETCHING DATA]'),
    pending.next(true), // If a new subscriber registers while the request is pending, the source won't be requested twice
    fetch(URL).then(r => r.json())
  )),

  // The request has finished, we have the new data
  tap(() => pending.next(false)),

  shareReplay(1, 1000),
);

答案 3 :(得分:0)

我有一个类似的用例,最终使用了以下自定义运算符。

import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';

export const cacheValue = <T>(windowTime: (value: T) => number) => (
  source: Observable<T>,
) => {
  let cache: { value: T; expires: number } | undefined = undefined;
  return new Observable<T>((observer) => {
    if (cache && cache.expires > Date.now()) {
      observer.next(cache.value);
      observer.complete();
    } else {
      return source
        .pipe(
          tap(
            (value) =>
              (cache = { value, expires: Date.now() + windowTime(value) }),
          ),
        )
        .subscribe(observer);
    }
  });
};

如果缓存在100毫秒后到期,则将其称为cacheValue(() => 100),如果API返回的值具有expiresIn属性,则将其称为cacheValue((value) => value.expiresIn)

答案 4 :(得分:0)

您可以更改流的启动方式,在这种情况下,使用 interval 创建一个立即发出的流,然后在满足间隔时使用它来触发数据加载。

当您第一次订阅流时,会触发间隔,加载数据,然后在三秒后再次订阅。

import { interval } from 'rxjs';

const interval$ = interval(3000); // emits straight away then every 3 seconds

interval$ 发出时,使用 switchMap 切换出 Observable 并使用 shareReplay 以允许多播。

// previous import
import { switchMap, shareReplay } from 'rxjs/operators';

// previous code

const data$ = interval$.pipe(
    switchMap(() => getData()),
    shareReplay()
);

您还可以将 interval$ 包装在 merge 中,以便您可以像 Subject 一样根据 interval 创建手动刷新。

import { BehaviorSubject, merge, interval } from "rxjs";
import { shareReplay, switchMap } from "rxjs/operators";

const interval$ = interval(3000);
const reloadCacheSubject = new BehaviorSubject(null);

const data$ = merge(reloadCacheSubject, interval$).pipe(
  switchMap(() => getData()),
  shareReplay()
);

reloadCacheSubject.next(null); // causes a reload

StackBlitz 示例与 mergerefreshCache Subject