Si realiza una solicitud GET a https://api.github.com/users/benawad/repos , obtendrá todos los repositorios que tiene el usuario benawad . Al menos eso es lo que se espera que devuelva el punto final de la API. El problema es que GitHub limita el número de repositorios a 30.
Al día de hoy (14/08/2021), el usuario benawad tiene 246 repositorios .
Para superar el problema mencionado anteriormente, debe realizar una solicitud GET pero con algunos parámetros adicionales... GitHub ha implementado la paginación en su API. Por lo tanto, en la URL debe especificar la página que desea recuperar y la cantidad de repositorios por página .
Nuestra nueva solicitud GET debería verse así:
https://api.github.com/users/benawad/repos?page=1&per_page=1000
El problema con esto es que GitHub ha limitado la cantidad de repositorios por página a 100. Entonces nuestra solicitud GET devuelve 100 repositorios y no los 246 que tiene actualmente el usuario benawad .
En mi servicio Angular, implementé el siguiente código para recuperar todos los repositorios de todas las páginas.
public getUserRepos(user: string): Observable<RepositoryI[]> { return new Observable((subscriber: Subscriber<RepositoryI[]>) => { this.getUserData(user).subscribe((data: UserI) => { //public_repos is the amt of repos the user has const pages: number = Math.ceil(data.public_repos / 100); for (let i = 1; i <= pages; i++) { this.http .get( `https://api.github.com/users/${user}/repos?page=${i}&per_page=100` ) .subscribe((data: RepositoryI[]) => { subscriber.next(data); }); } }); }); }Y en mi componente me suscribo con lo siguiente:
this.userService.getUserRepos(id).subscribe((repos)=>{ this.repositories.push(...repos); })El problema con este enfoque es que no tengo control de cuándo el Observable ha dejado de emitir valores. En mi componente, me gustaría activar una función cuando el Observable esté completo .
He intentado lo siguiente:
public getUserRepos(user: string): Observable<RepositoryI[]> { return new Observable((subscriber: Subscriber<RepositoryI[]>) => { this.getUserData(user).subscribe((data: UserI) => { const pages: number = Math.ceil(data.public_repos / 100); for (let i = 1; i <= pages; i++) { this.http .get( `https://api.github.com/users/${user}/repos?page=${i}&per_page=100` ) .subscribe((data: RepositoryI[]) => { subscriber.next(data); // If the for loop is complete -> complete the subscriber if(pages == i) subscriber.complete(); }); } }); }); }Y en mi componente hago lo siguiente:
this.userService.getUserRepos(id).subscribe( (repos) => { this.repositories.push(...repos); }, (err) => { console.log(err); }, () => { // When the observable is complete: console.log(this.repositories); // This only logs 46 repositores, it should log 246 // I want to trigger some function here } ); El console.log() solo registra 46 repositorios. ¿Por qué está pasando esto? Tal vez estoy completando el suscriptor antes de que pueda obtener las 3 páginas; pero estoy llamando al .complete() dentro de la suscripción. ¿Qué estoy haciendo mal? Gracias por adelantado.
let subs = interval(1000).subscribe(() => { this.http .get( `https://api.github.com/users/${user}/repos?page=${i}&per_page=100` ) .subscribe((data: RepositoryI[]) => { subscriber.next(data); if(!data) { // to check if data is null subs = null; // if null, cancel the interval subscription return; // and return (not continue the subscription) } }); }) }De esta forma almacenas la suscripción y después de comprobar si hay datos, te das de baja.
Por favor, avíseme si esto funciona.
Javascript ejecuta las cosas de forma asíncrona.
Su 'bucle For' ejecutará api en paralelo e incrementará el conteo al mismo tiempo.
A medida que realiza 3 llamadas API paralelas, es posible que regresen con una duración diferente.
Pero antes de eso, su valor 'i' llegará a 3.
Entonces, en su caso, la posibilidad es que su tercera llamada API que tiene menos registros (46 registros) regrese antes de las primeras 2 llamadas API y vaya primero a la lógica de suscripción y solo vea 46 registros.
subscriber.next(data); // 3rd api comes here if(pages == i) subscriber.complete(); // now your pages and i values are same and it gets completed. });SOLUCIÓN
import {defer } from 'rxjs'; defer(async () => { for (let i = 1; i <= pages; i++) { await this.http .get( `https://api.github.com/users/${user}/repos?page=${i}&per_page=100` ) .toPromise().then((data: RepositoryI[]) => { subscriber.next(data); }); } }.subscribe(x => { subscriber.complete(); });Describiré dos formas de lograr esto, una es imperativa (usando un bucle) y la otra reactiva (usando rxjs)
Puede hacer un bucle simple para obtener todas las páginas (este devuelve una promesa, pero si es necesario, puede convertirlo en un observable usando from ):
public getUserRepos(user: string): Promise<Repository[]> { let pageNumber = 0; let results = []; const getPage = pageNumber => this.http .get<Repository[]>(`https://api.github.com/users/${user}/repos?page=${pageNumber}&per_page=100`) .toPromise(); do { const pageResults = await getPage(pageNumber++); results = results.concat(pageResults); } while(pageResults.length !== 0); return results; } Puede usar el operador de expand de rxjs para consultar una página, luego la página siguiente, etc.
El uso requiere que asigne el resultado de http a un objeto que también incluya el número de página, de esa manera expand puede saber qué página obtener a continuación.
Ejemplo en tu caso:
public getUserRepos(user: string): Observable<Repository[]> { const getPage = pageNumber => this.http .get<Repository[]>(`https://api.github.com/users/${user}/repos?page=${pageNumber}&per_page=100`) .pipe(map(z => ({ results: z, page: pageNumber }))); // Add page number to results return getPage(0).pipe( expand(pr => getPage(pr.page + 1)), takeWhile(pr => pr.results.length !== 0), map(pr => pr.results), scan((acc, v) => [...acc, ...v]) ); }El resultado sería una consulta para la página 0, luego, cuando se resuelva, una consulta para la página 1, etc. cuando una página resulte en 0 entradas, el observable se completará.
Usé el scan para proporcionar resultados parciales a medida que se recuperan las páginas, de esa manera puede ver que sucede algo, en lugar de esperar a que se resuelvan todas las páginas antes de mostrar algo.
Ejemplo de StackBlitz
Nota: Habrá una consulta adicional para una página después de la última que se cancelará, es inofensivo, pero si desea evitar eso, cambie su llamada de expand a esto:
expand(pr => (pr.results.length !== 0 ? getPage(pr.page + 1) : EMPTY))Puede lograrlo con una suscripción usando operadores y funciones RxJS, como las siguientes:
public getUserRepos(user: string): Observable<RepositoryI[]> { // will return one observable that contains all the repos after concating them to each other: return this.getUserData(user).pipe( switchMap((data: UserI) => { //public_repos is the amt of repos the user has const pages: number = Math.ceil(data.public_repos / 100); // forkJoin will emit the result as (RepositoryI[][]) once all the sub-observables are completed: return forkJoin( Array.from(new Array(pages)).map((_, page) => this.http.get<RepositoryI[]>( `https://api.github.com/users/${user}/repos?page=${page + 1}&per_page=100` ) ) ).pipe( // will reduce the RepositoryI[][] to be RepositoryI[] after concating them to each other: map((res) => res.reduce((acc, value) => { return acc.concat(value); }, []) ) ); }) ); }Luego, en su componente, puede suscribirse al observable que devolverá todos los repositorios después de obtenerlos todos:
this.userService.getUserRepos(id).subscribe((repos) => { this.repositories = repos; // You can trigger the function that you need here... });Intentando dar una respuesta más directa. Si no le gusta el anidamiento, siempre puede declarar un observable interno como un método separado.
public getUserRepos = (user:string) => this.getUserData(user) .pipe( map(({public_repos})=>Math.ceil(public_repos / 100)), switchMap(max=>interval(1) .pipe( takeWhile(i=>++i<=max) mergeMap(i=>this.http.get<RepositoryI[]>(`https://api.github.com/users/${user}/repos?page=${++i}&per_page=100`)) )), scan((acc, repo)=>[...acc, ...repo], [] as RepositoryI[]) );Aquí está la estrategia que usé:
getUserData() como lo que ya tiene.interval() para comenzar un conteo observable (comienza con 0 , por lo tanto, el uso de ++ más adelante).takeWhile() para verificar si el contador ha excedido el límite de páginas; de lo contrario, el observable se completa aquí.mergeMap() para poner en cola una serie de llamadas API para cada número emitido desde interval() .scan() .Actualizar Si prueba este método, deberá actualizar la lógica de su componente.
Debido a que scan() reduce todas las solicitudes de API a un solo observable, puede asignar estos valores a su matriz de estado en lugar de enviarlos.
Este observable se emitirá inmediatamente después de la primera solicitud de API. A medida que se complete cada solicitud de API adicional, el observable emitirá una nueva matriz con los elementos agregados. Esto se hace para que su componente no tenga que esperar hasta que se completen todas las solicitudes de API antes de que pueda mostrar algunos datos.