fromEvent/fromEventPattern - removal not happening on unsubscribe if its pipe has startWith - rxjs

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.

Related

Store dispatch recalls the http get of the effect several times

During dispatch, my effect is called repeatedly until my backend responds and the data is loaded. I need help in understanding how to load the data with just one GET REQUEST and then load from the store if the data is actually already present.
this.cases$ = this.store
.pipe(
takeWhileAlive(this),
select(selectImportTaskCasesData),
tap(
(cases) => {
if (cases.length <= 0) {
this.store.dispatch(new ImportTaskLoadCasesAction());
}
}),
filter((cases) => {
return cases.length > 0;
}),
tap(() => {
this.store.dispatch(new ImportTaskLoadCasesLoadedFromStoreAction());
}),
shareReplay()
);
export const selectCasesData = createSelector(
selectImportTaskCasesState,
state => state ? state.cases : []
);
export const selectImportTaskCasesData = createSelector(
selectCasesData,
cases => {
return cases.slice(0);
}
);
#Effect()
ImportCasesLoad$: Observable<any> = this.actions$
.pipe(
ofType<ImportTaskLoadCasesAction>(ImportCasesActionTypes.ImportTaskLoadCasesAction),
map((action: ImportTaskLoadCasesAction) => action),
switchMap((payload) => {
return this.importCases.get()
.pipe(
map(response => {
return new ImportTaskLoadCasesSuccessAction({ total: response['count'], cases: response['results'] });
}),
catchError((error) => {
this.logger.error(error);
return of(new ImportTaskLoadCasesLoadErrorAction(error));
})
);
})
);
Yes i have a reducer for handeling my Success Action like this :
case ImportCasesActionTypes.ImportTaskLoadCasesSuccessAction:
return {
...state,
loading: false,
cases: action.payload.cases,
total: action.payload.total
};
It's called in my effects.
Does the below work? This is assuming you have a reducer that handles the ImportTaskLoadCasesSuccessAction; Maybe supplying a working example will help, as there is a bit of guessing as how state is being managed.
this.cases$ = this.store
.pipe(
takeWhileAlive(this),
select(selectImportTaskCasesData),
tap(
(cases) => {
if (cases.length <= 0) {
this.store.dispatch(new ImportTaskLoadCasesAction());
}
}),
// personally, I would have the component/obj that is consuming this.cases$ null check the cases$, removed for brevity
shareReplay()
);
export const selectCasesData = createSelector(
selectImportTaskCasesState,
state => state ? state.cases : []
);
export const selectImportTaskCasesData = createSelector(
selectCasesData,
cases => {
return cases.slice(0);
}
);
#Effect()
ImportCasesLoad$: Observable<any> = this.actions$
.pipe(
ofType<ImportTaskLoadCasesAction>(ImportCasesActionTypes.ImportTaskLoadCasesAction),
mergeMap(() => this.importCases.get()
.pipe(
map(response => {
return new ImportTaskLoadCasesSuccessAction({
total: response['count'],
cases: response['results']
});
}),
// catch error code removed for brevity
);
)
);
If you only want the call this.importCases.get() to fire one time, I suggest moving the action dispatch out of the .pipe(tap(...)). As this will fire every time a subscription happens.
Instead, set up this.cases$ to always return the result of select(selectImportTaskCasesData),. Functionally, you probably want it to always return an array. But that is up to your designed desire.
Foe example ...
this.cases$ = this.store
.pipe(
takeWhileAlive(this),
select(selectImportTaskCasesData),
);
Separately, like in a constructor, you can dispatch the this.store.dispatch(new ImportTaskLoadCasesAction());. If you want it to only get called when cases$ is empty, you can always wrap it in a method.
e.g.
export class exampleService() {
ensureCases(): void {
this.store.pipe(
select(selectImportTaskCasesData),
take(1)
).subscribe(_cases => {
if (_cases && _cases.length < 1 ) {
this.store.dispatch(new ImportTaskLoadCasesAction());
}
}),
}
}

how to access previous mergeMap values from rxjs

I am learning to use RXJS. In this scenario, I am chaining a few async requests using rxjs. At the last mergeMap, I'd like to have access to the first mergeMap's params. I have explored the option using Global or withLatest, but neither options seem to be the right fit here.
const arraySrc$ = from(gauges).pipe(
mergeMap(gauge => {
return readCSVFile(gauge.id);
}),
mergeMap((csvStr: any) => readStringToArray(csvStr.data)),
map((array: string[][]) => transposeArray(array)),
mergeMap((array: number[][]) => forkJoin(uploadToDB(array, gauge.id))),
catchError(error => of(`Bad Promise: ${error}`))
);
readCSVFile is an async request which returns an observable to read CSV from a remote server.
readStringToArray is another async request which returns an observable to convert string to Arrays
transposeArray just does the transpose
uploadToDB is async DB request, which needs gague.id from the first mergeMap.
How do I get that? It would be great to take some advice on why the way I am doing it is bad.
For now, I am just passing the ID layer by layer, but it doesn't feel to be correct.
const arraySrc$ = from(gauges).pipe(
mergeMap(gauge => readCSVFile(gauge.id)),
mergeMap(({ data, gaugeId }: any) => readStringToArray(data, gaugeId)),
map(({ data, gaugeId }) => transposeArray(data, gaugeId)),
mergeMap(({ data, gaugeId }) => uploadToDB(data, gaugeId)),
catchError(error => of(`Bad Promise: ${error}`))
);
Why don't you do simply this?
const arraySrc$ = from(gauges).pipe(
mergeMap(gauge => readCSVFile(gauge.id).pipe(
mergeMap((csvStr: any) => readStringToArray(csvStr.data)),
map((array: string[][]) => transposeArray(array)),
mergeMap((array: number[][]) => forkJoin(uploadToDB(array, gauge.id)))
)),
catchError(error => of(`Bad Promise: ${error}`))
);
You can also wrap the inner observable in a function:
uploadCSVFilesFromGaugeID(gaugeID): Observable<void> {
return readCSVFile(gaugeID).pipe(
mergeMap((csvStr: any) => readStringToArray(csvStr.data)),
map((array: string[][]) => transposeArray(array)),
mergeMap((array: number[][]) => forkJoin(uploadToDB(array, gaugeID))
);
}
In order to do this at the end:
const arraySrc$ = from(gauges).pipe(
mergeMap(gauge => uploadCSVFileFromGaugeID(gauge.id)),
catchError(error => of(`Bad Promise: ${error}`))
);
MergeMap requires all observable inputs; else, previous values may be returned.
It is a difficult job to concatenate and display the merging response. But here is a straightforward example I made so you can have a better idea. How do we easily perform sophisticated merging.
async playWithBbservable() {
const observable1 = new Observable((subscriber) => {
subscriber.next(this.test1());
});
const observable2 = new Observable((subscriber) => {
subscriber.next(this.test2());
});
const observable3 = new Observable((subscriber) => {
setTimeout(() => {
subscriber.next(this.test3());
subscriber.complete();
}, 1000);
});
console.log('just before subscribe');
let result = observable1.pipe(
mergeMap((val: any) => {
return observable2.pipe(
mergeMap((val2: any) => {
return observable3.pipe(
map((val3: any) => {
console.log(`${val} ${val2} ${val3}`);
})
);
})
);
})
);
result.subscribe({
next(x) {
console.log('got value ' + x);
},
error(err) {
console.error('something wrong occurred: ' + err);
},
complete() {
console.log('done');
},
});
console.log('just after subscribe');
}
test1() {
return 'ABC';
}
test2() {
return 'PQR';
}
test3() {
return 'ZYX';
}

Stream operations not in correct order

Can someone please help me restructure the following observable stream so if an exception occurs during the getPreferences on the local provider the remote provider getPreferences will still occur?
thanks!
#Effect() load$: Observable<Action> = this._actions$
.ofType<Load>(LOAD)
.pipe(
switchMap(() => {
return this._localProvider.getPreferences()
.pipe(
tap((preferences: Preferences) => {
this._store.dispatch(new LoadSuccess(preferences));
}),
switchMap((preferences: Preferences) => {
return this._remoteProvider.getPreferences()
.pipe(
filter((remotePref: Preferences) => {
return remotePref.timestamp$ > preferences.timestamp$;
}),
map((remotePref: Preferences) => {
return new LoadSuccess(remotePref);
}),
catchError(error => {
return of(new LoadError(error));
})
)
}),
catchError(error => {
return of(new LoadError(error));
})
)
}),
);

withLatestFrom does not emit value

I'm trying to block main stream until config is initialized using withLatestFrom operator and secondary stream (which contains info about config state).
Here is my code:
#Effect()
public navigationToActiveRedemptionOffers = this.actions$.ofType(ROUTER_NAVIGATION)
.filter((action: RouterNavigationAction<RouterStateProjection>) => {
return action.payload.event.url.includes('/redemption-offers/list/active');
})
.do(() => console.log('before withLatestFrom'))
.withLatestFrom(
this.store.select(selectConfigInitialized)
.do(initialized => console.log('before config filter', initialized))
.filter(initialized => initialized)
.do(initialized => console.log('after config filter', initialized)),
)
.do(() => console.log('after withLatestFrom'))
.switchMap(data => {
const action = data[0] as RouterNavigationAction<RouterStateProjection>;
return [
new MapListingOptionsAction(action.payload.routerState.queryParams),
new LoadActiveOffersAction(),
];
});
Problem is that second do (after withLatestFrom) block never gets called.
Log:
before config filter false
before withLatestFrom
before config filter true
after config filter true
Thank you for your suggestions.
I think what you want is .combineLatest.
withLatestFrom only treats what comes before it as the trigger.
combineLatest treats both the source and the observables provided to it as the trigger.
So your issue is that the source (route change) fires and the observable passed to withLatest (state initialized) has not emited yet because of its filter. It only emits a value after the source fires. So it is ignored.
Here is a running example of what you are doing:
const first = Rx.Observable.create((o) => {
setTimeout(() => {
o.next();
}, 2000);
});
const second = Rx.Observable.create((o) => {
setTimeout(() => {
o.next(false);
}, 1000);
setTimeout(() => {
o.next(true);
}, 3000);
});
first
.do(() => console.log('before withLatestFrom'))
.withLatestFrom(
second
.do(initialized => console.log('before config filter', initialized))
.filter(initialized => initialized)
.do(initialized => console.log('after config filter', initialized)),
)
.subscribe(() => console.log('after withLatestFrom'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.5.2/Rx.js"></script>
Here is with combineLatest:
const first = Rx.Observable.create((o) => {
setTimeout(() => {
o.next();
}, 2000);
});
const second = Rx.Observable.create((o) => {
setTimeout(() => {
o.next(false);
}, 1000);
setTimeout(() => {
o.next(true);
}, 3000);
});
first
.do(() => console.log('before withLatestFrom'))
.combineLatest(
second
.do(initialized => console.log('before config filter', initialized))
.filter(initialized => initialized)
.do(initialized => console.log('after config filter', initialized)),
)
.subscribe(() => console.log('after withLatestFrom'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.5.2/Rx.js"></script>
The withLatestFrom emits only when its source Observable emits. What happens is that this.store.select emits before the source to withLatestFrom. You can see it from the order of emissions:
before config filter false
before withLatestFrom
...
Now it depends on what you want to achieve. You can trigger this.store.select(selectConfigInitialized) only when the source emits by using concatMap:
navigationToActiveRedemptionOffers = this.actions$.ofType(ROUTER_NAVIGATION)
.filter(...)
.concatMap(() => this.store.select(selectConfigInitialized)...)
.switchMap(data => { ... })
...
Or you can pass along results from both Observables:
navigationToActiveRedemptionOffers = this.actions$.ofType(ROUTER_NAVIGATION)
.filter(...)
.concatMap(nav => this.store.select(selectConfigInitialized)
.map(result => [nav, result])
)
.switchMap(data => { ... })
...
You can make it simpler by inverting the observables.
public navigationToActiveRedemptionOffers =
return this.store.select(selectConfigInitialized)
.filter(initialized => initialized)
.flatMap(x =>
return this.actions$.ofType(ROUTER_NAVIGATION)
.filter((action: RouterNavigationAction<RouterStateProjection>) => {
return action.payload.event.url.includes('/redemption-offers/list/active');
})
.map(action =>
return [
new MapListingOptionsAction(action.payload.routerState.queryParams),
new LoadActiveOffersAction(),
];
)
As this is a common pattern, I use a generic waitFor method.
export const waitFor$ = function() {
return this.filter(data => !!data ).take(1);
};
Usage,
return this.config$
.waitFor$()
.flatMap(config => {
return observableWithRequiredValue()
}
Fluent syntax provided by
Observable.prototype.waitFor$ = waitFor$;
declare module 'rxjs/Observable' {
interface Observable<T> {
waitFor$: typeof waitFor$;
}
}
Note, this method of operator creation has been superseded by Lettable Operators in rxjs v5.5 (but the above still works).

rxJS and ngrx - what is the right structure of success / fail inside an effect?

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 })
);

Resources