refactor(patch-db): use PatchDB class declaratively (#1562)

* refactor(patch-db): use PatchDB class declaratively

* chore: remove initial source before init

* chore: show spinner

* fix: show Connecting to Embassy spinner until first connection

* fix: switching marketplaces

* allow for subscription to end with take when installing a package

* update patchdb

Co-authored-by: Lucy Cifferello <12953208+elvece@users.noreply.github.com>
This commit is contained in:
Alex Inkin
2022-06-23 01:09:14 +03:00
committed by GitHub
parent a8749f574a
commit 53ca9b0420
19 changed files with 142 additions and 105 deletions

View File

@@ -1,14 +1,20 @@
import { Component } from '@angular/core'
import { Component, Inject, OnDestroy } from '@angular/core'
import { AuthService } from './services/auth.service'
import { SplitPaneTracker } from './services/split-pane.service'
import { merge, Observable } from 'rxjs'
import { GLOBAL_SERVICE } from './app/global/global.module'
@Component({
selector: 'app-root',
templateUrl: 'app.component.html',
styleUrls: ['app.component.scss'],
})
export class AppComponent {
export class AppComponent implements OnDestroy {
readonly subscription = merge(...this.services).subscribe()
constructor(
@Inject(GLOBAL_SERVICE)
private readonly services: readonly Observable<unknown>[],
readonly authService: AuthService,
private readonly splitPane: SplitPaneTracker,
) {}
@@ -16,4 +22,8 @@ export class AppComponent {
splitPaneVisible({ detail }: any) {
this.splitPane.sidebarOpen$.next(detail.visible)
}
ngOnDestroy() {
this.subscription.unsubscribe()
}
}

View File

@@ -18,6 +18,7 @@ import { MenuModule } from './app/menu/menu.module'
import { EnterModule } from './app/enter/enter.module'
import { APP_PROVIDERS } from './app.providers'
import { GlobalModule } from './app/global/global.module'
import { PatchDbModule } from './services/patch-db/patch-db.module'
@NgModule({
declarations: [AppComponent],
@@ -46,6 +47,7 @@ import { GlobalModule } from './app/global/global.module'
SharedPipesModule,
MarketplaceModule,
GlobalModule,
PatchDbModule,
],
providers: APP_PROVIDERS,
bootstrap: [AppComponent],

View File

@@ -1,4 +1,4 @@
import { DOCUMENT } from '@angular/common'
import { Bootstrapper, DBCache } from 'patch-db-client'
import { APP_INITIALIZER, ErrorHandler, Provider } from '@angular/core'
import { FormBuilder } from '@angular/forms'
import { Router, RouteReuseStrategy } from '@angular/router'
@@ -9,15 +9,11 @@ import { WorkspaceConfig } from '@start9labs/shared'
import { ApiService } from './services/api/embassy-api.service'
import { MockApiService } from './services/api/embassy-mock-api.service'
import { LiveApiService } from './services/api/embassy-live-api.service'
import {
PATCH_SOURCE,
mockSourceFactory,
realSourceFactory,
} from './services/patch-db/patch-db.factory'
import { ConfigService } from './services/config.service'
import { BOOTSTRAPPER, PATCH_CACHE } from './services/patch-db/patch-db.factory'
import { GlobalErrorHandler } from './services/global-error-handler.service'
import { AuthService } from './services/auth.service'
import { LocalStorageService } from './services/local-storage.service'
import { DataModel } from './services/patch-db/data-model'
const { useMocks } = require('../../../../config.json') as WorkspaceConfig
@@ -32,18 +28,20 @@ export const APP_PROVIDERS: Provider[] = [
provide: ApiService,
useClass: useMocks ? MockApiService : LiveApiService,
},
{
provide: PATCH_SOURCE,
deps: [ApiService, ConfigService, DOCUMENT],
useFactory: useMocks ? mockSourceFactory : realSourceFactory,
},
{
provide: ErrorHandler,
useClass: GlobalErrorHandler,
},
{
provide: APP_INITIALIZER,
deps: [Storage, AuthService, LocalStorageService, Router],
deps: [
Storage,
AuthService,
LocalStorageService,
Router,
BOOTSTRAPPER,
PATCH_CACHE,
],
useFactory: appInitializer,
multi: true,
},
@@ -54,12 +52,19 @@ export function appInitializer(
auth: AuthService,
localStorage: LocalStorageService,
router: Router,
bootstrapper: Bootstrapper<DataModel>,
cache: DBCache<DataModel>,
): () => Promise<void> {
return async () => {
await storage.create()
await auth.init()
await localStorage.init()
const localCache = await bootstrapper.init()
cache.sequence = localCache.sequence
cache.data = localCache.data
router.initialNavigation()
}
}

View File

@@ -17,10 +17,11 @@ import { UnreadToastService } from './services/unread-toast.service'
import { RefreshToastService } from './services/refresh-toast.service'
import { UpdateToastService } from './services/update-toast.service'
const GLOBAL_SERVICE = new InjectionToken<readonly Observable<unknown>[]>(
'A multi token of global Observable services',
)
export const GLOBAL_SERVICE = new InjectionToken<
readonly Observable<unknown>[]
>('A multi token of global Observable services')
// This module is purely for providers organization purposes
@NgModule({
providers: [
[
@@ -34,18 +35,7 @@ const GLOBAL_SERVICE = new InjectionToken<readonly Observable<unknown>[]>(
[PatchDataService, PatchMonitorService].map(useExisting),
],
})
export class GlobalModule implements OnDestroy {
readonly subscription = merge(...this.services).subscribe()
constructor(
@Inject(GLOBAL_SERVICE)
private readonly services: readonly Observable<unknown>[],
) {}
ngOnDestroy() {
this.subscription.unsubscribe()
}
}
export class GlobalModule {}
function useClass(useClass: Type<unknown>): ClassProvider {
return {

View File

@@ -6,7 +6,6 @@ import { ApiService } from '../../services/api/embassy-api.service'
import { AppWizardComponent, SlideDefinition } from './app-wizard.component'
import { ConfigService } from 'src/app/services/config.service'
import { MarketplaceService } from 'src/app/services/marketplace.service'
import { first } from 'rxjs/operators'
@Injectable({ providedIn: 'root' })
export class WizardDefs {
@@ -45,7 +44,6 @@ export class WizardDefs {
id,
'version-spec': version ? `=${version}` : undefined,
})
.pipe(first())
.toPromise(),
},
},
@@ -87,7 +85,6 @@ export class WizardDefs {
id,
'version-spec': version ? `=${version}` : undefined,
})
.pipe(first())
.toPromise(),
},
},

View File

@@ -9,13 +9,12 @@
<ion-content class="ion-padding">
<!-- loading -->
<text-spinner
*ngIf="!patch.loaded else data"
text="Connecting to Embassy"
></text-spinner>
<ng-template #loading>
<text-spinner text="Connecting to Embassy"></text-spinner>
</ng-template>
<!-- not loading -->
<ng-template #data>
<ng-container *ngIf="connected$ | async else loading">
<app-list-empty
*ngIf="empty; else list"
class="ion-text-center ion-padding"
@@ -40,5 +39,5 @@
</ion-item-group>
</ng-container>
</ng-template>
</ng-template>
</ng-container>
</ion-content>

View File

@@ -18,18 +18,17 @@ export class AppListPage {
recoveredPkgs: readonly RecoveredInfo[] = []
order: readonly string[] = []
reordering = false
loading = true
readonly connected$ = this.patch.connected$
constructor(
private readonly api: ApiService,
private readonly destroy$: DestroyService,
public readonly patch: PatchDbService,
private readonly patch: PatchDbService,
) {}
get empty(): boolean {
return (
!this.loading && !this.pkgs.length && isEmptyObject(this.recoveredPkgs)
)
return !this.pkgs.length && isEmptyObject(this.recoveredPkgs)
}
ngOnInit() {
@@ -43,7 +42,6 @@ export class AppListPage {
this.pkgs = pkgs
this.recoveredPkgs = recoveredPkgs
this.order = order
this.loading = false
// set order in UI DB if there were unknown packages
if (order.length < pkgs.length) {

View File

@@ -32,7 +32,7 @@ export class DeveloperMenuPage {
) {}
get name(): string {
return this.patchDb.data.ui?.dev?.[this.projectId]?.name || ''
return this.patchDb.getData().ui?.dev?.[this.projectId]?.name || ''
}
ngOnInit() {

View File

@@ -8,7 +8,7 @@
<ion-content class="ion-padding">
<marketplace-list-content
*ngIf="loaded else loading"
*ngIf="connected$ | async else loading"
[localPkgs]="(localPkgs$ | async) || {}"
[pkgs]="pkgs$ | async"
[categories]="categories$ | async"

View File

@@ -15,6 +15,8 @@ import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
templateUrl: './marketplace-list.page.html',
})
export class MarketplaceListPage {
readonly connected$ = this.patch.connected$
readonly localPkgs$: Observable<Record<string, PackageDataEntry>> = this.patch
.watch$('package-data')
.pipe(
@@ -44,8 +46,4 @@ export class MarketplaceListPage {
private readonly patch: PatchDbService,
private readonly marketplaceService: AbstractMarketplaceService,
) {}
get loaded(): boolean {
return this.patch.loaded
}
}

View File

@@ -21,7 +21,6 @@ import { LocalStorageService } from 'src/app/services/local-storage.service'
import { MarketplaceService } from 'src/app/services/marketplace.service'
import { hasCurrentDeps } from 'src/app/util/has-deps'
import { Emver } from '../../../../../../../shared/src/services/emver.service'
import { first } from 'rxjs/operators'
import { ErrorToastService } from '../../../../../../../shared/src/services/error-toast.service'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { isEmptyObject } from '../../../../../../../shared/src/util/misc.util'
@@ -145,7 +144,6 @@ export class MarketplaceShowControlsComponent {
id,
'version-spec': `=${version}`,
})
.pipe(first())
.toPromise()
} catch (e: any) {
this.errToast.present(e)

View File

@@ -75,10 +75,12 @@
<ion-label>
<h2>
<b>
<span *ngIf="not['package-id'] && patch.data['package-data']">
{{ patch.data['package-data'][not['package-id']] ?
patch.data['package-data'][not['package-id']].manifest.title :
not['package-id'] }} -
<span
*ngIf="not['package-id'] && patch.getData()['package-data']"
>
{{ patch.getData()['package-data'][not['package-id']] ?
patch.getData()['package-data'][not['package-id']].manifest.title
: not['package-id'] }} -
</span>
<ion-text [color]="getColor(not)"> {{ not.title }} </ion-text>
</b>

View File

@@ -1,6 +1,6 @@
<ion-header>
<ion-toolbar>
<ion-title *ngIf="patch.loaded else loading">
<ion-title *ngIf="connected$ | async else loading">
{{ (ui$ | async)?.name || "Embassy-" + (server$ | async)?.id }}
</ion-title>
<ng-template #loading>
@@ -14,13 +14,12 @@
<ion-content class="ion-padding">
<!-- loading -->
<text-spinner
*ngIf="!patch.loaded else data"
text="Connecting to Embassy"
></text-spinner>
<ng-template #spinner>
<text-spinner text="Connecting to Embassy"></text-spinner>
</ng-template>
<!-- not loading -->
<ng-template #data>
<ng-container *ngIf="connected$ | async else spinner">
<ion-item-group *ngIf="server$ | async as server">
<div *ngFor="let cat of settings | keyvalue : asIsOrder">
<ion-item-divider>
@@ -98,5 +97,5 @@
</ng-container>
</div>
</ion-item-group>
</ng-template>
</ng-container>
</ion-content>

View File

@@ -10,7 +10,7 @@ import { ApiService } from 'src/app/services/api/embassy-api.service'
import { ActivatedRoute } from '@angular/router'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import { Observable, of } from 'rxjs'
import { filter, map, take } from 'rxjs/operators'
import { filter, take } from 'rxjs/operators'
import { exists, isEmptyObject, ErrorToastService } from '@start9labs/shared'
import { EOSService } from 'src/app/services/eos.service'
import { LocalStorageService } from 'src/app/services/local-storage.service'
@@ -28,6 +28,7 @@ export class ServerShowPage {
readonly server$ = this.patch.watch$('server-info')
readonly ui$ = this.patch.watch$('ui')
readonly connected$ = this.patch.connected$
constructor(
private readonly alertCtrl: AlertController,
@@ -37,8 +38,8 @@ export class ServerShowPage {
private readonly embassyApi: ApiService,
private readonly navCtrl: NavController,
private readonly route: ActivatedRoute,
private readonly patch: PatchDbService,
public readonly eosService: EOSService,
public readonly patch: PatchDbService,
public readonly localStorageService: LocalStorageService,
) {}

View File

@@ -6,7 +6,7 @@ import {
AbstractMarketplaceService,
Marketplace,
} from '@start9labs/marketplace'
import { defer, from, Observable, of } from 'rxjs'
import { from, Observable, of } from 'rxjs'
import { RR } from 'src/app/services/api/api.types'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { ConfigService } from 'src/app/services/config.service'
@@ -32,7 +32,7 @@ export class MarketplaceService extends AbstractMarketplaceService {
private readonly altMarketplaceData$: Observable<
UIMarketplaceData | undefined
> = this.patch.watch$('ui', 'marketplace').pipe(shareReplay())
> = this.patch.watch$('ui', 'marketplace').pipe(shareReplay(1))
private readonly marketplace$ = this.altMarketplaceData$.pipe(
map(data => this.toMarketplace(data)),
@@ -51,7 +51,7 @@ export class MarketplaceService extends AbstractMarketplaceService {
),
),
map(({ categories }) => categories),
shareReplay(),
shareReplay(1),
)
private readonly pkg$: Observable<MarketplacePkg[]> =
@@ -74,7 +74,7 @@ export class MarketplaceService extends AbstractMarketplaceService {
return of([])
}),
shareReplay(),
shareReplay(1),
)
constructor(

View File

@@ -1,8 +1,9 @@
import { inject, InjectionToken } from '@angular/core'
import { InjectionToken } from '@angular/core'
import { exists } from '@start9labs/shared'
import { filter } from 'rxjs/operators'
import {
Bootstrapper,
DBCache,
MockSource,
PollSource,
Source,
@@ -10,17 +11,20 @@ import {
} from 'patch-db-client'
import { ConfigService } from '../config.service'
import { LocalStorageBootstrap } from './local-storage-bootstrap'
import { ApiService } from '../api/embassy-api.service'
import { MockApiService } from '../api/embassy-mock-api.service'
import { DataModel } from './data-model'
import { BehaviorSubject } from 'rxjs'
export const PATCH_SOURCE = new InjectionToken<Source<DataModel>[]>(
'[wsSources, pollSources]',
)
export const BOOTSTRAPPER = new InjectionToken<Bootstrapper<DataModel>>('', {
factory: () => inject(LocalStorageBootstrap),
// [wsSources, pollSources]
export const PATCH_SOURCE = new InjectionToken<Source<DataModel>[]>('')
export const PATCH_SOURCE$ = new InjectionToken<
BehaviorSubject<Source<DataModel>[]>
>('')
export const PATCH_CACHE = new InjectionToken<DBCache<DataModel>>('', {
factory: () => ({} as any),
})
export const BOOTSTRAPPER = new InjectionToken<Bootstrapper<DataModel>>('')
export function mockSourceFactory({
mockPatch$,

View File

@@ -0,0 +1,44 @@
import { PatchDB } from 'patch-db-client'
import { NgModule } from '@angular/core'
import { DOCUMENT } from '@angular/common'
import { WorkspaceConfig } from '@start9labs/shared'
import {
BOOTSTRAPPER,
mockSourceFactory,
PATCH_CACHE,
PATCH_SOURCE,
PATCH_SOURCE$,
realSourceFactory,
} from './patch-db.factory'
import { LocalStorageBootstrap } from './local-storage-bootstrap'
import { ApiService } from '../api/embassy-api.service'
import { ConfigService } from '../config.service'
import { ReplaySubject } from 'rxjs'
const { useMocks } = require('../../../../../../config.json') as WorkspaceConfig
// This module is purely for providers organization purposes
@NgModule({
providers: [
{
provide: BOOTSTRAPPER,
useExisting: LocalStorageBootstrap,
},
{
provide: PATCH_SOURCE,
deps: [ApiService, ConfigService, DOCUMENT],
useFactory: useMocks ? mockSourceFactory : realSourceFactory,
},
{
provide: PATCH_SOURCE$,
useValue: new ReplaySubject(1),
},
{
provide: PatchDB,
deps: [PATCH_SOURCE$, ApiService, PATCH_CACHE],
useClass: PatchDB,
},
],
})
export class PatchDbModule {}

View File

@@ -14,16 +14,17 @@ import {
filter,
finalize,
mergeMap,
shareReplay,
switchMap,
take,
tap,
withLatestFrom,
} from 'rxjs/operators'
import { isEmptyObject, pauseFor } from '@start9labs/shared'
import { pauseFor } from '@start9labs/shared'
import { DataModel } from './data-model'
import { ApiService } from '../api/embassy-api.service'
import { AuthService } from '../auth.service'
import { BOOTSTRAPPER, PATCH_SOURCE } from './patch-db.factory'
import { BOOTSTRAPPER, PATCH_SOURCE, PATCH_SOURCE$ } from './patch-db.factory'
export enum PatchConnection {
Initializing = 'initializing',
@@ -39,49 +40,40 @@ export class PatchDbService {
private patchConnection$ = new ReplaySubject<PatchConnection>(1)
private wsSuccess$ = new BehaviorSubject(false)
private polling$ = new BehaviorSubject(false)
private patchDb: PatchDB<DataModel>
private subs: Subscription[] = []
private sources$: BehaviorSubject<Source<DataModel>[]> = new BehaviorSubject([
this.sources[0],
])
data: DataModel
readonly connected$ = this.watchPatchConnection$().pipe(
filter(status => status === PatchConnection.Connected),
take(1),
shareReplay(),
)
errors = 0
getData() {
return this.patchDb.store.cache.data
}
// TODO: Refactor to use `Observable` so that we can react to PatchDb becoming loaded
get loaded(): boolean {
return (
this.patchDb?.store?.cache?.data &&
!isEmptyObject(this.patchDb.store.cache.data)
)
}
constructor(
// [wsSources, pollSources]
@Inject(PATCH_SOURCE) private readonly sources: Source<DataModel>[],
@Inject(BOOTSTRAPPER)
private readonly bootstrapper: Bootstrapper<DataModel>,
@Inject(PATCH_SOURCE$)
private readonly sources$: BehaviorSubject<Source<DataModel>[]>,
private readonly http: ApiService,
private readonly auth: AuthService,
private readonly storage: Storage,
private readonly patchDb: PatchDB<DataModel>,
) {}
async init(): Promise<void> {
const cache = await this.bootstrapper.init()
init() {
this.sources$.next([this.sources[0], this.http])
this.patchDb = new PatchDB(this.sources$, this.http, cache)
this.patchConnection$.next(PatchConnection.Initializing)
this.data = this.patchDb.store.cache.data
}
async start(): Promise<void> {
await this.init()
this.init()
this.subs.push(
// Connection Error
@@ -165,11 +157,9 @@ export class PatchDbService {
}
stop(): void {
if (this.patchDb) {
console.log('patchDB: STOPPING')
this.patchConnection$.next(PatchConnection.Initializing)
this.patchDb.store.reset()
}
console.log('patchDB: STOPPING')
this.patchConnection$.next(PatchConnection.Initializing)
this.patchDb.store.reset()
this.subs.forEach(x => x.unsubscribe())
this.subs = []
}