问题
Lets say that I have the following events:
- DoSomething
- FetchSomething
- FetchSomethingSuccessful
DoSomething
does something that needs some cached data. When I fire off the event I want to look in the cache and do something with it if it exists. If it doesn't then I want to fetch it, wait for it to be in cache, and try again.
I came up with the following solution but it feels like I'm abusing the map
operator. Is there a more appropriate operator to use to observe the stream and throw an error or achieve the effect which is a retry?
const DO_SOMETHING = 'DO_SOMETHING';
const FETCH_SOMETHING = 'FETCH_SOMETHING';
const FETCH_SOMETHING_SUCCESSFUL = 'FETCH_SOMETHING_SUCCESSFUL';
const events = new Rx.Subject();
const cache = new Rx.BehaviorSubject(null);
// the magic sauce
const cacheInitializer = cache
// is there a better way than this?
.map(x => {
if (!x) { throw 'empty'; }
return x;
})
.retryWhen(x => {
console.log('Cache is empty');
events.next(FETCH_SOMETHING);
return events.filter(x => x === FETCH_SOMETHING_SUCCESSFUL)
.first();
});
// fake getting the data
events.filter(x => x === FETCH_SOMETHING)
.do(() => { console.log('Fetching data'); })
.delay(1000)
.subscribe(x => {
console.log('Data fetched and put into cache');
cache.next({ data: 1 });
events.next(FETCH_SOMETHING_SUCCESSFUL);
});
// handle doing something
events.filter(x => x === DO_SOMETHING)
.do(() => { console.log('Check the cache'); })
.switchMapTo(cacheInitializer)
.subscribe(x => {
console.log('Do something', x);
});
events.next(DO_SOMETHING);
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.5.6/Rx.min.js"></script>
Context: I am using ngrx and effects. FETCH_SOMETHING
would trigger another effect and FETCH_SOMETHING_SUCCESSFUL
would indicate that the effect completed successfully. My cache in this case is ngrx. I don't think that I would want to cache in the API layer since that would still cause me to update state from the cached response rather than just relying on the data in state.
回答1:
You want a simple shareReplay
, but you need rxjs 5.5.0 or later as there was a bug that has been fixed.
const cached$ = request$.shareReplay(1);
This will trigger the request when the first subscription is made, but subsequent subscriptions will use the cached value.
Errors are passed to the subscriber and then the internal subject is destroyed so that the error itself isn't cached. This makes the observable retryable. So you can attach whatever retry logic you want (such as retrying until successful).
Lastly, the cache also persists if the refCount goes to 0 at some point.
shareReplay
also takes a second argument, much like the ReplaySubject
constructor, defining a time window for which to keep the cache.
// Faked request which sometimes errors
const request$ = Rx.Observable
.defer(() => Rx.Observable.of(Math.random()))
.do(() => console.log('API called'))
.map(val => {
if (val <= 0.3) {
console.log('API error');
throw val;
} else {
return val;
}
})
.delay(250);
const cached$ = request$.shareReplay(1);
Rx.Observable.timer(0, 1000)
.take(5)
.switchMap(() => cached$.retry(5))
.subscribe(console.log);
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.5.6/Rx.min.js"></script>
回答2:
When caching HTTP responses I use something like this:
let counter = 1;
const cache$ = new ReplaySubject(1, 1000);
// Mock HTTP request
const http$ = Observable
.defer(() => Observable.of(counter++).delay(100).do(val => cache$.next(val)))
.share();
const source$ = Observable
.merge(cache$, http$)
.take(1);
See live demo: https://jsbin.com/rogetem/9/edit?js,console
It still needs one extra variable cache$
. I believe it's possible to implement this without it but I think I'd have to use materialize()
and dematerialize()
which makes is quite confusing.
You know the cache is empty when the callback for defer()
is called. Right now it just increments counter
.
Note, that I still had to use .do(val => cache$.next(val))
to pass only next
notifications to the ReplaySubject
and avoid error
and complete
because these would stop the Subject.
You can see that it really works with for example this:
source$.subscribe(val => console.log("Response 0:", val));
setTimeout(() => source$.subscribe(val => console.log("Response 50:", val)), 50);
setTimeout(() => source$.subscribe(val => console.log("Response 60:", val)), 60);
setTimeout(() => source$.subscribe(val => console.log("Response 200:", val)), 200);
setTimeout(() => source$.subscribe(val => console.log("Response 1200:", val)), 1200);
setTimeout(() => source$.subscribe(val => console.log("Response 1500:", val)), 1500);
setTimeout(() => source$.subscribe(val => console.log("Response 3500:", val)), 3500);
This prints the following output (each value is cached for 1 second):
Response 0: 1
Response 50: 1
Response 60: 1
Response 200: 1
Response 1200: 2
Response 1500: 2
Response 3500: 3
来源:https://stackoverflow.com/questions/48568104/initialize-cache-when-it-is-used