mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-04-01 21:13:09 +00:00
Merge branch 'next/minor' of github.com:Start9Labs/start-os into next/major
This commit is contained in:
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -1,26 +1,34 @@
|
||||
import { Dump, Revision } from 'patch-db-client'
|
||||
import { MarketplacePkg, StoreInfo } from '@start9labs/marketplace'
|
||||
import {
|
||||
DataModel,
|
||||
DomainInfo,
|
||||
NetworkStrategy,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import {
|
||||
StartOSDiskInfo,
|
||||
FetchLogsReq,
|
||||
FetchLogsRes,
|
||||
FollowLogsRes,
|
||||
FollowLogsReq,
|
||||
} from '@start9labs/shared'
|
||||
import { CT, T } from '@start9labs/start-sdk'
|
||||
import { FetchLogsReq, FetchLogsRes } from '@start9labs/shared'
|
||||
import { config } from '@start9labs/start-sdk'
|
||||
import { Dump } from 'patch-db-client'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { StartOSDiskInfo } from '@start9labs/shared'
|
||||
import { CT, T } from '@start9labs/start-sdk'
|
||||
import { WebSocketSubjectConfig } from 'rxjs/webSocket'
|
||||
|
||||
export module RR {
|
||||
// websocket
|
||||
|
||||
export type WebsocketConfig<T> = Omit<WebSocketSubjectConfig<T>, 'url'>
|
||||
|
||||
// state
|
||||
|
||||
export type EchoReq = { message: string } // server.echo
|
||||
export type EchoRes = string
|
||||
|
||||
export type ServerState = 'initializing' | 'error' | 'running'
|
||||
|
||||
// DB
|
||||
|
||||
export type GetRevisionsRes = Revision[] | Dump<DataModel>
|
||||
|
||||
export type GetDumpRes = Dump<DataModel>
|
||||
export type SubscribePatchReq = {}
|
||||
export type SubscribePatchRes = {
|
||||
dump: Dump<DataModel>
|
||||
guid: string
|
||||
}
|
||||
|
||||
export type SetDBValueReq<T> = { pointer: string; value: T } // db.put.ui
|
||||
export type SetDBValueRes = null
|
||||
@@ -30,6 +38,7 @@ export module RR {
|
||||
export type LoginReq = {
|
||||
password: string
|
||||
metadata: SessionMetadata
|
||||
ephemeral?: boolean
|
||||
} // auth.login - unauthed
|
||||
export type loginRes = null
|
||||
|
||||
@@ -42,10 +51,22 @@ export module RR {
|
||||
} // auth.reset-password
|
||||
export type ResetPasswordRes = null
|
||||
|
||||
// server
|
||||
// diagnostic
|
||||
|
||||
export type EchoReq = { message: string; timeout?: number } // server.echo
|
||||
export type EchoRes = string
|
||||
export type DiagnosticErrorRes = {
|
||||
code: number
|
||||
message: string
|
||||
data: { details: string }
|
||||
}
|
||||
|
||||
// init
|
||||
|
||||
export type InitGetProgressRes = {
|
||||
progress: T.FullProgress
|
||||
guid: string
|
||||
}
|
||||
|
||||
// server
|
||||
|
||||
export type GetSystemTimeReq = {} // server.time
|
||||
export type GetSystemTimeRes = {
|
||||
@@ -56,8 +77,16 @@ export module RR {
|
||||
export type GetServerLogsReq = FetchLogsReq // server.logs & server.kernel-logs & server.tor-logs
|
||||
export type GetServerLogsRes = FetchLogsRes
|
||||
|
||||
export type FollowServerLogsReq = FollowLogsReq & { limit?: number } // server.logs.follow & server.kernel-logs.follow & server.tor-logs.follow
|
||||
export type FollowServerLogsRes = FollowLogsRes
|
||||
// @param limit: BE default is 50
|
||||
// @param boot: number is offset (0: current, -1 prev, +1 first), string is a specific boot id, and null is all
|
||||
export type FollowServerLogsReq = {
|
||||
limit?: number
|
||||
boot?: number | string | null
|
||||
} // server.logs.follow & server.kernel-logs.follow
|
||||
export type FollowServerLogsRes = {
|
||||
startCursor: string
|
||||
guid: string
|
||||
}
|
||||
|
||||
export type GetServerMetricsReq = {} // server.metrics
|
||||
export type GetServerMetricsRes = {
|
||||
@@ -65,7 +94,7 @@ export module RR {
|
||||
metrics: Metrics
|
||||
}
|
||||
|
||||
export type UpdateServerReq = { marketplaceUrl: string } // server.update
|
||||
export type UpdateServerReq = { registry: string } // server.update
|
||||
export type UpdateServerRes = 'updating' | 'no-updates'
|
||||
|
||||
export type SetServerClearnetAddressReq = { domainInfo: DomainInfo | null } // server.set-clearnet
|
||||
@@ -77,8 +106,8 @@ export module RR {
|
||||
export type ShutdownServerReq = {} // server.shutdown
|
||||
export type ShutdownServerRes = null
|
||||
|
||||
export type SystemRebuildReq = {} // server.rebuild
|
||||
export type SystemRebuildRes = null
|
||||
export type DiskRepairReq = {} // server.disk.repair
|
||||
export type DiskRepairRes = null
|
||||
|
||||
export type ResetTorReq = {
|
||||
wipeState: boolean
|
||||
@@ -310,19 +339,18 @@ export module RR {
|
||||
export type FollowPackageLogsReq = FollowServerLogsReq & { id: string } // package.logs.follow
|
||||
export type FollowPackageLogsRes = FollowServerLogsRes
|
||||
|
||||
export type InstallPackageReq = {
|
||||
id: string
|
||||
versionSpec?: string
|
||||
versionPriority?: 'min' | 'max'
|
||||
marketplaceUrl: string
|
||||
} // package.install
|
||||
export type GetPackageMetricsReq = { id: string } // package.metrics
|
||||
// @TODO Matt create package metrics type
|
||||
export type GetPackageMetricsRes = any
|
||||
|
||||
export type InstallPackageReq = T.InstallParams
|
||||
export type InstallPackageRes = null
|
||||
|
||||
export type GetPackageConfigReq = { id: string } // package.config.get
|
||||
export type GetPackageConfigRes = { spec: CT.InputSpec; config: object }
|
||||
|
||||
export type DrySetPackageConfigReq = { id: string; config: object } // package.config.set.dry
|
||||
export type DrySetPackageConfigRes = Breakages
|
||||
export type DrySetPackageConfigRes = T.PackageId[]
|
||||
|
||||
export type SetPackageConfigReq = DrySetPackageConfigReq // package.config.set
|
||||
export type SetPackageConfigRes = null
|
||||
@@ -331,6 +359,7 @@ export module RR {
|
||||
// package.backup.restore
|
||||
ids: string[]
|
||||
targetId: string
|
||||
serverId: string
|
||||
password: string
|
||||
}
|
||||
export type RestorePackagesRes = null
|
||||
@@ -369,7 +398,10 @@ export module RR {
|
||||
icon: string // base64
|
||||
size: number // bytes
|
||||
}
|
||||
export type SideloadPacakgeRes = string //guid
|
||||
export type SideloadPackageRes = {
|
||||
upload: string
|
||||
progress: string
|
||||
}
|
||||
|
||||
export type SetInterfaceClearnetAddressReq = SetServerClearnetAddressReq & {
|
||||
packageId: string
|
||||
@@ -389,7 +421,8 @@ export module RR {
|
||||
export type GetMarketplaceInfoRes = StoreInfo
|
||||
|
||||
export type GetMarketplaceEosReq = { serverId: string }
|
||||
export type GetMarketplaceEosRes = MarketplaceEOS
|
||||
// @TODO Matt fix type
|
||||
export type GetMarketplaceEosRes = any
|
||||
|
||||
export type GetMarketplacePackagesReq = {
|
||||
ids?: { id: string; version: string }[]
|
||||
@@ -399,13 +432,17 @@ export module RR {
|
||||
page?: number
|
||||
perPage?: number
|
||||
}
|
||||
export type GetMarketplacePackagesRes = MarketplacePkg[]
|
||||
|
||||
export type GetReleaseNotesReq = { id: string }
|
||||
export type GetReleaseNotesRes = { [version: string]: string }
|
||||
// registry
|
||||
|
||||
/** these are returned in ASCENDING order. the newest available version will be the LAST in the object */
|
||||
export type GetRegistryOsUpdateRes = { [version: string]: T.OsVersionInfo }
|
||||
|
||||
export type CheckOSUpdateReq = { serverId: string }
|
||||
export type CheckOSUpdateRes = OSUpdate
|
||||
}
|
||||
|
||||
export interface MarketplaceEOS {
|
||||
export interface OSUpdate {
|
||||
version: string
|
||||
headline: string
|
||||
releaseNotes: { [version: string]: string }
|
||||
@@ -461,6 +498,7 @@ export interface Metrics {
|
||||
}
|
||||
|
||||
export interface Session {
|
||||
loggedIn: string
|
||||
lastActive: string
|
||||
userAgent: string
|
||||
metadata: SessionMetadata
|
||||
@@ -499,7 +537,7 @@ export interface UnknownDisk {
|
||||
label: string | null
|
||||
capacity: number
|
||||
used: number | null
|
||||
startOs: StartOSDiskInfo | null
|
||||
startOs: Record<string, StartOSDiskInfo>
|
||||
}
|
||||
|
||||
export interface BaseBackupTarget {
|
||||
@@ -508,7 +546,7 @@ export interface BaseBackupTarget {
|
||||
name: string
|
||||
mountable: boolean
|
||||
path: string
|
||||
startOs: StartOSDiskInfo | null
|
||||
startOs: Record<string, StartOSDiskInfo>
|
||||
}
|
||||
|
||||
export interface DiskBackupTarget extends UnknownDisk, BaseBackupTarget {
|
||||
|
||||
@@ -1,23 +1,52 @@
|
||||
import { Observable } from 'rxjs'
|
||||
import { Update } from 'patch-db-client'
|
||||
import { RR, BackupTargetType, Metrics } from './api.types'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { Log, SetupStatus } from '@start9labs/shared'
|
||||
import { WebSocketSubjectConfig } from 'rxjs/webSocket'
|
||||
import { SetupStatus } from '@start9labs/shared'
|
||||
import { RPCOptions } from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import {
|
||||
GetPackageRes,
|
||||
GetPackagesRes,
|
||||
MarketplacePkg,
|
||||
} from '@start9labs/marketplace'
|
||||
|
||||
export abstract class ApiService {
|
||||
// http
|
||||
|
||||
// for getting static files: ex icons, instructions, licenses
|
||||
abstract getStatic(url: string): Promise<string>
|
||||
|
||||
// for sideloading packages
|
||||
abstract uploadPackage(guid: string, body: Blob): Promise<void>
|
||||
abstract uploadPackage(guid: string, body: Blob): Promise<string>
|
||||
|
||||
abstract uploadFile(body: Blob): Promise<string>
|
||||
|
||||
// for getting static files: ex icons, instructions, licenses
|
||||
abstract getStaticProxy(
|
||||
pkg: MarketplacePkg,
|
||||
path: 'LICENSE.md' | 'instructions.md',
|
||||
): Promise<string>
|
||||
|
||||
abstract getStaticInstalled(
|
||||
id: T.PackageId,
|
||||
path: 'LICENSE.md' | 'instructions.md',
|
||||
): Promise<string>
|
||||
|
||||
// websocket
|
||||
|
||||
abstract openWebsocket$<T>(
|
||||
guid: string,
|
||||
config: RR.WebsocketConfig<T>,
|
||||
): Observable<T>
|
||||
|
||||
// state
|
||||
|
||||
abstract echo(params: RR.EchoReq, url: string): Promise<RR.EchoRes>
|
||||
|
||||
abstract getState(): Promise<RR.ServerState>
|
||||
|
||||
// db
|
||||
|
||||
abstract subscribeToPatchDB(
|
||||
params: RR.SubscribePatchReq,
|
||||
): Promise<RR.SubscribePatchRes>
|
||||
|
||||
abstract setDbValue<T>(
|
||||
pathArr: Array<string | number>,
|
||||
value: T,
|
||||
@@ -37,20 +66,26 @@ export abstract class ApiService {
|
||||
params: RR.ResetPasswordReq,
|
||||
): Promise<RR.ResetPasswordRes>
|
||||
|
||||
// diagnostic
|
||||
|
||||
abstract diagnosticGetError(): Promise<RR.DiagnosticErrorRes>
|
||||
abstract diagnosticRestart(): Promise<void>
|
||||
abstract diagnosticForgetDrive(): Promise<void>
|
||||
abstract diagnosticRepairDisk(): Promise<void>
|
||||
abstract diagnosticGetLogs(
|
||||
params: RR.GetServerLogsReq,
|
||||
): Promise<RR.GetServerLogsRes>
|
||||
|
||||
// init
|
||||
|
||||
abstract initGetProgress(): Promise<RR.InitGetProgressRes>
|
||||
|
||||
abstract initFollowLogs(
|
||||
params: RR.FollowServerLogsReq,
|
||||
): Promise<RR.FollowServerLogsRes>
|
||||
|
||||
// server
|
||||
|
||||
abstract echo(params: RR.EchoReq, urlOverride?: string): Promise<RR.EchoRes>
|
||||
|
||||
abstract openPatchWebsocket$(): Observable<Update<DataModel>>
|
||||
|
||||
abstract openLogsWebsocket$(
|
||||
config: WebSocketSubjectConfig<Log>,
|
||||
): Observable<Log>
|
||||
|
||||
abstract openMetricsWebsocket$(
|
||||
config: WebSocketSubjectConfig<Metrics>,
|
||||
): Observable<Metrics>
|
||||
|
||||
abstract getSystemTime(
|
||||
params: RR.GetSystemTimeReq,
|
||||
): Promise<RR.GetSystemTimeRes>
|
||||
@@ -95,11 +130,7 @@ export abstract class ApiService {
|
||||
params: RR.ShutdownServerReq,
|
||||
): Promise<RR.ShutdownServerRes>
|
||||
|
||||
abstract systemRebuild(
|
||||
params: RR.SystemRebuildReq,
|
||||
): Promise<RR.SystemRebuildRes>
|
||||
|
||||
abstract repairDisk(params: RR.SystemRebuildReq): Promise<RR.SystemRebuildRes>
|
||||
abstract repairDisk(params: RR.DiskRepairReq): Promise<RR.DiskRepairRes>
|
||||
|
||||
abstract resetTor(params: RR.ResetTorReq): Promise<RR.ResetTorRes>
|
||||
|
||||
@@ -109,13 +140,22 @@ export abstract class ApiService {
|
||||
|
||||
// marketplace URLs
|
||||
|
||||
abstract marketplaceProxy<T>(
|
||||
path: string,
|
||||
params: Record<string, unknown>,
|
||||
url: string,
|
||||
abstract registryRequest<T>(
|
||||
registryUrl: string,
|
||||
options: RPCOptions,
|
||||
): Promise<T>
|
||||
|
||||
abstract getEos(): Promise<RR.GetMarketplaceEosRes>
|
||||
abstract checkOSUpdate(qp: RR.CheckOSUpdateReq): Promise<RR.CheckOSUpdateRes>
|
||||
|
||||
abstract getRegistryInfo(registryUrl: string): Promise<T.RegistryInfo>
|
||||
|
||||
abstract getRegistryPackage(
|
||||
url: string,
|
||||
id: string,
|
||||
versionRange: string | null,
|
||||
): Promise<GetPackageRes>
|
||||
|
||||
abstract getRegistryPackages(registryUrl: string): Promise<GetPackagesRes>
|
||||
|
||||
// notification
|
||||
|
||||
@@ -308,11 +348,7 @@ export abstract class ApiService {
|
||||
params: RR.DryConfigureDependencyReq,
|
||||
): Promise<RR.DryConfigureDependencyRes>
|
||||
|
||||
abstract sideloadPackage(
|
||||
params: RR.SideloadPackageReq,
|
||||
): Promise<RR.SideloadPacakgeRes>
|
||||
|
||||
abstract getSetupStatus(): Promise<SetupStatus | null>
|
||||
abstract sideloadPackage(): Promise<RR.SideloadPackageRes>
|
||||
|
||||
abstract setInterfaceClearnetAddress(
|
||||
params: RR.SetInterfaceClearnetAddressReq,
|
||||
|
||||
@@ -3,22 +3,29 @@ import {
|
||||
HttpOptions,
|
||||
HttpService,
|
||||
isRpcError,
|
||||
Log,
|
||||
Method,
|
||||
RpcError,
|
||||
RPCOptions,
|
||||
SetupStatus,
|
||||
} from '@start9labs/shared'
|
||||
import { PATCH_CACHE } from 'src/app/services/patch-db/patch-db-source'
|
||||
import { ApiService } from './embassy-api.service'
|
||||
import { BackupTargetType, Metrics, RR } from './api.types'
|
||||
import { BackupTargetType, RR } from './api.types'
|
||||
import { ConfigService } from '../config.service'
|
||||
import { webSocket, WebSocketSubjectConfig } from 'rxjs/webSocket'
|
||||
import { webSocket } from 'rxjs/webSocket'
|
||||
import { Observable, filter, firstValueFrom } from 'rxjs'
|
||||
import { AuthService } from '../auth.service'
|
||||
import { DOCUMENT } from '@angular/common'
|
||||
import { DataModel } from '../patch-db/data-model'
|
||||
import { PatchDB, pathFromArray, Update } from 'patch-db-client'
|
||||
import { getServerInfo } from 'src/app/utils/get-server-info'
|
||||
import { Dump, pathFromArray } from 'patch-db-client'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import {
|
||||
GetPackageReq,
|
||||
GetPackageRes,
|
||||
GetPackagesReq,
|
||||
GetPackagesRes,
|
||||
MarketplacePkg,
|
||||
} from '@start9labs/marketplace'
|
||||
import { blake3 } from '@noble/hashes/blake3'
|
||||
|
||||
@Injectable()
|
||||
export class LiveApiService extends ApiService {
|
||||
@@ -27,7 +34,7 @@ export class LiveApiService extends ApiService {
|
||||
private readonly http: HttpService,
|
||||
private readonly config: ConfigService,
|
||||
private readonly auth: AuthService,
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
@Inject(PATCH_CACHE) private readonly cache$: Observable<Dump<DataModel>>,
|
||||
) {
|
||||
super()
|
||||
|
||||
@@ -35,17 +42,9 @@ export class LiveApiService extends ApiService {
|
||||
this.document.defaultView.rpcClient = this
|
||||
}
|
||||
|
||||
// for getting static files: ex icons, instructions, licenses
|
||||
async getStatic(url: string): Promise<string> {
|
||||
return this.httpRequest({
|
||||
method: Method.GET,
|
||||
url,
|
||||
responseType: 'text',
|
||||
})
|
||||
}
|
||||
|
||||
// for sideloading packages
|
||||
async uploadPackage(guid: string, body: Blob): Promise<void> {
|
||||
|
||||
async uploadPackage(guid: string, body: Blob): Promise<string> {
|
||||
return this.httpRequest({
|
||||
method: Method.POST,
|
||||
body,
|
||||
@@ -59,12 +58,73 @@ export class LiveApiService extends ApiService {
|
||||
method: Method.POST,
|
||||
body,
|
||||
url: `/rest/upload`,
|
||||
})
|
||||
}
|
||||
|
||||
// for getting static files: ex. instructions, licenses
|
||||
|
||||
async getStaticProxy(
|
||||
pkg: MarketplacePkg,
|
||||
path: 'LICENSE.md' | 'instructions.md',
|
||||
): Promise<string> {
|
||||
const encodedUrl = encodeURIComponent(pkg.s9pk.url)
|
||||
|
||||
return this.httpRequest({
|
||||
method: Method.GET,
|
||||
url: `/s9pk/proxy/${encodedUrl}/${path}`,
|
||||
params: {
|
||||
rootSighash: pkg.s9pk.commitment.rootSighash,
|
||||
rootMaxsize: pkg.s9pk.commitment.rootMaxsize,
|
||||
},
|
||||
responseType: 'text',
|
||||
})
|
||||
}
|
||||
|
||||
async getStaticInstalled(
|
||||
id: T.PackageId,
|
||||
path: 'LICENSE.md' | 'instructions.md',
|
||||
): Promise<string> {
|
||||
return this.httpRequest({
|
||||
method: Method.GET,
|
||||
url: `/s9pk/installed/${id}.s9pk/${path}`,
|
||||
responseType: 'text',
|
||||
})
|
||||
}
|
||||
|
||||
// websocket
|
||||
|
||||
openWebsocket$<T>(
|
||||
guid: string,
|
||||
config: RR.WebsocketConfig<T>,
|
||||
): Observable<T> {
|
||||
const { location } = this.document.defaultView!
|
||||
const protocol = location.protocol === 'http:' ? 'ws' : 'wss'
|
||||
const host = location.host
|
||||
|
||||
return webSocket({
|
||||
url: `${protocol}://${host}/ws/rpc/${guid}`,
|
||||
...config,
|
||||
})
|
||||
}
|
||||
|
||||
// state
|
||||
|
||||
async echo(params: RR.EchoReq, url: string): Promise<RR.EchoRes> {
|
||||
return this.rpcRequest({ method: 'echo', params }, url)
|
||||
}
|
||||
|
||||
async getState(): Promise<RR.ServerState> {
|
||||
return this.rpcRequest({ method: 'state', params: {} })
|
||||
}
|
||||
|
||||
// db
|
||||
|
||||
async subscribeToPatchDB(
|
||||
params: RR.SubscribePatchReq,
|
||||
): Promise<RR.SubscribePatchRes> {
|
||||
return this.rpcRequest({ method: 'db.subscribe', params })
|
||||
}
|
||||
|
||||
async setDbValue<T>(
|
||||
pathArr: Array<string | number>,
|
||||
value: T,
|
||||
@@ -98,35 +158,59 @@ export class LiveApiService extends ApiService {
|
||||
return this.rpcRequest({ method: 'auth.reset-password', params })
|
||||
}
|
||||
|
||||
// diagnostic
|
||||
|
||||
async diagnosticGetError(): Promise<RR.DiagnosticErrorRes> {
|
||||
return this.rpcRequest<RR.DiagnosticErrorRes>({
|
||||
method: 'diagnostic.error',
|
||||
params: {},
|
||||
})
|
||||
}
|
||||
|
||||
async diagnosticRestart(): Promise<void> {
|
||||
return this.rpcRequest<void>({
|
||||
method: 'diagnostic.restart',
|
||||
params: {},
|
||||
})
|
||||
}
|
||||
|
||||
async diagnosticForgetDrive(): Promise<void> {
|
||||
return this.rpcRequest<void>({
|
||||
method: 'diagnostic.disk.forget',
|
||||
params: {},
|
||||
})
|
||||
}
|
||||
|
||||
async diagnosticRepairDisk(): Promise<void> {
|
||||
return this.rpcRequest<void>({
|
||||
method: 'diagnostic.disk.repair',
|
||||
params: {},
|
||||
})
|
||||
}
|
||||
|
||||
async diagnosticGetLogs(
|
||||
params: RR.GetServerLogsReq,
|
||||
): Promise<RR.GetServerLogsRes> {
|
||||
return this.rpcRequest<RR.GetServerLogsRes>({
|
||||
method: 'diagnostic.logs',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
// init
|
||||
|
||||
async initGetProgress(): Promise<RR.InitGetProgressRes> {
|
||||
return this.rpcRequest({ method: 'init.subscribe', params: {} })
|
||||
}
|
||||
|
||||
async initFollowLogs(
|
||||
params: RR.FollowServerLogsReq,
|
||||
): Promise<RR.FollowServerLogsRes> {
|
||||
return this.rpcRequest({ method: 'init.logs.follow', params })
|
||||
}
|
||||
|
||||
// server
|
||||
|
||||
async echo(params: RR.EchoReq, urlOverride?: string): Promise<RR.EchoRes> {
|
||||
return this.rpcRequest({ method: 'echo', params }, urlOverride)
|
||||
}
|
||||
|
||||
openPatchWebsocket$(): Observable<Update<DataModel>> {
|
||||
const config: WebSocketSubjectConfig<Update<DataModel>> = {
|
||||
url: `/db`,
|
||||
closeObserver: {
|
||||
next: val => {
|
||||
if (val.reason === 'UNAUTHORIZED') this.auth.setUnverified()
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return this.openWebsocket(config)
|
||||
}
|
||||
|
||||
openLogsWebsocket$(config: WebSocketSubjectConfig<Log>): Observable<Log> {
|
||||
return this.openWebsocket(config)
|
||||
}
|
||||
|
||||
openMetricsWebsocket$(
|
||||
config: WebSocketSubjectConfig<Metrics>,
|
||||
): Observable<Metrics> {
|
||||
return this.openWebsocket(config)
|
||||
}
|
||||
|
||||
async getSystemTime(
|
||||
params: RR.GetSystemTimeReq,
|
||||
): Promise<RR.GetSystemTimeRes> {
|
||||
@@ -175,7 +259,7 @@ export class LiveApiService extends ApiService {
|
||||
|
||||
async updateServer(url?: string): Promise<RR.UpdateServerRes> {
|
||||
const params = {
|
||||
marketplaceUrl: url || this.config.marketplace.start9,
|
||||
registry: url || this.config.marketplace.start9,
|
||||
}
|
||||
return this.rpcRequest({ method: 'server.update', params })
|
||||
}
|
||||
@@ -198,12 +282,6 @@ export class LiveApiService extends ApiService {
|
||||
return this.rpcRequest({ method: 'server.shutdown', params })
|
||||
}
|
||||
|
||||
async systemRebuild(
|
||||
params: RR.RestartServerReq,
|
||||
): Promise<RR.RestartServerRes> {
|
||||
return this.rpcRequest({ method: 'server.rebuild', params })
|
||||
}
|
||||
|
||||
async repairDisk(params: RR.RestartServerReq): Promise<RR.RestartServerRes> {
|
||||
return this.rpcRequest({ method: 'disk.repair', params })
|
||||
}
|
||||
@@ -220,27 +298,61 @@ export class LiveApiService extends ApiService {
|
||||
|
||||
// marketplace URLs
|
||||
|
||||
async marketplaceProxy<T>(
|
||||
path: string,
|
||||
qp: Record<string, string>,
|
||||
baseUrl: string,
|
||||
async registryRequest<T>(
|
||||
registryUrl: string,
|
||||
options: RPCOptions,
|
||||
): Promise<T> {
|
||||
const fullUrl = `${baseUrl}${path}?${new URLSearchParams(qp).toString()}`
|
||||
return this.rpcRequest({
|
||||
method: 'marketplace.get',
|
||||
params: { url: fullUrl },
|
||||
...options,
|
||||
method: `registry.${options.method}`,
|
||||
params: { registry: registryUrl, ...options.params },
|
||||
})
|
||||
}
|
||||
|
||||
async getEos(): Promise<RR.GetMarketplaceEosRes> {
|
||||
const { id } = await getServerInfo(this.patch)
|
||||
const qp: RR.GetMarketplaceEosReq = { serverId: id }
|
||||
async checkOSUpdate(qp: RR.CheckOSUpdateReq): Promise<RR.CheckOSUpdateRes> {
|
||||
const { serverId } = qp
|
||||
|
||||
return this.marketplaceProxy(
|
||||
'/eos/v0/latest',
|
||||
qp,
|
||||
this.config.marketplace.start9,
|
||||
)
|
||||
return this.registryRequest(this.config.marketplace.start9, {
|
||||
method: 'os.version.get',
|
||||
params: { serverId },
|
||||
})
|
||||
}
|
||||
|
||||
async getRegistryInfo(registryUrl: string): Promise<T.RegistryInfo> {
|
||||
return this.registryRequest(registryUrl, {
|
||||
method: 'info',
|
||||
params: {},
|
||||
})
|
||||
}
|
||||
|
||||
async getRegistryPackage(
|
||||
registryUrl: string,
|
||||
id: string,
|
||||
versionRange: string | null,
|
||||
): Promise<GetPackageRes> {
|
||||
const params: GetPackageReq = {
|
||||
id,
|
||||
version: versionRange,
|
||||
otherVersions: 'short',
|
||||
}
|
||||
|
||||
return this.registryRequest<GetPackageRes>(registryUrl, {
|
||||
method: 'package.get',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
async getRegistryPackages(registryUrl: string): Promise<GetPackagesRes> {
|
||||
const params: GetPackagesReq = {
|
||||
id: null,
|
||||
version: null,
|
||||
otherVersions: 'short',
|
||||
}
|
||||
|
||||
return this.registryRequest<GetPackagesRes>(registryUrl, {
|
||||
method: 'package.get',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
// notification
|
||||
@@ -463,8 +575,8 @@ export class LiveApiService extends ApiService {
|
||||
}
|
||||
|
||||
async followPackageLogs(
|
||||
params: RR.FollowServerLogsReq,
|
||||
): Promise<RR.FollowServerLogsRes> {
|
||||
params: RR.FollowPackageLogsReq,
|
||||
): Promise<RR.FollowPackageLogsRes> {
|
||||
return this.rpcRequest({ method: 'package.logs.follow', params })
|
||||
}
|
||||
|
||||
@@ -533,12 +645,10 @@ export class LiveApiService extends ApiService {
|
||||
})
|
||||
}
|
||||
|
||||
async sideloadPackage(
|
||||
params: RR.SideloadPackageReq,
|
||||
): Promise<RR.SideloadPacakgeRes> {
|
||||
async sideloadPackage(): Promise<RR.SideloadPackageRes> {
|
||||
return this.rpcRequest({
|
||||
method: 'package.sideload',
|
||||
params,
|
||||
params: {},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -554,23 +664,6 @@ export class LiveApiService extends ApiService {
|
||||
return this.rpcRequest({ method: 'package.proxy.set-outbound', params })
|
||||
}
|
||||
|
||||
async getSetupStatus() {
|
||||
return this.rpcRequest<SetupStatus | null>({
|
||||
method: 'setup.status',
|
||||
params: {},
|
||||
})
|
||||
}
|
||||
|
||||
private openWebsocket<T>(config: WebSocketSubjectConfig<T>): Observable<T> {
|
||||
const { location } = this.document.defaultView!
|
||||
const protocol = location.protocol === 'http:' ? 'ws' : 'wss'
|
||||
const host = location.host
|
||||
|
||||
config.url = `${protocol}://${host}/ws${config.url}`
|
||||
|
||||
return webSocket(config)
|
||||
}
|
||||
|
||||
private async rpcRequest<T>(
|
||||
options: RPCOptions,
|
||||
urlOverride?: string,
|
||||
@@ -589,9 +682,7 @@ export class LiveApiService extends ApiService {
|
||||
const patchSequence = res.headers.get('x-patch-sequence')
|
||||
if (patchSequence)
|
||||
await firstValueFrom(
|
||||
this.patch.cache$.pipe(
|
||||
filter(({ sequence }) => sequence >= Number(patchSequence)),
|
||||
),
|
||||
this.cache$.pipe(filter(({ id }) => id >= Number(patchSequence))),
|
||||
)
|
||||
|
||||
return body.result
|
||||
@@ -599,6 +690,29 @@ export class LiveApiService extends ApiService {
|
||||
|
||||
private async httpRequest<T>(opts: HttpOptions): Promise<T> {
|
||||
const res = await this.http.httpRequest<T>(opts)
|
||||
if (res.headers.get('Repr-Digest')) {
|
||||
// verify
|
||||
const digest = res.headers.get('Repr-Digest')!
|
||||
let data: Uint8Array
|
||||
if (opts.responseType === 'arrayBuffer') {
|
||||
data = Buffer.from(res.body as ArrayBuffer)
|
||||
} else if (opts.responseType === 'text') {
|
||||
data = Buffer.from(res.body as string)
|
||||
} else if ((opts.responseType as string) === 'blob') {
|
||||
data = Buffer.from(await (res.body as Blob).arrayBuffer())
|
||||
} else {
|
||||
console.warn(
|
||||
`could not verify Repr-Digest for responseType ${
|
||||
opts.responseType || 'json'
|
||||
}`,
|
||||
)
|
||||
return res.body
|
||||
}
|
||||
const computedDigest = Buffer.from(blake3(data)).toString('base64')
|
||||
if (`blake3=:${computedDigest}:` === digest) return res.body
|
||||
console.debug(computedDigest, digest)
|
||||
throw new Error('File digest mismatch.')
|
||||
}
|
||||
return res.body
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { pauseFor, Log, getSetupStatusMock } from '@start9labs/shared'
|
||||
import {
|
||||
pauseFor,
|
||||
Log,
|
||||
getSetupStatusMock,
|
||||
RPCErrorDetails,
|
||||
RPCOptions,
|
||||
} from '@start9labs/shared'
|
||||
import { ApiService } from './embassy-api.service'
|
||||
import {
|
||||
Operation,
|
||||
PatchOp,
|
||||
pathFromArray,
|
||||
RemoveOperation,
|
||||
Update,
|
||||
Revision,
|
||||
} from 'patch-db-client'
|
||||
import {
|
||||
DataModel,
|
||||
InstallingState,
|
||||
PackageDataEntry,
|
||||
Proxy,
|
||||
@@ -19,24 +24,25 @@ import {
|
||||
import { BackupTargetType, Metrics, RR } from './api.types'
|
||||
import { Mock } from './api.fixures'
|
||||
import {
|
||||
EMPTY,
|
||||
iif,
|
||||
from,
|
||||
interval,
|
||||
map,
|
||||
Observable,
|
||||
shareReplay,
|
||||
startWith,
|
||||
Subject,
|
||||
switchMap,
|
||||
tap,
|
||||
timer,
|
||||
} from 'rxjs'
|
||||
import { LocalStorageBootstrap } from '../patch-db/local-storage-bootstrap'
|
||||
import { mockPatchData } from './mock-patch'
|
||||
import { WebSocketSubjectConfig } from 'rxjs/webSocket'
|
||||
import { AuthService } from '../auth.service'
|
||||
import { ConnectionService } from '../connection.service'
|
||||
import { StoreInfo } from '@start9labs/marketplace'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import {
|
||||
GetPackageRes,
|
||||
GetPackagesRes,
|
||||
MarketplacePkg,
|
||||
} from '@start9labs/marketplace'
|
||||
import { WebSocketSubjectConfig } from 'rxjs/webSocket'
|
||||
import markdown from 'raw-loader!../../../../../shared/assets/markdown/md-sample.md'
|
||||
|
||||
const PROGRESS: T.FullProgress = {
|
||||
overall: {
|
||||
@@ -53,10 +59,7 @@ const PROGRESS: T.FullProgress = {
|
||||
},
|
||||
{
|
||||
name: 'Validating',
|
||||
progress: {
|
||||
done: 0,
|
||||
total: 40,
|
||||
},
|
||||
progress: null,
|
||||
},
|
||||
{
|
||||
name: 'Installing',
|
||||
@@ -70,44 +73,25 @@ const PROGRESS: T.FullProgress = {
|
||||
|
||||
@Injectable()
|
||||
export class MockApiService extends ApiService {
|
||||
readonly mockWsSource$ = new Subject<Update<DataModel>>()
|
||||
readonly mockWsSource$ = new Subject<Revision>()
|
||||
private readonly revertTime = 1800
|
||||
sequence = 0
|
||||
|
||||
constructor(
|
||||
private readonly bootstrapper: LocalStorageBootstrap,
|
||||
private readonly connectionService: ConnectionService,
|
||||
private readonly auth: AuthService,
|
||||
) {
|
||||
constructor(private readonly auth: AuthService) {
|
||||
super()
|
||||
this.auth.isVerified$
|
||||
.pipe(
|
||||
tap(() => {
|
||||
this.sequence = 0
|
||||
}),
|
||||
switchMap(verified =>
|
||||
iif(
|
||||
() => verified,
|
||||
timer(2000).pipe(
|
||||
tap(() => {
|
||||
this.connectionService.websocketConnected$.next(true)
|
||||
}),
|
||||
),
|
||||
EMPTY,
|
||||
),
|
||||
),
|
||||
)
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
async getStatic(url: string): Promise<string> {
|
||||
await pauseFor(2000)
|
||||
return `* Test markdown instructions
|
||||
* Test markdown instructions with [link](https://start9.com)`
|
||||
}
|
||||
|
||||
async uploadPackage(guid: string, body: Blob): Promise<void> {
|
||||
async uploadPackage(guid: string, body: Blob): Promise<string> {
|
||||
await pauseFor(2000)
|
||||
// @TODO Matt what should this return?
|
||||
return ''
|
||||
}
|
||||
|
||||
async uploadFile(body: Blob): Promise<string> {
|
||||
@@ -115,8 +99,83 @@ export class MockApiService extends ApiService {
|
||||
return 'returnedhash'
|
||||
}
|
||||
|
||||
async getStaticProxy(
|
||||
pkg: MarketplacePkg,
|
||||
path: 'LICENSE.md' | 'instructions.md',
|
||||
): Promise<string> {
|
||||
await pauseFor(2000)
|
||||
return markdown
|
||||
}
|
||||
|
||||
async getStaticInstalled(
|
||||
id: T.PackageId,
|
||||
path: 'LICENSE.md' | 'instructions.md',
|
||||
): Promise<string> {
|
||||
await pauseFor(2000)
|
||||
return markdown
|
||||
}
|
||||
|
||||
// websocket
|
||||
|
||||
openWebsocket$<T>(
|
||||
guid: string,
|
||||
config: RR.WebsocketConfig<T>,
|
||||
): Observable<T> {
|
||||
if (guid === 'db-guid') {
|
||||
return this.mockWsSource$.pipe<any>(
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
)
|
||||
} else if (guid === 'logs-guid') {
|
||||
return interval(50).pipe<any>(
|
||||
map((_, index) => {
|
||||
// mock fire open observer
|
||||
if (index === 0) config.openObserver?.next(new Event(''))
|
||||
if (index === 100) throw new Error('HAAHHA')
|
||||
return Mock.ServerLogs[0]
|
||||
}),
|
||||
)
|
||||
} else if (guid === 'init-progress-guid') {
|
||||
return from(this.initProgress()).pipe(
|
||||
startWith(PROGRESS),
|
||||
) as Observable<T>
|
||||
} else {
|
||||
throw new Error('invalid guid type')
|
||||
}
|
||||
}
|
||||
|
||||
// state
|
||||
|
||||
async echo(params: RR.EchoReq, url: string): Promise<RR.EchoRes> {
|
||||
if (url) {
|
||||
const num = Math.floor(Math.random() * 10) + 1
|
||||
if (num > 8) return params.message
|
||||
throw new Error()
|
||||
}
|
||||
await pauseFor(2000)
|
||||
return params.message
|
||||
}
|
||||
|
||||
private stateIndex = 0
|
||||
async getState(): Promise<RR.ServerState> {
|
||||
await pauseFor(1000)
|
||||
|
||||
this.stateIndex++
|
||||
|
||||
return this.stateIndex === 1 ? 'running' : 'running'
|
||||
}
|
||||
|
||||
// db
|
||||
|
||||
async subscribeToPatchDB(
|
||||
params: RR.SubscribePatchReq,
|
||||
): Promise<RR.SubscribePatchRes> {
|
||||
await pauseFor(2000)
|
||||
return {
|
||||
dump: { id: 1, value: mockPatchData },
|
||||
guid: 'db-guid',
|
||||
}
|
||||
}
|
||||
|
||||
async setDbValue<T>(
|
||||
pathArr: Array<string | number>,
|
||||
value: T,
|
||||
@@ -140,11 +199,6 @@ export class MockApiService extends ApiService {
|
||||
|
||||
async login(params: RR.LoginReq): Promise<RR.loginRes> {
|
||||
await pauseFor(2000)
|
||||
|
||||
setTimeout(() => {
|
||||
this.mockWsSource$.next({ id: 1, value: mockPatchData })
|
||||
}, 2000)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -170,34 +224,63 @@ export class MockApiService extends ApiService {
|
||||
return null
|
||||
}
|
||||
|
||||
// server
|
||||
// diagnostic
|
||||
|
||||
async echo(params: RR.EchoReq, url?: string): Promise<RR.EchoRes> {
|
||||
if (url) {
|
||||
const num = Math.floor(Math.random() * 10) + 1
|
||||
if (num > 8) return params.message
|
||||
throw new Error()
|
||||
async getError(): Promise<RPCErrorDetails> {
|
||||
await pauseFor(1000)
|
||||
return {
|
||||
code: 15,
|
||||
message: 'Unknown server',
|
||||
data: { details: 'Some details about the error here' },
|
||||
}
|
||||
}
|
||||
|
||||
async diagnosticGetError(): Promise<RR.DiagnosticErrorRes> {
|
||||
await pauseFor(1000)
|
||||
return {
|
||||
code: 15,
|
||||
message: 'Unknown server',
|
||||
data: { details: 'Some details about the error here' },
|
||||
}
|
||||
}
|
||||
|
||||
async diagnosticRestart(): Promise<void> {
|
||||
await pauseFor(1000)
|
||||
}
|
||||
|
||||
async diagnosticForgetDrive(): Promise<void> {
|
||||
await pauseFor(1000)
|
||||
}
|
||||
|
||||
async diagnosticRepairDisk(): Promise<void> {
|
||||
await pauseFor(1000)
|
||||
}
|
||||
|
||||
async diagnosticGetLogs(
|
||||
params: RR.GetServerLogsReq,
|
||||
): Promise<RR.GetServerLogsRes> {
|
||||
return this.getServerLogs(params)
|
||||
}
|
||||
|
||||
// init
|
||||
|
||||
async initGetProgress(): Promise<RR.InitGetProgressRes> {
|
||||
await pauseFor(250)
|
||||
return {
|
||||
progress: PROGRESS,
|
||||
guid: 'init-progress-guid',
|
||||
}
|
||||
}
|
||||
|
||||
async initFollowLogs(): Promise<RR.FollowServerLogsRes> {
|
||||
await pauseFor(2000)
|
||||
return params.message
|
||||
return {
|
||||
startCursor: 'start-cursor',
|
||||
guid: 'logs-guid',
|
||||
}
|
||||
}
|
||||
|
||||
openPatchWebsocket$(): Observable<Update<DataModel>> {
|
||||
return this.mockWsSource$.pipe(
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
)
|
||||
}
|
||||
|
||||
openLogsWebsocket$(config: WebSocketSubjectConfig<Log>): Observable<Log> {
|
||||
return interval(50).pipe(
|
||||
map((_, index) => {
|
||||
// mock fire open observer
|
||||
if (index === 0) config.openObserver?.next(new Event(''))
|
||||
if (index === 100) throw new Error('HAAHHA')
|
||||
return Mock.ServerLogs[0]
|
||||
}),
|
||||
)
|
||||
}
|
||||
// server
|
||||
|
||||
openMetricsWebsocket$(
|
||||
config: WebSocketSubjectConfig<Metrics>,
|
||||
@@ -265,7 +348,7 @@ export class MockApiService extends ApiService {
|
||||
await pauseFor(2000)
|
||||
return {
|
||||
startCursor: 'start-cursor',
|
||||
guid: '7251d5be-645f-4362-a51b-3a85be92b31e',
|
||||
guid: 'logs-guid',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -275,7 +358,7 @@ export class MockApiService extends ApiService {
|
||||
await pauseFor(2000)
|
||||
return {
|
||||
startCursor: 'start-cursor',
|
||||
guid: '7251d5be-645f-4362-a51b-3a85be92b31e',
|
||||
guid: 'logs-guid',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -285,11 +368,11 @@ export class MockApiService extends ApiService {
|
||||
await pauseFor(2000)
|
||||
return {
|
||||
startCursor: 'start-cursor',
|
||||
guid: '7251d5be-645f-4362-a51b-3a85be92b31e',
|
||||
guid: 'logs-guid',
|
||||
}
|
||||
}
|
||||
|
||||
randomLogs(limit = 1): Log[] {
|
||||
private randomLogs(limit = 1): Log[] {
|
||||
const arrLength = Math.ceil(limit / Mock.ServerLogs.length)
|
||||
const logs = new Array(arrLength)
|
||||
.fill(Mock.ServerLogs)
|
||||
@@ -404,12 +487,6 @@ export class MockApiService extends ApiService {
|
||||
return null
|
||||
}
|
||||
|
||||
async systemRebuild(
|
||||
params: RR.SystemRebuildReq,
|
||||
): Promise<RR.SystemRebuildRes> {
|
||||
return this.restartServer(params)
|
||||
}
|
||||
|
||||
async repairDisk(params: RR.RestartServerReq): Promise<RR.RestartServerRes> {
|
||||
await pauseFor(2000)
|
||||
return null
|
||||
@@ -439,45 +516,41 @@ export class MockApiService extends ApiService {
|
||||
|
||||
// marketplace URLs
|
||||
|
||||
async marketplaceProxy(
|
||||
path: string,
|
||||
params: Record<string, string>,
|
||||
url: string,
|
||||
async registryRequest(
|
||||
registryUrl: string,
|
||||
options: RPCOptions,
|
||||
): Promise<any> {
|
||||
await pauseFor(2000)
|
||||
|
||||
if (path === '/package/v0/info') {
|
||||
const info: StoreInfo = {
|
||||
name: 'Start9 Registry',
|
||||
categories: [
|
||||
'bitcoin',
|
||||
'lightning',
|
||||
'data',
|
||||
'featured',
|
||||
'messaging',
|
||||
'social',
|
||||
'alt coin',
|
||||
'ai',
|
||||
],
|
||||
}
|
||||
return info
|
||||
} else if (path === '/package/v0/index') {
|
||||
const version = params['ids']
|
||||
? (JSON.parse(params['ids']) as { version: string; id: string }[])[0]
|
||||
.version
|
||||
: undefined
|
||||
return Mock.marketplacePkgsList(version)
|
||||
} else if (path.startsWith('/package/v0/release-notes')) {
|
||||
return Mock.ReleaseNotes
|
||||
} else if (path.includes('instructions') || path.includes('license')) {
|
||||
return `* Test markdown instructions
|
||||
* Test markdown instructions with [link](https://start9.com)`
|
||||
return Error('do not call directly')
|
||||
}
|
||||
|
||||
async checkOSUpdate(qp: RR.CheckOSUpdateReq): Promise<RR.CheckOSUpdateRes> {
|
||||
await pauseFor(2000)
|
||||
return Mock.MarketplaceEos
|
||||
}
|
||||
|
||||
async getRegistryInfo(registryUrl: string): Promise<T.RegistryInfo> {
|
||||
await pauseFor(2000)
|
||||
return Mock.RegistryInfo
|
||||
}
|
||||
|
||||
async getRegistryPackage(
|
||||
url: string,
|
||||
id: string,
|
||||
versionRange: string,
|
||||
): Promise<GetPackageRes> {
|
||||
await pauseFor(2000)
|
||||
if (!versionRange) {
|
||||
return Mock.RegistryPackages[id]
|
||||
} else {
|
||||
return Mock.OtherPackageVersions[id][versionRange]
|
||||
}
|
||||
}
|
||||
|
||||
async getEos(): Promise<RR.GetMarketplaceEosRes> {
|
||||
async getRegistryPackages(registryUrl: string): Promise<GetPackagesRes> {
|
||||
await pauseFor(2000)
|
||||
return Mock.MarketplaceEos
|
||||
return Mock.RegistryPackages
|
||||
}
|
||||
|
||||
// notification
|
||||
@@ -776,7 +849,7 @@ export class MockApiService extends ApiService {
|
||||
path: path.replace(/\\/g, '/'),
|
||||
username: 'mockusername',
|
||||
mountable: true,
|
||||
startOs: null,
|
||||
startOs: {},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -936,13 +1009,13 @@ export class MockApiService extends ApiService {
|
||||
await pauseFor(2000)
|
||||
let entries
|
||||
if (Math.random() < 0.2) {
|
||||
entries = Mock.PackageLogs
|
||||
entries = Mock.ServerLogs
|
||||
} else {
|
||||
const arrLength = params.limit
|
||||
? Math.ceil(params.limit / Mock.PackageLogs.length)
|
||||
? Math.ceil(params.limit / Mock.ServerLogs.length)
|
||||
: 10
|
||||
entries = new Array(arrLength)
|
||||
.fill(Mock.PackageLogs)
|
||||
.fill(Mock.ServerLogs)
|
||||
.reduce((acc, val) => acc.concat(val), [])
|
||||
}
|
||||
return {
|
||||
@@ -958,7 +1031,7 @@ export class MockApiService extends ApiService {
|
||||
await pauseFor(2000)
|
||||
return {
|
||||
startCursor: 'start-cursor',
|
||||
guid: '7251d5be-645f-4362-a51b-3a85be92b31e',
|
||||
guid: 'logs-guid',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -968,7 +1041,7 @@ export class MockApiService extends ApiService {
|
||||
await pauseFor(2000)
|
||||
|
||||
setTimeout(async () => {
|
||||
this.updateProgress(params.id)
|
||||
this.installProgress(params.id)
|
||||
}, 1000)
|
||||
|
||||
const patch: Operation<
|
||||
@@ -981,11 +1054,11 @@ export class MockApiService extends ApiService {
|
||||
...Mock.LocalPkgs[params.id],
|
||||
stateInfo: {
|
||||
// if installing
|
||||
// state: 'installing',
|
||||
state: 'installing',
|
||||
|
||||
// if updating
|
||||
state: 'updating',
|
||||
manifest: mockPatchData.packageData[params.id].stateInfo.manifest!,
|
||||
// state: 'updating',
|
||||
// manifest: mockPatchData.packageData[params.id].stateInfo.manifest!,
|
||||
|
||||
// both
|
||||
installingInfo: {
|
||||
@@ -1015,7 +1088,7 @@ export class MockApiService extends ApiService {
|
||||
params: RR.DrySetPackageConfigReq,
|
||||
): Promise<RR.DrySetPackageConfigRes> {
|
||||
await pauseFor(2000)
|
||||
return {}
|
||||
return []
|
||||
}
|
||||
|
||||
async setPackageConfig(
|
||||
@@ -1040,7 +1113,7 @@ export class MockApiService extends ApiService {
|
||||
await pauseFor(2000)
|
||||
const patch: Operation<PackageDataEntry>[] = params.ids.map(id => {
|
||||
setTimeout(async () => {
|
||||
this.updateProgress(id)
|
||||
this.installProgress(id)
|
||||
}, 2000)
|
||||
|
||||
return {
|
||||
@@ -1267,15 +1340,12 @@ export class MockApiService extends ApiService {
|
||||
}
|
||||
}
|
||||
|
||||
async sideloadPackage(
|
||||
params: RR.SideloadPackageReq,
|
||||
): Promise<RR.SideloadPacakgeRes> {
|
||||
async sideloadPackage(): Promise<RR.SideloadPackageRes> {
|
||||
await pauseFor(2000)
|
||||
return '4120e092-05ab-4de2-9fbd-c3f1f4b1df9e' // no significance, randomly generated
|
||||
}
|
||||
|
||||
async getSetupStatus() {
|
||||
return getSetupStatusMock()
|
||||
return {
|
||||
upload: '4120e092-05ab-4de2-9fbd-c3f1f4b1df9e', // no significance, randomly generated
|
||||
progress: '5120e092-05ab-4de2-9fbd-c3f1f4b1df9e', // no significance, randomly generated
|
||||
}
|
||||
}
|
||||
|
||||
async setInterfaceClearnetAddress(
|
||||
@@ -1310,11 +1380,61 @@ export class MockApiService extends ApiService {
|
||||
return null
|
||||
}
|
||||
|
||||
private async updateProgress(id: string): Promise<void> {
|
||||
private async initProgress(): Promise<T.FullProgress> {
|
||||
const progress = JSON.parse(JSON.stringify(PROGRESS))
|
||||
|
||||
for (let [i, phase] of progress.phases.entries()) {
|
||||
if (typeof phase.progress !== 'object' || !phase.progress.total) {
|
||||
if (
|
||||
!phase.progress ||
|
||||
typeof phase.progress !== 'object' ||
|
||||
!phase.progress.total
|
||||
) {
|
||||
await pauseFor(2000)
|
||||
|
||||
progress.phases[i].progress = true
|
||||
|
||||
if (
|
||||
progress.overall &&
|
||||
typeof progress.overall === 'object' &&
|
||||
progress.overall.total
|
||||
) {
|
||||
const step = progress.overall.total / progress.phases.length
|
||||
progress.overall.done += step
|
||||
}
|
||||
} else {
|
||||
const step = phase.progress.total / 4
|
||||
|
||||
while (phase.progress.done < phase.progress.total) {
|
||||
await pauseFor(200)
|
||||
|
||||
phase.progress.done += step
|
||||
|
||||
if (
|
||||
progress.overall &&
|
||||
typeof progress.overall === 'object' &&
|
||||
progress.overall.total
|
||||
) {
|
||||
const step = progress.overall.total / progress.phases.length / 4
|
||||
|
||||
progress.overall.done += step
|
||||
}
|
||||
|
||||
if (phase.progress.done === phase.progress.total) {
|
||||
await pauseFor(250)
|
||||
|
||||
progress.phases[i].progress = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return progress
|
||||
}
|
||||
|
||||
private async installProgress(id: string): Promise<void> {
|
||||
const progress = JSON.parse(JSON.stringify(PROGRESS))
|
||||
|
||||
for (let [i, phase] of progress.phases.entries()) {
|
||||
if (!phase.progress || phase.progress === true || !phase.progress.total) {
|
||||
await pauseFor(2000)
|
||||
|
||||
const patches: Operation<any>[] = [
|
||||
@@ -1326,7 +1446,11 @@ export class MockApiService extends ApiService {
|
||||
]
|
||||
|
||||
// overall
|
||||
if (typeof progress.overall === 'object' && progress.overall.total) {
|
||||
if (
|
||||
progress.overall &&
|
||||
typeof progress.overall === 'object' &&
|
||||
progress.overall.total
|
||||
) {
|
||||
const step = progress.overall.total / progress.phases.length
|
||||
|
||||
progress.overall.done += step
|
||||
@@ -1356,7 +1480,11 @@ export class MockApiService extends ApiService {
|
||||
]
|
||||
|
||||
// overall
|
||||
if (typeof progress.overall === 'object' && progress.overall.total) {
|
||||
if (
|
||||
progress.overall &&
|
||||
typeof progress.overall === 'object' &&
|
||||
progress.overall.total
|
||||
) {
|
||||
const step = progress.overall.total / progress.phases.length / 4
|
||||
|
||||
progress.overall.done += step
|
||||
@@ -1469,10 +1597,6 @@ export class MockApiService extends ApiService {
|
||||
}
|
||||
|
||||
private async mockRevision<T>(patch: Operation<T>[]): Promise<void> {
|
||||
if (!this.sequence) {
|
||||
const { sequence } = this.bootstrapper.init()
|
||||
this.sequence = sequence
|
||||
}
|
||||
const revision = {
|
||||
id: ++this.sequence,
|
||||
patch,
|
||||
|
||||
@@ -129,11 +129,13 @@ export const mockPatchData: DataModel = {
|
||||
login: '',
|
||||
password: '',
|
||||
},
|
||||
platform: 'x86_64-nonfree',
|
||||
// @TODO Matt zram needs to be added?
|
||||
// zram: true,
|
||||
governor: 'performance',
|
||||
passwordHash:
|
||||
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
|
||||
platform: 'x86_64-nonfree',
|
||||
arch: 'x86_64',
|
||||
governor: 'performance',
|
||||
},
|
||||
packageData: {
|
||||
bitcoind: {
|
||||
@@ -141,7 +143,7 @@ export const mockPatchData: DataModel = {
|
||||
state: 'installed',
|
||||
manifest: {
|
||||
...Mock.MockManifestBitcoind,
|
||||
version: '0.20.0',
|
||||
version: '0.20.0:0',
|
||||
},
|
||||
},
|
||||
icon: '/assets/img/service-icons/bitcoind.svg',
|
||||
@@ -181,9 +183,8 @@ export const mockPatchData: DataModel = {
|
||||
},
|
||||
},
|
||||
},
|
||||
dependencyConfigErrors: {},
|
||||
},
|
||||
actions: {}, // @TODO
|
||||
actions: {},
|
||||
serviceInterfaces: {
|
||||
ui: {
|
||||
id: 'ui',
|
||||
@@ -197,66 +198,11 @@ export const mockPatchData: DataModel = {
|
||||
addressInfo: {
|
||||
username: null,
|
||||
hostId: 'abcdefg',
|
||||
bindOptions: {
|
||||
scheme: 'http',
|
||||
preferredExternalPort: 80,
|
||||
addSsl: {
|
||||
// addXForwardedHeaders: false,
|
||||
preferredExternalPort: 443,
|
||||
scheme: 'https',
|
||||
alpn: { specified: ['http/1.1', 'h2'] },
|
||||
},
|
||||
secure: null,
|
||||
},
|
||||
internalPort: 80,
|
||||
scheme: 'http',
|
||||
sslScheme: 'https',
|
||||
suffix: '',
|
||||
},
|
||||
hostInfo: {
|
||||
id: 'abcdefg',
|
||||
kind: 'multi',
|
||||
hostnames: [
|
||||
{
|
||||
kind: 'ip',
|
||||
networkInterfaceId: 'elan0',
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'local',
|
||||
value: 'adjective-noun.local',
|
||||
port: null,
|
||||
sslPort: 1234,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'onion',
|
||||
hostname: {
|
||||
value: 'bitcoin-ui-address.onion',
|
||||
port: 80,
|
||||
sslPort: 443,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'ip',
|
||||
networkInterfaceId: 'elan0',
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'ipv4',
|
||||
value: '192.168.1.5',
|
||||
port: null,
|
||||
sslPort: 1234,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'ip',
|
||||
networkInterfaceId: 'elan0',
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'ipv6',
|
||||
value: '[2001:db8:85a3:8d3:1319:8a2e:370:7348]',
|
||||
port: null,
|
||||
sslPort: 1234,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
rpc: {
|
||||
id: 'rpc',
|
||||
@@ -270,66 +216,11 @@ export const mockPatchData: DataModel = {
|
||||
addressInfo: {
|
||||
username: null,
|
||||
hostId: 'bcdefgh',
|
||||
bindOptions: {
|
||||
scheme: 'http',
|
||||
preferredExternalPort: 80,
|
||||
addSsl: {
|
||||
// addXForwardedHeaders: false,
|
||||
preferredExternalPort: 443,
|
||||
scheme: 'https',
|
||||
alpn: { specified: ['http/1.1'] },
|
||||
},
|
||||
secure: null,
|
||||
},
|
||||
internalPort: 8332,
|
||||
scheme: 'http',
|
||||
sslScheme: 'https',
|
||||
suffix: '',
|
||||
},
|
||||
hostInfo: {
|
||||
id: 'bcdefgh',
|
||||
kind: 'multi',
|
||||
hostnames: [
|
||||
{
|
||||
kind: 'ip',
|
||||
networkInterfaceId: 'elan0',
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'local',
|
||||
value: 'adjective-noun.local',
|
||||
port: null,
|
||||
sslPort: 2345,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'onion',
|
||||
hostname: {
|
||||
value: 'bitcoin-rpc-address.onion',
|
||||
port: 80,
|
||||
sslPort: 443,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'ip',
|
||||
networkInterfaceId: 'elan0',
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'ipv4',
|
||||
value: '192.168.1.5',
|
||||
port: null,
|
||||
sslPort: 2345,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'ip',
|
||||
networkInterfaceId: 'elan0',
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'ipv6',
|
||||
value: '[2001:db8:85a3:8d3:1319:8a2e:370:7348]',
|
||||
port: null,
|
||||
sslPort: 2345,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
p2p: {
|
||||
id: 'p2p',
|
||||
@@ -343,69 +234,117 @@ export const mockPatchData: DataModel = {
|
||||
addressInfo: {
|
||||
username: null,
|
||||
hostId: 'cdefghi',
|
||||
bindOptions: {
|
||||
scheme: 'bitcoin',
|
||||
preferredExternalPort: 8333,
|
||||
addSsl: null,
|
||||
secure: {
|
||||
ssl: false,
|
||||
},
|
||||
},
|
||||
internalPort: 8333,
|
||||
scheme: 'bitcoin',
|
||||
sslScheme: null,
|
||||
suffix: '',
|
||||
},
|
||||
hostInfo: {
|
||||
id: 'cdefghi',
|
||||
kind: 'multi',
|
||||
hostnames: [
|
||||
},
|
||||
},
|
||||
currentDependencies: {},
|
||||
hosts: {
|
||||
abcdefg: {
|
||||
kind: 'multi',
|
||||
bindings: [],
|
||||
addresses: [],
|
||||
hostnameInfo: {
|
||||
80: [
|
||||
{
|
||||
kind: 'ip',
|
||||
networkInterfaceId: 'elan0',
|
||||
networkInterfaceId: 'eth0',
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'local',
|
||||
value: 'adjective-noun.local',
|
||||
port: 3456,
|
||||
sslPort: null,
|
||||
port: null,
|
||||
sslPort: 1234,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'ip',
|
||||
networkInterfaceId: 'wlan0',
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'local',
|
||||
value: 'adjective-noun.local',
|
||||
port: null,
|
||||
sslPort: 1234,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'ip',
|
||||
networkInterfaceId: 'eth0',
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'ipv4',
|
||||
value: '10.0.0.1',
|
||||
port: null,
|
||||
sslPort: 1234,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'ip',
|
||||
networkInterfaceId: 'wlan0',
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'ipv4',
|
||||
value: '10.0.0.2',
|
||||
port: null,
|
||||
sslPort: 1234,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'ip',
|
||||
networkInterfaceId: 'eth0',
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'ipv6',
|
||||
value: '[FE80:CD00:0000:0CDE:1257:0000:211E:729CD]',
|
||||
port: null,
|
||||
sslPort: 1234,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'ip',
|
||||
networkInterfaceId: 'wlan0',
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'ipv6',
|
||||
value: '[FE80:CD00:0000:0CDE:1257:0000:211E:1234]',
|
||||
port: null,
|
||||
sslPort: 1234,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'onion',
|
||||
hostname: {
|
||||
value: 'bitcoin-p2p-address.onion',
|
||||
port: 8333,
|
||||
sslPort: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'ip',
|
||||
networkInterfaceId: 'elan0',
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'ipv4',
|
||||
value: '192.168.1.5',
|
||||
port: 3456,
|
||||
sslPort: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'ip',
|
||||
networkInterfaceId: 'elan0',
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'ipv6',
|
||||
value: '[2001:db8:85a3:8d3:1319:8a2e:370:7348]',
|
||||
port: 3456,
|
||||
sslPort: null,
|
||||
value: 'bitcoin-p2p.onion',
|
||||
port: 80,
|
||||
sslPort: 443,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
bcdefgh: {
|
||||
kind: 'multi',
|
||||
bindings: [],
|
||||
addresses: [],
|
||||
hostnameInfo: {
|
||||
8332: [],
|
||||
},
|
||||
},
|
||||
cdefghi: {
|
||||
kind: 'multi',
|
||||
bindings: [],
|
||||
addresses: [],
|
||||
hostnameInfo: {
|
||||
8333: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
currentDependencies: {},
|
||||
hosts: {},
|
||||
storeExposedDependents: [],
|
||||
marketplaceUrl: 'https://registry.start9.com/',
|
||||
registry: 'https://registry.start9.com/',
|
||||
developerKey: 'developer-key',
|
||||
outboundProxy: null,
|
||||
},
|
||||
@@ -414,7 +353,7 @@ export const mockPatchData: DataModel = {
|
||||
state: 'installed',
|
||||
manifest: {
|
||||
...Mock.MockManifestLnd,
|
||||
version: '0.11.0',
|
||||
version: '0.11.0:0.0.1',
|
||||
},
|
||||
},
|
||||
icon: '/assets/img/service-icons/lnd.png',
|
||||
@@ -426,9 +365,6 @@ export const mockPatchData: DataModel = {
|
||||
main: {
|
||||
status: 'stopped',
|
||||
},
|
||||
dependencyConfigErrors: {
|
||||
'btc-rpc-proxy': 'This is a config unsatisfied error',
|
||||
},
|
||||
},
|
||||
actions: {},
|
||||
serviceInterfaces: {
|
||||
@@ -444,63 +380,11 @@ export const mockPatchData: DataModel = {
|
||||
addressInfo: {
|
||||
username: null,
|
||||
hostId: 'qrstuv',
|
||||
bindOptions: {
|
||||
scheme: 'grpc',
|
||||
preferredExternalPort: 10009,
|
||||
addSsl: null,
|
||||
secure: {
|
||||
ssl: true,
|
||||
},
|
||||
},
|
||||
internalPort: 10009,
|
||||
scheme: null,
|
||||
sslScheme: 'grpc',
|
||||
suffix: '',
|
||||
},
|
||||
hostInfo: {
|
||||
id: 'qrstuv',
|
||||
kind: 'multi',
|
||||
hostnames: [
|
||||
{
|
||||
kind: 'ip',
|
||||
networkInterfaceId: 'elan0',
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'local',
|
||||
value: 'adjective-noun.local',
|
||||
port: 5678,
|
||||
sslPort: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'onion',
|
||||
hostname: {
|
||||
value: 'lnd-grpc-address.onion',
|
||||
port: 10009,
|
||||
sslPort: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'ip',
|
||||
networkInterfaceId: 'elan0',
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'ipv4',
|
||||
value: '192.168.1.5',
|
||||
port: 5678,
|
||||
sslPort: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'ip',
|
||||
networkInterfaceId: 'elan0',
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'ipv6',
|
||||
value: '[2001:db8:85a3:8d3:1319:8a2e:370:7348]',
|
||||
port: 5678,
|
||||
sslPort: null,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
lndconnect: {
|
||||
id: 'lndconnect',
|
||||
@@ -514,63 +398,11 @@ export const mockPatchData: DataModel = {
|
||||
addressInfo: {
|
||||
username: null,
|
||||
hostId: 'qrstuv',
|
||||
bindOptions: {
|
||||
scheme: 'lndconnect',
|
||||
preferredExternalPort: 10009,
|
||||
addSsl: null,
|
||||
secure: {
|
||||
ssl: true,
|
||||
},
|
||||
},
|
||||
internalPort: 10009,
|
||||
scheme: null,
|
||||
sslScheme: 'lndconnect',
|
||||
suffix: 'cert=askjdfbjadnaskjnd&macaroon=ksjbdfnhjasbndjksand',
|
||||
},
|
||||
hostInfo: {
|
||||
id: 'qrstuv',
|
||||
kind: 'multi',
|
||||
hostnames: [
|
||||
{
|
||||
kind: 'ip',
|
||||
networkInterfaceId: 'elan0',
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'local',
|
||||
value: 'adjective-noun.local',
|
||||
port: 5678,
|
||||
sslPort: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'onion',
|
||||
hostname: {
|
||||
value: 'lnd-grpc-address.onion',
|
||||
port: 10009,
|
||||
sslPort: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'ip',
|
||||
networkInterfaceId: 'elan0',
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'ipv4',
|
||||
value: '192.168.1.5',
|
||||
port: 5678,
|
||||
sslPort: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'ip',
|
||||
networkInterfaceId: 'elan0',
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'ipv6',
|
||||
value: '[2001:db8:85a3:8d3:1319:8a2e:370:7348]',
|
||||
port: 5678,
|
||||
sslPort: null,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
p2p: {
|
||||
id: 'p2p',
|
||||
@@ -584,61 +416,11 @@ export const mockPatchData: DataModel = {
|
||||
addressInfo: {
|
||||
username: null,
|
||||
hostId: 'rstuvw',
|
||||
bindOptions: {
|
||||
scheme: null,
|
||||
preferredExternalPort: 9735,
|
||||
addSsl: null,
|
||||
secure: { ssl: true },
|
||||
},
|
||||
internalPort: 8333,
|
||||
scheme: 'bitcoin',
|
||||
sslScheme: null,
|
||||
suffix: '',
|
||||
},
|
||||
hostInfo: {
|
||||
id: 'rstuvw',
|
||||
kind: 'multi',
|
||||
hostnames: [
|
||||
{
|
||||
kind: 'ip',
|
||||
networkInterfaceId: 'elan0',
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'local',
|
||||
value: 'adjective-noun.local',
|
||||
port: 6789,
|
||||
sslPort: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'onion',
|
||||
hostname: {
|
||||
value: 'lnd-p2p-address.onion',
|
||||
port: 9735,
|
||||
sslPort: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'ip',
|
||||
networkInterfaceId: 'elan0',
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'ipv4',
|
||||
value: '192.168.1.5',
|
||||
port: 6789,
|
||||
sslPort: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'ip',
|
||||
networkInterfaceId: 'elan0',
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'ipv6',
|
||||
value: '[2001:db8:85a3:8d3:1319:8a2e:370:7348]',
|
||||
port: 6789,
|
||||
sslPort: null,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
currentDependencies: {
|
||||
@@ -646,22 +428,22 @@ export const mockPatchData: DataModel = {
|
||||
title: 'Bitcoin Core',
|
||||
icon: 'assets/img/service-icons/bitcoind.svg',
|
||||
kind: 'running',
|
||||
registryUrl: 'https://registry.start9.com',
|
||||
versionSpec: '>=26.0.0',
|
||||
versionRange: '>=26.0.0',
|
||||
healthChecks: [],
|
||||
configSatisfied: true,
|
||||
},
|
||||
'btc-rpc-proxy': {
|
||||
title: 'Bitcoin Proxy',
|
||||
icon: 'assets/img/service-icons/btc-rpc-proxy.png',
|
||||
kind: 'running',
|
||||
registryUrl: 'https://community-registry.start9.com',
|
||||
versionSpec: '>2.0.0',
|
||||
versionRange: '>2.0.0',
|
||||
healthChecks: [],
|
||||
configSatisfied: false,
|
||||
},
|
||||
},
|
||||
hosts: {},
|
||||
storeExposedDependents: [],
|
||||
marketplaceUrl: 'https://registry.start9.com/',
|
||||
registry: 'https://registry.start9.com/',
|
||||
developerKey: 'developer-key',
|
||||
outboundProxy: null,
|
||||
},
|
||||
|
||||
@@ -11,7 +11,7 @@ export enum AuthState {
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class AuthService {
|
||||
private readonly LOGGED_IN_KEY = 'loggedInKey'
|
||||
private readonly LOGGED_IN_KEY = 'loggedIn'
|
||||
private readonly authState$ = new ReplaySubject<AuthState>(1)
|
||||
|
||||
readonly isVerified$ = this.authState$.pipe(
|
||||
|
||||
@@ -81,42 +81,50 @@ export class ConfigService {
|
||||
}
|
||||
|
||||
/** ${scheme}://${username}@${host}:${externalPort}${suffix} */
|
||||
launchableAddress(ui: T.ServiceInterfaceWithHostInfo): string {
|
||||
if (ui.type !== 'ui') return ''
|
||||
launchableAddress(
|
||||
interfaces: PackageDataEntry['serviceInterfaces'],
|
||||
hosts: PackageDataEntry['hosts'],
|
||||
): string {
|
||||
const ui = Object.values(interfaces).find(
|
||||
i =>
|
||||
i.type === 'ui' &&
|
||||
(i.addressInfo.scheme === 'http' ||
|
||||
i.addressInfo.sslScheme === 'https'),
|
||||
) // TODO select if multiple
|
||||
|
||||
if (!ui) return ''
|
||||
|
||||
const hostnameInfo =
|
||||
hosts[ui.addressInfo.hostId]?.hostnameInfo[ui.addressInfo.internalPort]
|
||||
|
||||
if (!hostnameInfo) return ''
|
||||
|
||||
const host = ui.hostInfo
|
||||
const addressInfo = ui.addressInfo
|
||||
const scheme = this.isHttps() ? 'https' : 'http'
|
||||
const scheme = this.isHttps()
|
||||
? ui.addressInfo.sslScheme === 'https'
|
||||
? 'https'
|
||||
: 'http'
|
||||
: ui.addressInfo.scheme === 'http'
|
||||
? 'http'
|
||||
: 'https'
|
||||
const username = addressInfo.username ? addressInfo.username + '@' : ''
|
||||
const suffix = addressInfo.suffix || ''
|
||||
const url = new URL(`${scheme}://${username}placeholder${suffix}`)
|
||||
|
||||
if (host.kind === 'multi') {
|
||||
const onionHostname = host.hostnames.find(h => h.kind === 'onion')
|
||||
?.hostname as T.ExportedOnionHostname
|
||||
const onionHostname = hostnameInfo.find(h => h.kind === 'onion')
|
||||
?.hostname as T.OnionHostname | undefined
|
||||
|
||||
if (this.isTor() && onionHostname) {
|
||||
url.hostname = onionHostname.value
|
||||
} else {
|
||||
const ipHostname = host.hostnames.find(h => h.kind === 'ip')
|
||||
?.hostname as T.ExportedIpHostname
|
||||
|
||||
if (!ipHostname) return ''
|
||||
|
||||
url.hostname = this.hostname
|
||||
url.port = String(ipHostname.sslPort || ipHostname.port)
|
||||
}
|
||||
if (this.isTor() && onionHostname) {
|
||||
url.hostname = onionHostname.value
|
||||
} else {
|
||||
const hostname = {} as T.ExportedHostnameInfo // host.hostname
|
||||
const ipHostname = hostnameInfo.find(h => h.kind === 'ip')?.hostname as
|
||||
| T.IpHostname
|
||||
| undefined
|
||||
|
||||
if (!hostname) return ''
|
||||
if (!ipHostname) return ''
|
||||
|
||||
if (this.isTor() && hostname.kind === 'onion') {
|
||||
url.hostname = (hostname.hostname as T.ExportedOnionHostname).value
|
||||
} else {
|
||||
url.hostname = this.hostname
|
||||
url.port = String(hostname.hostname.sslPort || hostname.hostname.port)
|
||||
}
|
||||
url.hostname = this.hostname
|
||||
url.port = String(ipHostname.sslPort || ipHostname.port)
|
||||
}
|
||||
|
||||
return url.href
|
||||
|
||||
@@ -1,32 +1,23 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import {
|
||||
combineLatest,
|
||||
distinctUntilChanged,
|
||||
map,
|
||||
startWith,
|
||||
fromEvent,
|
||||
merge,
|
||||
ReplaySubject,
|
||||
} from 'rxjs'
|
||||
import { inject, Injectable } from '@angular/core'
|
||||
import { combineLatest, Observable, shareReplay } from 'rxjs'
|
||||
import { distinctUntilChanged, map } from 'rxjs/operators'
|
||||
import { NetworkService } from 'src/app/services/network.service'
|
||||
import { StateService } from 'src/app/services/state.service'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ConnectionService {
|
||||
readonly networkConnected$ = merge(
|
||||
fromEvent(window, 'online'),
|
||||
fromEvent(window, 'offline'),
|
||||
).pipe(
|
||||
startWith(null),
|
||||
map(() => navigator.onLine),
|
||||
distinctUntilChanged(),
|
||||
)
|
||||
readonly websocketConnected$ = new ReplaySubject<boolean>(1)
|
||||
readonly connected$ = combineLatest([
|
||||
this.networkConnected$,
|
||||
this.websocketConnected$.pipe(distinctUntilChanged()),
|
||||
export class ConnectionService extends Observable<boolean> {
|
||||
private readonly stream$ = combineLatest([
|
||||
inject(NetworkService),
|
||||
inject(StateService).pipe(map(Boolean)),
|
||||
]).pipe(
|
||||
map(([network, websocket]) => network && websocket),
|
||||
distinctUntilChanged(),
|
||||
shareReplay(1),
|
||||
)
|
||||
|
||||
constructor() {
|
||||
super(subscriber => this.stream$.subscribe(subscriber))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { Emver } from '@start9labs/shared'
|
||||
import { Exver } from '@start9labs/shared'
|
||||
import { distinctUntilChanged, map, shareReplay } from 'rxjs/operators'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import {
|
||||
@@ -42,7 +42,7 @@ export class DepErrorService {
|
||||
)
|
||||
|
||||
constructor(
|
||||
private readonly emver: Emver,
|
||||
private readonly exver: Exver,
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
) {}
|
||||
|
||||
@@ -86,20 +86,26 @@ export class DepErrorService {
|
||||
}
|
||||
}
|
||||
|
||||
const versionSpec = pkg.currentDependencies[depId].versionSpec
|
||||
const currentDep = pkg.currentDependencies[depId]
|
||||
const depManifest = dep.stateInfo.manifest
|
||||
|
||||
// incorrect version
|
||||
if (!this.emver.satisfies(depManifest.version, versionSpec)) {
|
||||
return {
|
||||
type: 'incorrectVersion',
|
||||
expected: versionSpec,
|
||||
received: depManifest.version,
|
||||
if (!this.exver.satisfies(depManifest.version, currentDep.versionRange)) {
|
||||
if (
|
||||
depManifest.satisfies.some(
|
||||
v => !this.exver.satisfies(v, currentDep.versionRange),
|
||||
)
|
||||
) {
|
||||
return {
|
||||
type: 'incorrectVersion',
|
||||
expected: currentDep.versionRange,
|
||||
received: depManifest.version,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// invalid config
|
||||
if (Object.values(pkg.status.dependencyConfigErrors).some(err => !!err)) {
|
||||
if (!currentDep.configSatisfied) {
|
||||
return {
|
||||
type: 'configUnsatisfied',
|
||||
}
|
||||
@@ -114,8 +120,6 @@ export class DepErrorService {
|
||||
}
|
||||
}
|
||||
|
||||
const currentDep = pkg.currentDependencies[depId]
|
||||
|
||||
// health check failure
|
||||
if (depStatus === 'running' && currentDep.kind === 'running') {
|
||||
for (let id of currentDep.healthChecks) {
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { Emver } from '@start9labs/shared'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { BehaviorSubject, distinctUntilChanged, map, combineLatest } from 'rxjs'
|
||||
import { MarketplaceEOS } from 'src/app/services/api/api.types'
|
||||
import { OSUpdate } from 'src/app/services/api/api.types'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { getServerInfo } from 'src/app/utils/get-server-info'
|
||||
import { DataModel } from './patch-db/data-model'
|
||||
import { Exver } from '@start9labs/shared'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class EOSService {
|
||||
eos?: MarketplaceEOS
|
||||
osUpdate?: OSUpdate
|
||||
updateAvailable$ = new BehaviorSubject<boolean>(false)
|
||||
|
||||
readonly updating$ = this.patch.watch$('serverInfo', 'statusInfo').pipe(
|
||||
@@ -46,14 +46,15 @@ export class EOSService {
|
||||
|
||||
constructor(
|
||||
private readonly api: ApiService,
|
||||
private readonly emver: Emver,
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
private readonly exver: Exver,
|
||||
) {}
|
||||
|
||||
async loadEos(): Promise<void> {
|
||||
const { version } = await getServerInfo(this.patch)
|
||||
this.eos = await this.api.getEos()
|
||||
const updateAvailable = this.emver.compare(this.eos.version, version) === 1
|
||||
const { version, id } = await getServerInfo(this.patch)
|
||||
this.osUpdate = await this.api.checkOSUpdate({ serverId: id })
|
||||
const updateAvailable =
|
||||
this.exver.compareOsVersion(this.osUpdate.version, version) === 'greater'
|
||||
this.updateAvailable$.next(updateAvailable)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,7 @@ import {
|
||||
ValidatorFn,
|
||||
Validators,
|
||||
} from '@angular/forms'
|
||||
import { getDefaultString } from 'src/app/utils/config-utilities'
|
||||
import { CT } from '@start9labs/start-sdk'
|
||||
import { CT, utils } from '@start9labs/start-sdk'
|
||||
const Mustache = require('mustache')
|
||||
|
||||
@Injectable({
|
||||
@@ -40,28 +39,25 @@ export class FormService {
|
||||
|
||||
getUnionObject(
|
||||
spec: CT.ValueSpecUnion,
|
||||
selection: string | null,
|
||||
selected: string | null,
|
||||
): UntypedFormGroup {
|
||||
const group = this.getFormGroup({
|
||||
[CT.unionSelectKey]: this.getUnionSelectSpec(spec, selection),
|
||||
selection: this.getUnionSelectSpec(spec, selected),
|
||||
})
|
||||
|
||||
group.setControl(
|
||||
CT.unionValueKey,
|
||||
this.getFormGroup(selection ? spec.variants[selection].spec : {}),
|
||||
'value',
|
||||
this.getFormGroup(selected ? spec.variants[selected].spec : {}),
|
||||
)
|
||||
|
||||
return group
|
||||
}
|
||||
|
||||
getListItem(spec: CT.ValueSpecList, entry?: any) {
|
||||
const listItemValidators = getListItemValidators(spec)
|
||||
if (CT.isValueSpecListOf(spec, 'text')) {
|
||||
return this.formBuilder.control(entry, listItemValidators)
|
||||
} else if (CT.isValueSpecListOf(spec, 'number')) {
|
||||
return this.formBuilder.control(entry, listItemValidators)
|
||||
return this.formBuilder.control(entry, stringValidators(spec.spec))
|
||||
} else if (CT.isValueSpecListOf(spec, 'object')) {
|
||||
return this.getFormGroup(spec.spec.spec, listItemValidators, entry)
|
||||
return this.getFormGroup(spec.spec.spec, [], entry)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +86,7 @@ export class FormService {
|
||||
if (currentValue !== undefined) {
|
||||
value = currentValue
|
||||
} else {
|
||||
value = spec.default ? getDefaultString(spec.default) : null
|
||||
value = spec.default ? utils.getDefaultString(spec.default) : null
|
||||
}
|
||||
return this.formBuilder.control(value, stringValidators(spec))
|
||||
case 'textarea':
|
||||
@@ -132,7 +128,7 @@ export class FormService {
|
||||
fileValidators(spec),
|
||||
)
|
||||
case 'union':
|
||||
const currentSelection = currentValue?.[CT.unionSelectKey]
|
||||
const currentSelection = currentValue?.selection
|
||||
const isValid = !!spec.variants[currentSelection]
|
||||
|
||||
return this.getUnionObject(
|
||||
@@ -154,13 +150,11 @@ export class FormService {
|
||||
}
|
||||
}
|
||||
|
||||
function getListItemValidators(spec: CT.ValueSpecList) {
|
||||
if (CT.isValueSpecListOf(spec, 'text')) {
|
||||
return stringValidators(spec.spec)
|
||||
} else if (CT.isValueSpecListOf(spec, 'number')) {
|
||||
return numberValidators(spec.spec)
|
||||
}
|
||||
}
|
||||
// function getListItemValidators(spec: CT.ValueSpecList) {
|
||||
// if (CT.isValueSpecListOf(spec, 'text')) {
|
||||
// return stringValidators(spec.spec)
|
||||
// }
|
||||
// }
|
||||
|
||||
function stringValidators(
|
||||
spec: CT.ValueSpecText | CT.ListValueSpecText,
|
||||
@@ -224,9 +218,7 @@ function datetimeValidators({
|
||||
return validators
|
||||
}
|
||||
|
||||
function numberValidators(
|
||||
spec: CT.ValueSpecNumber | CT.ListValueSpecNumber,
|
||||
): ValidatorFn[] {
|
||||
function numberValidators(spec: CT.ValueSpecNumber): ValidatorFn[] {
|
||||
const validators: ValidatorFn[] = []
|
||||
|
||||
validators.push(isNumber())
|
||||
@@ -416,7 +408,6 @@ function listItemEquals(spec: CT.ValueSpecList, val1: any, val2: any): boolean {
|
||||
// TODO: fix types
|
||||
switch (spec.spec.type) {
|
||||
case 'text':
|
||||
case 'number':
|
||||
return val1 == val2
|
||||
case 'object':
|
||||
const obj = spec.spec
|
||||
@@ -528,12 +519,12 @@ function unionEquals(
|
||||
val1: any,
|
||||
val2: any,
|
||||
): boolean {
|
||||
const variantSpec = spec.variants[val1[CT.unionSelectKey]].spec
|
||||
const variantSpec = spec.variants[val1.selection].spec
|
||||
if (!uniqueBy) {
|
||||
return false
|
||||
} else if (typeof uniqueBy === 'string') {
|
||||
if (uniqueBy === CT.unionSelectKey) {
|
||||
return val1[CT.unionSelectKey] === val2[CT.unionSelectKey]
|
||||
if (uniqueBy === 'selection') {
|
||||
return val1.selection === val2.selection
|
||||
} else {
|
||||
return itemEquals(variantSpec[uniqueBy], val1[uniqueBy], val2[uniqueBy])
|
||||
}
|
||||
@@ -623,18 +614,13 @@ export function convertValuesRecursive(
|
||||
convertValuesRecursive(valueSpec.spec, group.get(key) as UntypedFormGroup)
|
||||
} else if (valueSpec.type === 'union') {
|
||||
const formGr = group.get(key) as UntypedFormGroup
|
||||
const spec =
|
||||
valueSpec.variants[formGr.controls[CT.unionSelectKey].value].spec
|
||||
const spec = valueSpec.variants[formGr.controls['selection'].value].spec
|
||||
convertValuesRecursive(spec, formGr)
|
||||
} else if (valueSpec.type === 'list') {
|
||||
const formArr = group.get(key) as UntypedFormArray
|
||||
const { controls } = formArr
|
||||
|
||||
if (valueSpec.spec.type === 'number') {
|
||||
controls.forEach(control => {
|
||||
control.setValue(control.value ? Number(control.value) : null)
|
||||
})
|
||||
} else if (valueSpec.spec.type === 'text') {
|
||||
if (valueSpec.spec.type === 'text') {
|
||||
controls.forEach(control => {
|
||||
if (!control.value) control.setValue(null)
|
||||
})
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { sameUrl } from '@start9labs/shared'
|
||||
import {
|
||||
AbstractMarketplaceService,
|
||||
Marketplace,
|
||||
MarketplacePkg,
|
||||
StoreData,
|
||||
StoreIdentity,
|
||||
StoreInfo,
|
||||
MarketplacePkg,
|
||||
GetPackageRes,
|
||||
StoreData,
|
||||
} from '@start9labs/marketplace'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import {
|
||||
@@ -20,8 +19,8 @@ import {
|
||||
mergeMap,
|
||||
Observable,
|
||||
of,
|
||||
pairwise,
|
||||
scan,
|
||||
pairwise,
|
||||
shareReplay,
|
||||
startWith,
|
||||
switchMap,
|
||||
@@ -32,7 +31,9 @@ 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 { ConfigService } from './config.service'
|
||||
import { Exver, sameUrl } from '@start9labs/shared'
|
||||
import { ClientStorageService } from './client-storage.service'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
|
||||
@Injectable()
|
||||
export class MarketplaceService implements AbstractMarketplaceService {
|
||||
@@ -87,11 +88,9 @@ export class MarketplaceService implements AbstractMarketplaceService {
|
||||
mergeMap(({ url, name }) =>
|
||||
this.fetchStore$(url).pipe(
|
||||
tap(data => {
|
||||
if (data?.info) this.updateStoreName(url, name, data.info.name)
|
||||
}),
|
||||
map<StoreData | null, [string, StoreData | null]>(data => {
|
||||
return [url, data]
|
||||
if (data?.info.name) this.updateStoreName(url, name, data.info.name)
|
||||
}),
|
||||
map<StoreData | null, [string, StoreData | null]>(data => [url, data]),
|
||||
startWith<[string, StoreData | null]>([url, null]),
|
||||
),
|
||||
),
|
||||
@@ -142,6 +141,7 @@ export class MarketplaceService implements AbstractMarketplaceService {
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
private readonly config: ConfigService,
|
||||
private readonly clientStorageService: ClientStorageService,
|
||||
private readonly exver: Exver,
|
||||
) {}
|
||||
|
||||
getKnownHosts$(filtered = false): Observable<StoreIdentity[]> {
|
||||
@@ -190,28 +190,29 @@ export class MarketplaceService implements AbstractMarketplaceService {
|
||||
|
||||
getPackage$(
|
||||
id: string,
|
||||
version: string,
|
||||
optionalUrl?: string,
|
||||
version: string | null,
|
||||
flavor: string | null,
|
||||
registryUrl?: string,
|
||||
): Observable<MarketplacePkg> {
|
||||
return this.patch.watch$('ui', 'marketplace').pipe(
|
||||
switchMap(uiMarketplace => {
|
||||
const url = optionalUrl || uiMarketplace.selectedUrl
|
||||
return this.selectedHost$.pipe(
|
||||
switchMap(selected =>
|
||||
this.marketplace$.pipe(
|
||||
switchMap(m => {
|
||||
const url = registryUrl || selected.url
|
||||
|
||||
if (version !== '*' || !uiMarketplace.knownHosts[url]) {
|
||||
return this.fetchPackage$(id, version, url)
|
||||
}
|
||||
const pkg = m[url]?.packages.find(
|
||||
p =>
|
||||
p.id === id &&
|
||||
p.flavor === flavor &&
|
||||
(!version || this.exver.compareExver(p.version, version) === 0),
|
||||
)
|
||||
|
||||
return this.marketplace$.pipe(
|
||||
map(m => m[url]),
|
||||
filter(Boolean),
|
||||
take(1),
|
||||
map(
|
||||
store =>
|
||||
store.packages.find(p => p.manifest.id === id) ||
|
||||
({} as MarketplacePkg),
|
||||
),
|
||||
)
|
||||
}),
|
||||
return !!pkg
|
||||
? of(pkg)
|
||||
: this.fetchPackage$(url, id, version, flavor)
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -230,56 +231,22 @@ export class MarketplaceService implements AbstractMarketplaceService {
|
||||
): Promise<void> {
|
||||
const params: RR.InstallPackageReq = {
|
||||
id,
|
||||
versionSpec: `=${version}`,
|
||||
marketplaceUrl: url,
|
||||
version,
|
||||
registry: url,
|
||||
}
|
||||
|
||||
await this.api.installPackage(params)
|
||||
}
|
||||
|
||||
fetchInfo$(url: string): Observable<StoreInfo> {
|
||||
return this.patch.watch$('serverInfo').pipe(
|
||||
take(1),
|
||||
switchMap(serverInfo => {
|
||||
const qp: RR.GetMarketplaceInfoReq = { serverId: serverInfo.id }
|
||||
return this.api.marketplaceProxy<RR.GetMarketplaceInfoRes>(
|
||||
'/package/v0/info',
|
||||
qp,
|
||||
url,
|
||||
)
|
||||
}),
|
||||
)
|
||||
fetchInfo$(url: string): Observable<T.RegistryInfo> {
|
||||
return from(this.api.getRegistryInfo(url))
|
||||
}
|
||||
|
||||
fetchReleaseNotes$(
|
||||
id: string,
|
||||
url?: string,
|
||||
): Observable<Record<string, string>> {
|
||||
return this.selectedHost$.pipe(
|
||||
switchMap(m => {
|
||||
return from(
|
||||
this.api.marketplaceProxy<Record<string, string>>(
|
||||
`/package/v0/release-notes/${id}`,
|
||||
{},
|
||||
url || m.url,
|
||||
),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fetchStatic$(id: string, type: string, url?: string): Observable<string> {
|
||||
return this.selectedHost$.pipe(
|
||||
switchMap(m => {
|
||||
return from(
|
||||
this.api.marketplaceProxy<string>(
|
||||
`/package/v0/${type}/${id}`,
|
||||
{},
|
||||
url || m.url,
|
||||
),
|
||||
)
|
||||
}),
|
||||
)
|
||||
fetchStatic$(
|
||||
pkg: MarketplacePkg,
|
||||
type: 'LICENSE.md' | 'instructions.md',
|
||||
): Observable<string> {
|
||||
return from(this.api.getStaticProxy(pkg, type))
|
||||
}
|
||||
|
||||
private fetchStore$(url: string): Observable<StoreData | null> {
|
||||
@@ -293,33 +260,57 @@ export class MarketplaceService implements AbstractMarketplaceService {
|
||||
)
|
||||
}
|
||||
|
||||
private fetchPackages$(
|
||||
url: string,
|
||||
params: Omit<RR.GetMarketplacePackagesReq, 'page' | 'per-page'> = {},
|
||||
): Observable<MarketplacePkg[]> {
|
||||
const qp: RR.GetMarketplacePackagesReq = {
|
||||
...params,
|
||||
page: 1,
|
||||
perPage: 100,
|
||||
}
|
||||
if (qp.ids) qp.ids = JSON.stringify(qp.ids)
|
||||
|
||||
return from(
|
||||
this.api.marketplaceProxy<RR.GetMarketplacePackagesRes>(
|
||||
'/package/v0/index',
|
||||
qp,
|
||||
url,
|
||||
),
|
||||
private fetchPackages$(url: string): Observable<MarketplacePkg[]> {
|
||||
return from(this.api.getRegistryPackages(url)).pipe(
|
||||
map(packages => {
|
||||
return Object.entries(packages).flatMap(([id, pkgInfo]) =>
|
||||
Object.keys(pkgInfo.best).map(version =>
|
||||
this.convertToMarketplacePkg(
|
||||
id,
|
||||
version,
|
||||
this.exver.getFlavor(version),
|
||||
pkgInfo,
|
||||
),
|
||||
),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
private fetchPackage$(
|
||||
convertToMarketplacePkg(
|
||||
id: string,
|
||||
version: string,
|
||||
version: string | null,
|
||||
flavor: string | null,
|
||||
pkgInfo: GetPackageRes,
|
||||
): MarketplacePkg {
|
||||
version =
|
||||
version ||
|
||||
Object.keys(pkgInfo.best).find(v => this.exver.getFlavor(v) === flavor) ||
|
||||
null
|
||||
|
||||
return !version || !pkgInfo.best[version]
|
||||
? ({} as MarketplacePkg)
|
||||
: {
|
||||
id,
|
||||
version,
|
||||
flavor,
|
||||
...pkgInfo,
|
||||
...pkgInfo.best[version],
|
||||
}
|
||||
}
|
||||
|
||||
private fetchPackage$(
|
||||
url: string,
|
||||
id: string,
|
||||
version: string | null,
|
||||
flavor: string | null,
|
||||
): Observable<MarketplacePkg> {
|
||||
return this.fetchPackages$(url, { ids: [{ id, version }] }).pipe(
|
||||
map(pkgs => pkgs[0] || {}),
|
||||
return from(
|
||||
this.api.getRegistryPackage(url, id, version ? `=${version}` : null),
|
||||
).pipe(
|
||||
map(pkgInfo =>
|
||||
this.convertToMarketplacePkg(id, version, flavor, pkgInfo),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
22
web/projects/ui/src/app/services/network.service.ts
Normal file
22
web/projects/ui/src/app/services/network.service.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { inject, Injectable } from '@angular/core'
|
||||
import { WINDOW } from '@ng-web-apis/common'
|
||||
import { fromEvent, merge, Observable, shareReplay } from 'rxjs'
|
||||
import { distinctUntilChanged, map, startWith } from 'rxjs/operators'
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class NetworkService extends Observable<boolean> {
|
||||
private readonly win = inject(WINDOW)
|
||||
private readonly stream$ = merge(
|
||||
fromEvent(this.win, 'online'),
|
||||
fromEvent(this.win, 'offline'),
|
||||
).pipe(
|
||||
startWith(null),
|
||||
map(() => this.win.navigator.onLine),
|
||||
distinctUntilChanged(),
|
||||
shareReplay(1),
|
||||
)
|
||||
|
||||
constructor() {
|
||||
super(subscriber => this.stream$.subscribe(subscriber))
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Inject, Injectable } from '@angular/core'
|
||||
import { AbstractMarketplaceService } from '@start9labs/marketplace'
|
||||
import { TuiDialogService } from '@taiga-ui/core'
|
||||
import { filter, share, switchMap, take, tap, Observable } from 'rxjs'
|
||||
import { filter, share, switchMap, take, Observable, map } from 'rxjs'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { EOSService } from 'src/app/services/eos.service'
|
||||
@@ -11,21 +11,25 @@ import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { MarketplaceService } from 'src/app/services/marketplace.service'
|
||||
import { ConnectionService } from 'src/app/services/connection.service'
|
||||
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
|
||||
import { LocalStorageBootstrap } from './patch-db/local-storage-bootstrap'
|
||||
|
||||
// Get data from PatchDb after is starts and act upon it
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class PatchDataService extends Observable<DataModel> {
|
||||
private readonly stream$ = this.connectionService.connected$.pipe(
|
||||
export class PatchDataService extends Observable<void> {
|
||||
private readonly stream$ = this.connection$.pipe(
|
||||
filter(Boolean),
|
||||
switchMap(() => this.patch.watch$()),
|
||||
take(1),
|
||||
tap(({ ui }) => {
|
||||
// check for updates to eOS and services
|
||||
this.checkForUpdates()
|
||||
// show eos welcome message
|
||||
this.showEosWelcome(ui.ackWelcome)
|
||||
map((cache, index) => {
|
||||
this.bootstrapper.update(cache)
|
||||
|
||||
if (index === 0) {
|
||||
// check for updates to StartOS and services
|
||||
this.checkForUpdates()
|
||||
// show eos welcome message
|
||||
this.showEosWelcome(cache.ui.ackWelcome)
|
||||
}
|
||||
}),
|
||||
share(),
|
||||
)
|
||||
@@ -38,7 +42,8 @@ export class PatchDataService extends Observable<DataModel> {
|
||||
private readonly embassyApi: ApiService,
|
||||
@Inject(AbstractMarketplaceService)
|
||||
private readonly marketplaceService: MarketplaceService,
|
||||
private readonly connectionService: ConnectionService,
|
||||
private readonly connection$: ConnectionService,
|
||||
private readonly bootstrapper: LocalStorageBootstrap,
|
||||
) {
|
||||
super(subscriber => this.stream$.subscribe(subscriber))
|
||||
}
|
||||
|
||||
@@ -134,6 +134,10 @@ export type PackageDataEntry<T extends StateInfo = StateInfo> =
|
||||
nextBackup: string | null
|
||||
}
|
||||
|
||||
export type AllPackageData = NonNullable<
|
||||
T.AllPackageData & Record<string, PackageDataEntry<StateInfo>>
|
||||
>
|
||||
|
||||
export type StateInfo = InstalledState | InstallingState | UpdatingState
|
||||
|
||||
export type InstalledState = {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Bootstrapper, DBCache } from 'patch-db-client'
|
||||
import { Dump } from 'patch-db-client'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { Injectable } from '@angular/core'
|
||||
import { StorageService } from '../storage.service'
|
||||
@@ -6,20 +6,18 @@ import { StorageService } from '../storage.service'
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class LocalStorageBootstrap implements Bootstrapper<DataModel> {
|
||||
static CONTENT_KEY = 'patch-db-cache'
|
||||
export class LocalStorageBootstrap {
|
||||
static CONTENT_KEY = 'patchDB'
|
||||
|
||||
constructor(private readonly storage: StorageService) {}
|
||||
|
||||
init(): DBCache<DataModel> {
|
||||
const cache = this.storage.get<DBCache<DataModel>>(
|
||||
LocalStorageBootstrap.CONTENT_KEY,
|
||||
)
|
||||
init(): Dump<DataModel> {
|
||||
const cache = this.storage.get<DataModel>(LocalStorageBootstrap.CONTENT_KEY)
|
||||
|
||||
return cache || { sequence: 0, data: {} as DataModel }
|
||||
return cache ? { id: 1, value: cache } : { id: 0, value: {} as DataModel }
|
||||
}
|
||||
|
||||
update(cache: DBCache<DataModel>): void {
|
||||
update(cache: DataModel): void {
|
||||
this.storage.set(LocalStorageBootstrap.CONTENT_KEY, cache)
|
||||
}
|
||||
}
|
||||
|
||||
58
web/projects/ui/src/app/services/patch-db/patch-db-source.ts
Normal file
58
web/projects/ui/src/app/services/patch-db/patch-db-source.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { inject, Injectable, InjectionToken } from '@angular/core'
|
||||
import { Dump, Revision, Update } from 'patch-db-client'
|
||||
import { BehaviorSubject, EMPTY, Observable } from 'rxjs'
|
||||
import {
|
||||
bufferTime,
|
||||
catchError,
|
||||
filter,
|
||||
skip,
|
||||
startWith,
|
||||
switchMap,
|
||||
take,
|
||||
} from 'rxjs/operators'
|
||||
import { StateService } from 'src/app/services/state.service'
|
||||
import { ApiService } from '../api/embassy-api.service'
|
||||
import { AuthService } from '../auth.service'
|
||||
import { DataModel } from './data-model'
|
||||
import { LocalStorageBootstrap } from './local-storage-bootstrap'
|
||||
|
||||
export const PATCH_CACHE = new InjectionToken('', {
|
||||
factory: () =>
|
||||
new BehaviorSubject<Dump<DataModel>>({
|
||||
id: 0,
|
||||
value: {} as DataModel,
|
||||
}),
|
||||
})
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class PatchDbSource extends Observable<Update<DataModel>[]> {
|
||||
private readonly api = inject(ApiService)
|
||||
private readonly state = inject(StateService)
|
||||
private readonly stream$ = inject(AuthService).isVerified$.pipe(
|
||||
switchMap(verified => (verified ? this.api.subscribeToPatchDB({}) : EMPTY)),
|
||||
switchMap(({ dump, guid }) =>
|
||||
this.api.openWebsocket$<Revision>(guid, {}).pipe(
|
||||
bufferTime(250),
|
||||
filter(revisions => !!revisions.length),
|
||||
startWith([dump]),
|
||||
),
|
||||
),
|
||||
catchError((_, original$) => {
|
||||
this.state.retrigger()
|
||||
|
||||
return this.state.pipe(
|
||||
skip(1), // skipping previous value stored due to shareReplay
|
||||
filter(current => current === 'running'),
|
||||
take(1),
|
||||
switchMap(() => original$),
|
||||
)
|
||||
}),
|
||||
startWith([inject(LocalStorageBootstrap).init()]),
|
||||
)
|
||||
|
||||
constructor() {
|
||||
super(subscriber => this.stream$.subscribe(subscriber))
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
import { InjectionToken, Injector } from '@angular/core'
|
||||
import { Update } from 'patch-db-client'
|
||||
import {
|
||||
bufferTime,
|
||||
catchError,
|
||||
filter,
|
||||
switchMap,
|
||||
take,
|
||||
tap,
|
||||
defer,
|
||||
EMPTY,
|
||||
from,
|
||||
interval,
|
||||
Observable,
|
||||
} from 'rxjs'
|
||||
import { DataModel } from './data-model'
|
||||
import { AuthService } from '../auth.service'
|
||||
import { ConnectionService } from '../connection.service'
|
||||
import { ApiService } from '../api/embassy-api.service'
|
||||
import { ConfigService } from '../config.service'
|
||||
|
||||
export const PATCH_SOURCE = new InjectionToken<Observable<Update<DataModel>[]>>(
|
||||
'',
|
||||
)
|
||||
|
||||
export function sourceFactory(
|
||||
injector: Injector,
|
||||
): Observable<Update<DataModel>[]> {
|
||||
// defer() needed to avoid circular dependency with ApiService, since PatchDB is needed there
|
||||
return defer(() => {
|
||||
const api = injector.get(ApiService)
|
||||
const authService = injector.get(AuthService)
|
||||
const connectionService = injector.get(ConnectionService)
|
||||
const configService = injector.get(ConfigService)
|
||||
const isTor = configService.isTor()
|
||||
const timeout = isTor ? 16000 : 4000
|
||||
|
||||
const websocket$ = api.openPatchWebsocket$().pipe(
|
||||
bufferTime(250),
|
||||
filter(updates => !!updates.length),
|
||||
catchError((_, watch$) => {
|
||||
connectionService.websocketConnected$.next(false)
|
||||
|
||||
return interval(timeout).pipe(
|
||||
switchMap(() =>
|
||||
from(api.echo({ message: 'ping', timeout })).pipe(
|
||||
catchError(() => EMPTY),
|
||||
),
|
||||
),
|
||||
take(1),
|
||||
switchMap(() => watch$),
|
||||
)
|
||||
}),
|
||||
tap(() => connectionService.websocketConnected$.next(true)),
|
||||
)
|
||||
|
||||
return authService.isVerified$.pipe(
|
||||
switchMap(verified => (verified ? websocket$ : EMPTY)),
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -3,7 +3,6 @@ import { tap, Observable } from 'rxjs'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { AuthService } from 'src/app/services/auth.service'
|
||||
import { DataModel } from './patch-db/data-model'
|
||||
import { LocalStorageBootstrap } from './patch-db/local-storage-bootstrap'
|
||||
|
||||
// Start and stop PatchDb upon verification
|
||||
@Injectable({
|
||||
@@ -11,15 +10,12 @@ import { LocalStorageBootstrap } from './patch-db/local-storage-bootstrap'
|
||||
})
|
||||
export class PatchMonitorService extends Observable<unknown> {
|
||||
private readonly stream$ = this.authService.isVerified$.pipe(
|
||||
tap(verified =>
|
||||
verified ? this.patch.start(this.bootstrapper) : this.patch.stop(),
|
||||
),
|
||||
tap(verified => (verified ? this.patch.start() : this.patch.stop())),
|
||||
)
|
||||
|
||||
constructor(
|
||||
private readonly authService: AuthService,
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
private readonly bootstrapper: LocalStorageBootstrap,
|
||||
) {
|
||||
super(subscriber => this.stream$.subscribe(subscriber))
|
||||
}
|
||||
|
||||
136
web/projects/ui/src/app/services/state.service.ts
Normal file
136
web/projects/ui/src/app/services/state.service.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import { inject, Injectable } from '@angular/core'
|
||||
import { CanActivateFn, IsActiveMatchOptions, Router } from '@angular/router'
|
||||
import { ALWAYS_TRUE_HANDLER } from '@taiga-ui/cdk'
|
||||
import { TuiAlertService, TuiNotification } from '@taiga-ui/core'
|
||||
import {
|
||||
BehaviorSubject,
|
||||
combineLatest,
|
||||
concat,
|
||||
EMPTY,
|
||||
exhaustMap,
|
||||
from,
|
||||
merge,
|
||||
Observable,
|
||||
startWith,
|
||||
Subject,
|
||||
timer,
|
||||
} from 'rxjs'
|
||||
import {
|
||||
catchError,
|
||||
filter,
|
||||
map,
|
||||
shareReplay,
|
||||
skip,
|
||||
switchMap,
|
||||
take,
|
||||
takeUntil,
|
||||
tap,
|
||||
} from 'rxjs/operators'
|
||||
import { RR } from 'src/app/services/api/api.types'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { NetworkService } from 'src/app/services/network.service'
|
||||
|
||||
const OPTIONS: IsActiveMatchOptions = {
|
||||
paths: 'subset',
|
||||
queryParams: 'exact',
|
||||
fragment: 'ignored',
|
||||
matrixParams: 'ignored',
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class StateService extends Observable<RR.ServerState | null> {
|
||||
private readonly alerts = inject(TuiAlertService)
|
||||
private readonly api = inject(ApiService)
|
||||
private readonly router = inject(Router)
|
||||
private readonly network$ = inject(NetworkService)
|
||||
|
||||
private readonly single$ = new Subject<RR.ServerState>()
|
||||
|
||||
private readonly trigger$ = new BehaviorSubject<void>(undefined)
|
||||
private readonly poll$ = this.trigger$.pipe(
|
||||
switchMap(() =>
|
||||
timer(0, 2000).pipe(
|
||||
switchMap(() =>
|
||||
from(this.api.getState()).pipe(catchError(() => EMPTY)),
|
||||
),
|
||||
take(1),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
private readonly stream$ = merge(this.single$, this.poll$).pipe(
|
||||
tap(state => {
|
||||
switch (state) {
|
||||
case 'initializing':
|
||||
this.router.navigate(['initializing'], { replaceUrl: true })
|
||||
break
|
||||
case 'error':
|
||||
this.router.navigate(['diagnostic'], { replaceUrl: true })
|
||||
break
|
||||
case 'running':
|
||||
if (
|
||||
this.router.isActive('initializing', OPTIONS) ||
|
||||
this.router.isActive('diagnostic', OPTIONS)
|
||||
) {
|
||||
this.router.navigate([''], { replaceUrl: true })
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}),
|
||||
startWith(null),
|
||||
shareReplay(1),
|
||||
)
|
||||
|
||||
private readonly alert = merge(
|
||||
this.trigger$.pipe(skip(1)),
|
||||
this.network$.pipe(filter(v => !v)),
|
||||
)
|
||||
.pipe(
|
||||
exhaustMap(() =>
|
||||
concat(
|
||||
this.alerts
|
||||
.open('Trying to reach server', {
|
||||
label: 'State unknown',
|
||||
autoClose: false,
|
||||
status: TuiNotification.Error,
|
||||
})
|
||||
.pipe(
|
||||
takeUntil(
|
||||
combineLatest([this.stream$, this.network$]).pipe(
|
||||
filter(state => state.every(Boolean)),
|
||||
),
|
||||
),
|
||||
),
|
||||
this.alerts.open('Connection restored', {
|
||||
label: 'Server reached',
|
||||
status: TuiNotification.Success,
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
.subscribe()
|
||||
|
||||
constructor() {
|
||||
super(subscriber => this.stream$.subscribe(subscriber))
|
||||
}
|
||||
|
||||
retrigger() {
|
||||
this.trigger$.next()
|
||||
}
|
||||
|
||||
async syncState() {
|
||||
const state = await this.api.getState()
|
||||
this.single$.next(state)
|
||||
}
|
||||
}
|
||||
|
||||
export function stateNot(state: RR.ServerState[]): CanActivateFn {
|
||||
return () =>
|
||||
inject(StateService).pipe(
|
||||
filter(current => !current || !state.includes(current)),
|
||||
map(ALWAYS_TRUE_HANDLER),
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Inject, Injectable } from '@angular/core'
|
||||
import { DOCUMENT } from '@angular/common'
|
||||
|
||||
const PREFIX = '_embassystorage/_embassykv/'
|
||||
const PREFIX = '_startos/'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
@@ -15,16 +15,21 @@ export class StorageService {
|
||||
return JSON.parse(String(this.storage.getItem(`${PREFIX}${key}`)))
|
||||
}
|
||||
|
||||
set<T>(key: string, value: T) {
|
||||
set(key: string, value: any) {
|
||||
this.storage.setItem(`${PREFIX}${key}`, JSON.stringify(value))
|
||||
}
|
||||
|
||||
clear() {
|
||||
Array.from(
|
||||
{ length: this.storage.length },
|
||||
(_, i) => this.storage.key(i) || '',
|
||||
)
|
||||
.filter(key => key.startsWith(PREFIX))
|
||||
.forEach(key => this.storage.removeItem(key))
|
||||
this.storage.clear()
|
||||
}
|
||||
|
||||
migrate036() {
|
||||
const oldPrefix = '_embassystorage/_embassykv/'
|
||||
if (!!this.storage.getItem(`${oldPrefix}loggedInKey`)) {
|
||||
const cache = this.storage.getItem(`${oldPrefix}patch-db-cache`)
|
||||
this.clear()
|
||||
this.set('loggedIn', true)
|
||||
this.set('patchDB', cache)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user