import { Injectable } from '@angular/core' import { MarketplacePkg, AbstractMarketplaceService, StoreData, Marketplace, StoreInfo, StoreIdentity, } from '@start9labs/marketplace' import { BehaviorSubject, combineLatest, distinctUntilKeyChanged, from, mergeMap, Observable, of, scan, } from 'rxjs' import { RR } from 'src/app/services/api/api.types' import { ApiService } from 'src/app/services/api/embassy-api.service' import { DataModel, UIMarketplaceData, UIStore, } from 'src/app/services/patch-db/data-model' import { PatchDB } from 'patch-db-client' import { catchError, filter, map, pairwise, shareReplay, startWith, switchMap, take, tap, } from 'rxjs/operators' import { ConfigService } from './config.service' import { sameUrl } from '@start9labs/shared' import { ClientStorageService } from './client-storage.service' @Injectable() export class MarketplaceService implements AbstractMarketplaceService { private readonly knownHosts$: Observable = this.patch .watch$('ui', 'marketplace', 'knownHosts') .pipe( map((hosts: UIMarketplaceData['knownHosts']) => { const { start9, community } = this.config.marketplace let arr = [ toStoreIdentity(start9, hosts[start9]), toStoreIdentity(community, hosts[community]), ] return arr.concat( Object.entries(hosts) .filter(([url, _]) => ![start9, community].includes(url as any)) .map(([url, store]) => toStoreIdentity(url, store)), ) }), ) private readonly filteredKnownHosts$: Observable = combineLatest([ this.clientStorageService.showDevTools$, this.knownHosts$, ]).pipe( map(([devMode, knownHosts]) => devMode ? knownHosts : knownHosts.filter( ({ url }) => !url.includes('alpha') && !url.includes('beta'), ), ), ) private readonly selectedHost$: Observable = this.patch .watch$('ui', 'marketplace') .pipe( distinctUntilKeyChanged('selectedUrl'), map(({ selectedUrl: url, knownHosts: hosts }) => toStoreIdentity(url, hosts[url]), ), shareReplay({ bufferSize: 1, refCount: true }), ) private readonly marketplace$ = this.knownHosts$.pipe( startWith([]), pairwise(), mergeMap(([prev, curr]) => curr.filter(c => !prev.find(p => sameUrl(c.url, p.url))), ), mergeMap(({ url, name }) => this.fetchStore$(url).pipe( tap(data => { if (data?.info) this.updateStoreName(url, name, data.info.name) }), map(data => { return [url, data] }), startWith<[string, StoreData | null]>([url, null]), ), ), scan<[string, StoreData | null], Record>( (requests, [url, store]) => { requests[url] = store return requests }, {}, ), shareReplay({ bufferSize: 1, refCount: true }), ) private readonly filteredMarketplace$ = combineLatest([ this.clientStorageService.showDevTools$, this.marketplace$, ]).pipe( map(([devMode, marketplace]) => Object.entries(marketplace).reduce( (filtered, [url, store]) => !devMode && (url.includes('alpha') || url.includes('beta')) ? filtered : { [url]: store, ...filtered, }, {} as Marketplace, ), ), ) private readonly selectedStore$: Observable = this.selectedHost$.pipe( switchMap(({ url }) => this.marketplace$.pipe( map(m => m[url]), filter(Boolean), take(1), ), ), ) private readonly requestErrors$ = new BehaviorSubject([]) constructor( private readonly api: ApiService, private readonly patch: PatchDB, private readonly config: ConfigService, private readonly clientStorageService: ClientStorageService, ) {} getKnownHosts$(filtered = false): Observable { // option to filter out hosts containing 'alpha' or 'beta' substrings in registryURL return filtered ? this.filteredKnownHosts$ : this.knownHosts$ } getSelectedHost$(): Observable { return this.selectedHost$ } getMarketplace$(filtered = false): Observable { // option to filter out hosts containing 'alpha' or 'beta' substrings in registryURL return filtered ? this.filteredMarketplace$ : this.marketplace$ } getSelectedStore$(): Observable { return this.selectedStore$ } getPackage$( id: string, version: string, optionalUrl?: string, ): Observable { return this.patch.watch$('ui', 'marketplace').pipe( switchMap(uiMarketplace => { const url = optionalUrl || uiMarketplace.selectedUrl if (version !== '*' || !uiMarketplace.knownHosts[url]) { return this.fetchPackage$(id, version, url) } return this.marketplace$.pipe( map(m => m[url]), filter(Boolean), take(1), map( store => store.packages.find(p => p.manifest.id === id) || ({} as MarketplacePkg), ), ) }), ) } // UI only readonly updateErrors: Record = {} readonly updateQueue: Record = {} getRequestErrors$(): Observable { return this.requestErrors$ } async installPackage( id: string, version: string, url: string, ): Promise { const params: RR.InstallPackageReq = { id, versionSpec: `=${version}`, marketplaceUrl: url, } await this.api.installPackage(params) } fetchInfo$(url: string): Observable { return this.patch.watch$('serverInfo').pipe( take(1), switchMap(serverInfo => { const qp: RR.GetMarketplaceInfoReq = { serverId: serverInfo.id } return this.api.marketplaceProxy( '/package/v0/info', qp, url, ) }), ) } fetchReleaseNotes$( id: string, url?: string, ): Observable> { return this.selectedHost$.pipe( switchMap(m => { return from( this.api.marketplaceProxy>( `/package/v0/release-notes/${id}`, {}, url || m.url, ), ) }), ) } fetchStatic$(id: string, type: string, url?: string): Observable { return this.selectedHost$.pipe( switchMap(m => { return from( this.api.marketplaceProxy( `/package/v0/${type}/${id}`, {}, url || m.url, ), ) }), ) } private fetchStore$(url: string): Observable { return combineLatest([this.fetchInfo$(url), this.fetchPackages$(url)]).pipe( map(([info, packages]) => ({ info, packages })), catchError(e => { console.error(e) this.requestErrors$.next(this.requestErrors$.value.concat(url)) return of(null) }), ) } private fetchPackages$( url: string, params: Omit = {}, ): Observable { const qp: RR.GetMarketplacePackagesReq = { ...params, page: 1, perPage: 100, } if (qp.ids) qp.ids = JSON.stringify(qp.ids) return from( this.api.marketplaceProxy( '/package/v0/index', qp, url, ), ) } private fetchPackage$( id: string, version: string, url: string, ): Observable { return this.fetchPackages$(url, { ids: [{ id, version }] }).pipe( map(pkgs => pkgs[0] || {}), ) } private async updateStoreName( url: string, oldName: string | undefined, newName: string, ): Promise { if (oldName !== newName) { this.api.setDbValue( ['marketplace', 'knownHosts', url, 'name'], newName, ) } } } function toStoreIdentity(url: string, uiStore: UIStore): StoreIdentity { return { url, ...uiStore, } }