I am unable to get two actions to execute sequentially within a NGRX effect.
Here are the effects
registerFinishSave$ = createEffect(
() => this.actions$.pipe(
ofType(registerFinishSave),
withLatestFrom(
this.store.select(selectRouteState),
(action, state) => ({
firstName: action.request.firstName,
id: state.queryParams.id,
lastName: action.request.lastName,
password: action.request.password,
repeatPassword: action.request.repeatPassword,
token: state.queryParams.token,
userName: action.request.userName
})),
switchMap((request) =>
this.accountService.updateAndVerifyAccount(request).pipe(
mergeMap((response: any) => from([registerFinishSaveSuccess({ response }), loginSuccess({response})])),
// map((response: any) => registerFinishSaveSuccess({ response })),
catchError((response: HttpErrorResponse) => {
if (response.status === 400) {
return of(registerFinishSaveFailure({ response }));
}
throw response;
})
)
)
)
);
registerFinishSaveSuccess$ = createEffect(
() => this.actions$.pipe(
ofType(registerFinishSaveSuccess),
tap(({ response }) => {
console.log('registerFinishSaveSuccess ');
// this.router.navigate(['/']);
this.notificationService.info(response.message);
})
), {
dispatch: false
}
);
loginSuccess$ = createEffect(
() => this.actions$.pipe(
ofType(loginSuccess),
tap(() => {
console.log('loginSuccess ');
return this.router.navigate(['/']);
})
), {
dispatch: false
}
);
Here are the reducers
on<AuthenticationState>(loginSuccess, (state: AuthenticationState, { response }) => ({
...state, accessToken: response.accessToken, displayName: response.displayName, id: response.id, isAuthenticated: true, refreshToken: response.refreshToken
})),
on<AccountState>(registerFinishSaveSuccess, (state: AccountState, { response }) => ({
...state, message: null
})),
The action and effect for registerFinishSaveSuccess appears to work. The reducer appears to set the state for the action loginSuccess. However the effect loginSuccess does not appear to fire.
I need to fire the registerFinishSaveSuccess and loginSuccess actions and their effects sequentially. Not sure if the code below is correct
'''
mergeMap((response: any) => from([registerFinishSaveSuccess({ response }), loginSuccess({response})])),
'''
There is no need to dispatch both in a mergeMap. You can dispatch your registerFinishSaveSuccess and then his effect would be triggered. In the registerFinishSaveSuccess effect, you can dispatch loginSuccess and that way you would be sure the order is kept.
Related
So I basically have a websocket connection, this allows me to send generic messages via WEBSOCKET_MESSAGE_SEND and receive them via WEBSOCKET_MESSAGE_RECEIVED actions.
However there are cases where I want to make a request in a similar manner to a Ajax REST call. Eg to request a list of documents for a user I probably want to have an epic:
Receive an action eg ({ type: GET_DOCUMENTS })
Generate a random key to track the current request, we will call it 'request_id'
Send a ({ type: WEBSOCKET_MESSAGE_SEND, request_id }) action.
Wait for either of
an action ({ type: WEBSOCKET_MESSAGE_RECEIVED, request_id, message }) **Must be with a matching 'request_id' otherwise it should be ignored.
-> Emit an action eg ({ type: GET_DOCUMENTS_SUCCESS, documents: message })
a timeout eg 10 seconds
-> Emit an action eg ({ type: GET_DOCUMENTS_TIMEOUT })
I have been struggling to put this into code, I think the most awkward part of the whole epic is that I want to emit an action in the middle of my epic and wait. This doesn't feel quite right to me... ani-pattern? But I am not really sure how I should be doing this.
That's right. There is no good way to emit an action in the middle of an epic. How about splitting the epic into two?
const getDocumentsEpic = action$ =>
action$.pipe(
ofType("GET_DOCUMENTS"),
map(() => {
const requestId = generateRequestId();
return {
type: "WEBSOCKET_MESSAGE_SEND",
requestId
};
})
);
const websocketMessageEpic = action$ =>
action$.pipe(
ofType("WEBSOCKET_MESSAGE_SEND"),
switchMap(requestId => {
return action$.pipe(
ofType("WEBSOCKET_MESSAGE_RECEIVED"),
filter(action => action.requestId === requestId),
timeout(10000),
map(({ message }) => ({
type: "GET_DOCUMENTS_SUCCESS",
documents: message
})),
catchError(() => of({ type: "GET_DOCUMENTS_TIMEOUT" }))
);
})
);
Updated answer (2020-04-17):
I was unhappy with my original answer so decided to give it another shot.
NotificationOperators.js
import { of } from 'rxjs';
import { map, switchMap, filter, timeout, catchError, first, mergeMap } from 'rxjs/operators';
import { notificationActionTypes } from '../actions';
const NOTIFICATION_TIMEOUT = 60 * 1000;
const generateRequestId = () => Math.random().toString(16).slice(2);
const toNotificationRequest = notificationRequest => input$ =>
input$.pipe(mergeMap(async action => ({
type: notificationActionTypes.WEBSOCKET_MESSAGE_SEND,
message: {
request_id: generateRequestId(),
...(
typeof notificationRequest === "function" ?
await Promise.resolve(notificationRequest(action)) :
({ eventType: notificationRequest })
)
}
})));
const mapNotificationRequestResponses = (notificationRequest, mapper) => $input =>
$input.pipe(
filter(action =>
action.type === notificationActionTypes.WEBSOCKET_MESSAGE_SEND &&
action.message.eventType === notificationRequest),
concatMap(sendAction =>
$input.pipe(
filter(receiveAction => {
return (
receiveAction.type === notificationActionTypes.WEBSOCKET_MESSAGE_RECEIVED &&
receiveAction.message.request_id === sendAction.message.request_id
)
}),
first(),
timeout(NOTIFICATION_TIMEOUT),
map(({ message }) => mapper(message.success ? false : message.error, message.result, sendAction.message)),
catchError(errorMessage => of(mapper(errorMessage && errorMessage.message, null, sendAction.message))))));
export { toNotificationRequest, mapNotificationRequestResponses };
Usage:
export const getDocumentsReqEpic = action$ => action$.pipe(
ofType(documentActionTypes.REFRESH_DOCUMENTS_REQUEST),
toNotificationRequest(EventTypes.get_user_documents_req)
);
export const getDocumentsRecEpic = action$ => action$.pipe(
mapNotificationRequestResponses(
EventTypes.get_user_documents_req,
(error, result) => error ? refreshDocumentsError(error) : refreshDocumentsSuccess(result))
);
Original answer:
As I felt I would likely need to repeat this process many more times, this seemed like a reasonable amount of duplicated boilterplate that I should create a method to generate epics based on requirements. For this reason I have expanded upon #sneas awesome answer and have posted below incase it helps others.
Note this implementation assumes the websocket implementation from the other answer. It also assumes that the server websocket implementation will accept a 'request_id' and respond with the same 'request_id' so that request and response messages can be linked. Probably also worth noting that the 'epicLinkId' is client-side only, and simply enables the 2 epics being created to be linked to each other, without this you would only be able to call createNotifyReqResEpics() once.
createNotifyReqResEpics.js (helper based on code above)
import { ofType } from 'redux-observable';
import { of } from 'rxjs';
import { map, switchMap, filter, timeout, catchError, first } from 'rxjs/operators';
import { notificationActionTypes } from '../actions';
const generateRequestId = () => Math.random().toString(16).slice(2);
export default ({
requestFilter,
requestMessageMapper,
responseMessageMapper
}) => {
if (typeof requestFilter !== "function")
throw new Error("Invalid function passed into createNotifyReqResEpics 'requestFilter' argument.");
if (typeof requestMessageMapper !== "function")
throw new Error("Invalid function passed into createNotifyReqResEpics 'requestMessageMapper' argument.");
if (typeof responseMessageMapper !== "function")
throw new Error("Invalid function passed into createNotifyReqResEpics 'responseMessageMapper' argument.");
const epicLinkId = generateRequestId();
const websocketSendEpic = action$ =>
action$.pipe(
filter(requestFilter),
map(action => ({
epic_link_id: epicLinkId,
type: notificationActionTypes.WEBSOCKET_MESSAGE_SEND,
message: {
request_id: generateRequestId(),
...requestMessageMapper(action)
}
}))
);
const websocketReceiveEpic = action$ =>
action$.pipe(
ofType(notificationActionTypes.WEBSOCKET_MESSAGE_SEND),
filter(action => action.epic_link_id === epicLinkId),
switchMap(sendAction =>
action$.pipe(
ofType(notificationActionTypes.WEBSOCKET_MESSAGE_RECEIVED),
filter(receiveAction => receiveAction.request_id === sendAction.request_id),
first(),
timeout(10000),
map(receiveAction => responseMessageMapper(false, receiveAction.message)),
catchError(errorMessage => of(responseMessageMapper(errorMessage && errorMessage.message, null))))));
return [websocketSendEpic, websocketReceiveEpic];
};
documents.js (epics)
import EventTypes from '../shared-dependencies/EventTypes';
import { documentActionTypes, refreshDocumentsError, refreshDocumentsSuccess } from '../actions';
import { createNotifyReqResEpics } from '../utils';
const [getDocumentsReqEpic, getDocumentsRespEpic] = createNotifyReqResEpics({
requestFilter: action => action.type === documentActionTypes.REFRESH_DOCUMENTS_REQUEST,
requestMessageMapper: action => ({ eventType: EventTypes.get_user_documents_req }),
responseMessageMapper: (error, action) => error ? refreshDocumentsError(error) : refreshDocumentsSuccess(action.result)
});
export { getDocumentsReqEpic, getDocumentsRespEpic };
Where the 2 exported epics from documents.js make thie way into combineEpics.
I was using fromEvent but I switched to fromEventPattern so I can console.log to troubleshoot. I see that when I unsubscribe(), only the first fromEventPattern remove method is called. Does anyone know why the remove handlers of the the window.remveEventListener('online') and offline are not getting called?
If I remove the startWith(window.navigator.onLine) from the the .pipe it works, but I need the the startWith(window.navigator.onLine) for at least one of them.
Here is my pipeline:
pipeline$ = combineLatest(
merge(
fromEventPattern(
handler => {
console.log('adding io.socket.disconnect');
io.socket.on('disconnect', handler);
},
handler => {
console.log('removing io.socket.disconnect');
io.socket.off('disconnect', handler);
},
).pipe(
mapTo(false),
tap(() => this.setState({ isConnected: false })),
),
this.isConnectedSubject.pipe(
tap(isConnected => this.setState({ isConnected })),
startWith(io.socket.isConnected())
)
),
merge(
fromEventPattern(
handler => {
console.log('adding window.online');
window.addEventListener('online', handler, false);
},
handler => {
console.log('removing window.online');
window.removeEventListener('online', handler, false);
}
).pipe(
tap(() => console.log('online')),
mapTo(true),
tap(() => this.setState({ isOnline: true })),
startWith(window.navigator.onLine)
),
fromEventPattern(
handler => {
console.log('adding window.offline');
window.addEventListener('offline', handler, false);
},
handler => {
console.log('removing window.offline');
window.removeEventListener('offline', handler, false);
}
).pipe(
tap(() => console.log('offline')),
mapTo(false),
tap(() => this.setState({ isOnline: false })),
startWith(window.navigator.onLine)
)
)
).pipe(
switchMap(([ isConnected, isOnline, ...rest ]) => {
console.log('isConnected:', isConnected, 'isOnline:', isOnline, 'rest:', rest);
console.log(!isConnected && isOnline ? 'RE-CON now' : 'DO NOT re-con');
return !isConnected && isOnline
? defer(() => connectSocket()).pipe(
retryWhen(error$ =>
error$.pipe(
tap(error => console.log('got socket connect error!', error.message)),
delayWhen((_, i) => {
const retryIn = 10000;
this.setState({
retryAt: Date.now() + retryIn
});
return timer(retryIn);
})
)
),
tap(() => isConnectedSubject.next(true))
)
: EMPTY;
}),
takeUntil(mSessionSubject.pipe(
filter(action => action.type === 'LOGOUT'),
))
);
I subscribe to it like this:
const sub = pipeline$.subscribe();
and then I unsubscribe like this:
sub.unsubscribe();
After calling this unsubscribe, I am not seeing the online/offline removal methods trigger.
did you check if the subscription is defined before doing unsubscribe?
something like
if (sub !== undefined) {
sub.unsubscribe();
}
because you might unsubscribe before sub emit any data however It's not recommended to use unsubscribe , but you could use take(n),
takeWhile(predicate), first() or first(predicate) instead.
Based on redux-observable I have this epic:
export const someEpic = action$ => {
return action$.pipe(
ofType('FETCH_DATA'),
switchMap(({ payload: { filter, bookingCenter } }) => {
return concat(
of(searchInProgress()),
from(
ApiManager.call('/v1/some-data', {
method: 'GET',
schema: false,
params: {
term: filter,
bookingCenter,
},
})
).pipe(
map(response => {
if (!response) throw 'No result';
return response;
}),
map(({ _embedded }) => updateResult(_embedded)
)
).pipe(retry(2));
})
);
};
As you can see, I have a retry operator. Basically if server returns no response, I throw an error and retry is happening.
However, it is working only partialy i.e. in console I see that searchInProgress action is dispatched, however second observable created with from is not being executed once again.
ApiManager.call is returning regular fetch API Promise.
What is that I'm doing wrong?
The solution is to use defer that will defer creation of observable from Promise:
defer(() =>
ApiManager.call('/v1/some-data', {
method: 'GET',
schema: false,
params: {
term: filter,
bookingCenter,
limit: 500,
},
})
)
I have been playing with rxjs and redux-observable for the last few days and have been struggle to find a way to a test for Observable.ajax. I have the following epic which create a request to https://jsonplaceholder.typicode.com/,
export function testApiEpic (action$) {
return action$.ofType(REQUEST)
.switchMap(action =>
Observable.ajax({ url, method })
.map(data => successTestApi(data.response))
.catch(error => failureTestApi(error))
.takeUntil(action$.ofType(CLEAR))
)
}
where,
export const REQUEST = 'my-app/testApi/REQUEST'
export const SUCCESS = 'my-app/testApi/SUCCESS'
export const FAILURE = 'my-app/testApi/FAILURE'
export const CLEAR = 'my-app/testApi/CLEAR'
export function requestTestApi () {
return { type: REQUEST }
}
export function successTestApi (response) {
return { type: SUCCESS, response }
}
export function failureTestApi (error) {
return { type: FAILURE, error }
}
export function clearTestApi () {
return { type: CLEAR }
}
The code works fine when runs in browser but not when testing with Jest.
I have try,
1) Create a test based on https://redux-observable.js.org/docs/recipes/WritingTests.html. The store.getActions() returns only { type: REQUEST }.
const epicMiddleware = createEpicMiddleware(testApiEpic)
const mockStore = configureMockStore([epicMiddleware])
describe.only('fetchUserEpic', () => {
let store
beforeEach(() => {
store = mockStore()
})
afterEach(() => {
epicMiddleware.replaceEpic(testApiEpic)
})
it('returns a response, () => {
store.dispatch({ type: REQUEST })
expect(store.getActions()).toEqual([
{ type: REQUEST },
{ type: SUCCESS, response }
])
})
})
2) Create a test based on Redux-observable: failed jest test for epic. It returns with
Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.
it('returns a response', (done) => {
const action$ = ActionsObservable.of({ type: REQUEST })
const store = { getState: () => {} }
testApiEpic(action$, store)
.toArray()
.subscribe(actions => {
expect(actions).to.deep.equal([
{ type: SUCCESS, response }
])
done()
})
})
Can someone point me out what is the correct way to test Observable.ajax ?
I would follow the second example, from StackOverflow. To make it work you'll need to make some minor adjustments. Instead of importing Observable.ajax in your epic file and using that reference directly, you need to use some form of dependency injection. One way is to provide it to the middleware when you create it.
import { ajax } from 'rxjs/observable/dom/ajax';
const epicMiddleware = createEpicMiddleware(rootEpic, {
dependencies: { ajax }
});
The object we passed as dependencies will be give to all epics as the third argument
export function testApiEpic (action$, store, { ajax }) {
return action$.ofType(REQUEST)
.switchMap(action =>
ajax({ url, method })
.map(data => successTestApi(data.response))
.catch(error => failureTestApi(error))
.takeUntil(action$.ofType(CLEAR))
);
}
Alternatively, you could not use the dependencies option of the middleware and instead just use default parameters:
export function testApiEpic (action$, store, ajax = Observable.ajax) {
return action$.ofType(REQUEST)
.switchMap(action =>
ajax({ url, method })
.map(data => successTestApi(data.response))
.catch(error => failureTestApi(error))
.takeUntil(action$.ofType(CLEAR))
);
}
Either one you choose, when we test the epic we can now call it directly and provide our own mock for it. Here are examples for success/error/cancel paths These are untested and might have issues, but should give you the general idea
it('handles success path', (done) => {
const action$ = ActionsObservable.of(requestTestApi())
const store = null; // not used by epic
const dependencies = {
ajax: (url, method) => Observable.of({ url, method })
};
testApiEpic(action$, store, dependencies)
.toArray()
.subscribe(actions => {
expect(actions).to.deep.equal([
successTestApi({ url: '/whatever-it-is', method: 'WHATEVERITIS' })
])
done();
});
});
it('handles error path', (done) => {
const action$ = ActionsObservable.of(requestTestApi())
const store = null; // not used by epic
const dependencies = {
ajax: (url, method) => Observable.throw({ url, method })
};
testApiEpic(action$, store, dependencies)
.toArray()
.subscribe(actions => {
expect(actions).to.deep.equal([
failureTestApi({ url: '/whatever-it-is', method: 'WHATEVERITIS' })
])
done();
});
});
it('supports cancellation', (done) => {
const action$ = ActionsObservable.of(requestTestApi(), clearTestApi())
const store = null; // not used by epic
const dependencies = {
ajax: (url, method) => Observable.of({ url, method }).delay(100)
};
const onNext = chai.spy();
testApiEpic(action$, store, dependencies)
.toArray()
.subscribe({
next: onNext,
complete: () => {
onNext.should.not.have.been.called();
done();
}
});
});
For the first way:
First, use isomorphic-fetch instead of Observable.ajax for nock support, like this
const fetchSomeData = (api: string, params: FetchDataParams) => {
const request = fetch(`${api}?${stringify(params)}`)
.then(res => res.json());
return Observable.from(request);
};
So my epic is:
const fetchDataEpic: Epic<GateAction, ImGateState> = action$ =>
action$
.ofType(FETCH_MODEL)
.mergeMap((action: FetchModel) =>
fetchDynamicData(action.url, action.params)
.map((payload: FetchedData) => fetchModelSucc(payload.data))
.catch(error => Observable.of(
fetchModelFail(error)
)));
Then, you may need an interval to decide when to finish the test.
describe("epics", () => {
let store: MockStore<{}>;
beforeEach(() => {
store = mockStore();
});
afterEach(() => {
nock.cleanAll();
epicMiddleware.replaceEpic(epic);
});
it("fetch data model succ", () => {
const payload = {
code: 0,
data: someData,
header: {},
msg: "ok"
};
const params = {
data1: 100,
data2: "4"
};
const mock = nock("https://test.com")
.get("/test")
.query(params)
.reply(200, payload);
const go = new Promise((resolve) => {
store.dispatch({
type: FETCH_MODEL,
url: "https://test.com/test",
params
});
let interval: number;
interval = window.setInterval(() => {
if (mock.isDone()) {
clearInterval(interval);
resolve(store.getActions());
}
}, 20);
});
return expect(go).resolves.toEqual([
{
type: FETCH_MODEL,
url: "https://test.com/assignment",
params
},
{
type: FETCH_MODEL_SUCC,
data: somData
}
]);
});
});
enjoy it :)
I work in angular 2 Project and use ngrx and rxjs technologies.
Now I have a problem:
I try to declare an Effect.
The effect has http request, and only when it success I want to call other http-request, and so only if it also success - then dispatch an success-action.
I has tested it by throw an error but it always dispatch the action!
See:
#Effect()
createEntity$ = this.actions$.ofType(CREATE_ENTITY)
.switchMap((action: CreateEntity) => {
return this.httpService.getDefaultEntityData(action.payload.type).map((entity) => {
return Observable.throw("testing only");
/*if (entity) {
entity.title = entity.type;
return this.httpService.addEntity(entity);
}*/
})
.catch((error) => Observable.of(new createEntityFailure(error)))
.map(mappedResponse => ({ type: CREATE_ENTITY_SUCCESS, payload: mappedResponse }))
});
How about this:
this.actions$
.ofType(CREATE_ENTITY)
.map((action: CreateEntity) => action.payload)
.switchMap(payload =>
this.httpService.getDefaultEntityData(payload.type)
.mergeMap(entity => this.httpService.addEntity(entity))
// .mergeMap(entity => Observable.throw('error')) // or this for testing
.mergeMap(response => new actions.Action(...))
.catch(error => new actions.Error(...))
);
You can either split this up into multiple actions or just add another API call in the same effect using Observable.forkJoin
#Effect() createEntity$ = this.actions$.ofType(CREATE_ENTITY)
.switchMap((action: CreateEntity) => {
return Observable.forkJoin(
this.httpService.callOne(),
this.httpService.callTwo()
)
.catch((error) => Observable.of(new createEntityFailure(error)))
.map(mappedResponse => ({ type: CREATE_ENTITY_SUCCESS, payload: mappedResponse }))
});
As forkJoin is parallel that won't work for you. You can just switchMap on the first API call and return the second:
#Effect() createEntity$ = this.actions$.ofType(CREATE_ENTITY)
.switchMap((action: CreateEntity) => {
return this.httpService.callOne();
})
.switchMap((response) => {
return this.httpService.callTwo()
.map(secondResponse => ({
type: CREATE_ENTITY_SUCCESS,
payload: {
first: response,
second: secondResponse
}
}))
})
.catch((error) => Observable.of(new createEntityFailure(error)))
});
1) If you returning Observable you probably want swithMap instead of map
2) Action always has been dispatched because you return non error Observable from catch. Changing Observable.of to Observable.throw will throw error further
#Effect()
createEntity$ = this.actions$.ofType(CREATE_ENTITY)
.switchMap((action: CreateEntity) =>
this.httpService.getDefaultEntityData(action.payload.type)
)
.switchMap((entity) => { // <------ switchMap here
return Observable.throw("testing only");
/*if (entity) {
entity.title = entity.type;
return this.httpService.addEntity(entity);
}*/
})
.catch((error) =>
Observable.throw(new createEntityFailure(error)) // <------ throw here
)
.map((mappedResponse) =>
({ type: CREATE_ENTITY_SUCCESS, payload: mappedResponse })
);