import { inject, Injectable } from '@angular/core' import { GetPackageRes, Marketplace, MarketplacePkg, StoreDataWithUrl, StoreIdentity, } from '@start9labs/marketplace' import { defaultRegistries, Exver, sameUrl } from '@start9labs/shared' import { T } from '@start9labs/start-sdk' import { PatchDB } from 'patch-db-client' import { BehaviorSubject, catchError, combineLatest, distinctUntilChanged, filter, from, map, mergeMap, Observable, of, pairwise, ReplaySubject, scan, shareReplay, startWith, switchMap, tap, } from 'rxjs' import { ApiService } from 'src/app/services/api/embassy-api.service' import { DataModel } from 'src/app/services/patch-db/data-model' const { start9, community } = defaultRegistries @Injectable({ providedIn: 'root', }) export class MarketplaceService { private readonly api = inject(ApiService) private readonly patch: PatchDB = inject(PatchDB) private readonly exver = inject(Exver) readonly registries$: Observable = this.patch .watch$('ui', 'registries') .pipe( map(registries => [ toStoreIdentity(start9, registries[start9]), toStoreIdentity(community, registries[community]), ...Object.entries(registries) .filter(([u, _]) => !sameUrl(start9, u) && !sameUrl(community, u)) .map(([url, name]) => toStoreIdentity(url, name)), ]), ) readonly newRegistry$ = this.registries$.pipe( startWith([]), pairwise(), mergeMap(([p, c]) => c.filter(a => !p.find(b => sameUrl(a.url, b.url)))), ) readonly currentRegistryUrl$ = new ReplaySubject(1) readonly requestErrors$ = new BehaviorSubject([]) readonly marketplace$: Observable = combineLatest([ this.newRegistry$.pipe( mergeMap(({ url, name }) => this.fetchRegistry$(url).pipe( tap(data => { if (data?.info.name) this.updateRegistryName(url, name, data.info.name) }), map(data => [url, data] satisfies [string, StoreDataWithUrl | null]), startWith<[string, StoreDataWithUrl | null]>([url, null]), ), ), scan<[string, StoreDataWithUrl | null], Marketplace>( (requests, [url, store]) => ({ ...requests, [url]: store, }), {}, ), ), this.registries$, ]).pipe( map(([marketplace, registries]) => Object.fromEntries( Object.entries(marketplace).filter(([url]) => registries.find(store => sameUrl(store.url, url)), ), ), ), shareReplay(1), ) readonly currentRegistry$: Observable = combineLatest([ this.marketplace$, this.currentRegistryUrl$, this.currentRegistryUrl$.pipe( distinctUntilChanged(), switchMap(url => this.fetchRegistry$(url).pipe(startWith(null))), ), ]).pipe( map(([all, url, current]) => current || all[url]), filter(Boolean), shareReplay(1), ) getPackage$( id: string, version: string | null, flavor: string | null, registryUrl?: string, ): Observable { return this.currentRegistry$.pipe( switchMap(registry => { const url = registryUrl || registry.url const pkg = registry.packages.find( p => p.id === id && p.flavor === flavor && (!version || this.exver.compareExver(p.version, version) === 0), ) return pkg ? of(pkg) : this.fetchPackage$(url, id, version, flavor) }), ) } fetchInfo$(registry: string): Observable { return from(this.api.getRegistryInfo({ registry })).pipe( map(info => ({ ...info, categories: { all: { name: 'All' }, ...info.categories, }, })), ) } fetchStatic$(pkg: MarketplacePkg): Observable { const registryAsset = pkg.s9pks[0]?.[1] if (!registryAsset) { throw new Error('No s9pk') } const urls = registryAsset.urls.map( u => `/s9pk/proxy/${encodeURIComponent(u)}/LICENSE.md`, ) || [] return from( this.api.getStatic(urls, { rootSighash: registryAsset.commitment.rootSighash, rootMaxsize: registryAsset.commitment.rootMaxsize, }), ) } private fetchRegistry$(url: string): Observable { console.log('FETCHING REGISTRY: ', url) return combineLatest([this.fetchInfo$(url), this.fetchPackages$(url)]).pipe( map(([info, packages]) => ({ info, packages, url })), catchError(e => { console.error(e) this.requestErrors$.next(this.requestErrors$.value.concat(url)) return of(null) }), ) } private fetchPackages$(url: string): Observable { return from( this.api.getRegistryPackages({ registry: url, id: null, targetVersion: null, otherVersions: 'short', }), ).pipe( map(packages => { return Object.entries(packages).flatMap(([id, pkgInfo]) => Object.keys(pkgInfo.best).map(version => this.convertRegistryPkgToMarketplacePkg( id, version, this.exver.getFlavor(version), pkgInfo, ), ), ) }), ) } private fetchPackage$( url: string, id: string, version: string | null, flavor: string | null, ): Observable { return from( this.api.getRegistryPackage({ registry: url, id, targetVersion: version ? `=${version}` : null, otherVersions: 'short', }), ).pipe( map(pkgInfo => this.convertRegistryPkgToMarketplacePkg(id, version, flavor, pkgInfo), ), ) } private convertRegistryPkgToMarketplacePkg( id: string, version: string | null | undefined, flavor: string | null, pkgInfo: GetPackageRes, ): MarketplacePkg { const ver = version || Object.keys(pkgInfo.best).find(v => this.exver.getFlavor(v) === flavor) || null const best = ver && pkgInfo.best[ver] if (!best) { return {} as MarketplacePkg } return { id, flavor, version: ver || '', ...pkgInfo, ...best, } } async installPackage( id: string, version: string, url: string, ): Promise { const params: T.InstallParams = { id, version, registry: url, } await this.api.installPackage(params) } private async updateRegistryName( url: string, oldName: string | null, newName: string, ): Promise { if (oldName !== newName) { this.api.setDbValue(['registries', url], newName) } } } function toStoreIdentity(url: string, name?: string | null): StoreIdentity { return { url, name: name || url, } }