How to implement a paging solution using RxJs? - javascript

What is the best way to implement a paging solution in RxJs?
If want to have an observable that emits new data based on an event (could be a click, or a JavaScript function call). For instance if I have an Observable retrieving data from a Web API, and I don't want it to just keep on hammering away HTTP requests, I only want that to happen on an event triggered by the subscriber. Scenarios could be infinite scrolls (event triggered by scrolling), classic paging (event triggered by user clicking on next page) etc.

This is the solution I came up with based on the comments and using RxJs 6.3.3
export default class Test extends React.Component<any, any> {
private subscription: Subscription;
public componentDidMount() {
const client = new MyClient();
let source = client.get('/data');
const buttonNotifier = defer(() => {
console.log("waiting");
return fromEventPattern(
(handler) => { this.next = handler; }
).pipe(tap(() =>
console.log("sending more data")
));
});
const pagedData: Observable<{Value:any, NextLink:string}> = source.pipe(
expand(({ NextLink }) => NextLink ?
buttonNotifier.pipe(take(1), concatMap(() => client.get(NextLink))) :
empty()));
this.subscription = pagedData.subscribe(
result => {
this.setState({
Value: result.Value,
});
},
error => {
this.setState({
Error: `ERROR: ${error}`
});
},
() => {
this.setState({
Done: `DONE`
});
}
);
}
public componentWillUnmount() {
if (this.subscription) {
this.subscription.unsubscribe();
}
}
private next: Function;
public render(): React.ReactElement<any> {
return (<button onClick={()=> {this.next();}}>Next</button>);
}
}

Related

Interact with React app in Android WebView

Here is my simplified setup
//--- sample.com app ---//
async fetchFromServer() {
// return promise..
}
function App() {
const [sampleState, setSampleState] = React.useState(null)
React.useEffect(() => {
fetchFromServer().then((data) => {
setSampleState(data)
})
}, [])
return (
<p>{JSON.stringify(sampleState)}</p>
);
}
//--- android app ---//
public class SampleWebActivity extends Activity {
SamleWebInterface mWebInterface;
WebView mWebView;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mWebView = new WebView(this);
setContentView(mWebView);
mWebInterface = new SamleWebInterface();
mWebView.addJavascriptInterface(mWebInterface, "Native");
//...
mWebView.loadUrl("https://sample.com");
}
}
// Somewhere in another activity
private void showWebScreen() {
Intent intent = new Intent(this, SampleWebActivity.class);
// use existing activity if present
intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
startActivity(intent);
}
//--- Question ---//
How can I re-fetch sampleState from server every time SampleWebActivity is shown without reloading the whole page?
I want to aviod reloading because the actual web app is much bigger than the sample. Also I don't know the exact state of the web app so it's not clear which url to load.
I want to show whetever whas shown before the activity was switched but with updated data.
I'm aware of WebView.evaluateJavascript() but don't know how to interact with the react app after it's compiled into vanilla js.
Using the Webview (browser) itself:
You could just use the webview's built in event system, i.e. When the webview returns from being backgrounded for instance you can leverage the document.visibilityState API (https://developer.mozilla.org/en-US/docs/Web/API/Document/visibilityState):
async fetchFromServer() {
// return promise..
}
function App() {
const [sampleState, setSampleState] = React.useState(null)
React.useEffect(() => {
const fetchData = () => {
if (document.visibilityState !== "hidden") {
fetchFromServer().then((data) => {
setSampleState(data)
})
}
}
fetchData();
document.addEventListener("visibilitychange", fetchData);
return () => document.removeEventListener("visibilitychange", fetchData);
}, [])
return (
<p>{JSON.stringify(sampleState)}</p>
);
}
Injecting/Calling from Java:
async fetchFromServer() {
// return promise..
}
function App() {
const [sampleState, setSampleState] = React.useState(null)
React.useEffect(() => {
const fetchData = () => {
fetchFromServer().then((data) => {
setSampleState(data)
})
}
/**
* Make some globals
*/
window.__triggerRefetch__ = fetchData;
window.__setState__ = setSampleState;
fetchData();
return () => {
/**
* Remove the globals
*/
delete window.__triggerRefetch__;
delete window.__setState__;
}
}, [])
return (
<p>{JSON.stringify(sampleState)}</p>
);
}
You trigger a refetch with WebView.evaluateJavascript("window.__triggerRefetch__()") or you can inject data with something like WebView.evaluateJavascript("window.__setState__({foo: "bar"})")

Firebase Firestore: globally detect when there are pending writes

I've got a simple requirement to show a warning to the user if they leave the browser window while a pending write is happening in Firestore using a beforeunload listener:
window.addEventListener('beforeunload', e => {
if (NO_PENDING_SAVES) return;
e.preventDefault();
e.returnValue =
'Your changes are still being saved. Are you sure you want to leave?';
}, {
capture: true,
});
In Firestore using the Web SDK, how do I detect whether there are pending saves or not globally? There is a waitForPendingWrites() method on the Firestore object, but it would require polling and it's also asynchronous, so it won't work inside of beforeunload.
Solved doing this (async way) using Angular Framework and dependency "#angular/fire": "^7.4.1" :
export class FirebaseService {
constructor(private db: AngularFirestore) {
}
pendingWrites(): Observable<any> {
return defer(() => this.db.firestore.waitForPendingWrites())
}
And then:
export class PendingWritesService {
hasPendingWrites$: Observable<boolean>;
constructor(
private firebaseService: FirebaseService
){
this.hasPendingWrites$ = this.somethingPending$();
}
somethingPending$(): Observable<boolean> {
const timeEmmiter = timer(1000);
return race(timeEmmiter, this.firebaseService.pendingWrites()).pipe(
switchMap((pendingWrites, noPendingWrites) => {
const arePendingsWrites = 0;
return of(pendingWrites === arePendingsWrites);
})
)
}
}

EventSource stop recieving any update from server

I have a strange bug concerning EventSource.
I have a server that is permanently sending some event to the UI via EventSource. Until some days ago, everything was working fine.
Recently there was an update, and the server now send new data on some channel.
The thing is, for reason I haven't yet found out, sometimes, EventSource now stop working.
The connection is still open, the status of the request is still pending and not closed, and no error on console at all. The server is also still streaming event to the UI. But EventSource just don't update anymore. I also tried to hit directly with a curl request, but he do not get any update, until I refresh manually.
here is my Client Code =>
import { Injectable, HostListener, OnDestroy } from '#angular/core';
import { environment } from 'src/environments/environment';
#Injectable({
providedIn: 'root'
})
export class SSEService implements OnDestroy {
private eventSource: EventSource;
private callbacks: Map<string, (e: any) => void> = new Map<string, (e: any) => void>();
init(channel: string) {
if (this.eventSource) { return; }
console.log('SSE Channel Start');
this.eventSource = new EventSource(`${environment.SSE_URL}?channel=${channel}`);
}
protected callListener(cb: (d) => void): (e) => void {
const callee = ({data}): void => {
try {
const d = JSON.parse(data);
cb(d.message);
} catch (e) {
console.error(e, data);
}
};
return callee;
}
private addEventToEventSrc(event: string, callback: any, owner: string): void {
console.log(`Subscribed to ⇢ ${event} (owned by: ${owner})`);
const cb = this.callListener(callback);
this.eventSource.addEventListener(event, cb);
if (!this.callbacks.get(`${event}_${owner}`)) {
this.callbacks.set(`${event}_${owner}`, cb);
}
}
subscribe(event: string, callback: any, owner?: string): void {
if (!this.eventSource) { return; }
if (!owner) { owner = 'default'; }
if (!event) { event = 'message'; }
this.addEventToEventSrc(event, callback, owner);
}
unsubscribe(event: string, owner?: string): void {
if (!this.eventSource) { return; }
if (!owner) { owner = 'default'; }
if (!event) { event = 'message'; }
if (this.callbacks.get(`${event}_${owner}`)) {
console.log(`Unsubscribed to ⇢ ${event} (owned by: ${owner})`);
this.eventSource.removeEventListener(event, this.callbacks.get(`${event}_${owner}`));
}
this.callbacks.delete(`${event}_${owner}`);
}
#HostListener('window:beforeunload')
onBrowserClose() {
this.clearAll();
}
ngOnDestroy() {
this.clearAll();
}
clearAll() {
if (this.eventSource) {
console.log('SSE Channel Closed');
this.eventSource.close();
this.eventSource = null;
}
this.callbacks = new Map<string, (e: any) => void>();
}
}
I tried to log the data received, to put try catch... but it is never showing any error, it just stop getting updates.
The data send from the server my be the issue, but the server do not stop stream, so my EventSource should either crash or keep going.
but right now it's silently cutting any updates.
If you could give some idea of where the cause might be I would be very grateful
It turned out that, my Client Implementation of EventSource is correct.
Server side, we had 1 server and a dispatcher, the server was sending a lot of update on the same channel with very short delay to the dispatcher. EventSource was considering all those message as a Single big message, and was stopping the refresh until he considered the message ended. But this was not happening, so it got stuck in open trying to download.

Retrying a polling service with n seconds delay

private pollSubscriptions: Subscription;
private defaultPollTime: number = 2000;
constructor(
private http: HttpClient,
) {
this.pollSubscriptions = new Subscription();
}
pollRequest<T>(
url: string,
updateStatus: any,
pollWhileCondition: Function,
onPollingSuccessCallback?: Function,
timer = this.defaultPollTime
) {
this.pollSubscriptions.add(timer(0, 2000).pipe(
switchMap(() => this.http.get<T>(url).pipe(
catchError((error: any) => empty()))),
tap(updateStatus),
takeWhile(data => pollWhileCondition(data)))
.subscribe());
}
ngOnDestroy(): void {
this.pollSubscriptions.unsubscribe();
}
I am able to poll for multiple urls simultaneously. But how can I enhance current functionality so that I can meet the following requirements:
If a url which is polled gets failed then how can we retry that poll url with a delay of 3(n) secs for 3 times?
How can we add distinct operator on the urls being polled?
STILL NO SOLUTION
Thanks in advance
Hopefully this helps..
A couple of things:
You'll probably want to use a shared timer for your polling if you'd like all of your polls to happen at the same time. So I've added a pollWhen property below.
You're looking for retryWhen, it's a pretty tricky operator to understand, but basically it works like this: When you subscribe, it calls the function you pass it with an observable of potential errors, when an error is emitted, you're expected to scrub that into a "nexted" value to retry, an immediate completion (ex empty) to complete quietly, or an error (ex throwError) to kill the observable with an error.
class Component() {
/** ticks every 10 seconds */
pollWhen = timer(0, 10000)
.pipe(share());
private pollSubscriptions: Subscription;
constructor(
private http: HttpClient,
) {
this.pollSubscriptions = new Subscription();
}
pollRequest<T>(
url: string,
updateStatus: any,
pollWhileCondition: Function,
onPollingSuccessCallback?: Function,
timer = this.defaultPollTime
) {
this.pollSubscriptions.add(this.pollWhen.pipe(
switchMap(() =>
this.http.get<T>(url).pipe(
// Setup retries
retryWhen(
errors => errors.switchMap(
// if more than 3 retries,
// stop retrying quietly
(_, i) => i < 3
? timer(1000)
: EMPTY
)
)
)
),
tap(updateStatus),
takeWhile(pollWhileCondition)
).subscribe());
}
ngOnDestroy(): void {
this.pollSubscriptions.unsubscribe();
}
}
To answer your first question you can use retryWhen operator and replace catch.
For 2 question, method needs a bit of rewrite, you can change pollRequest to a subject() to store and emit the distinct url to the stream for processing.
var pollUrl = new rxjs.Subject()
const updateStatus = () => true
const pollWhileCondition = () => true
const http = url => {
console.log('http call...',url)
return rxjs.timer(1000).pipe(
rxjs.operators.tap(()=>{
throw "http call error"
})
)
}
const distinctUrl = pollUrl.pipe(rxjs.operators.distinct())
distinctUrl.pipe(
rxjs.operators.mergeMap(url => {
return rxjs.timer(0, 2000).pipe(rxjs.operators.map(() => url))
}),
rxjs.operators.tap(()=>console.log('xxx')),
rxjs.operators.mergeMap(url => http(url).pipe(
rxjs.operators.retry(3),
)),
rxjs.operators.catchError(()=>rxjs.empty()),
rxjs.operators.repeat()
).subscribe(()=>{},err=>{
console.warn(err)
},()=>console.log('comple'))
pollUrl.next('http://google.com')
setTimeout(()=> pollUrl.next('http://twitter.com') ,7000)
http://jsfiddle.net/cy0nbs3x/1535/
Not the complete solution but here I am able to achieve retrying 3 times with delay of 3 seconds. I am still seeking for how can I make the active poll urls distinct. Any help is much appreciated.
import { timer as observableTimer, Subscription, interval, of, concat, Observable } from 'rxjs';
import { takeWhile, tap, take, switchMap, repeat, retryWhen, scan, mapTo, expand, exhaustMap } from 'rxjs/operators';
#Injectable()
export class ReportPollingService {
private pollSubscriptions: Subscription;
constructor(private http: HttpClient){}
pollRequest<T>(url: string, updateStatus: any, pollWhileCondition: Function){
if (this.pollSubscriptions.closed) {
this.pollSubscriptions = new Subscription();// re-open polling
}
const request$ = this.http.get<T>(url);
const firstRequest$ = request$;
const polling$ = interval(options.interval).pipe(
take(1),
exhaustMap(() => request$),
repeat()
);
this.pollSubscriptions.add(concat(firstRequest$, polling$).pipe(
retryWhen(errors$ => {
return errors$.pipe(
scan(
({ errorCount, error }, err) => {
return { errorCount: errorCount + 1, error: err };
},
{ errorCount: 0, error: null }
),
switchMap(({ errorCount, error }) => {
if (errorCount >= 3) {
throw error;
}
return observableTimer(3000, null);
})
);
}),
).pipe(tap(updateStatus), takeWhile(data => pollWhileCondition(data))).subscribe());
}
stopPolling(): void {
this.pollSubscriptions.unsubscribe();
}

how to add delay in http request using angular? [duplicate]

I understand that Observable.debounce() can be used to process rapid fire form input. As Http GET also returns an Observable, I wonder it it is possible to debounce rapid http requests? I tried debounceTime() but it did not appear to do anything.
public getStuff(p1, area:string, p2:string): Observable<number> {
return this.http.get(some_url)
.map(r => r.json())
.debounceTime(10000)
.catch(this.handleError);
};
The debounceTime allows to buffer events and only handle the last one after an amount of time.
It's useful in the context of inputs but it should be defined on the observable that triggers the event not on the one created for the HTTP request.
Here is a example on a control associated with an input that leverages the debounceTime operator:
#Component({
(...)
template: `
<input [ngFormControl]="ctrl"/>
`
})
export class MyComponent {
constructor() {
this.ctrl = new Control();
this.ctrl.valueChanges
.debounceTime(500)
.distinctUntilChanged()
.switchMap((value: string) => {
// Get data according to the filled value
return this.service.getData(entry);
})
.subscribe(data => {
// Update the linked list
this.list = data;
});
}
}
This article could also interest you:
https://jaxenter.com/reactive-programming-http-and-angular-2-124560.html (see section "Linking with user events")
Following the micronyks's comment, here is an additional link:
Everything is a stream: http://slides.com/robwormald/everything-is-a-stream (youtube: https://www.youtube.com/watch?v=UHI0AzD_WfY)
You have to transform from subject observable into an http observable with switchMap like this:
observableObj$: Observable<any>;
subjectObj = new Subject();
ngOnInit() {
this.observableObj$ = this.subjectObj.pipe(
debounceTime(1000),
switchMap(() => {
...
return this.http.get(some_url).map(r => r.json());
}),
);
this.observableObj$.subscribe((data) => {
// result of http get...
...
});
}
getStuff() {
this.subjectObj.next();
}
in Angular7:
import { Observable, of, timer } from 'rxjs';
import { catchError, retry, map, debounce } from 'rxjs/operators';
public getStuff(p1, area:string, p2:string): Observable<number> {
return this.http.get(some_url)
.pipe(
debounce(() => timer(10000)),
catchError(this.handleError)
);
};

Categories

Resources