fix(twitch): token partagé + timeouts + fan-out réduit sur Home/Explore
CI / build-and-test (push) Successful in 14m10s

This commit is contained in:
2026-09-29 09:56:15 -04:00
parent 5994841bda
commit 3fe05b3c30
3 changed files with 121 additions and 43 deletions
@@ -14,6 +14,7 @@ import { ChannelIdentityComponent } from '../../app/shared/components/channel-id
import { InfiniteAnchorComponent } from '../shared/infinite-anchor/infinite-anchor.component';
import { classifyVideo, isLiveVideo, isShortVideo } from '../../app/shared/utils/video-kind';
import { map, of, switchMap, catchError, forkJoin, Observable } from 'rxjs';
import { timeout } from 'rxjs/operators';
import { PagedResult } from '../../services/youtube-api.service';
const TWITCH_THEME_KEYWORDS: Record<string, string[]> = {
@@ -316,17 +317,27 @@ export class ProviderThemePageComponent implements OnDestroy {
this.cdr.markForCheck();
};
// Une section lente ne doit jamais bloquer les autres (forkJoin
// échoue dès qu'un appel erreur : chaque section est bornée à 25 s
// et retombe sur une page vide, le filet top-streams suit).
const bounded = (call: Observable<PagedResult<Video>>): Observable<PagedResult<Video>> =>
call.pipe(
timeout(25000),
catchError(() => of({ items: [], nextCursor: null } as PagedResult<Video>))
);
const fetchAll = (gameId: string | null) => {
const calls: Record<string, Observable<PagedResult<Video>>> = {};
if (wantLive) {
calls['live'] = gameId
const liveCall = gameId
? this.api.getTwitchGameStreamsPage(gameId, this.twitchCursors.live || undefined, 12).pipe(
catchError(() => this.api.getTwitchTopStreamsPage(this.twitchCursors.live || undefined)))
: this.api.getTwitchTopStreamsPage(this.twitchCursors.live || undefined);
calls['live'] = bounded(liveCall);
}
if (wantVod) calls['vod'] = this.api.searchTwitchVodsPage(query, this.twitchCursors.vod || undefined);
if (wantClip) calls['clip'] = this.api.searchTwitchClipsPage(query, this.twitchCursors.clip || undefined);
if (wantChannel) calls['channel'] = this.api.searchTwitchAllChannelsPage(query, this.twitchCursors.channel || undefined, 20);
if (wantVod) calls['vod'] = bounded(this.api.searchTwitchVodsPage(query, this.twitchCursors.vod || undefined));
if (wantClip) calls['clip'] = bounded(this.api.searchTwitchClipsPage(query, this.twitchCursors.clip || undefined));
if (wantChannel) calls['channel'] = bounded(this.api.searchTwitchAllChannelsPage(query, this.twitchCursors.channel || undefined, 20));
if (Object.keys(calls).length === 0) {
this.loading.set(false);
this.loadingMore.set(false);
@@ -359,8 +370,13 @@ export class ProviderThemePageComponent implements OnDestroy {
};
if (firstPage) {
// Première page : résoudre la catégorie Twitch pour des lives thématiques.
this.api.searchTwitchCategory(query).subscribe({
// Première page : résoudre la catégorie Twitch pour des lives
// thématiques, SANS bloquer l'affichage (4 s max, la recherche de
// catégorie séquentielle prenait plusieurs secondes à elle seule).
this.api.searchTwitchCategory(query).pipe(
timeout(4000),
catchError(() => of(null))
).subscribe({
next: (game) => {
if (snapshotVersion !== this.version) return;
if (game) this.twitchGame.set(game);
+23 -5
View File
@@ -219,15 +219,33 @@ export class ThemePageComponent {
if (block.provider === 'twitch') {
const raw = tokens.join(' ').trim();
const q = (raw.split(/[^A-Za-z0-9à-ÿ]+/).filter(w => w.length >= 3)[0] || raw).trim();
const show = (items: Video[]) => {
block.items = (items || []).slice(0, max);
block.loading = false;
if (!block.items.length) console.info('[ThemePage] Empty block page', { provider: block.provider, theme: slug, query: q });
this.blocks.update(a => [...a]);
};
const fail = () => { block.error = 'Erreur de chargement'; block.loading = false; this.blocks.update(a => [...a]); };
const call = q ? this.api.searchTwitchThemePage(q, null) : this.api.getTwitchTopStreamsPage(null);
call.subscribe({
next: res => {
block.items = (res.items || []).slice(0, max);
block.loading = false;
if (!block.items.length) console.info('[ThemePage] Empty block page', { provider: block.provider, theme: slug, query: q });
this.blocks.update(a => [...a]);
// Thème vide malgré les replis : dernière chance top streams live.
if (!(res.items || []).length && q) {
this.api.getTwitchTopStreamsPage(null).subscribe({
next: top => show(top.items || []),
error: () => fail(),
});
return;
}
show(res.items || []);
},
error: () => { block.error = 'Erreur de chargement'; block.loading = false; this.blocks.update(a => [...a]); }
error: () => {
// Erreur (timeout 30 s…) : une tentative top streams avant l'erreur.
this.api.getTwitchTopStreamsPage(null).subscribe({
next: top => { if ((top.items || []).length) show(top.items || []); else fail(); },
error: () => fail(),
});
}
});
return;
}