I'm trying to hook into a stream a) before the request is executed and b) after the request is received (e.g. to show/hide a loading bar). Without using interval I could set a boolean isLoaded to false and set it to true in the subscribe function.
But how can I do that with using interval?
Observable.interval(5000).
timeout(3500, new Error('Server not available.')).
flatMap(() => this._http.get('http://api.dev/get/events')).
map(res => (<Response>res).json())
.subscribe(data => this.events = data,
error => console.debug('ERROR', error),
() => console.log('END')
);
Do I have to wrap angulars http.get method in my own observable? Or is there a better / "more angular" way to do it?
I would suggest to not use interval because network request can delay and take longer/shorter times then the interval which would/could make it inconsistent. The way to do it though is...
// the import needed is
import { interval } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
private intervalSub: Subscription;
private ngUnsubscribe = new Subject();
somefunction (){
// Interval_Time was 5000
this.intervalSub = interval(Interval_Time)
.pipe(takeUntil(this.ngUnsubscribe)).subscribe(() => {
// call your api and do the request every Interval time
}
}
// make sure you have
ngOnDestroy() {
this.ngUnsubscribe.next();
this.ngUnsubscribe.complete();
}
the reason you need the ngUnsubscribe subject is so if you route away from the component then it won't keep polling.
Related
I'm implementing the cache using BehaviorSubject and multicast. The stream returned from the cache should start with HTTP request. I should also be able to force refresh the cache by triggering manually next on subject. The common approach with two subjects outlined by Poul Kruijt is well known and suggested everywhere. My idea is to find a way to achieve the following using only one subject throughout lifecycle of a stream.
It would be easy to achieve with multicast like this
const cache = new BehaviorSubject(null);
const shared = queryThatCompletes.pipe(multicast(cache)) as any;
// sets up subscription, waits for connect
shared.subscribe((values) => console.log(values));
// triggers http request
shared.connect();
setTimeout(() => {
// will only emit COMPLETE from subject
shared.subscribe((values) => console.log(values));
}, 2000);
// force refresh the cache
cache.next();
but since the HTTP query stream completes, the second subscription doesn't get any value, just COMPLETE notification from subject. This behavior is described in detail here.
The other option is to pass a factory function instead of the subject instance like this:
const cache = ()=> new BehaviorSubject(null);
const shared = queryThatCompletes.pipe(multicast(cache)) as any;
This will re-create the subject, that will subscribe to queryThatCompletes and re-trigger HTTP request. But the downsides is the need to call connect multiple times and redundant queries.
const cache = () => new BehaviorSubject(null);
const shared = queryThatCompletes.pipe(multicast(cache)) as any;
// sets up subscription, waits for connect
shared.subscribe((values) => console.log(values));
// triggers http request
shared.connect();
setTimeout(() => {
// sets up subscription, waits for connect
shared.subscribe((values) => console.log(values));
// triggers http request
shared.connect();
}, 2000);
So I simply implemented HTTP stream that doesn't complete by itself and use it like this:
const queryOnceButDontComplete = new Observable((observer) => {
fetch('https://jsonplaceholder.typicode.com/todos/1')
.then(response => response.json())
.then(data => observer.next(data));
return () => {};
});
const cache = new BehaviorSubject(null);
const shared = queryOnceButDontComplete.pipe(multicast(cache)) as any;
// sets up subscription, waits for connect
shared.subscribe((values) => console.log(values));
// triggers http request
shared.connect();
setTimeout(() => {
// sets up subscription, waits for connect
shared.subscribe((values) => console.log(values));
}, 2000);
This works, but I'm wondering if there's a way to achieve what I want without the use of custom observable. Any ideas?
Best would be to just use a shareReplay(1):
const shared = queryThatCompletes.pipe(shareReplay(1));
// sets up subscription, waits for connect
shared.subscribe((values) => console.log(values));
// triggers http request
shared.connect();
setTimeout(() => {
shared.subscribe((values) => console.log(values));
}, 2000);
Not entirely sure what the subscribe and connect are doing there, but if you are just returning a HttpClient get call, then you should just return the shared observable, and whomever subscribes first to it, will trigger the http request. No need for connect. Any subsequent subscriptions will wait for the request to finish or receive the last emitted value from the observable.
Based on your comment, let's wrap this in a service (untested):
#Injectable()
SomeDataService {
readonly refresh$ = new BehaviorSubject(undefined);
readonly get$ = this.httpClient.get(/*url here*/);
readonly shared$ = this.refresh$.pipe(
switchMap(() => this.get$),
shareReplay(1)
);
constructor(private httpClient: HttpClient) {}
getData(): Observable<unknown> {
return this.shared$;
}
refreshData(): void {
this.refresh$.next();
}
}
Does this make sense? Basically you start with a refresh subject, which get mapped to the actual network call. On the first getData(), the network request gets triggered. Any call on getData() after that will get the cached value. Calling refreshData will refresh the data for any subscription
I have made functional approach that I believe is much more simpler
it uses two observables refresh$ and the desired observable,
I hope this solution might give you some thoughts
const makeRestartableCahcedObservable = (ob:Observable<any>)=>{
const refresh$ = new BehaviorSubject(null)
return [
refresh$.pipe(
switchMap(()=>ob),
shareReplay(1)
),
()=>refresh$.next(null)
]
}
const [ob, refreshFun] = makeRestartableCahcedObservable(of('string'))
ob.subscribe(console.log)
setTimeout(()=>{refreshFun()}, 3000)
this basically can refresh your data with much more simpler API,
you can even wrap it with an object to make it a proxy with included refresh function
const makeRestartableCahcedObservable = (ob:Observable<any>)=>{
const refresh$ = new BehaviorSubject(null)
const wrappedOb$ = refresh$.pipe(switchMap(()=>ob),shareReplay(1))
return {
subscribe:(...args)=>wrappedOb$.subscribe(...args),
pipe:(...funs)=>wrappedOb$.pipe(...funs),
refresh:()=>refresh$.next(null)
}
}
const ob = makeRestartableCahcedObservable(of('string'))
ob.subscribe(console.error)
setTimeout(()=>{ob.refresh()}, 3000)
I have a service layer responsible for handling the data and sending it to components that use the same value.
I need to make requests to the api, as long as the value is isProcessed == false it will do 10 attempts every half a second.
If it fails during the attempts, it will only make 3 more requests.
If isProcessed == true, stopped the requisitions.
All the logic is built in, but I can't output the last observable value. All responses are sent.
All requests are sent by observables, both false and true, but I only need the last one.
Here all requests responses are arriving in the component, not just the last
Service responsible for accessing API:
public getPositionConsolidate(): Observable<PosicaoConsolidada> {
return this.http.get<PosicaoConsolidada>(`${environment.api.basePosicaoConsolidada}/consolidado`)
.pipe(
map(res => res),
retryWhen(genericRetryStrategy()),
shareReplay(1),
catchError(err => {
console.log('Error in Position Consolidate', err);
return throwError(err);
})
)
}
Service responsible for handling the data and sending it to the component:
public positionConsolidate() {
let subject = new BehaviorSubject<any>([]);
this.api.getPositionConsolidate().subscribe(response => {
if(response.hasProcessado == false) {
for (let numberRequest = 0; numberRequest < 10; numberRequest++) {
setTimeout(() => {
//subject.next(this.api.getPosicaoConsolidada().subscribe());
this.api.getPositionConsolidate().subscribe(res => {
subject.next(res)
})
}, numberRequest * 500, numberRequest);
}
} else {
retryWhen(genericRetryStrategy()),
finalize(() => this.loadingService.loadingOff())
}
})
return subject.asObservable()
}
In component:
public ngOnInit() {
this.coreState.positionConsolidate().subscribe(res => console.log(res))
}
The easiest part to answer of your question, is that if you just want the last emission from an observable, then just use the last operator. However, the way you've written things, makes it difficult to incorperate. The following refactors your code as a single stream without any non-rxjs control structures.
public positionConsolidate() {
return this.api.getPositionConsolidate().pipe(
concatMap(res => iif(() => res.hasProcessado,
of(res),
interval(500).pipe(
take(10),
concatMap(() => this.api.getPositionConsolidate())
)
)),
retryWhen(genericRetryStrategy()),
finalize(() => this.loadingService.loadingOff()),
last()
);
}
What's happening
First this executes the initial api call.
Then based on the result, either...
Returns the initial result
Calls the api 10 times every 500ms.
Implements what you have for retryWhen and finalize.
Returns the last emitted result.
Also don't subscribe to observables inside of observables - that's what higher order observables such as concatMap are for.
I have two components that share data using a service:
export class ShareDataService {
msg: BehaviorSubject<string>;
constructor(
) {
this.msg = new BehaviorSubject("Default Message")
}
changeMessage(message: string) {
this.msg.next(message)
}
}
First component works as a router, when i click a name it redirects to the profile of that name using its id. I import the share-data service i created above, when clicking a name it triggers de sendData() function which gets the id of the clicked element and passes it as a parameter on the service changeMessagge() function:
constructor(
private _data: ShareDataService
) { }
message: string = "";
ngOnInit(): void {
this._data.msg.subscribe(new_msg => this.message = new_msg)
}
sendData() {
var self = this
$("ul.users-list").on("click", ".user", function(event) {
self._data.changeMessage(event.target.id)
});
}
}
After that, when it redirects to the new component, this new component also imports data from the service:
message: string = "";
constructor(
private _http: HttpClient,
private _data: ShareDataService,
private _router: Router
) { }
async ngOnInit() {
this._data.msg.subscribe(new_msg => this.message = new_msg)
await new Promise(r => setTimeout(r, 1)); // Sleeps 1ms so it can load message
if (this.message === "Default Message") { // Returns to landing if no user loaded
this._router.navigateByUrl("/landing");
return
}
this._http.get<UserInterface>(`https://jsonplaceholder.typicode.com/users/${this.message}`)
.subscribe((data: UserInterface) => this.user = data)
}
As you can see, it subscribes to the service and gets the value of messagge, which should have been changed when i clicked a name on the other component. The issue is, it fristly doesn't detect the change, and i have to use the new Promise() function to make it sleep for 1ms so it can load the new value of messagge from the service and then continue. Is there any way i can make it wait for the subscribe to finish loading the new messagge without using the Promise for 1ms? already tried putting the code inside the subscribe after it assigns messagge.
I see multiple issues
The variable this.message is assigned asynchronously. By the time the HTTP request is sent, it isn't assigned a value yet. You need to use higher order mapping operator like switchMap to map from one observable to another.
import { switchMap } from 'rxjs/operators';
this._data.msg.pipe(
switchMap(message =>
this._http.get<UserInterface>(`https://jsonplaceholder.typicode.com/users/${message}`)
)
).subscribe((data: UserInterface) => this.user = data)
You could see my post here for brief info on handling multiple co-dependent observables.
await new Promise(r => setTimeout(r, 1)); looks inelegant however small the timeout be. With the above mapping operator it shouldn't be needed anymore.
Avoid using jQuery in Angular. Almost everything from jQuery could be accomplished directly in Angular. Mixing them might lead to maintenance later.
Update: returning observable conditionally
Based on your condition, you could use RxJS iif function with new Observable() construct to either return the HTTP request or do something else and close the observable based on a condition.
Try the following
import { iif, EMPTY, Observable } from 'rxjs';
import { switchMap } from 'rxjs/operators';
this._data.msg.pipe(
switchMap(message =>
iif(
() => message === "Default Message",
new Observable(subscriber => {
this._router.navigateByUrl("/landing");
subscriber.complete();
}),
this._http.get<UserInterface>(`https://jsonplaceholder.typicode.com/users/${message}`)
)
)
).subscribe((data: UserInterface) => this.user = data)
What I want to do:
Send a 'request' event to a websocket server.
Receive an immediate response event bundled with some additional data.
Keep receiving responses over time.
My question is:
Is there a cleaner way of doing what I'm doing below? Without using setTimeout.
First, look at this simplified working example:
const { Subject, defer, interval, of} = rxjs;
const EventEmitter = EventEmitter3;
const emitter = new EventEmitter();
const subject = new Subject();
// The next lines mock a websocket server message listener, imagine this being present on server side
emitter.on("request", () => {
subject.next(`Immediate response`);
interval(1000).subscribe((index) =>
subject.next(`Delayed response...${index}`)
);
});
// Imagine the following code being present in the browser
function getData() {
return defer(() => {
// The next line mocks a websocket client sent event
setTimeout(() => emitter.emit("request"), 0);
return subject;
});
}
getData().subscribe((data) => console.log(data));
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.5.3/rxjs.umd.min.js"></script>
<script src="https://unpkg.com/eventemitter3#latest/umd/eventemitter3.min.js"></script>
Now look at a non-working example. Here, the client doesn't get the immediate response, because the 'request' event is being sent before the subscription is set up.
const { Subject, defer, interval, of} = rxjs;
const EventEmitter = EventEmitter3;
const emitter = new EventEmitter();
const subject = new Subject();
// The next lines mock a websocket server message listener, imagine this being present on server side
emitter.on("request", () => {
subject.next(`Immediate response`);
interval(1000).subscribe((index) =>
subject.next(`Delayed response...${index}`)
);
});
// Imagine the following code being present in the browser
function getData() {
return defer(() => {
emitter.emit('request');
return subject;
});
}
getData().subscribe((data) => console.log(data));
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.5.3/rxjs.umd.min.js"></script>
<script src="https://unpkg.com/eventemitter3#latest/umd/eventemitter3.min.js"></script>
You could use a BehaviorSubject instead of a Subject:
const { BehaviorSubject, defer, interval, of } = rxjs;
const EventEmitter = EventEmitter3;
const emitter = new EventEmitter();
const subject = new BehaviorSubject();
// The next lines mock a websocket server message listener, imagine this being present on server side
emitter.on("request", () => {
// the timing of this synchronous response would be impossible in a real socket
subject.next(`Immediate response`);
interval(1000).subscribe((index) =>
subject.next(`Delayed response...${index}`)
);
});
// Imagine the following code being present in the browser
function getData() {
return defer(() => {
emitter.emit('request');
return subject;
});
}
getData().subscribe((data) => console.log(data));
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.5.3/rxjs.umd.min.js"></script>
<script src="https://unpkg.com/eventemitter3#latest/umd/eventemitter3.min.js"></script>
Keep in mind though that it's impossible for the server's response to arrive back synchronously after the socket request is sent in the same way you've implemented your mock for this demo, so while Subject does not produce the expected result here, it would work fine with a real socket.
In the example provided in the aor-realtime readme
import realtimeSaga from 'aor-realtime';
const observeRequest = (fetchType, resource, params) => {
// Use your apollo client methods here or sockets or whatever else including the following very naive polling mechanism
return {
subscribe(observer) {
const intervalId = setInterval(() => {
fetchData(fetchType, resource, params)
.then(results => observer.next(results)) // New data received, notify the observer
.catch(error => observer.error(error)); // Ouch, an error occured, notify the observer
}, 5000);
const subscription = {
unsubscribe() {
// Clean up after ourselves
clearInterval(intervalId);
// Notify the saga that we cleaned up everything
observer.complete();
}
};
return subscription;
},
};
};
const customSaga = realtimeSaga(observeRequest);
fetchData function is mentioned, but it's not accessible from that scope, is it just a symbolic/abstract call ?
If I really wanted to refresh the data based on some realtime trigger how could i dispatch the data fetching command from this scope ?
You're right, it is a symbolic/abstract call. It's up to you to implement observeRequest, by initializing it with your restClient for example and calling the client methods accordingly using the fetchType, resource and params parameters.
We currently only use this saga with the aor-simple-graphql-client client