I have a stream of files and I want to fill additional information about it, but I would like to present the currently obtained data to the user, as it is all that is initially visible anyway.
I want observable that:
Get cancelled on new emission (like switchMap)
Does not wait for the observable to finish before emitting (like tap)
What I have currently is awaiting the result, before emitting the files.
Set-up & current try itteration:
this.pagedFLFiles = fileService.getFiles().pipe(
switchMap(response => concat(
of(response),
fileService.getAdditionalInfo(response.items).pipe(
switchMap(() => EMPTY),
),
)),
shareReplay(1),
);
fileService.getAdditionalInfo(response.items) - it is modifing the data
getAdditionalInfo(files: FLFile[]): Observable<FLFile[]> {
return this.api.getWithToken(token => {
return { path: `v5/user/${token}/files/${files.map(file => file.id).join(',')}}/facilities` };
}).pipe(
map(information => {
files.forEach(file => {
const info = information[file.id];
(Object.entries(info) as [keyof typeof info, any][]).forEach(([key, value]) => {
file[key] = value;
});
});
return files;
}),
);
}
Use merge instead of concat.
Concat waits for both observables, of(reponse) and getAdditionalInfo, before emitting a value.
Merge emits each time one of its observables emits.
Example:
getFiles will emit each second for 3 seconds
getAdditionalInfo will be cancelled 2 times (because it runs longer than 1 seond), and therefore will only modify the last emitted files array
import { merge, EMPTY, timer, of, interval } from 'rxjs';
import { finalize, switchMap, map, take, shareReplay } from 'rxjs/operators';
const fileService = {
getFiles: () => interval(1000).pipe(
take(3),
map(x => {
const items = [0, 1, 2].map(i => { return { 'info1': i }; })
return { 'index': x, 'items': items };
})
),
getAdditionalInfo: (files) => {
let wasModified = false;
return timer(2000).pipe(
map(information => {
files.forEach(file => {
file['info2'] = 'information' + files.length;
});
console.log('getAdditionalInfo: modified data');
wasModified = true;
return files;
}),
finalize(() => {
if (!wasModified) {
console.log('getAdditionalInfo: cancelled');
}
})
);
}
}
const pagedFLFiles = fileService.getFiles().pipe(
switchMap(response => {
return merge(
of(response),
fileService.getAdditionalInfo(response.items).pipe(
switchMap(() => EMPTY),
));
}
),
shareReplay(1),
);
pagedFLFiles.subscribe(x => {
console.log('immediate', x.index);
});
Stackblitz
Related
I need to make two calls to Firebase (as it doesn't support OR queries) and merge the output into one array at the end to return to the calling service.
I have something that gets pretty close but it outputs a 2D array of arrays (one for each call to Firebase). I've tried a few things and this is the best I can get to. Any help on tidying up the below would be great.
getAllFriends(): Observable<[Friendship[], Friendship[]]> {
const invitesSent = from(this.afAuth.currentUser.then(user => {
return user.uid;
}))
.pipe(
switchMap(
userid => {
return this.db.collection('friendships', ref => ref.where('inviter', '==', userid)).snapshotChanges().pipe(map(actions => {
return actions.map(action => {
const data = new Friendship(action.payload.doc.data());
data.id = action.payload.doc.id;
console.log(data);
return data;
});
}));
}
)
);
const invitesReceived = from(this.afAuth.currentUser.then(user => {
return user.uid;
}))
.pipe(
switchMap(
userid => {
return this.db.collection('friendships', ref => ref.where('invitee', '==', userid)).snapshotChanges().pipe(map(actions => {
return actions.map(action => {
const data = new Friendship(action.payload.doc.data());
data.id = action.payload.doc.id;
console.log(data);
return data;
});
}));
}
)
);
return combineLatest([invitesSent, invitesReceived]);
}
Friendship is just an object with property: value pairs, nothing special.
I have tried then putting a .pipe() after this returned observable but that just stops the subscription firing in the calling service.
What about returning, at the end, something like this
return combineLatest([invitesSent, invitesReceived]).pipe(
map(([frienships_1, friendships_2]) => ([...friedships_1, ...friendships_2]))
)
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());
}
}),
}
}
I have a function that wraps observable with error handling, but to do so I need some code to run once it's inner observable is subscribed.
I also need that cancelling the higher Observable cancels the inner one, as it is doing HTTP call.
Context
slideshow: string[] = [];
currentIndex = 0;
private is = {
loading: new BehaviorSubject(false),
}
private loadImage(src: string): Observable;
private loadNextImage(index = this.currentIndex, preload = false): Observable<number> {
const nextIndex = (index + 1) % this.slideshow.length;
if (this.currentIndex == nextIndex) {
if (!preload) {
this.is.loading.next(false);
}
throw new Error('No other images are valid');
}
return ( possible code below )
}
Defer - This worked nicely until I realised this will create a new instance for every subscriber.
defer(() => {
if (!preload) {
this.is.loading.next(true);
}
return this.loadImage(this.slideshow[nextIndex]).pipe(
finalize(() => {
if (!preload) {
this.is.loading.next(false);
}
}),
map(() => nextIndex),
catchError(err => this.loadNextImage(nextIndex)),
);
});
Of(void 0).pipe(mergeMap(...)) - This does what is should, but it is really ugly
of(void 0).pipe(
mergeMap(() => {
if (!preload) {
this.is.loading.next(true);
}
return this.loadImage(this.slideshow[nextIndex]).pipe(
finalize(() => {
if (!preload) {
this.is.loading.next(false);
}
}),
map(() => nextIndex),
catchError(err => this.loadNextImage(nextIndex)),
);
}),
)
new Observable - I think there should be a solution that I am missing
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';
}
I just realized that inner-observables (like those defined in a mergeMap or switchMap operator) do not "stop" even when the outer-observable has no subscription left.
For a better example, let's show some code:
const {
Subject,
of: obsOf,
concat: obsConcat,
defer,
} = require("rxjs");
const {
finalize,
mergeMap,
tap,
takeUntil,
} = require("rxjs/operators");
const subject = new Subject();
obsOf(null).pipe(
mergeMap(() =>
obsConcat(
defer(() => {
console.log("side-effect 1");
return obsOf(1);
}),
defer(() => {
console.log("side-effect 2");
return obsOf(2);
}),
defer(() => {
console.log("side-effect 3");
return obsOf(3);
})
)
),
finalize(() => {
console.log("finalized");
})
)
.pipe(
takeUntil(subject),
tap((i) => {
if (i === 2) {
subject.next();
}
})
).subscribe(
(i) => { console.log("next", i); },
(e) => { console.log("error", e); },
() => { console.log("complete"); },
);
// Ouput:
// > side-effect 1
// > next 1
// > side-effect 2
// > complete
// > finalized
// > side-effect 3
The fact that the side-effect 3 line is logged is weird since the outer observable already called finalize.
As all those side-effects are in a defer, they could perfectly be avoided after unsubscription. From my point-of-view, those side-effects provide no value at all.
Any idea why RxJS still execute those ?
This is unfortunately by design (as of RxJS 6) - concat will buffer the observables and will subscribe to each buffered one even after you unsubscribe (if the subscription is closed it will subscribe and immediately unsubscribe).
You have to prevent the observables from getting buffered...
obsOf(null).pipe(
mergeMap(() => obsOf(
defer(() => {
console.log("side-effect 1");
return obsOf(1);
}),
defer(() => {
console.log("side-effect 2");
return obsOf(2);
}),
defer(() => {
console.log("side-effect 3");
return obsOf(3);
})
)),
concatAll(),
finalize(() => {
console.log("finalized");
}),
takeUntil(subject),
tap((i) => {
if (i === 2) {
subject.next();
}
})
).subscribe(
(i) => { console.log("next", i); },
(e) => { console.log("error", e); },
() => { console.log("complete"); },
);
One could think the code above works, but only until you delay one of the observables. Replace obsOf(1) with timer(100).pipe(mapTo(1)); and behavior is exactly the same.
The only workaround is to make sure you are not buffering anything (mean don't use concat* operators) or limit observable production some other way (use separate Subject and control the production manually).