Observable combine multiple function calls into single Observable - javascript

I have a function that does an http request based on a parameter. And I want to add some kind of "debounce" functionality. So if the function gets called multiple times in a set time window, I want to combine the parameters into one request instead of making multiple requests.
I want to achieve this with Observables and in Angular. This does not sound that complicated, however I'm not able to get it running, maybe I'm missing something.
For now let's just skip the combining in a single request as this can be done with an aggregated debounce or a Oberservable.buffer. I have trouble combining the single Observables.
Here's what I've tried so far.
I tried using a Subject, as this seemed to be the proper object for this case (https://stackblitz.com/edit/angular-hcn41v?file=src%2Fapp%2Fapp.component.ts).
constructor(private http: HttpClient) {
this.makeRequest('1').subscribe(x => console.log(x))
this.makeRequest('2').subscribe(console.log)
setTimeout(() => {
this.makeRequest('3').subscribe(console.log)
}, 1000)
}
private makeRequest(id: string) {
this.observable = this.observable.pipe(
merge(Observable.of(id).pipe(delay(1)))
)
return this.aggregateDebounce(this.observable)
}
private getUrl(value) {
console.log('getUrl Call', value);
return 'https://jsonplaceholder.typicode.com/posts/1';
}
private aggregateDebounce(ob$) {
const shared$ = ob$.publishReplay(1).refCount()
return shared$.buffer(shared$.debounceTime(75))
}
I expect to have one 'getUrl Call' log for each function call and one result log. However I only get results if I add more than 1 calls to this.makeRequest() and the result is also weird. All previous values are always returned as well. I think I don't fully understand how Subject works in this case.
Another approach (taken from here RXJS: Aggregated debounce) was to create some sort of aggregate debounce (https://stackblitz.com/edit/angular-mx232d?file=src/app/app.component.ts)
constructor(private http: HttpClient) {
this.makeRequest('1').subscribe(x => console.log(x))
this.makeRequest('2').subscribe(console.log)
setTimeout(() => {
this.makeRequest('3').subscribe(console.log)
}, 1000)
}
private makeRequest(id: string) {
this.observable = this.observable.pipe(
merge(Observable.of(id).pipe(delay(1)))
)
return this.aggregateDebounce(this.observable)
}
private getUrl(value) {
console.log('getUrl Call', value);
return 'https://jsonplaceholder.typicode.com/posts/1';
}
private aggregateDebounce(ob$) {
const shared$ = ob$.publishReplay(1).refCount()
return shared$.buffer(shared$.debounceTime(75))
}
In this scenario I have the problem I'm also getting all previous values as well.
In theory (at least to me) both variants sounded plausible, however it seems like I'm missing something. Any wink in the right direction is highly appreciated.
Edit:
As requested I added the final real-world goal.
Imagine a service that requests information from an API. Within 50-75ms you call the service with a certain id. I want to group those ids together to a single request instead of doing 3. And if 100ms later another call to the service is made, a new request will be done

this.makeRequest(1).subscribe();
private makeRequest(number: number) {
this.values.next(number);
return this.idObservable.pipe(
You emit the value before you subscribe -> The value gets lost.
private values: Subject = new Subject();
private idObservable = this.values.pipe(
private makeRequest(number: number) {
this.values.next(number);
return this.idObservable.pipe(
Every call creates a new observable based on the subject. Whenever you emit a value, all subscribers receive the value.
A possible solution could look something like this (I'm using the new rxjs syntax here):
subject: Subject<String> = null;
observable = null;
window = 100;
constructor() {
this.subject = null;
this.window = 100;
this.makeRequest('1').subscribe(console.log)
this.makeRequest('2').subscribe(console.log)
setTimeout(() => {
this.makeRequest('3').subscribe(console.log)
}, 1000)
}
private makeRequest(id: string) {
if (!this.subject) {
this.subject = new ReplaySubject()
this.observable = this.subject.pipe(
takeUntil(timer(this.window).pipe(take(1))),
reduce((url, id, index) => this.combine(url, id), baseUrl),
flatMap(url => this.request(url)),
tap(() => this.subject = null),
share()
)
}
this.subject.next(id);
return this.observable;
}
Where combine creates the url and request makes the actual request.

Rxjs is quite good at handling this kind of case. You'll need two different Subjects:
One will be used to collect and combine all requests
The second will be used for subscribing to results
When a request is made, the value will be pushed onto the first subject but the second will be returned, abstracting away the internal logic of combining requests.
private values: Subject = new Subject();
private results: Subject = new Subject();
private makeRequest(number: number) {
this.values.next(number);
return this.results;
}
The pipeline for merging the requests could be a buffer and debounceTime as indicated in the question or other logic, as required. When a response is recieved, it just needs to be pushed onto the results Subject:
constructor(private http: HttpClient) {
this.values
.pipe(
buffer(this.values.pipe(debounceTime(1000))),
switchMap(values => this.getUrl(values)),
map(response => this.results.next(response)))
.subscribe();
}
I've used a switchMap to simulate an asynchronous request before pushing the response onto the results.
Full example here: https://angular-8yyvku.stackblitz.io

Related

Promise resolves earlier than expected and not returning an AXIOS call value

I'm making a request to a service that generates several strings in rapid succession. My problem arise from having to return a promise with this service, as I do not control the service process that returns the string.
I want to emphasize the fact that I necessarily need to return a promise.
Right now, what I have is the main function (handler.ts) and it doesn't mantain any business logic, including only the following:
public static callback: any;
public static async init(configuration, callback): Promise<any> {
this.callback = callback;
...
return new Promise((resolve, reject) => {
try {
const result = await Service.bootstrap(configuration)
return resolve(result)
} catch(err) {
reject(err)
}
}
}
This handler calls the service, which has the bootstrap function that performs a call to a .js file that obtains some information from my computer and then returns a string about it.
import loadComputerInformation from 'load.js'
public async bootstrap() : Promise<any> {
loadComputerInformation();
function useComputerInfoString() {
// window.getInfo is generated by loadComputerInformation(), and I can not control
// when does it return it, as it generates several strings with rapid succession
// and until the function exists, I will not get my string.
if (typeof window.getInfo !== 'function') {return;}
const data = window.getInfo();
if (data.finished) {
clearTimeout(timeoutId);
const infoString = data.computerInfo;
Service.axiosCall(infoString);
}
}
// I have to set an interval to perform the call several times, and until it resolves it
// it will not stop performing this call.
const timeoutId = setInterval(useComputerInfoString, 500);
}
return;
}
Therefore, the problem that I'm facing is that my promise gets lost in another thread, and I can not return the value from Service.axiosCall(infoString), which is just a standard axios call, but that necessarily needs the infoString.
Adding the axios call function just in case it is useful. Right now, I'm using the callback passed to the handler.js to return the axios call, but I want to be able to include it in the Promise without the necessity of a callback function
public static async axiosCall(blackbox): Promise<any> {
await axios.post('https://somecall.com/info', blackbox)
.then((response) => { this.callback(element)
return element);
}
}
Any idea of how to solve this?
Highlight
Please note that loadComputerInformation() asynchronously loads Window.getInfo(), but it does not resolve only one value, but several, therefore I can not await on Window.getInfo() because at the beggining it does not exist, and will only return undefined
Also, right now, the code is up and running, but the way it is returning the value is with the callback and not as a promise.
Try a bootstrap function whose returned promise resolves with the response returned from an axios call.
This suggestion (based on information in the post) is vanilla JavaScript to demonstrate how to the problem might be approached. Obviously modification to integrate it into the application will be needed:
const bootstrap = () => new Promise( resolve => {
loadComputerInformation();
let state = "load";
const timer = setInterval( ()=> {
if( state == "load") {
if( typeof window.getData != "function") {
return;
}
state = "get";
}
let data;
if( state == "get") {
data = window.getData();
if(!data.finished) {
return;
}
// state = "request";
}
clearInterval(timer);
resolve( axios('post', "https:example.com/info", data.computerInfo) );
}, 100); // 1/10th second?
};
Note this answer has been modified to use a state machine to wait for getData to be loaded asynchronously, then to wait for a call to getData to return a data object with a finished property, before resolving the promise returned with the promise returned by the axios call - the first answer was simpler right up until it needed to wait for getData code to be loaded asynchronously.
Beneath the question lies a problem that may have to be written off to code debt: JavaScript is event driven. loadComputerInformation appears to have been written for its results to be polled. It is hard to imagine a justifiable reason why it was left in that state.

Rxjs : Retry http call with updated parameters if no results

I am a novice with Rxjs, I'm trying to do a request to my API with limits passed in parameters.
My problem is sometimes the returned result is empty for some reasons. The thing I need to do is retry this API call with updated parameters (skip param)
pollService.getPoll$(skip, limit).subscribe((pollList) => {
doStuff;
},
(error) => {
doStuff;
});
I read some topics about the RetryWhen RXJS function but it is about errors when the request fail and you want to retry the same one but I ve no errors and I don't want to retry the same request, I also saw topics about Replay function but it is not very clear to me.
Can someone explain me what to do here please !!
Thanks
Alex
Consider utilizing the expand operator as demonstrated below:
import { EMPTY, of } from "rxjs"
import { expand } from "rxjs/operators"
public startPolling(skip: number, limit: number): void {
of([])
.pipe(
expand(x => x.length < 2 ? pollService.getPoll$(skip--, limit) : EMPTY)
)
.subscribe(pollList => {})
}
Update:
public poll = (skip: number, limit: number): void => {
defer(() => getPoll$(1 + skip--, limit))
.pipe(
repeat(),
first(x => {
if(x.length < 2){
// update some variable in the component
return false;
}
return true;
})
)
.subscribe(pollList => { })
}
If I understand correctly, your backend is paging data using the skip and limit parameters.
So, if you have a skip value that is too high, you want to reduce it automatically.
There are many, many ways to solve this problem in RxJS:
you could insert a switchMap after getPoll$. SwitchMap would return a new observable, either wrapping the result if it's ok (with of(pollList)), or returning pollService.getPoll$(newSkipValue, newLimitValue)
you could map the result and throw an Error if the result doesn't pass validation. Then you could catchError and return the result of a new call to getPoll$
However, what I suggest is modelling the call differently. I would use a Subject as a source of requests, and switchMap to execute the requests.
// inside the component or service
interface GetPollRequest {
skip: number;
limit: number;
}
private _callSource = new Subject<GetPollRequest>();
public triggerCall(skip: number, limit: number) {
this._callSource.next({skip, limit});
}
constructor(...) {
this._callSource.pipe(
// every time _callSource emits, we call the server
switchMap(({skip, limit) => pollService.getPoll$(skip, limit).pipe(
map(pollList => ({ pollList, skip, limit }))
),
tap(({pollList, skip, limit}) => {
// update the request in any way you need. You need to make sure
// that the next automatic trigger doesn't repeat indefinitely,
// or you'll simulate a DOS attack to your backend
if (pollList.length < 2) this.triggerCall(skip - 2, limit);
})
).subscribe(pollList => // update the component status);
}
Using this pattern, you use subjects as triggers (or custom events, they are pretty much the same), and you wrap them up during constructor time.
SwitchMap is used to create an observable (in this case, performing a request) every time the source emits.
Tap is used to perform an operation (pretty much like a subscribe), embedded in the chain of transformations inside the pipe.

Async in RxJS Observable

First time with RxJS. Basically, I'm trying to make a twitter scraper which retrieves tweets from a query string. The search url allows specifying of a min_position parameter which can be the last id of the previous search to sort of paginate.
The process kind of looks like this (where it loops back at the end):
get page -> next() each scraped tweet -> set min_position -> get page (until !has_more_items)
Requesting the page returns a promise and so I somehow have to wait until this is completed until I can proceed. I was hoping to pass an async function to Observable.create() but that doesn't seem to work, it's only called a single time.
EDIT
I've had a play around after reading your resources as best as I could. I came up with the following abstraction of my problem.
import { from, Observable } from 'rxjs'
import { concatMap, map, switchMap } from 'rxjs/operators'
let pageNumber = 0
const PAGE_SIZE = 3, MAX_PAGES = 3
async function nextPage() {
if (pageNumber >= MAX_PAGES) {
throw new Error('No more pages available')
}
await new Promise(res => setTimeout(res, 500)) // delay 500ms
const output = []
const base = pageNumber++ * PAGE_SIZE
for (let i = 0; i < PAGE_SIZE; i++) {
output.push(base + i)
}
return output
}
function parseTweet(tweet: number): string {
// simply prepend 'tweet' to the tweet
return 'tweet ' + tweet
}
const getTweets = (): Observable<string> => {
return from(nextPage()) // gets _html_ of next page
.pipe(
concatMap(page => page), // spreads out tweet strings in page
map(tweet => parseTweet(tweet)), // parses each tweet's html
switchMap(() => getTweets()) // concat to next page's tweets
// stop/finish observable when getTweets() observable returns an error
)
}
getTweets()
.subscribe(val => console.log(val))
It's quite close to working but now whenever nextPage() returns a rejected promise, the entire observable breaks (nothing logged to the console).
I've attempted inserting a catchError after the pipe to finish the observable instead of running through and throwing an error but I can't get it to work.
Also this implementation is recursive which I was hoping to avoid because it's not scalable. I don't know how many tweets/pages will be processed in the observable in future. It also seems that tweets from all 3 pages must be processed before the observable starts emitting values which of course is not how it should work.
Thanks for your help! :)
We need to loadTwits until some condition and somehow work with Promise? Take a look at example:
function loadTwits(id) {
// Observable that replay last value and have default one
twitId$ = new BehaviorSubject(id);
return twitId$.pipe(
// concatMap - outside stream emit in order inner do
// from - convert Promise to Observable
concatMap(id => from(fetchTwits(id))),
map(parseTwits),
// load more twits or comlete
tap(twits => getLastTwitId(twits) ? twitId$.next(getLastTwitId(twits)) : twitId$.complete())
)
}
I figured it out after looking further into expand and realising it was recursion that I needed in my observable. This is the code that creates the observable:
const nextPage$f = () => from(nextPage()) // gets _html_ of next page
.pipe(
concatMap(page => page), // spreads out tweet strings in page
map(tweet => parseTweet(tweet)) // parses each tweet's html
)
const tweets$ = nextPage$f()
.pipe(
expand(() => morePages() ? nextPage$f() : empty())
)

Implementing getOrCreate method async issue

I have a class that encapsulate communication with a server
and it has a getOrCreate method to create a ShoppingCart resource in the server (I am using firebase, but my question is for all kind of server implementations)
Once the application is loaded in the client (I am using angular but again it does not matter for this question), two different components (areas of the screen) get the service class injected to them (it is a singleton) and they call a method getOrCreateCart()
This method should check in the localStorage if a cartId exists and if so return it. Otherwise, it should create it with an async call to the server.
The issue is that if both components call this method at the same time, with the lack of locking mechanisme, I can not block the second operation until the first one is completed and on the second time return the cartId from the localStrogate instead of creating another resource in the database
Checking if cartId already exists in the database is not an option, since the cartId is generated upon request to the server.
A little bit of code to make things more clear:
private async getOrCreateCartId() {
let cartId = localStorage.getItem('cartId');
if (cartId) return cartId;
let newCartId = await this.create();
localStorage.setItem('cartId', newCartId);
return newCartId;
}
// Returns a promise with the new created id
private create() {
return this.db.list('/shopping-carts/').push({
dateCreated: new Date().getTime()
});
}
Thanks to Roamer-1888 I've figured it out
This is my solution:
Cache the promise that returns from the create() method
For all subsequent calls, return this promise so when the result will arrive from the server, all the returned promises will be invoked
Before returning the promise, I call then and transform the value from the server to a simple string that I am interested in.
I ended up with this code:
private getOrCreateCartId() {
let cartId = localStorage.getItem('cartId');
if (cartId) return Promise.resolve(cartId);
if (this.createPromise) return this.createPromise.then(result => result.key);
this.createPromise = this.create();
this.createPromise.then(result => {
localStorage.setItem('cartId', result.key);
})
return this.createPromise.then(result => result.key);
}
Congratulations on figuring it out.
Here's what I ended up with, starting with an emulation of your singleton :
// singleton FOO
const FOO = (function() {
var cartId = localStorage.getItem('cartId'); // cache, initialised from localStorage
async function getOrCreateCartId() {
if (!cartId) {
cartId = create(); // cache promise
localStorage.setItem('cartId', await cartId);
}
return cartId;
}
// Returns a promise with the new created id
function create() {
return new Promise(function(resolve, reject) {
setTimeout(() => {
resolve(Date.now());
}, 2000);
});
}
return {
'getOrCreateCartId': getOrCreateCartId
};
}());
At first glance, you would think getOrCreateCartId() is synchronous but that's an affect of async/await, which allow async code to be structured almost identically to sync code.
And here are two rapid-fire calls to FOO.getOrCreateCartId()
// call from component 1
FOO.getOrCreateCartId().then(function(cartId) {
console.log(cartId);
});
// call from component 2
FOO.getOrCreateCartId().then(function(cartId) {
console.log(cartId);
});
The console will log the same cartId twice even if the call from component 1 needs to call create().
DEMO: I included a .clear() method so you can play around with it and convice yourself it works as it should.

Subsequential promises in ionic2/angular2

I know, it is a newbie question:
I created a mobile application reading an information feed from a bluetooth serial device.
Data are retrieved using a promise in this way:
myServiceClass.getRemoteValue(valueId).then(reply: number) {
...
}
I need to read multiple parameters coming from this feed and I have to wait the previous call to finish before requesting the new value.
If I run:
let requiredValues = [1, 2, 3, 4, ..., n];
for (let i=0; i<requiredValues.length; i++) {
myServiceClass.getRemoteValue(valueId).then(reply: number) {
...
}
}
In this way request will run in parallel, but I need them to run in sequence one after the other. Is there any solution to subsequentially chain an array of promises someway?
In other words I need to run the n-th promise only after the previous promise has been resolved.
Thank you very much for your time.
Well, you can use a recursive method to achieve that... Please take a look at this plunker (when running the plunker, please notice that the values are being printed in the console)
I'm just using some fake data, but I guess it's enough to give you the overall idea:
public start(): void {
this.getSeveralRemoteValues([1,2,3,4,5,6,7,8,9]);
}
private getSeveralRemoteValues(array): Promise<boolean> {
if(array && array.length) {
return this.getRemoteValueFromService(array[0]).then(() => {
array.shift(); // remove the first item of the array
this.getSeveralRemoteValues(array); // call the same method with the items left
})
} else {
this.logEnd();
}
}
private logEnd(): void {
alert('All promises are done!');
}
private getRemoteValueFromService(value: number): Promise<boolean> {
// this simulates the call to the service
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(`Promise: ${value}`);
resolve(true);
}, 1000);
});
}

Categories

Resources