Observables and fetching paged data? - rxjs

I need to create an observable, which I can "pull" data from, to work with a pageable api. I can only fetch 100 items per request, I want to be able to use observable as a generator function (on which I can call .next() to issue a request to get next 100 items.
I can't unfortunately find a way to do it with Rx. I suppose it's possible using controlled observable or a subject. Can you guys show me an example.
this is what I've gotten so far:
function list(entityType, viewName, fetchAll = false) {
var skip = 0,
total = 0;
const subject = new Rx.Subject(),
response$ = subject
.takeWhile(() => skip <= total)
.startWith(skip)
.flatMap((skip) => fetchPagePromise(skip)),
next = () => subject.onNext(skip);
if (fetchAll) {
Rx.Observable.timer(100, 100).subscribe(() => next());
}
return {
data$: response$.map(response => response),
next: fetchAll === true ? undefined : next
};
function fetchPagePromise() {
let limit = 100,
obj = {
viewName, limit, skip
},
qs = objectToQueryString(obj);
return $http.get(`${apiBase}/api/data/${entityType}${qs}`).then((res) => {
total = res.data.Total;
skip += limit;
return res.data.Rows;
});
}
}
this kinda works like a generator. it returns an Observable and next handler. Whenever next is called it pulls next 100 items from api and pushes into the Observable. Also if there’s a third parameter fetchAll passed, then it will keep fetching data until there’s no more. What scares me though that there are 2 mutating vars in function's closure - skip and total, and I don't know if managing them like this in asynchronous/unpredictable environment is ok.

One of the things you generally want to avoid is trying to make Rx into a plain old event emitter. Usually it is an indicator when you try and just trigger Observables manually by passing around a Subjects observer interface.
You should ask yourself, where is my data coming from? What calls next(), what calls that, etc. After enough of these you will generally find that this will lead you to something that can be wrapped by an Observable directly rather than explicitly calling next(). Also, I think the fetchAll flag should really be kept externally. You are only making the interface confusing by essentially turning it into a void method just by passing in a flag.
So I would recommend refactoring like so:
Rx.Observable.prototype.lazyRequest = function(entityType, viewName, limit = 100) {
var source = this;
return Rx.Observable.create(obs => {
var response = source
//Skip is really just the (limit * index)
.map((x, i) => i * limit)
.flatMap((skip) => {
let obj = {viewName, skip, limit},
qs = objectToQueryString(obj);
//Handle promises implicitly
return $http.get(`${apiBase}/api/data/${entityType}${qs}`);
},
//Return this with our skip information
(skip, res) => {skip, res})
//Publish it so the stream get shared.
.publish();
//This will emit once once you are out of data
var stop = response.first(x => x.skip >= x.res.data.Total);
return new CompositeDisposable(
//Complete this stream when stop emits
response.takeUntil(stop)
//Downstream only cares about the data rows
.map(x => x.res.data.Rows)
.subscribe(obs),
//Hook everything up
response.connect());
});
}
Then you can use it like so:
//An example of a "starting point", a button click
//Update the rows every time a new event comes through
Rx.Observable.fromEvent($button, 'click')
.startWith(0) //Inject some data into the pipeline
.lazyRequest(entityType, viewName)
.subscribe(/*Do something with the returned rows*/);
//Get all of the rows, will keep hitting the endpoint until it completes
Rx.Observable.interval(100)
.lazyRequest(entityType, viewName)
//Gather all the values into an array and emit that.
.toArray()
.subscribe();

Related

Angular 11 finalize is not called when the subject type value emitted is null and not processed in switchmap

I have a subject which emits a string value and the code is as below: when the components get initialized, the subjectTypeSubject is null. But there is another method in a component get subscribed to this observable where i set isLoading to true. Because the finalize is not getting called, the loading is always set to true. How to make it work so it gets completed when the value is null as well.
private subjectTypeSubject = new BehaviorSubject<string>(null);
private getPage() {
this.subjectTypeSubject.pipe(
filter((selectedSubjectType) => {
console.log('subject type', selectedSubjectType); //first time it comes as null. so it wont go inside switchmap.
return selectedSubjectType && selectedSubjectType !== '';
}),
switchMap((selectedSubjectType) => {
return this.customListsService
.getCustomListItemsByTypeName()
}),
map((customItemsData) => {
return customItemsData
})
);
}
private _getPage(pageNumber: number, search: string) {
this.loading = true;
this._pageSubscription = this.getPage({
pageSize: this._config.pageSize,
pageNumber,
search
})
.pipe(finalize(() => (this.loading = false))) //this is not called
.subscribe((p) => {
this._currentPage = p.pageNumber;
this.options = p.options;
this._allLoaded = p.isLast;
this.loading = false;
});
}
Adding a takeWhile() instead of filter worked for me. If there is any other better solution. please let me know. thanks
BehaviorSubject doesn't complete unless you complete it
There are multiple ways to call complete an observable in a pipe. take, takeWhile and takeUntil are some of them. Calling .complete on the BehaviorSubject is also an option.
But you should ask yourself: is this really what you want to achieve here? After completion it's not possible to pass any data to the subscription, even if the initial BehaviorSubject emits a new value.
One thing that this strange about your code: it should not work at all. In getPage() you are creating a new observable (by piping the BehaviorSubject), but you are not returning it. Therefore it should return undefined. It‘s also a little bit odd that you are using pipe in a function call. You should either declare the pipe during initialization or directly subscribe to a newly created observable.

RxJS test equality of two streams regardless of order

RxJS provides the sequenceEqual operator to compare two streams in order. How would one go about testing equality of two streams regardless of order?
Pseudocode:
//how do we implement sequenceEqualUnordered?
from([1,2,3]).pipe(sequenceEqualUnordered(from([3,2,1]))).subscribe((eq) =>
console.log("Eq should be true because both observables contain the same values")
)
In my particular use case I need to wait until a certain set of values has been emitted or error but I don't care what order they're emitted in. I just care that each value of interest is emitted once.
Here's my solution:
import { Observable, OperatorFunction, Subscription } from 'rxjs';
export function sequenceEqualUnordered<T>(compareTo: Observable<T>, comparator?: (a: T, b: T) => number): OperatorFunction<T, boolean> {
return (source: Observable<T>) => new Observable<boolean>(observer => {
const sourceValues: T[] = [];
const destinationValues: T[] = [];
let sourceCompleted = false;
let destinationCompleted = false;
function onComplete() {
if (sourceCompleted && destinationCompleted) {
if (sourceValues.length !== destinationValues.length) {
emit(false);
return;
}
sourceValues.sort(comparator);
destinationValues.sort(comparator);
emit(JSON.stringify(sourceValues) === JSON.stringify(destinationValues));
}
}
function emit(value: boolean) {
observer.next(value);
observer.complete();
}
const subscriptions = new Subscription();
subscriptions.add(source.subscribe({
next: next => sourceValues.push(next),
error: error => observer.error(error),
complete: () => {
sourceCompleted = true;
onComplete();
}
}));
subscriptions.add(compareTo.subscribe({
next: next => destinationValues.push(next),
error: error => observer.error(error),
complete: () => {
destinationCompleted = true;
onComplete();
}
}));
return () => subscriptions.unsubscribe();
});
}
As many of RxJS operators have some input parameters and as all of them return functions, sequenceEqualUnordered also has some input parameter (mostly the same as Rx's sequenceEqual operator) and it returns a function. And this returned function has the Observable<T> as the source type, and has Observable<boolean> as the return type.
Creating a new Observable that will emit boolean values is exactly what you need. You'd basically want to collect all the values from both source and compareTo Observables (and store them to sourceValues and destinationValues arrays). To do this, you need to subscribe to both source and compareTo Observables. But, to be able to handle subscriptions, a subscriptions object has to be created. When creating a new subscriptions to source and compareTo, just add those subscriptions to subscriptions object.
When subscribing to any of them, collect emitted values to appropriate sourceValues or destinationValues arrays in next handlers. Should any errors happen, propagate them to the observer in error handlers. And in complete handlers, set the appropriate sourceCompleted or destinationCompleted flags to indicate which Observable has completed.
Then, in onComplete check if both of them have completed, and if they all are, compare the emitted values and emit appropriate boolean value. If sourceValues and destinationValues arrays don't have the same lengths, they can't equal the same, so emit false. After that, basically sort the arrays and then compare the two.
When emitting, emit both the value and complete notification.
Also, the return value of function passed to the new Observable<boolean> should be the unsubscribe function. Basically, when someone unsubscribes from new Observable<boolean>, it should also unsubscribe from both source and compareTo Observables and this is done by calling () => subscriptions.unsubscribe(). subscriptions.unsubscribe() will unsubscribe from all subscriptions that are added to it.
TBH, I haven't wrote any tests for this operator, so I'm not entirely sure that I have covered all edge cases.
My first idea. Use toArray on both then zip them together finally sort and compare results?

RXJS wait for other Observable

I need to subscribe to a result, but wait for intermediate operations to finish before getting the result. The trick is that I "visit" my result to populate it:
// a service that gets a model
service.getModel(): Observable<MyModel>;
// I need to enrich my model before consuming it
service.getModel()
.makeSureAllCodesAreFetched(data => visitModel(model))
.subscribe(data => console.log("data is ready: ", data));
// a visitor that visits the model tree and enriches the leaves
// recursively visit the branches
visitModel(model: MyModel) {
if (model.isLeaf) {
// on condition, call a service to fetch additional data
service.fetchCodes(model.codeKey).subscribe(codes => model.codes = codes);
} else {
model.properties.forEach(prop: MyModel => visit(prop));
}
}
I tried to play with merges and forkJoin() without success. I just want to make sure that all calls to fetchCodes(), regardless of the result, are done before my data is subscribed to.
I found a solution, but it's not the cleanest to my opinion.
// a service that gets a model
service.getModel(): Observable<MyModel>;
// I need to enrich my model before consuming it
service.getModel()
.pipe(
mergeMap(data => forkJoin(visitModel(model))))
.subscribe(data => console.log("data is ready: ", data[0]));
// a visitor that visits the model tree and enriches the leaves
// recursively visit the branches
visitModel(model: MyModel, obs?: Observable<MyModel>[]): Observable<MyModel>[] {
if (obs === undefined) {
obs = [];
obs.push(of(model)); // make sure the very first Observable is the root
}
if (model.isLeaf) {
// on condition, call a service to fetch additional data
// push Observable result in the array
obs.push(service.fetchCodes(model.codeKey).map(codes => {
model.codes = codes;
return model;
}));
} else {
model.properties.forEach(prop: MyModel => visit(prop, obs)); // recursive call
}
return obs;
}
My visitor will actually append all the calls to fetchCodes() into an array of Observables and return it. That way forkJoin will wait for all the calls to be completed. The trick (and dirty part) is that I must make sure the first Observable is actually the root element I am interested in.

RxJS incrementally validate a stream of events

I have a state object with string keys and values. Events are coming in, containing key-value pairs to change the state.
I need a debounced stream that:
validates the events and drop all modifications in the debounce cycle if they lead to an invalid state
outputs the diff to the last valid state
For example, for the initial state of {k1: "v1"}, and an event of {k2: "v2"}, output {k2: "v2"}.
But for the events: {k3: "v3"} and {k4: "invalid"}, drop both changes. So when a new event {k5: "v5"} comes in, the k3 key is still undefined.
I was able to implement it, but only by using a new Subject that keeps track of the last valid state: (jsfiddle)
const lastValidState = new Rx.Subject();
const res = modifications
.buffer(debounce)
.withLatestFrom(lastValidState.startWith(state))
.map(([mods, last]) => {
// calculate next state
return [Object.assign({}, last, ...mods), last];
}).filter(([newState]) => {
// check new state
return Object.keys(newState).every((k) => !newState[k].startsWith("invalid"));
// update Subject
}).do(([newState]) => lastValidState.next(newState)).share()
.map(([newState, last]) => {
// output diff
return Object.assign({}, ...Object.keys(newState).filter((k) => newState[k] !== last[k]).map((k) => ({[k]: newState[k]})))
}
)
This code works well, but I don't like the new Subject it introduces. I would prefer a solution that does not rely on that and use only RxJS operators.
I've tried to use pairwise, but I could not figure out how to pair a stream with the last value of itself.
Thanks to cartant's comment, using scan is the way to go.
The only trick is to use distinctUntilChanged to prevent emits for invalid changes.
The modified code: jsfiddle
const res = modifications
.buffer(debounce)
.scan((last, mods) => {
const newState = Object.assign({}, last, ...mods);
const valid = Object.keys(newState).every((k) => !newState[k].startsWith("invalid"));
if (valid) {
return Object.assign({}, ...Object.keys(newState).filter((k) => newState[k] !== last[k]).map((k) => ({[k]: newState[k]})));
}else {
return last;
}
}, state)
.distinctUntilChanged()

RxJS: Setting default value for observable from another observable

I'm trying to create observable stream which takes user id from cookie and, if not found in cookie, fetches it from API. How can I do it in RxJS?
var userIdRequest = Rx.Observable.bindCallback(generateIdAsync);
var cookieUserIdStream = Rx.Observable.of(getCookieValue("user_id"))
.filter(x => x !== null);
var userIdStream = cookieUserIdStream.__ifEmptyThen__(userIdRequest()); // <<< ???
// Emulating async request for user id
// Will be a JSONp call in real app
function generateIdAsync(cb) {
setTimeout(() => {
cb(`id_${new Date().getTime()}`);
}, 300);
}
function getCookieValue(name) {
var regexp = new RegExp(`${name}=([^;]*)`);
var match = document.cookie.match(regexp);
return match && match[1];
}
There's a defaultIfEmpty method which works with simple values only, not with observables. In Bacon.js there's or method for streams, which works perfectly fine, but I don't see anything similar in RxJS. Do I miss something or do I need to implement a custom observer?
You may concat the 2 observables and get the first emitted value:
var userIdStream = Rx.Observable.concat(cookieUserIdStream, userIdRequest).first();

Resources