I am writing a script using an external API that needs to limit the requests based on:
a maximum number of requests per second
a maximum of current requests
I found and achieved have this behavior working for a single request, but I have a workflow where I need to:
deal with pagination
make two different kind of requests to the same API, the second one being dependent of the first one.
The snippet below illustrates the requests workflow, I tried several things with the mergeMap and expand concurrent parameters and some techniques I found here that has I said work well for one request but I am a little bit confused on how to track all the requests to "sync" the limits across all the requests.
* Get a page of 1000 companies and the total number of companies.
*
* #param afterId Optional id of the last fetched company (to get the next ones).
*/
function getCompanies(afterId?: string): Observable<{ count: number; companies: Company[] }> {
const options = { limit: 1000, afterId }
return this.http.post('https://example.com/searches/companies', options)
}
function getAllCompanies(): Observable<Company[]> {
let alreadyFetchedCount = 0
this.getCompanies().pipe(
expand(({ count, companies }) =>
count <= alreadyFetchedCount ? EMPTY : this.getCompanies(companies[companies.length - 1].uuid)
),
tap(({ companies }) => (alreadyFetchedCount += companies.length))
)
}
/**
* Get a page of 1000 funding rounds.
*
* #param companyUuid The funding rounds company uuid.
* #param afterId Optional id of the last fetched company (to get the next ones).
*/
function getFundingRounds(
companyUuid: string,
afterId?: string
): Observable<{ count: number; fundingRounds: FundingRound[] }> {
const options = { limit: 1000, companyUuid, afterId }
return this.http.post('https://example.com/searches/companies', options)
}
function getAllFundingRounds(companyUuid: string): Observable<FundingRound[]> {
let alreadyFetchedCount = 0
this.getFundingRounds().pipe(
expand(({ count, fundingrounds }) =>
count <= alreadyFetchedCount ? EMPTY : this.getFundingRounds(fundingrounds[fundingrounds.length - 1].uuid)
),
tap(({ fundingrounds }) => (alreadyFetchedCount += fundingrounds.length)),
reduce((acc, value) => [...acc, ...value], [])
)
}
function main() {
getAllCompanies().pipe(
// Here I get a stream of 1000 or less companies until all companies have been fetched.
// Let's work one company by one company.
mergeMap((companies) => companies),
// For each company, get the funding rounds and return the company extended with them.
mergeMap((company) =>
getAllFundingRounds(company.uuid).pipe(map((fundingRounds) => ({ ...company, fundingRounds })))
),
toArray(),
tap(companies =>
// Do something with the result
)
)
}
You can make use of delay that will make sure that you are making http requests within specified time and concatMap that will help you make requests in sequence
this.function1().pipe(
mergeAll(),
delay(1000),
concatMap((data: any) => this.function2(data.id)),
).subscribe(
console.log
);
function2(id: string = ''): Observable<any> {
console.log('id', id);
return serviceCAll().pipe(
delay(1000)
);
}
Related
Considering the following simplified data structure:
Teacher
{
id: number,
name: string
students?: Student[] // filled with an inner second request
}
Student
{
name: string
}
TeachersResult (response of the first api request with no students inside)
{
teachers: Teacher[]
pagesCount: number // metadata for the pagination
}
My main question is how use RxJS to fill the students-property for every teacher with these two api endpoints:
GET http://localhost:1337/api/teachers
GET http://localhost:1337/api/students/{teacherId}
The first idea was to start with something like this:
getTeachersWithStudents(): Observable<TeachersResult> {
return this.apiService.getTeachers().pipe(
concatMap(teachersResult => {
const studentsObservables$: Observable<Student[]>[] = [];
teachersResult.teachers.foreach(teacher =>
studentsObservables$.push(this.apiService.getStudents(teacher.id)));
// Add students to associated teacher here? Is forkJoin() the right choice?
});
);
}
It feels complicated for me to add every students-result from the second api request to the associated teacher. This is the end result for me I want to achieve:
{
teachers: [
{
id: 1,
name: 'Uncle Bob',
students: [
{ name: 'Alice' },
{ name: 'Caren' }
]
},
{
...
}
],
pagesCount: 42
}
You may try something like this
getTeachersWithStudents(): Observable<TeachersResult> {
return this.apiService.getTeachers().pipe(
// concatMap here is right since you want to continue only after upstream
// has notifies the list of teachers
concatMap(teachersResult => {
const teachers = teachersResult.teachers;
const secondCalls = teachers.map(teacher => {
return this.apiService.getStudents(teacher.id).pipe(
// set the students into the teacher object and return the teacher
// filled with the students
map(students => {
teacher.students = students;
return teacher
})
);
})
// now you have an array of Observables for the second calls, you
// can therefore use forkJoin to execute them in parallel
return forkJoin(secondCalls).pipe(
// add the page number
map(teachersWithStudents => {
return {
teachers: teachersWithStudents,
pagesCount: teachersResult.pagesCount
}
})
)
})
);
}
In this way you are executing all the calls to get the students concurrently.
If you want to limit the concurrency rate, then you can use mergeMap in a slightly more complex stream, something like this
getTeachersWithStudents(): Observable<TeachersResult> {
return this.apiService.getTeachers().pipe(
concatMap(teachersResult => {
// transform the teachers array into a stream of teachers
const teachers = teachersResult.teachers;
return from(teachers).pipe(
// here you use mergeMap with the rate of concurrency desired
// in this example I set it to 10, which means there will be at
// most 10 requests for students on flight at the same time
mergeMap(teacher => {
return this.apiService.getStudents(teacher.id).pipe(
// set the students into the teacher object and return the teacher
// filled with the students
map(students => {
teacher.students = students;
return teacher
})
)
}, 10),
// now we want to repack all the teachers in one array using toArray
toArray(),
// here we create the response desired and return it
map(teachersWithStudents => {
return {
teachers: teachersWithStudents,
pagesCount: teachersResult.pagesCount
}
})
)
})
);
}
This stackblitz shows an example of the above 2 implementations.
I need to loop looking for an item in a table and if it's not found, click a refresh button to reload the table. I know I can't use a simple while loop due to the asynchronous nature of cypress. Is there another way to accomplish something like this.
I tried to tweak an example from another post but no luck. Here's my failed attempt.
let arry = []
for (let i = 0; i < 60; i++) { arry.push(i) }
cy.wrap(arry).each(() => {
cy.get('table[class*="MyTableClass"]').then(function($lookForTheItemInTheTable) {
if($lookForTheItemInTheTable.find("MySearchValue")) {
return true
}
else {
cy.get('a[class*="selRefreshTable"]').click()
cy.wait(2000)
}
})
})
Cypress is bundled with lodash. Instead of using a for loop, you can the _.times(). However, I wouldn't recommend this for your situation as you do not know how many times you would like to reiterate.
You'll want to use the cypress-recurse plugin and use it like this example in that repo:
import { recurse } from 'cypress-recurse'
it('gets 7 after 50 iterations or 30 seconds', () => {
recurse(
() => cy.task('randomNumber'), // actions you want to iterate
(n) => n === 7, // until this condition is satisfied
{ // options to pass along
log: true,
limit: 50, // max number of iterations
timeout: 30000, // time limit in ms
delay: 300 // delay before next iteration, ms
},
)
})
Even with the above mentioned, there may be a simplified approach to solving your problem with setting up your app to have the table always display what you are seeking on the first try.
I have a long chain of operations within a pipe. Sub-parts of this chain represent some sort of high level operation. So, for instance, the code could look something like
firstObservable().pipe(
// FIRST high level operation
map(param_1_1 => doStuff_1_1(param_1_1)),
concatMap(param_1_2 => doStuff_1_2(param_1_2)),
concatMap(param_1_3 => doStuff_1_3(param_1_3)),
// SECOND high level operation
map(param_2_1 => doStuff_2_1(param_2_1)),
concatMap(param_2_2 => doStuff_2_2(param_2_2)),
concatMap(param_2_3 => doStuff_2_3(param_2_3)),
)
To improve readability of the code, I can refactor the example above as follows
firstObservable().pipe(
performFirstOperation(),
performSecondOperation(),
}
performFirstOperation() {
return pipe(
map(param_1_1 => doStuff_1_1(param_1_1)),
concatMap(param_1_2 => doStuff_1_2(param_1_2)),
concatMap(param_1_3 => doStuff_1_3(param_1_3)),
)
}
performSecondOperation() {
return pipe(
map(param_2_1 => doStuff_2_1(param_2_1)),
concatMap(param_2_2 => doStuff_2_2(param_2_2)),
concatMap(param_2_3 => doStuff_2_3(param_2_3)),
)
}
Now, the whole thing works and I personally find the code in the second version more readable. What I loose though is the information that performFirstOperation() returns a parameter, param_2_1, which is then used by performSecondOperation().
Is there any different strategy to break a long pipe chain without actually loosing the information of the parameters passed from sub-pipe to sub-pipe?
setting aside the improper usage of forkJoin here, if you want to preserve that data, you should set things up a little differently:
firstObservable().pipe(
map(param_1_1 => doStuff_1_1(param_1_1)),
swtichMap(param_1_2 => doStuff_1_2(param_1_2)),
// forkJoin(param_1_3 => doStuff_1_3(param_1_3)), this isn't an operator
concatMap(param_2_1 => {
const param_2_2 = doStuff_2_1(param_2_1); // run this sync operation inside
return doStuff_2_2(param_2_2).pipe(
concatMap(param_2_3 => doStuff_2_3(param_2_3)),
map(param_2_4 => ([param_2_1, param_2_4])) // add inner map to gather data
);
})
)
this way you've built your second pipeline inside of your higher order operator, so that you can preserve the data from the first set of operations, and gather it with an inner map once the second set of operations has concluded.
for readability concerns, you could do something like what you had:
firstObservable().pipe(
performFirstOperation(),
performSecondOperation(),
}
performFirstOperation() {
return pipe(
map(param_1_1 => doStuff_1_1(param_1_1)),
swtichMap(param_1_2 => doStuff_1_2(param_1_2)),
// forkJoin(param_1_3 => doStuff_1_3(param_1_3)), this isn't an operator
)
}
performSecondOperation() {
return pipe(
concatMap(param_2_1 => {
const param_2_2 = doStuff_2_1(param_2_1);
return doStuff_2_2(param_2_2).pipe(
concatMap(param_2_3 => doStuff_2_3(param_2_3)),
map(param_2_4 => ([param_2_1, param_2_4]))
);
})
)
}
an alternative solution would involve multiple subscribers:
const pipe1$ = firstObservable().pipe(
performFirstOperation(),
share() // don't repeat this part for all subscribers
);
const pipe2$ = pipe1$.pipe(performSecondOperation());
then you could subscribe to each pipeline independently.
I broke one complex operation into two like this:
Main Code
dataForUser$ = this.userSelectedAction$
.pipe(
// Handle the case of no selection
filter(userName => Boolean(userName)),
// Get the user given the user name
switchMap(userName =>
this.performFirstOperation(userName)
.pipe(
switchMap(user => this.performSecondOperation(user))
))
);
First Operation
// Maps the data to the desired format
performFirstOperation(userName: string): Observable<User> {
return this.http.get<User[]>(`${this.userUrl}?username=${userName}`)
.pipe(
// The query returns an array of users, we only want the first one
map(users => users[0])
);
}
Second Operation
// Merges with the other two streams
performSecondOperation(user: User) {
return forkJoin([
this.http.get<ToDo[]>(`${this.todoUrl}?userId=${user.id}`),
this.http.get<Post[]>(`${this.postUrl}?userId=${user.id}`)
])
.pipe(
// Map the data into the desired format for display
map(([todos, posts]) => ({
name: user.name,
todos: todos,
posts: posts
}) as UserData)
);
}
Notice that I used another operator (switchMap in this case), to pass the value from one operator method to another.
I have a blitz here: https://stackblitz.com/edit/angular-rxjs-passdata-deborahk
If my typeahead gets an empty search result, any subsequent query with a norrowed down search query should be prevented. E.g. if the search for 'red' returns empty, a search for 'redcar' makes no sense.
I tried using pairwise() and scan() operator. Code snippet:
import { tap, switchMap, filter, pairwise, scan, map } from 'rxjs/operators';
this.searchForm.get('search').valueChanges
.pipe(
switchMap( queryString => this.backend.search(queryString))
)
.subscribe()
Update
Given a simplified scenario: There is only the term 'apple' in the backend. The user is typing the search string (the request is not aborted by the switchMap()):
'a' -------> backend call returns 'apple'
'ap' ------> backend call returns 'apple'
'app' -----> backend call returns 'apple'
'appl' ----> backend call returns 'apple'
'apple' ---> backend call returns 'apple'
'apple p' -----> backend call returns EMPTY
'apple pi' ----> backend call returns EMPTY
'apple pie' ---> backend call returns EMPTY
The backend calls for 7. and 8. are unnecessary, because 6. already returns EMPTY. Therfore any subsequent call could be omitted. In my opinion some memoization is needed.
I would like to prevent unnecessary backend calls (http). Is there any way to achieve this in rxjs?
This is an interesting use-case and one of a very few situations where mergeScan is useful.
Basically, you want to remember the previous search term and the previous remote call result and based on their combination you'll decide whether you should make another remote call or just return EMPTY.
import { of, EMPTY, Subject, forkJoin } from 'rxjs';
import { mergeScan, tap, filter, map } from 'rxjs/operators';
const source$ = new Subject();
// Returns ['apple'] only when the entire search string is contained inside the word "apple".
// 'apple'.indexOf('app') returns 0
// 'apple'.indexOf('apple ap') returns -1
const makeRemoteCall = (str: string) =>
of('apple'.indexOf(str) === 0 ? ['apple'] : []).pipe(
tap(results => console.log(`remote returns`, results)),
);
source$
.pipe(
tap(value => console.log(`searching "${value}""`)),
mergeScan(([acc, previousValue], value: string) => {
// console.log(acc, previousValue, value);
return (acc === null || acc.length > 0 || previousValue.length > value.length)
? forkJoin([makeRemoteCall(value), of(value)]) // Make remote call and remember the previous search term
: EMPTY;
}, [null, '']),
map(acc => acc[0]), // Get only the array of responses without the previous search term
filter(results => results.length > 0), // Ignore responses that didn't find any results
)
.subscribe(results => console.log('results', results));
source$.next('a');
source$.next('ap');
source$.next('app');
source$.next('appl');
source$.next('apple');
source$.next('apple ');
source$.next('apple p');
source$.next('apple pi');
source$.next('apple pie');
setTimeout(() => source$.next('app'), 3000);
setTimeout(() => source$.next('appl'), 4000);
Live demo: https://stackblitz.com/edit/rxjs-do457
Notice that after searching for "apple " there are no more remote calls. Also, after 3s when you try searching a different term "'app'" it does make a remote call again.
You can use the filter operator:
this.searchForm.get('search').valueChanges.pipe(
filter(query => query)
switchMap(query => this.backend.search(queryString))
)
You can try out this mechanism here: RxJS-Editor
Code-share did not work so you get the code here:
const { of } = Rx;
const { filter } = RxOperators;
of('foo1', 'foo2', undefined, undefined, 'foo3').pipe(
filter(value => value)
)
Sounds like you want to keep all failed searches and check whether current search would fail also if HTTP is called. I cant think of any elegant way of having this in one stream, but with two streams:
_failedStreams = new Subject();
failedStreams$ = _failedStreams.asObservable().pipe(
scan((acc, curr) => [...acc, curr], []),
startWith('')
);
this.searchForm.get('search').valueChanges
.pipe(
withLatestFrom(failedStreams$),
switchMap([queryString, failedQueries] => {
return iif(() => failedQueries.find(failed => failed.startsWith(queryString)) ?
of('Not found') :
callBackend(queryString);
)
}
)
.subscribe()
callBackend(queryString) {
this.backend.search(queryString)).pipe(
.catchError(err => if(error.status===404) {
this._failedStreams.next(queryString);
// do something with error stream, for ex:
throwError(error.status)
}
)
}
Code is not tested, but you get the idea
I'm trying to build a query with the GitHub API v4 (GraphQL) to get the number of contributors.
At the moment I have something of the likes of
query ($owner: String!, $name: String!) {
repository(owner: $owner, name: $name) {
ref(qualifiedName: "master") {
target {
... on Commit {
history(first: 100) {
nodes {
author {
name
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
}
}
}
}
where I'm going through all the commits and get the name of the Authors (at the time I was trying to get the number of commits for contributor), but for repositories with a large amount of commits, this takes a lot of time.
So back to my question, is the a way to get only the number of contributors in a repository?
As far as I know, this is only possible with the REST API (v3), by requesting only one item per page, and extracting the total number of pages in the Response headers.
With this graphql query you can get the:
- total releases count
- total branches count
- total commits count
query{
repository(owner:"kasadawa", name:"vmware-task") {
Releases:refs(first: 0, refPrefix: "refs/tags/") {
totalCount
}
Branches:refs(first: 0, refPrefix: "refs/heads/") {
totalCount
}
object(expression:"master") {
... on Commit {
history {
totalCount
}
}
}
}
}
But if you want to get the contributors, you should do it with the REST API, because currently there is no simple way to implement it with GRAPHQL API.
Here is a solutions with the REST API.
const options = {
url: 'https://api.github.com/repos/vmware/contributors' ,
'json': true ,
headers: {
'User-Agent':'request',
'Authorization': 'token API_KEY_GENERATED_FROM_GITHUB'
}
};
var lastPageNum = 0 ; // global variable
request.getAsync(options).then((res) =>{
this.checkHeaders(res.headers.status) // just check the header status
// check if there are more pages
if(!!res.headers.link){
const [ , lastURL] = res.headers.link.split(','); // getting the last page link from the header
lastPageNum = +lastURL.match(/page=(\d+)/)[1]; // get the number from the url string
options.url = licenseUrl + lastPageNum;
return request.getAsync(options) // make Request with the last page, in order to get the last page results, they could be less than 30
}else{
// if its single page just resolve on to the chain
return Promise.resolve(res.body.length);
}
})
.then((lastPageRes)=>{
return (lastPageNum !== 0
? ( (lastPageNum-1)*30 + lastPageRes.body.length )
: lastPageRes)
})
.catch() // handle errors
Checkout for updates: https://github.community/t5/GitHub-API-Development-and/Get-contributor-count-with-the-graphql-api/td-p/18593