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, 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' @Injectable() export class MarketplaceService implements AbstractMarketplaceService { private readonly knownHosts$: Observable = this.patch .watch$('ui', 'marketplace', 'known-hosts') .pipe( map(hosts => { 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 selectedHost$: Observable = this.patch .watch$('ui', 'marketplace') .pipe( distinctUntilKeyChanged('selected-url'), map(({ 'selected-url': url, 'known-hosts': hosts }) => toStoreIdentity(url, hosts[url]), ), shareReplay(1), ) private readonly marketplace$ = this.knownHosts$.pipe( startWith([]), pairwise(), mergeMap(([prev, curr]) => curr.filter(c => !prev.find(p => 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(1), ) private readonly selectedStore$: Observable = this.selectedHost$.pipe( switchMap(({ url }) => this.marketplace$.pipe( filter(m => !!m[url]), take(1), map(m => m[url]), ), ), ) private readonly requestErrors$ = new BehaviorSubject([]) constructor( private readonly api: ApiService, private readonly patch: PatchDB, private readonly config: ConfigService, ) {} getKnownHosts$(): Observable { return this.knownHosts$ } getSelectedHost$(): Observable { return this.selectedHost$ } getMarketplace$(): Observable { return this.marketplace$ } getSelectedStore$(): Observable { return this.selectedStore$ } getPackage$( id: string, version: string, optionalUrl?: string, ): Observable { return this.patch.watch$('ui', 'marketplace').pipe( switchMap(marketplace => { const url = optionalUrl || marketplace['selected-url'] if (version !== '*' || !marketplace['known-hosts'][url]) { return this.fetchPackage$(id, version, url) } return this.selectedStore$.pipe( filter(Boolean), map(s => s.packages.find(p => p.manifest.id === id)), ) }), ) } // UI only getRequestErrors$(): Observable { return this.requestErrors$ } async installPackage( id: string, version: string, url: string, ): Promise { const params: RR.InstallPackageReq = { id, 'version-spec': `=${version}`, 'marketplace-url': url, } await this.api.installPackage(params) } fetchInfo$(url: string): Observable { return this.patch.watch$('server-info').pipe( switchMap(serverInfo => { const qp: RR.GetMarketplaceInfoReq = { 'server-id': serverInfo.id, 'eos-version': serverInfo.version, } return this.api.marketplaceProxy( '/package/v0/info', qp, 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< RR.GetMarketplacePackagesReq, 'eos-version-compat' | 'page' | 'per-page' > = {}, ): Observable { return this.patch.watch$('server-info', 'eos-version-compat').pipe( switchMap(versionCompat => { const qp: RR.GetMarketplacePackagesReq = { ...params, 'eos-version-compat': versionCompat, page: 1, 'per-page': 100, } if (qp.ids) qp.ids = JSON.stringify(qp.ids) return this.api.marketplaceProxy( '/package/v0/index', qp, url, ) }), ) } fetchPackage$( id: string, version: string, url: string, ): Observable { return this.fetchPackages$(url, { ids: [{ id, version }] }).pipe( map(pkgs => pkgs[0]), ) } 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 async updateStoreName( url: string, oldName: string | undefined, newName: string, ): Promise { if (oldName !== newName) { this.api.setDbValue( ['marketplace', 'known-hosts', url, 'name'], newName, ) } } } function toStoreIdentity(url: string, uiStore: UIStore): StoreIdentity { return { url, ...uiStore, } }