how to use multicasting obs with behavioursubject? - rxjs

In general we need behavior subject functionality. But only on first subscription we should send subscribe to server in REST. And to send unsubscribe on the last unsubscribe, and all late observers subscribed will gwt the latest json recwived from the first. can i do it using rxjs operaTors and how? or shoul i use custom obserbale ?
currently the custom code for this is this:
public observable: Observable<TPattern> = new Observable((observer: Observer<TPattern>) => {
this._observers.push(observer);
if (this._observers.length === 1) {
this._subscription = this.httpRequestStream$
.pipe(
map((jsonObj: any) => {
this._pattern = jsonObj.Data;
return this._pattern;
})
)
.subscribe(
(data) => this._observers.forEach((obs) => obs.next(data)),
(error) => this._observers.forEach((obs) => obs.error(error)),
() => this._observers.forEach((obs) => obs.complete())
);
}
if (this._pattern !== null) {
observer.next(this._pattern); // send last updated array
}
return () => {
const index: number = this._observers.findIndex((element) => element === observer);
this._observers.splice(index, 1);
if (this._observers.length === 0) {
this._subscription.unsubscribe();
this._pattern = null; // clear pattern when unsubscribed
}
};
});

Sounds like you need a shareReplay(1), it will share the latest response with all subscribes.
const stream$ = httpRequestStream$.pipe(
shareReplay(1),
),
stream$.subscribe(); // sends the request and gets its result
stream$.subscribe(); // doesn't send it but gets cached result
stream$.subscribe(); // doesn't send it but gets cached result
stream$.subscribe(); // doesn't send it but gets cached result

Related

Intercept each request with before and after hooks

I have to make 5 requests (order doesn't matter) to 5 different endpoints. The URL of these endpoints is the same, except for the business line. These business lines are the array of the from.
I want show a skeleton loader before each request and hide once it finish. So, basically the flow is:
1. [Hook - before request]
2. [Log of data fetched]
3. [Hook - after request]
This is my service:
export function getInsurances(
userIdentity: string,
hooks?: RequestHooks
): Observable<Policy[]> {
return from(["all", "vehicle", "health", "soat", "plans"]).pipe(
tap(() => hooks?.beforeRequest && hooks.beforeRequest()),
flatMap<string, Observable<Policy[]>>(businessLine => {
return InsurancesApi.getPolicies<Policy>(
userIdentity,
businessLine
).pipe(
map(policies => {
return policies.map(policy => PolicyStandarizer(policy));
}),
finalize(() => {
hooks?.afterRequest && hooks.afterRequest();
})
);
}),
catchError(err => of(err)),
takeUntil(HttpCancelator)
);
}
This is my subscribe:
const hooks = {
beforeRequest() {
Log.info("Before Request");
setStatus(HttpRequestStatus.PENDING);
},
afterRequest() {
Log.warn("After Request");
setStatus(HttpRequestStatus.RESOLVED);
},
};
getInsurances(userIdentity, hooks).subscribe(
policies => {
Log.normal("Policies:", policies);
setInsurances(policies);
},
(err: Error) => {
setError(err);
}
);
And have this output (sorry for paste the link, I can't embed the image because rep):
https://i.stack.imgur.com/Nbq49.png
The finalize is working fine, but the tap is executing five times at once.
Thank you.
I think you get this behavior because from emits the items synchronously, so its essentially the same as doing:
for (let i = 0; i < arr.length; i++) {
console.log('before req');
observer.next(arr[i]);
}
observer.complete();
afterRequest is shown properly because the actions involved are asynchronous.
If you want to trigger that event only once, before all the requests are fired, you could try this:
from([...])
.pipe(
finalize(() => hooks?.beforeRequest && hooks.beforeRequest()),
flatMap(/* ... */)
)
EDIT - log event before each request
flatMap(
value => concat(
of(null).pipe(
tap(() => hooks?.beforeRequest && hooks.beforeRequest()),
ignoreElements(), // Not interested in this observable's values
),
InsurancesApi.getPolicies(/* ... */)
)
)

Subscribe two times to one observable

first func:
updateMark(item: MarkDTO) {
this.service
.put(item, this.resource)
.subscribe(() => this.markEdit = null);
}
second func:
put(item: MarkDTO, rcc: string): Observable<MarkDTO> {
const rdto = new MarkRDTO(item);
const url = `${this.getUrl('base')}${rcc}/marks/${rdto.rid}`;
const obs = this.http.put<MarkDTO>(url, rdto, { withCredentials: true })
.pipe(map((r: MarkDTO) => new MarkDTO(r)))
.share();
obs.subscribe(newMark => this.storage.update(newMark, rcc));
return obs;
}
in service i need to update data after request
but also in same time i need to clear current editItem
all of it must be done after i subscribe to one httpRequest
.share() - suport from rxjs-compat package (i want to remove this dep in closest time)
without .share() - work only 1 of 2 steps
current rxjs version is 6.3.3
Help who can...
There is a pipeable share operator, that you would use the same way you use map() (i.e. inside pipe()) and thus doesn't need rxjs-compat.
But you don't need share() here. All you need is the tap() operator:
put(item: MarkDTO, rcc: string): Observable<MarkDTO> {
const rdto = new MarkRDTO(item);
const url = `${this.getUrl('base')}${rcc}/marks/${rdto.rid}`;
return this.http.put<MarkDTO>(url, rdto, { withCredentials: true })
.pipe(
map(r => new MarkDTO(r)),
tap(newMark => this.storage.update(newMark, rcc))
);
}

RxJS throttle for AJAX requests

I want to create a function that will make AJAX requests to backend. And if this function is called many times at the same time, then it should not make many identical requests to the server. It must make only 1 request.
For example:
doAJAX('http://example-1.com/').subscribe(res => console.log); // must send a request
doAJAX('http://example-1.com/').subscribe(res => console.log); // must NOT send a request
doAJAX('http://example-2.com/').subscribe(res => console.log); // must send a request, bacause of different URL
window.setTimeout(() => {
doAJAX('http://example-2.com/').subscribe(res => console.log); // must send a request because too much time has passed since the last request
}, 3000)
All function calls should return a result, as if the request was actually made.
I think for this purpose I can use RxJS library.
I have done this:
const request$ = new Subject < string > ();
const response$ = request.pipe(
groupBy((url: string) => url),
flatMap(group => group.pipe(auditTime(500))), // make a request no more than once every 500 msec
map((url: string) => [
url,
from(fetch(url))
]),
share()
);
const doAJAX = (url: string): Observable <any> {
return new Observable(observe => {
response$
.pipe(
filter(result => result[0] === url),
first(),
flatMap(result => result[1])
)
.subscribe(
(response: any) => {
observe.next(response);
observe.complete();
},
err => {
observe.error(err);
}
);
request$.next(url);
});
}
I create request$ subject and response$ observable. doAjax function subscribes for response$ and send URL string to request$ subject. Also there are groupBy and auditTime operators in request$ stream. And filter operator in doAJAX function.
This code works but I think it is very difficult. Is there a way to make this task easier? Maybe RxJS scheduler or not use RxJS library at all
As the whole point of this is to memoize Http results and delay repeated calls, you might consider your own memoization. Example:
const memoise = (func) => {
let cache: { [key:string]: Observable<any> } = {};
return (...args): Observable<any> => {
const cacheKey = JSON.stringify(args)
cache[cacheKey] = cache[cacheKey] || func(...args).pipe(share());
return cache[cacheKey].pipe(
tap(() => timer(1000).subscribe(() => delete cache[cacheKey]))
);
}
}
Here is a Stackblitz DEMO

How to queue actions through time properly when using redux-observables

I'm trying to create an Epic that queues actions to be dispatched, in this case, the actions are messages to be displayed with a snackbar so that, for example, three errors happened almost simultaneously, and I want to display the three error messages with a SnackBar, the Epic should dispatch an action to display the first one for 3 seconds, then display the second one for 3 seconds and then the third one for 3 seconds.
Also if one of the snackbars gets closed, the second one in the "queue" should be displayed and so on. What I'm doing at the moment is dispatching the action that displays the message in Epics that catch the errors, then I'm creating Epics dispatch the action to close the snackbar after 3 seconds of delay, with the action CLOSE_SNACKBAR. Which methods should I use in order to achieve this?
A basic Epic I've implemented looks like this, basically the action that changes the state from the snackbar to open and displays the error message is dispatched from another epic, the one that catches the error, and then another Epic dispatches the action to close the snackbar after 3 seconds, but I haven't figured out how to do this queueing kind of action so that each message can be displayed for 3 seconds each, right now if an error occurs right after the first one, the second message will be displayed without waiting the 3 seconds after the previous message.
Here are some basic examples of my epics (Ignore de request part, ):
const getUserEpic = (action$, store) => (
action$.ofType(actionTypes.DATA_REQUESTED)
.switchMap(action => {
const { orderData, queryData, pagerData } = store.getState().usuario.list;
const params = constructParams(queryData, pagerData, orderData);
return Observable.defer(() => axios.get(`users`, { params }))
.retry(NETWORK.RETRIES).mergeMap(response => {
Observable.of(actions.dataRequestSucceeded(response.data.rows))
}).catch(error => Observable.concat(
Observable.of(actions.dataRequestFailed(error)),
Observable.of({
type:'DISPLAY_DATA_REQUEST_FAILED_MESSAGE',
open:true,
message:'Failed to get Users Data'
})
));
})
)
const getRoleEpic = (action$, store) => (
action$.ofType(actionTypes.DATA_REQUESTED)
.switchMap(action => {
const { orderData, queryData, pagerData } = store.getState().usuario.list;
const params = constructParams(queryData, pagerData, orderData);
return Observable.defer(() => axios.get(`role`, { params }))
.retry(NETWORK.RETRIES).mergeMap(response => {
Observable.of(actions.dataRequestSucceeded(response.data.rows))
}).catch(error => Observable.concat(
Observable.of(actions.dataRequestFailed(error)),
Observable.of({
type:'DISPLAY_DATA_REQUEST_FAILED_MESSAGE',
open:true,
message:'Failed to get Roles Data'
})
));
})
)
This two epics do basically the seme, they are doing a GET request to the backend, but if they fail, they dispatch the action that opens the snackbar with an error message, and both error messages are different.
And this one is the epic that currently closes the Snackbar after 3 seconds:
const displayDataRequestFailedEpic = (action$, store) => (
action$.ofType(actionTypes.DISPLAY_DATA_REQUEST_FAILED)
.debounceTime(3e3)
.switchMap( action => {
return Observable.of({
type:'CLOSE_SNACKBAR',
open:false,
message:''
})
})
)
Imagine I do both requests really fast and both of them fail. I want show all the errors that happened, one after another for 3 seconds each,
This hasn't been tested...
First, need to separate the action that displays messages from the action that is causing the display:
const getUserEpic = (action$, store) => (
action$.ofType(actionTypes.DATA_REQUESTED)
.switchMap(action => {
const { orderData, queryData, pagerData } = store.getState().usuario.list;
const params = constructParams(queryData, pagerData, orderData);
return Observable.defer(() => axios.get(`users`, { params }))
.retry(NETWORK.RETRIES).mergeMap(response => {
return Observable.of(actions.dataRequestSucceeded(response.data.rows))
}).catch(error => Observable.of(actions.dateRequestFailed(error))
// }).catch(error => Observable.concat(
// Observable.of(actions.dataRequestFailed(error)),
//
// this will be handled elsewhere
//
// Observable.of({
// type:'DISPLAY_DATA_REQUEST_FAILED_MESSAGE',
// open:true,
// message:'Failed to get Users Data'
// })
));
})
)
next, need to define some method that creates the DISPLAY_MESSAGE action from whatever action is triggering the message. Included in the action should be a unique id used to track which message needs to be closed.
Finally, use concatMap() to create and destroy the messages.
const displayEpic = action$ => action$
.ofType(actionTypes.DATA_REQUEST_FAILED)
.concatMap(action => {
const messageAction = actions.displayMessage(action);
const { id } = messageAction;
return Rx.Observable.merge(
Rx.Observable.of(messageAction),
Rx.Observable.of(actions.closeMessage(id))
.delay(3000)
.takeUntil(action$
.ofType(actionTypes.CLOSE_MESSAGE)
.filter(a => a.id === id))
);
});

Cancel previous inner Observables when a new higher-order Observable is emitted

Consider the following code:
this.msgService.getUserChatList()
.do( (list) => {
this.unread = false;
console.log(list);
} )
.mergeMap( chats => Observable.from(chats) )
.mergeMap( chat => this.msgService.getLastMessage(chat['id']).map( lastMessage => this.containsUnreadMessages(lastMessage, chat['lastPresence']) ) )
.filter( state => state === true )
.subscribe( (unread) => {
this.unread = true;
console.log('result ', res);
} );
getUserChatList():
- emits an element each time one of the chat changes
- an element is a raw array containing all chats meta-data
- never completes
getLastMessage():
- is an Observable that never completes
In the second mergeMap I am calling the function getLastMessage().
I need to be observe this observable only until a new item is emitted by the getUserChatList() otherwise I would multiple observations on last message of the same chat.
Illustration :
getUserChatList emits : [chatMetaA:{}, chatMetaB:{}]
code go through getLastMessage and start to observe lastMessage of chatA and chatB
one of the chat change so a new item is emitted by getUserChatList containing the new version of the meta-data of the chats: [chatMetaA:{}, chatMetaB:{}]
code go through getLastMessage and start to observe lastMessage of chatA and chatB. So we now observe twice last message of chatA and chatB
And it will go on and on...
My question is, how could I cancel observation on getLastMessage() once a new item is emitted by getUserChatList()? I tried using switch but couldn't manage to make it work
Solution was indeed to use switchMap:
this.msgService.getUserChatList()
.do( () => { this.unread = false } )
.switchMap(
chats => Observable.from(chats)
.mergeMap( chat => this.msgService.getLastMessage(chat['id'])
.map( lastMessage => this.containsUnreadMessages(lastMessage, chat['lastPresence']) ) )
)
.filter( state => state === true )
.subscribe( (unread) => {
this.unread = true;
console.log('result ', res);
} );

Resources