diff --git a/src/app/modules/guest/guest-show-data.service.ts b/src/app/modules/guest/guest-show-data.service.ts index 3aa47f5..a053081 100644 --- a/src/app/modules/guest/guest-show-data.service.ts +++ b/src/app/modules/guest/guest-show-data.service.ts @@ -1,6 +1,6 @@ import {Injectable} from '@angular/core'; -import {BehaviorSubject, Observable} from 'rxjs'; -import {map} from 'rxjs/operators'; +import {Observable} from 'rxjs'; +import {map, shareReplay} from 'rxjs/operators'; import {DbService} from 'src/app/services/db.service'; import {GuestShow} from './guest-show'; @@ -8,12 +8,15 @@ import {GuestShow} from './guest-show'; providedIn: 'root', }) export class GuestShowDataService { - public list$: BehaviorSubject = new BehaviorSubject([]); private collection = 'guest'; + public list$: Observable = this.dbService.col$(this.collection).pipe( + shareReplay({ + bufferSize: 1, + refCount: true, + }) + ); - public constructor(private dbService: DbService) { - this.dbService.col$(this.collection).subscribe(_ => this.list$.next(_)); - } + public constructor(private dbService: DbService) {} public read$: (id: string) => Observable = (id: string): Observable => this.list$.pipe(map(_ => _.find(s => s.id === id) || null)); public update$: (id: string, data: Partial) => Promise = async (id: string, data: Partial): Promise => diff --git a/src/app/modules/shows/services/show-data.service.ts b/src/app/modules/shows/services/show-data.service.ts index 16185a5..b2c53b9 100644 --- a/src/app/modules/shows/services/show-data.service.ts +++ b/src/app/modules/shows/services/show-data.service.ts @@ -1,19 +1,24 @@ import {Injectable} from '@angular/core'; -import {BehaviorSubject, Observable} from 'rxjs'; +import {Observable} from 'rxjs'; import {DbService} from '../../../services/db.service'; import {Show} from './show'; -import {map} from 'rxjs/operators'; +import {map, shareReplay} from 'rxjs/operators'; @Injectable({ providedIn: 'root', }) export class ShowDataService { - public list$ = new BehaviorSubject([]); private collection = 'shows'; + public list$: Observable = this.dbService.col$(this.collection).pipe( + // server-side ordering cuts client work and keeps stable order across subscribers + map(shows => [...shows].sort((a, b) => a.date.toMillis() - b.date.toMillis())), + shareReplay({ + bufferSize: 1, + refCount: true, + }) + ); - public constructor(private dbService: DbService) { - this.dbService.col$(this.collection).subscribe(_ => this.list$.next(_)); - } + public constructor(private dbService: DbService) {} public listRaw$ = () => this.dbService.col$(this.collection); diff --git a/src/app/modules/shows/services/show-song.service.ts b/src/app/modules/shows/services/show-song.service.ts index fa654ce..069cb1b 100644 --- a/src/app/modules/shows/services/show-song.service.ts +++ b/src/app/modules/shows/services/show-song.service.ts @@ -40,14 +40,21 @@ export class ShowSongService { public list = (showId: string): Promise => firstValueFrom(this.list$(showId)); public async delete$(showId: string, showSongId: string, index: number): Promise { - const showSong = await this.read(showId, showSongId); - await this.showSongDataService.delete(showId, showSongId); - const show = await firstValueFrom(this.showService.read$(showId)); + const [showSong, show] = await Promise.all([ + this.read(showId, showSongId), + firstValueFrom(this.showService.read$(showId)), + ]); if (!show) return; - const order = show.order; + if (!showSong) return; + + const order = [...show.order]; order.splice(index, 1); - await this.showService.update$(showId, {order}); - await this.userService.decSongCount(showSong.songId); + + await Promise.all([ + this.showSongDataService.delete(showId, showSongId), + this.showService.update$(showId, {order}), + this.userService.decSongCount(showSong.songId), + ]); } public update$ = async (showId: string, songId: string, data: Partial): Promise => await this.showSongDataService.update$(showId, songId, data); diff --git a/src/app/modules/shows/services/show.service.ts b/src/app/modules/shows/services/show.service.ts index 86b1dda..eee89d6 100644 --- a/src/app/modules/shows/services/show.service.ts +++ b/src/app/modules/shows/services/show.service.ts @@ -32,8 +32,7 @@ export class ShowService { ), map(s => s.shows - .sort((a, b) => a.date.toMillis() - b.date.toMillis()) - .filter(_ => !_.archived) + .filter(show => !show.archived) .filter(show => show.published || (show.owner === s.user?.id && !publishedOnly)) ) ); diff --git a/src/app/modules/songs/services/song-data.service.ts b/src/app/modules/songs/services/song-data.service.ts index 4d092a9..e3730cc 100644 --- a/src/app/modules/songs/services/song-data.service.ts +++ b/src/app/modules/songs/services/song-data.service.ts @@ -1,18 +1,25 @@ import {Injectable} from '@angular/core'; import {Song} from './song'; -import {BehaviorSubject, Observable} from 'rxjs'; +import {Observable} from 'rxjs'; import {DbService} from '../../../services/db.service'; -import {map} from 'rxjs/operators'; +import {map, shareReplay, startWith} from 'rxjs/operators'; @Injectable({ providedIn: 'root', }) export class SongDataService { - public list$ = new BehaviorSubject([]); private collection = 'songs'; + public list$: Observable = this.dbService.col$(this.collection).pipe( + startWith([] as Song[]), // immediate empty emit keeps UI responsive while first snapshot arrives + shareReplay({ + bufferSize: 1, + refCount: false, // keep the listener alive after first subscription to avoid reloading on navigation + }) + ); public constructor(private dbService: DbService) { - this.dbService.col$(this.collection).subscribe(_ => this.list$.next(_)); + // Warm the shared stream once at startup to avoid first-navigation delay. + // this.list$.subscribe(); } // public list$ = (): Observable => this.dbService.col$(this.collection); diff --git a/src/app/modules/songs/services/song-list.resolver.ts b/src/app/modules/songs/services/song-list.resolver.ts index e068e83..18d635e 100644 --- a/src/app/modules/songs/services/song-list.resolver.ts +++ b/src/app/modules/songs/services/song-list.resolver.ts @@ -3,7 +3,7 @@ import {Injectable} from '@angular/core'; import {Observable} from 'rxjs'; import {SongService} from './song.service'; import {Song} from './song'; -import {filter} from 'rxjs/operators'; +import {take} from 'rxjs/operators'; @Injectable({ providedIn: 'root', @@ -12,6 +12,6 @@ export class SongListResolver { public constructor(private songService: SongService) {} public resolve(): Observable { - return this.songService.list$().pipe(filter(_ => _.length > 0)); + return this.songService.list$().pipe(take(1)); } } diff --git a/src/app/modules/songs/song-list/song-list.component.ts b/src/app/modules/songs/song-list/song-list.component.ts index b5e0f1b..cbfab1d 100644 --- a/src/app/modules/songs/song-list/song-list.component.ts +++ b/src/app/modules/songs/song-list/song-list.component.ts @@ -26,16 +26,11 @@ import {FaIconComponent} from '@fortawesome/angular-fontawesome'; }) export class SongListComponent implements OnInit, OnDestroy { public anyFilterActive = false; - public songs$: Observable | null = combineLatest([ + public songs$: Observable = combineLatest([ this.activatedRoute.queryParams.pipe(map(_ => _ as FilterValues)), - this.activatedRoute.data.pipe( - map(data => data.songList as Song[]), - map(songs => songs.sort((a, b) => a.number - b.number)) - ), + this.songService.list$().pipe(map(songs => [...songs].sort((a, b) => a.number - b.number))), ]).pipe( - map(_ => { - const songs = _[1]; - const filter = _[0]; + map(([filter, songs]) => { this.anyFilterActive = this.checkIfFilterActive(filter); return songs.filter(song => this.filter(song, filter)).sort((a, b) => a.title?.localeCompare(b.title)); }) diff --git a/src/app/modules/songs/songs-routing.module.ts b/src/app/modules/songs/songs-routing.module.ts index b77d7bc..30dc1cf 100644 --- a/src/app/modules/songs/songs-routing.module.ts +++ b/src/app/modules/songs/songs-routing.module.ts @@ -5,13 +5,11 @@ import {SongListComponent} from './song-list/song-list.component'; import {EditComponent} from './song/edit/edit.component'; import {NewComponent} from './song/new/new.component'; import {EditSongGuard} from './song/edit/edit-song.guard'; -import {SongListResolver} from './services/song-list.resolver'; const routes: Routes = [ { path: '', component: SongListComponent, - resolve: {songList: SongListResolver}, pathMatch: 'full', }, { diff --git a/src/app/services/global-settings.service.ts b/src/app/services/global-settings.service.ts index f5ac13d..420df8a 100644 --- a/src/app/services/global-settings.service.ts +++ b/src/app/services/global-settings.service.ts @@ -2,6 +2,7 @@ import {Injectable} from '@angular/core'; import {DbService} from './db.service'; import {GlobalSettings} from './global-settings'; import {Observable} from 'rxjs'; +import {shareReplay} from 'rxjs/operators'; @Injectable({ providedIn: 'root', @@ -10,7 +11,12 @@ export class GlobalSettingsService { public constructor(private db: DbService) {} public get get$(): Observable { - return this.db.doc$('global/static'); + return this.db.doc$('global/static').pipe( + shareReplay({ + bufferSize: 1, + refCount: true, + }) + ); } public async set(data: Partial): Promise { diff --git a/src/app/services/user/user.service.ts b/src/app/services/user/user.service.ts index b39f902..1647437 100644 --- a/src/app/services/user/user.service.ts +++ b/src/app/services/user/user.service.ts @@ -1,7 +1,7 @@ import {Injectable} from '@angular/core'; import {AngularFireAuth} from '@angular/fire/compat/auth'; import {BehaviorSubject, firstValueFrom, Observable} from 'rxjs'; -import {filter, map, switchMap, tap} from 'rxjs/operators'; +import {filter, map, shareReplay, switchMap, tap} from 'rxjs/operators'; import {User} from './user'; import {DbService} from '../db.service'; import {environment} from '../../../environments/environment'; @@ -13,7 +13,7 @@ import {ShowSongDataService} from '../../modules/shows/services/show-song-data.s providedIn: 'root', }) export class UserService { - public users$ = new BehaviorSubject([]); + public users$ = this.db.col$('users').pipe(shareReplay({bufferSize: 1, refCount: true})); private iUserId$ = new BehaviorSubject(null); private iUser$ = new BehaviorSubject(null); @@ -32,8 +32,6 @@ export class UserService { switchMap(uid => this.readUser$(uid)) ) .subscribe(_ => this.iUser$.next(_)); - - this.db.col$('users/').subscribe(_ => this.users$.next(_)); } public get userId$(): Observable { @@ -62,7 +60,7 @@ export class UserService { public loggedIn$: () => Observable = () => this.afAuth.authState.pipe(map(_ => !!_)); - public list$: () => Observable = (): Observable => this.db.col$('users'); + public list$: () => Observable = (): Observable => this.users$; public async logout(): Promise { await this.afAuth.signOut(); diff --git a/src/main.ts b/src/main.ts index ab55f8e..dca5ca6 100644 --- a/src/main.ts +++ b/src/main.ts @@ -14,6 +14,8 @@ import {AngularFireAuthModule} from '@angular/fire/compat/auth'; import {AngularFireAuthGuardModule} from '@angular/fire/compat/auth-guard'; import {FontAwesomeModule} from '@fortawesome/angular-fontawesome'; import {AppComponent} from './app/app.component'; +import {provideFirebaseApp, initializeApp} from '@angular/fire/app'; +import {provideFirestore, getFirestore} from '@angular/fire/firestore'; if (environment.production) { enableProdMode(); @@ -35,8 +37,9 @@ bootstrapApplication(AppComponent, { AngularFireAuthGuardModule, FontAwesomeModule ), + provideFirebaseApp(() => initializeApp(environment.firebase)), + provideFirestore(() => getFirestore()), {provide: MAT_DATE_LOCALE, useValue: 'de-DE'}, provideAnimations(), - provideAnimations(), ], }).catch(err => console.error(err));