Observable Pagination for infinite scroll to stay up to date - rxjs

I am trying to get my observable to update automatically during pagination.
Only Page Trigger
When load() is called, the page observable gets triggered, but the subscription observable does not get triggered when it is updated...
pageSub = new BehaviorSubject<number>(1);
page = 1;
// in constructor...
this.posts = this.pageSub
.pipe(
mergeMap((page: number) => this.urql
.subscription(this.GET_POSTS,
{
first: this.ITEMS.toString(),
offset: (this.ITEMS * (page - 1)).toString()
}
)
),
scan((acc, value) => [...acc, ...value]),
);
// called from button
load() {
this.page += 1;
this.pageSub.next(this.page);
}
Only Subscription Trigger
Here a subscription change does get triggered, but when load() is called, a page change does not get triggered...
this.posts = combineLatest([this.pageSub, this.urql
.subscription(this.GET_POSTS,
{
first: this.ITEMS.toString(),
offset: (this.ITEMS * (this.page - 1)).toString()
}
)]).pipe(
map((arr: any[]) => arr[1])
);
Surely, there is a way so that the Observable gets updated with a page change and with a subscription change?
I should add that resetting the variable declaration this.posts does not work, as it refreshed the page and is not intended behavior.
Any ideas?
Thanks,
J

Got it:
mainStream: BehaviorSubject<Observable<any>>;
posts!: Observable<any>;
constructor(private urql: UrqlModule) {
this.mainStream = new BehaviorSubject<Observable<any>>(this.getPosts());
this.posts = this.mainStream.pipe(mergeAll());
}
async load() {
this.page += 1;
this.mainStream.next(this.combine(this.mainStream.value, this.getPosts()));
}
getPosts() {
return this.urql.subscription(this.GET_POSTS,
{
first: this.ITEMS.toString(),
offset: (this.ITEMS * (this.page - 1)).toString()
}
)
}
combine(o: Observable<any>, o2: Observable<any>) {
return combineLatest([o, o2]).pipe(
map((arr: any) => arr.reduce((acc: any, cur: any) => acc.concat(cur)))
);
}

Related

React - useEffect not using updated value in websocket onmessage

I have a simple issue where a state value updates in my code but is not using the new value. Any ideas what I can do to adjust this?
const [max, setMax] = useState<number>(10);
useEffect(() => {
console.log('max', max); //This outputs correct updated value.
ws.onmessage = (message: string => {
console.log('max', max); //This is always 10.
if (max > 100) {
doSomething(message);
}
}
},[]);
function onChange() {
setMax(1000);
}
<Select onChange={onChange}></Select> //this is abbrev for simplicity
Your useEffect is running only once, on the initial render, using the values from initial render, so max variable is closure captured and not updated in any way. But the solution will be pretty simple, using useRef and one more useEffect to update the ref variable when max variable updates.
const maxRef = useRef(10); // same value
const [max, setMax] = useState(10);
// Only used to update ref variable
useEffect(() => {
maxRef.current = max;
}, [max]);
useEffect(() => {
console.log("max", maxRef.current);
ws.onmessage = (message) => {
console.log("max", maxRef.current);
if (maxRef.current > 100) {
doSomething(message);
}
};
}, []);

RxJs - how to make observable behave like queue

I'm trying to achieve next:
private beginTransaction(): Observable() {
..
}
private test(): void {
this.beginTransaction().subscribe((): void => {
this.commitTransaction();
});
this.beginTransaction().subscribe((): void => {
this.commitTransaction();
});
}
beginTransaction can be called concurrently, but should delay the observable until first or only one beginTransaction finished.
In order words: Only one transaction can be in progress at any time.
What have I tried:
private transactionInProgress: boolean = false;
private canBeginTransaction: Subject<void> = new Subject<void>();
private bla3(): void {
this.beginTransaction().subscribe((): void => {
console.log('beginTransaction 1');
this.commitTransaction();
});
this.beginTransaction().subscribe((): void => {
console.log('beginTransaction 2');
this.commitTransaction();
});
this.beginTransaction().subscribe((): void => {
console.log('beginTransaction 3');
this.commitTransaction();
});
}
private commitTransaction(): void {
this.transactionInProgress = false;
this.canBeginTransaction.next();
}
private beginTransaction(): Observable<void> {
if(this.transactionInProgress) {
return of(undefined)
.pipe(
skipUntil(this.canBeginTransaction),
tap((): void => {
console.log('begin transaction');
})
);
}
this.transactionInProgress = true;
return of(undefined);
}
What you've asked about is pretty vague and general. Without a doubt, a more constrained scenario could probably look a whole lot simpler.
Regardless, here I create a pipeline that only lets transaction(): Observable be subscribed to once at a time.
Here's how that might look:
/****
* Represents what each transaction does. Isn't concerned about
* order/timing/'transactionInProgress' or anything like that.
*
* Here is a fake transaction that just takes 3-5 seconds to emit
* the string: `Hello ${name}`
****/
function transaction(args): Observable<string> {
const name = args?.message;
const duration = 3000 + (Math.random() * 2000);
return of("Hello").pipe(
tap(_ => console.log("starting transaction")),
switchMap(v => timer(duration).pipe(
map(_ => `${v} ${name}`)
)),
tap(_ => console.log("Ending transation"))
);
}
// Track transactions
let currentTransactionId = 0;
// Start transactions
const transactionSubj = new Subject<any>();
// Perform transaction: concatMap ensures we only start a new one if
// there isn't a current transaction underway
const transaction$ = transactionSubj.pipe(
concatMap(({id, args}) => transaction(args).pipe(
map(payload => ({id, payload}))
)),
shareReplay(1)
);
/****
* Begin a new transaction, we give it an ID since transactions are
* "hot" and we don't want to return the wrong (earlier) transactions,
* just the current one started with this call.
****/
function beginTransaction(args): Observable<any> {
return defer(() => {
const currentId = currentTransactionId++;
transactionSubj.next({id: currentId, args});
return transaction$.pipe(
first(({id}) => id === currentId),
map(({payload}) => payload)
);
})
}
// Queue up 3 transactions, each one will wait for the previous
// one to complete before it will begin.
beginTransaction({message: "Dave"}).subscribe(console.log);
beginTransaction({message: "Tom"}).subscribe(console.log);
beginTransaction({message: "Tim"}).subscribe(console.log);
Asynchronous Transactions
The current setup requires transactions to be asynchronous, or you risk losing the first one. The workaround for that is not simple, so I've built an operator that subscribes, then calls a function as soon as possible after that.
Here it is:
function initialize<T>(fn: () => void): MonoTypeOperatorFunction<T> {
return s => new Observable(observer => {
const bindOn = name => observer[name].bind(observer);
const sub = s.subscribe({
next: bindOn("next"),
error: bindOn("error"),
complete: bindOn("complete")
});
fn();
return {
unsubscribe: () => sub.unsubscribe
};
});
}
and here it is in use:
function beginTransaction(args): Observable<any> {
return defer(() => {
const currentId = currentTransactionId++;
return transaction$.pipe(
initialize(() => transactionSubj.next({id: currentId, args})),
first(({id}) => id === currentId),
map(({payload}) => payload)
);
})
}
Aside: Why Use defer?
Consider re-writting beginTransaction:
function beginTransaction(args): Observable<any> {
const currentId = currentTransactionId++;
return transaction$.pipe(
initialize(() => transactionSubj.next({id: currentId, args})),
first(({id}) => id === currentId),
map(({payload}) => payload)
);
}
In this case, the ID is set at the moment you invoke beginTransaction.
// The ID is set here, but it won't be used until subscribed
const preppedTransaction = beginTransaction({message: "Dave"});
// 10 seconds later, that ID gets used.
setTimeout(
() => preppedTransaction.subscribe(console.log),
10000
);
If transactionSubj.next is called without the initialize operator, then this problem gets even worse as transactionSubj.next would also get called 10 seconds before the observable is subscribed to (You're sure to miss the output)
The problems continue:
What if you want to subscribe to the same observable twice?
const preppedTransaction = beginTransaction({message: "Dave"});
preppedTransaction.subscribe(
value => console.log("First Subscribe: ", value)
);
preppedTransaction.subscribe(
value => console.log("Second Subscribe: ", value)
);
I would expect the output to be:
First Subscribe: Hello Dave
Second Subscribe: Hello Dave
Instead, you get
First Subscribe: Hello Dave
First Subscribe: Hello Dave
Second Subscribe: Hello Dave
Second Subscribe: Hello Dave
Because you don't get a new ID on subscribing, the two subscriptions share one ID. defer fixes this problem by not assigning an id until subscription. This becomes seriously important when managing errors in streams (letting you re-try an observable after it errors).
I am not sure I have understood the problem right, but it looks to me as concatMap is the operator you are looking for.
An example could be the following
const transactionTriggers$ = from([
't1', 't2', 't3'
])
function processTransation(trigger: string) {
console.log(`Start processing transation triggered by ${trigger}`)
// do whatever needs to be done and then return an Observable
console.log(`Transation triggered by ${trigger} processing ......`)
return of(`Transation triggered by ${trigger} processed`)
}
transactionTriggers$.pipe(
concatMap(trigger => processTransation(trigger)),
tap(console.log)
).subscribe()
You basically start from a stream of events, where each event is supposed to trigger the processing of the transaction.
Then you use processTransaction function to do whatever you have to do to process a transaction. processTransactio needs to return an Observable which emits the result of the processing when the transaction has been processed and then completes.
Then in the pipe you can use tap to do further stuff with the result of the processing, if required.
You can try the code in this stackblitz.

RXJS listen to first subscription

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

angular 5, subject.next(value) does not fire

I have a problem trying to pass values into my subject and subscribing to it from another component. Here is my code that is supposed to pass my value into an observable.
private pdfLink = new Subject<string>();
pdfLinkCast = this.pdfLink.asObservable();
getPdfById(id: string): void {
this.httpClient.get<any>(this.apiUrl + id + '/pdf', this.httpOptions).subscribe((pdf) => {
// this prints
console.log(pdf);
// not sure if this is working as expected
this.pdfLink.next(pdf.url);
});
}
In my component I subscribe to it on ngOnInit as follows:
ngOnInit() {
this.subscription.add(this.someService.pdfLinkCast.subscribe((pdf) => {
// these do not print for some reason
console.log('hello world');
console.log(pdf);
}));
}

Check if publishReplay().refCount() has observers or not

I define an Observable like this:
const obs$ = Observable.create(...)
.publishReplay(1)
.refCount();
So that it puts a ReplaySubject(1) between my source Observable and all observers.
Since ReplaySubject has in its state the number of observers (via its observers array property), how is it possible to access the ReplaySubject from obs$?
I actually only need to know if obs$ has any observers or not. RxJS4 had a hasObservers() method on Subject, but it got removed in RxJS5. How can I achieve this with RxJS5?
Not sure about your usage but for my needs I created a custom operator that allowed me to transparently perform side-effects (similar to tap) based on the state of the refCount. It just does a pass-through subscription and duck-punches the sub/unsub. The callback gets the current refCount and the previous so that you can tell the state and direction. I like using an operator for this since I can insert it at any point in my stream. If you simply want a binary output for whether there are any subscriptions or not it could be easily modified for that.
const { Observable, Observer, interval } = rxjs;
const { publishReplay, refCount } = rxjs.operators;
const tapRefCount = (onChange) => (source) => {
let refCount = 0;
// mute the operator if it has nothing to do
if (typeof onChange !== 'function') {
return source;
}
// mute errors from side-effects
const safeOnChange = (refCount, prevRefCount) => {
try {
onChange(refCount, prevRefCount);
} catch (e) {
}
};
// spy on subscribe
return Observable.create((observer) => {
const subscription = source.subscribe(observer);
const prevRefCount = refCount;
refCount++;
safeOnChange(refCount, prevRefCount);
// spy on unsubscribe
return () => {
subscription.unsubscribe();
const prevRefCount = refCount;
refCount--;
safeOnChange(refCount, prevRefCount);
};
});
};
const source = interval(1000).pipe(
publishReplay(1),
refCount(),
tapRefCount((refCount, prevRefCount) => { console.log('refCount', refCount, prevRefCount > refCount ? 'down': 'up'); })
);
const firstSub = source.subscribe((x) => { console.log('first', x); });
let secondSub;
setTimeout(() => {
secondSub = source.subscribe((x) => { console.log('second', x); });
}, 1500);
setTimeout(() => {
firstSub.unsubscribe();
}, 4500);
setTimeout(() => {
secondSub.unsubscribe();
}, 5500);
<script src="https://unpkg.com/rxjs#rc/bundles/rxjs.umd.min.js"></script>
The typescript version:
import { Observable } from 'rxjs/Observable';
import { Observer } from 'rxjs/Observer';
export const tapRefCount = (
onChange: (refCount: number, prevRefCount: number) => void
) => <T>(source: Observable<T>): Observable<T> => {
let refCount = 0;
// mute the operator if it has nothing to do
if (typeof onChange !== 'function') {
return source;
}
// mute errors from side-effects
const safeOnChange = (refCount, prevRefCount) => {
try {
onChange(refCount, prevRefCount);
} catch (e) {
}
};
// spy on subscribe
return Observable.create((observer: Observer<T>) => {
const subscription = source.subscribe(observer);
const prevRefCount = refCount;
refCount++;
safeOnChange(refCount, prevRefCount);
// spy on unsubscribe
return () => {
subscription.unsubscribe();
const prevRefCount = refCount;
refCount--;
safeOnChange(refCount, prevRefCount);
};
}) as Observable<T>;
};
The Subject class has a public property called observers (see https://github.com/ReactiveX/rxjs/blob/5.5.10/src/Subject.ts#L28)
So you can use just:
const s = new Subject();
...
if (s.observers.length > 0) {
// whatever
}
Be aware that refCount returns an Observable so you won't be able to do what I mentioned above. However, you can provide your own Subject instance to publishReplay as the third argument and use s.observers on that, see http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-publishReplay

Resources