I have epic that debounce until it successfully call ping api. I tested successfully ping case. I'd like to use import { TestScheduler } from 'rxjs/testing'; and test when I cannot ping to 1st call, but success in 2nd with marble diagram. is there any example?
export const resendData = (action$, store$, {service}) => {
return action$
.pipe(
ofType(
SOMETHING_FALL
),
debounce(() => service.get('/api/ping').pipe(
retryWhen(err =>
err.pipe(
scan((acc, _) => acc * 2, 2),
delayWhen(val => timer(val * 1000))
)
),
catchError(_ => empty())),
),
map(_ => retryAction()),
);
}
Related
I need to sequentially call multiple HTTP requests. Currently I have this huge epic where I pass all API requests and then try to fire them one by one using mergeMaps. It works, but I believe there must be some easier or cleaner way but. All I found for RxJS was forkJoin but it fires everything in parallel. Am I nitpicking here or is there some much smarter way? Any tips could be helpful here.
Pseudo-code:
All requests are similar and look like this:
import { ajax } from "rxjs/ajax";
import { map } from "rxjs/operators";
export const unpublishAbcApi = (id) =>
ajax({
method: "PATCH",
url: ...,
headers: ..,
}).pipe(
map(({ response }) => ({
published: response.published,
}))
);
And this is my epic
export const deleteEverything = (action$, _,) =>
action$.pipe(
actionOfType(delete.request),
mergeMap(({ payload: { id }}) =>
unpublishAbcApi(id).pipe(
mergeMap(() =>
deleteDefApi(id).pipe(
mergeMap(() =>
deleteGhiApi(otherId).pipe(
mergeMap(() =>
deleteJklApi(id).pipe(
map(() => ({ id }))
)
)
)
)
)
)
);
),
mergeMap(({ id }) =>
// ...get the id and dispatchSuccess etc.
),
catchError(error => /* error handling */)
);
Since the apis are not interdependent on each other, you can just use concat which will execute APIs in sequence!
export const deleteEverything = (action$, _) =>
action$.pipe(
actionOfType(delete.request),
switchMap(({ payload: { id }}) => concat(
unpublishAbcApi(id),
deleteDefApi(id),
deleteGhiApi(otherId),
deleteJklApi(id)
).pipe(map() => ({id}))),
mergeMap(({ id }) =>
// ...get the id and dispatchSuccess etc.
),
catchError(error => /* error handling */)
);
I have two observable streams which do very separate mapping logic, but then ultimately end with following 3 operators:
this.selection
.pipe(
..Custom mapping operators
tap(_ => this.devicesLoading = true),
switchMap(d => this.mapService.findLocationForDevices(d)),
map(loc => marker([loc.latitude, loc.longitude])
)
.subscribe(markers => this.plotMarkers(markers));
I want to move the last tap, switchMap, map operators to a common function so I can just apply these within both of my observable streams.
I thought of doing:
private resolveLocationsAndConvertToMarkers = (devices: String[]) => [
tap(_ => this.devicesLoading = true),
switchMap((devices: string[]) => this.mapService.findLocationForDevices(devices)),
map(loc => marker([loc.latitude, loc.longitude])
];
But I wasn't sure how to spread these operators into the pipe arguments, like:#
this.selection
.pipe(
// Custom mapping operators
... this.resolveLocationsAndConvertToMarkers
)
.subscribe(markers => this.plotMarkers(markers));
this errors that there are no overloads that expect 3 or 5 arguments..
You can try use native .apply()
this.selection
.pipe.apply(null,this.resolveLocationsAndConvertToMarkers)
or wrap the list of operator in pipe()
private resolveLocationsAndConvertToMarkers = (devices: String[]) => pipe(
tap(_ => this.devicesLoading = true),
switchMap((devices: string[]) => this.mapService.findLocationForDevices(devices)),
map(loc => marker([loc.latitude, loc.longitude])
);
or return higher order function
private resolveLocationsAndConvertToMarkers = (devices: String[]) => source=>source.pipe(
tap(_ => this.devicesLoading = true),
switchMap((devices: string[]) => this.mapService.findLocationForDevices(devices)),
map(loc => marker([loc.latitude, loc.longitude])
);
You could try a reactive approach (with no side effects unless really isolated):
const preSelection$ = this.selection
.pipe
//..Custom mapping operators
();
const selection$: Observable<Marker> = preSelection$.pipe(
switchMap(preSelection =>
concat(
of(null),
of(preSelection).pipe(
switchMap(d => this.mapService.findLocationForDevices(d)),
map(loc => marker([loc.latitude, loc.longitude]))
)
)
),
shareReplay({ bufferSize: 1, refCount: true })
);
const isLoading$: Observable<boolean> = selection$.pipe(map(x => !!x));
const sideEffectUpdatePlotMarkers$ = selection$.pipe(
tap(markers => this.plotMarkers(markers))
);
// isolate `subscribe` calls and side effects as much as possible
sideEffectUpdatePlotMarkers$.subscribe();
I'm hoping this answer will help anyone else who stumbles across this question. The accepted answer did not exactly work for me, with the primary reason being null was passed as the first parameter for .apply() instead of my observable function again. Here is an example similar to what I successfully implemented in my project.
private pipeActions = [
filter(...),
map(...),
];
private myObservable = combineLatest(...);
doThing(): Observable<any> {
return this.myObservable
.pipe.apply(this.myObservable, [...this.pipeActions]);
}
doOtherThing(): Observable<any> {
return this.myObservable
.pipe.apply(
this.myObservable,
[...this.pipeActions, map(...)], // Do something additionally after my list of pipe actions
);
}
There has to be a better way to do this, right?
const source$ = combineLatest([
folder$.pipe(
filter(folder => folder.canBeLoaded()),
),
page$,
sort$,
]).pipe(
takeUntil(this.onceDestroyed$),
switchMap(([folder, page, sort]) => combineLatest([
of(folder),
of(page),
of(sort),
// buffer$: BehaviourSubjhect<number>
buffer$.pipe(
startWith(-1),
pairwise(),
filter(([buffered, wanted]) => wanted > buffered ),
map(([, current]) => current),
distinctUntilChanged(),
),
])),
);
withLatestFrom comes to my mind, but it will return once per parent, I need buffer$ to be able to emit multiple times!
no need to be that complicated, below should work upper/lower stream are always the latest.
const source$ = combineLatest([
folder$.pipe(
filter(folder => folder.canBeLoaded()),
),
page$,
sort$,
]).pipe(
takeUntil(this.onceDestroyed$),
switchMap(([folder, page, sort]) =>
buffer$.pipe(
startWith(-1),
pairwise(),
filter(([buffered, wanted]) => wanted > buffered ),
map(([, current]) => current),
distinctUntilChanged(),
map(current => [folder, page, sort,current]),
),
),
);
The following code works without errors:
export const myEpic = (action$: any) => action$.pipe(
ofType("TEST"),
mergeMap(() => concat(
// fires an actionCreator and triggers another epic
of(actionOne()),
// fires an actionCreator
of(actionTwo())
))
);
The problem is that I need the data from actionOne to be available before actionTwo gets fired, and it doesn't seem to be happening. So I want to make this an async function like:
export const myEpic = (action$: any) => action$.pipe(
ofType("TEST"),
mergeMap(async () => concat(
of(await actionOne()),
of(actionTwo())
))
);
This throws an error:
Uncaught Error: Actions must be plain objects. Use custom middleware for async actions.
EDIT
Other relevant code:
// main component that loads
constructor(props) {
props.dispatch(init());
}
componentDidUpdate(prevProps) {
if (prevProps.actionTwoFlag !== this.props.actionTwoFlag) {
// do stuff with result from actionOne
// error is thrown here because there's no data
}
}
// actions
export const init = () => ({ type: "TEST" });
export const actionOne = () => ({ type: "ACTION_ONE" });
export const actionOneDone = (result) => ({ type: "ACTION_ONE_DONE", payload: result });
export const actionTwo = () => ({ type: "ACTION_TWO", payload: true });
// epics
export const actionOneEpic = (action$: any) => action$.pipe(
ofType("ACTION_ONE"),
mergeMap(() =>
ajax(..).pipe(
mergeMap(result => concat(
of(actionOneDone(result)),
...
))
)
)
);
);
There are various ways to solve this.
1- One way is just using defer() operator on the actionTwo. What defer() operator would do, is execute your code on subscription, since they are concatenated, the subscription to of(actionTwo()) would be done after of(actionOne()) is completed:
export const myEpic = (action$: any) => action$.pipe(
ofType("TEST"),
mergeMap(() => concat(
of(actionOne()),
defer(() => of(actionTwo()))
))
);
2- Another option is just do a switchMap(), this would ensure too that when you create the of(actionTwo()) observable, the of(actionOne()) observable has already been emitted and finished. switchMap() also ensures sequential order, so you can safely remove the concat() operator:
export const myEpic = (action$: any) => action$.pipe(
ofType("TEST"),
mergeMap(() =>
of(actionOne()).pipe(switchMap(() => of(actionTwo())))
)
);
EDIT:
Now I think I got it, although, I am not pretty familiar with redux observable epics. I have seen a solution here: Composing and sequencing multiple epics in redux-observable
that may solve your issue two. Based on that, I will give 2 proposals.
1st proposal:
This proposal just builds an epic that push action one at first, and waits for action one done in order to push the action two.
export const myEpic = (action$: any) => action$.pipe(
ofType('TEST'),
map(() => actionOne()),
mergeMap(() => {
return action$.pipe(
ofType('ACTION_ONE_DONE'),
take(1),
map(() => actionTwo()),
);
})
);
2nd proposal:
Do it all in one epic. Since both action one and action two are related (one depend on each other) it could make sense to merge both into only one epic, it would be something like this:
export const myEpic = (action$: any) => action$.pipe(
ofType('TEST'),
map(() => actionOne()),
mergeMap(() => {
return ajax(..).pipe(
mergeMap((data) => {
return concat(
actionOneDone(action),
of(actionTwo()).mergeMap(() => /* Do action two */ actionTwoDone())
)
}),
)
})
);
Hope this helps!
Able to catch error inside individual ajax requests using pipe operator, but how can i catch outside concat. getting error source.lift not a function. please point out where i am going wrong and what would be better option for serial requests.
export const deleteSettingsEpic = action$ => action$.pipe(
ofType('DELETE_SETTINGSDATA'),
flatMap(action$ => concat(
ajax.ajaxDelete(`${action$.payload.api}/${action$.payload.id}`)
.pipe(
map(r => ({ type: 'DELETE_SUCCESS' })),
// catchError( e => of({type: 'DELETE_ERROR'}))
),
ajax.get(`${action$.payload.api}`)
.pipe(
map(r => ({
type: action$.payload.getPaginationAction,
payload: {
leadSourceList: r.response.docs,
page: r.response.page,
totalPages: r.response.totalPages,
limit: r.response.limit,
totalDocs: r.response.totalDocs
}
})),
// catchError(e => of({type: 'FETCH_ERROR'}))
),
of({type: 'SET_DIMMER_FALSE'})
),
catchError(e => of({type: 'FETCH_ERROR'}))
),
);