mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-31 04:23:40 +00:00
Feat/combine uis (#2633)
* wip * restructure backend for new ui structure * new patchdb bootstrap, single websocket api, local storage migration, more * update db websocket * init apis * update patch-db * setup progress * feat: implement state service, alert and routing Signed-off-by: waterplea <alexander@inkin.ru> * update setup wizard for new types * feat: add init page Signed-off-by: waterplea <alexander@inkin.ru> * chore: refactor message, patch-db source stream and connection service Signed-off-by: waterplea <alexander@inkin.ru> * fix method not found on state * fix backend bugs * fix compat assets * address comments * remove unneeded styling * cleaner progress * bugfixes * fix init logs * fix progress reporting * fix navigation by getting state after init * remove patch dependency from live api * fix caching * re-add patchDB to live api * fix metrics values * send close frame * add bootId and fix polling --------- Signed-off-by: waterplea <alexander@inkin.ru> Co-authored-by: Aiden McClelland <me@drbonez.dev> Co-authored-by: waterplea <alexander@inkin.ru>
This commit is contained in:
@@ -16,7 +16,7 @@ export module Mock {
|
||||
restarting: false,
|
||||
shuttingDown: false,
|
||||
}
|
||||
export const MarketplaceEos: RR.GetMarketplaceEosRes = {
|
||||
export const MarketplaceEos: RR.CheckOSUpdateRes = {
|
||||
version: '0.3.5.2',
|
||||
headline: 'Our biggest release ever.',
|
||||
releaseNotes: {
|
||||
@@ -493,30 +493,23 @@ export module Mock {
|
||||
{
|
||||
timestamp: '2022-07-28T03:52:54.808769Z',
|
||||
message: '****** START *****',
|
||||
bootId: 'hsjnfdklasndhjasvbjamsksajbndjn',
|
||||
},
|
||||
{
|
||||
timestamp: '2019-12-26T14:21:30.872Z',
|
||||
message:
|
||||
'\u001b[34mPOST \u001b[0;32;49m200\u001b[0m photoview.startos/api/graphql \u001b[0;36;49m1.169406ms\u001b',
|
||||
bootId: 'hsjnfdklasndhjasvbjamsksajbndjn',
|
||||
},
|
||||
{
|
||||
timestamp: '2019-12-26T14:22:30.872Z',
|
||||
message: '****** FINISH *****',
|
||||
},
|
||||
]
|
||||
|
||||
export const PackageLogs: Log[] = [
|
||||
{
|
||||
timestamp: '2022-07-28T03:52:54.808769Z',
|
||||
message: '****** START *****',
|
||||
bootId: 'gvbwfiuasokdasjndasnjdmfvbahjdmdkfm',
|
||||
},
|
||||
{
|
||||
timestamp: '2019-12-26T14:21:30.872Z',
|
||||
message: 'PackageLogs PackageLogs PackageLogs PackageLogs PackageLogs',
|
||||
},
|
||||
{
|
||||
timestamp: '2019-12-26T14:22:30.872Z',
|
||||
message: '****** FINISH *****',
|
||||
timestamp: '2019-12-26T15:22:30.872Z',
|
||||
message: '****** AGAIN *****',
|
||||
bootId: 'gvbwfiuasokdasjndasnjdmfvbahjdmdkfm',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@@ -1,17 +1,28 @@
|
||||
import { Dump, Revision } from 'patch-db-client'
|
||||
import { Dump } from 'patch-db-client'
|
||||
import { MarketplacePkg, StoreInfo } from '@start9labs/marketplace'
|
||||
import { PackagePropertiesVersioned } from 'src/app/util/properties.util'
|
||||
import { ConfigSpec } from 'src/app/pkg-config/config-types'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { StartOSDiskInfo, LogsRes, ServerLogsReq } from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { WebSocketSubjectConfig } from 'rxjs/webSocket'
|
||||
|
||||
export module RR {
|
||||
// websocket
|
||||
|
||||
export type WebsocketConfig<T> = Omit<WebSocketSubjectConfig<T>, 'url'>
|
||||
|
||||
// server state
|
||||
|
||||
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
|
||||
@@ -33,10 +44,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 = {
|
||||
@@ -65,8 +88,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
|
||||
@@ -254,8 +277,8 @@ export module RR {
|
||||
export type GetMarketplaceInfoReq = { serverId: string }
|
||||
export type GetMarketplaceInfoRes = StoreInfo
|
||||
|
||||
export type GetMarketplaceEosReq = { serverId: string }
|
||||
export type GetMarketplaceEosRes = MarketplaceEOS
|
||||
export type CheckOSUpdateReq = { serverId: string }
|
||||
export type CheckOSUpdateRes = OSUpdate
|
||||
|
||||
export type GetMarketplacePackagesReq = {
|
||||
ids?: { id: string; version: string }[]
|
||||
@@ -271,7 +294,7 @@ export module RR {
|
||||
export type GetReleaseNotesRes = { [version: string]: string }
|
||||
}
|
||||
|
||||
export interface MarketplaceEOS {
|
||||
export interface OSUpdate {
|
||||
version: string
|
||||
headline: string
|
||||
releaseNotes: { [version: string]: string }
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import { Observable } from 'rxjs'
|
||||
import { Update } from 'patch-db-client'
|
||||
import { RR } from './api.types'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { Log } from '@start9labs/shared'
|
||||
import { WebSocketSubjectConfig } from 'rxjs/webSocket'
|
||||
|
||||
export abstract class ApiService {
|
||||
// http
|
||||
@@ -14,8 +10,23 @@ export abstract class ApiService {
|
||||
// for sideloading packages
|
||||
abstract uploadPackage(guid: string, body: Blob): Promise<string>
|
||||
|
||||
// websocket
|
||||
|
||||
abstract openWebsocket$<T>(
|
||||
guid: string,
|
||||
config: RR.WebsocketConfig<T>,
|
||||
): Observable<T>
|
||||
|
||||
// server state
|
||||
|
||||
abstract getState(): Promise<RR.ServerState>
|
||||
|
||||
// db
|
||||
|
||||
abstract subscribeToPatchDB(
|
||||
params: RR.SubscribePatchReq,
|
||||
): Promise<RR.SubscribePatchRes>
|
||||
|
||||
abstract setDbValue<T>(
|
||||
pathArr: Array<string | number>,
|
||||
value: T,
|
||||
@@ -35,16 +46,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 getSystemTime(
|
||||
params: RR.GetSystemTimeReq,
|
||||
): Promise<RR.GetSystemTimeRes>
|
||||
@@ -89,11 +110,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>
|
||||
|
||||
@@ -105,7 +122,7 @@ export abstract class ApiService {
|
||||
url: string,
|
||||
): Promise<T>
|
||||
|
||||
abstract getEos(): Promise<RR.GetMarketplaceEosRes>
|
||||
abstract checkOSUpdate(qp: RR.CheckOSUpdateReq): Promise<RR.CheckOSUpdateRes>
|
||||
|
||||
// notification
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
HttpOptions,
|
||||
HttpService,
|
||||
isRpcError,
|
||||
Log,
|
||||
Method,
|
||||
RpcError,
|
||||
RPCOptions,
|
||||
@@ -12,13 +11,12 @@ import { ApiService } from './embassy-api.service'
|
||||
import { RR } from './api.types'
|
||||
import { parsePropertiesPermissive } from 'src/app/util/properties.util'
|
||||
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/util/get-server-info'
|
||||
import { PatchDB, pathFromArray } from 'patch-db-client'
|
||||
|
||||
@Injectable()
|
||||
export class LiveApiService extends ApiService {
|
||||
@@ -30,10 +28,11 @@ export class LiveApiService extends ApiService {
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
) {
|
||||
super()
|
||||
;(window as any).rpcClient = this
|
||||
; (window as any).rpcClient = this
|
||||
}
|
||||
|
||||
// for getting static files: ex icons, instructions, licenses
|
||||
|
||||
async getStatic(url: string): Promise<string> {
|
||||
return this.httpRequest({
|
||||
method: Method.GET,
|
||||
@@ -43,6 +42,7 @@ export class LiveApiService extends ApiService {
|
||||
}
|
||||
|
||||
// for sideloading packages
|
||||
|
||||
async uploadPackage(guid: string, body: Blob): Promise<string> {
|
||||
return this.httpRequest({
|
||||
method: Method.POST,
|
||||
@@ -52,8 +52,36 @@ export class LiveApiService extends ApiService {
|
||||
})
|
||||
}
|
||||
|
||||
// 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 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,
|
||||
@@ -87,29 +115,57 @@ 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(): 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)
|
||||
}
|
||||
|
||||
async getSystemTime(
|
||||
params: RR.GetSystemTimeReq,
|
||||
): Promise<RR.GetSystemTimeRes> {
|
||||
@@ -175,12 +231,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 })
|
||||
}
|
||||
@@ -203,10 +253,7 @@ export class LiveApiService extends ApiService {
|
||||
})
|
||||
}
|
||||
|
||||
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> {
|
||||
return this.marketplaceProxy(
|
||||
'/eos/v0/latest',
|
||||
qp,
|
||||
@@ -417,16 +464,6 @@ export class LiveApiService extends ApiService {
|
||||
})
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -445,9 +482,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.patch.cache$.pipe(filter(({ id }) => id >= Number(patchSequence))),
|
||||
)
|
||||
|
||||
return body.result
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { Log, pauseFor } from '@start9labs/shared'
|
||||
import { Log, RPCErrorDetails, pauseFor } from '@start9labs/shared'
|
||||
import { ApiService } from './embassy-api.service'
|
||||
import {
|
||||
Operation,
|
||||
PatchOp,
|
||||
pathFromArray,
|
||||
RemoveOperation,
|
||||
Update,
|
||||
Revision,
|
||||
} from 'patch-db-client'
|
||||
import {
|
||||
DataModel,
|
||||
InstallingState,
|
||||
PackageDataEntry,
|
||||
StateInfo,
|
||||
@@ -20,22 +19,17 @@ import { parsePropertiesPermissive } from 'src/app/util/properties.util'
|
||||
import { Mock } from './api.fixures'
|
||||
import markdown from 'raw-loader!../../../../../shared/assets/markdown/md-sample.md'
|
||||
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'
|
||||
|
||||
@@ -71,32 +65,17 @@ 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()
|
||||
}
|
||||
@@ -111,8 +90,57 @@ export class MockApiService extends ApiService {
|
||||
return 'success'
|
||||
}
|
||||
|
||||
// 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')
|
||||
}
|
||||
}
|
||||
|
||||
// server state
|
||||
|
||||
private stateIndex = 0
|
||||
async getState(): Promise<RR.ServerState> {
|
||||
await pauseFor(1000)
|
||||
|
||||
this.stateIndex++
|
||||
|
||||
return this.stateIndex === 1 ? 'initializing' : '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,
|
||||
@@ -136,11 +164,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
|
||||
}
|
||||
|
||||
@@ -166,34 +189,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
|
||||
|
||||
async getSystemTime(
|
||||
params: RR.GetSystemTimeReq,
|
||||
@@ -248,7 +300,7 @@ export class MockApiService extends ApiService {
|
||||
await pauseFor(2000)
|
||||
return {
|
||||
startCursor: 'start-cursor',
|
||||
guid: '7251d5be-645f-4362-a51b-3a85be92b31e',
|
||||
guid: 'logs-guid',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -258,7 +310,7 @@ export class MockApiService extends ApiService {
|
||||
await pauseFor(2000)
|
||||
return {
|
||||
startCursor: 'start-cursor',
|
||||
guid: '7251d5be-645f-4362-a51b-3a85be92b31e',
|
||||
guid: 'logs-guid',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,11 +320,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)
|
||||
@@ -374,12 +426,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
|
||||
@@ -422,7 +468,7 @@ export class MockApiService extends ApiService {
|
||||
}
|
||||
}
|
||||
|
||||
async getEos(): Promise<RR.GetMarketplaceEosRes> {
|
||||
async checkOSUpdate(qp: RR.CheckOSUpdateReq): Promise<RR.CheckOSUpdateRes> {
|
||||
await pauseFor(2000)
|
||||
return Mock.MarketplaceEos
|
||||
}
|
||||
@@ -641,13 +687,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 {
|
||||
@@ -663,7 +709,7 @@ export class MockApiService extends ApiService {
|
||||
await pauseFor(2000)
|
||||
return {
|
||||
startCursor: 'start-cursor',
|
||||
guid: '7251d5be-645f-4362-a51b-3a85be92b31e',
|
||||
guid: 'logs-guid',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -673,7 +719,7 @@ export class MockApiService extends ApiService {
|
||||
await pauseFor(2000)
|
||||
|
||||
setTimeout(async () => {
|
||||
this.updateProgress(params.id)
|
||||
this.installProgress(params.id)
|
||||
}, 1000)
|
||||
|
||||
const patch: Operation<
|
||||
@@ -745,7 +791,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 {
|
||||
@@ -1013,7 +1059,57 @@ export class MockApiService extends ApiService {
|
||||
return '4120e092-05ab-4de2-9fbd-c3f1f4b1df9e' // no significance, randomly generated
|
||||
}
|
||||
|
||||
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 (
|
||||
!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()) {
|
||||
@@ -1194,10 +1290,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,
|
||||
|
||||
@@ -12,7 +12,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(
|
||||
|
||||
@@ -1,25 +1,23 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { combineLatest, fromEvent, merge, ReplaySubject } from 'rxjs'
|
||||
import { distinctUntilChanged, map, startWith } from 'rxjs/operators'
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Injectable } from '@angular/core'
|
||||
import { Emver } from '@start9labs/shared'
|
||||
import { BehaviorSubject, combineLatest } from 'rxjs'
|
||||
import { distinctUntilChanged, map } from 'rxjs/operators'
|
||||
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 { PatchDB } from 'patch-db-client'
|
||||
import { getServerInfo } from 'src/app/util/get-server-info'
|
||||
@@ -12,7 +12,7 @@ import { DataModel } from './patch-db/data-model'
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class EOSService {
|
||||
eos?: MarketplaceEOS
|
||||
osUpdate?: OSUpdate
|
||||
updateAvailable$ = new BehaviorSubject<boolean>(false)
|
||||
|
||||
readonly updating$ = this.patch.watch$('serverInfo', 'statusInfo').pipe(
|
||||
@@ -52,9 +52,10 @@ export class EOSService {
|
||||
) {}
|
||||
|
||||
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.emver.compare(this.osUpdate.version, version) === 1
|
||||
this.updateAvailable$.next(updateAvailable)
|
||||
}
|
||||
}
|
||||
|
||||
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,9 +1,9 @@
|
||||
import { Inject, Injectable } from '@angular/core'
|
||||
import { ModalController } from '@ionic/angular'
|
||||
import { Observable } from 'rxjs'
|
||||
import { filter, share, switchMap, take, tap } from 'rxjs/operators'
|
||||
import { filter, map, share, switchMap, take, tap } from 'rxjs/operators'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { DataModel, UIData } from 'src/app/services/patch-db/data-model'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { EOSService } from 'src/app/services/eos.service'
|
||||
import { OSWelcomePage } from 'src/app/modals/os-welcome/os-welcome.page'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
@@ -11,21 +11,25 @@ import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { MarketplaceService } from 'src/app/services/marketplace.service'
|
||||
import { AbstractMarketplaceService } from '@start9labs/marketplace'
|
||||
import { ConnectionService } from 'src/app/services/connection.service'
|
||||
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))
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import { InjectionToken, Injector } from '@angular/core'
|
||||
import { Revision, Update } from 'patch-db-client'
|
||||
import { defer, EMPTY, from, Observable } from 'rxjs'
|
||||
import {
|
||||
bufferTime,
|
||||
catchError,
|
||||
filter,
|
||||
startWith,
|
||||
switchMap,
|
||||
take,
|
||||
tap,
|
||||
} from 'rxjs/operators'
|
||||
import { Update } from 'patch-db-client'
|
||||
import { DataModel } from './data-model'
|
||||
import { defer, EMPTY, from, interval, Observable } from 'rxjs'
|
||||
import { AuthService } from '../auth.service'
|
||||
import { ConnectionService } from '../connection.service'
|
||||
import { StateService } from 'src/app/services/state.service'
|
||||
import { ApiService } from '../api/embassy-api.service'
|
||||
import { ConfigService } from '../config.service'
|
||||
import { AuthService } from '../auth.service'
|
||||
import { DataModel } from './data-model'
|
||||
import { LocalStorageBootstrap } from './local-storage-bootstrap'
|
||||
|
||||
export const PATCH_SOURCE = new InjectionToken<Observable<Update<DataModel>[]>>(
|
||||
'',
|
||||
@@ -25,33 +25,31 @@ export function sourceFactory(
|
||||
// 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 auth = injector.get(AuthService)
|
||||
const state = injector.get(StateService)
|
||||
const bootstrapper = injector.get(LocalStorageBootstrap)
|
||||
|
||||
const websocket$ = api.openPatchWebsocket$().pipe(
|
||||
bufferTime(250),
|
||||
filter(updates => !!updates.length),
|
||||
catchError((_, watch$) => {
|
||||
connectionService.websocketConnected$.next(false)
|
||||
return auth.isVerified$.pipe(
|
||||
switchMap(verified =>
|
||||
verified ? from(api.subscribeToPatchDB({})) : EMPTY,
|
||||
),
|
||||
switchMap(({ dump, guid }) =>
|
||||
api.openWebsocket$<Revision>(guid, {}).pipe(
|
||||
bufferTime(250),
|
||||
filter(revisions => !!revisions.length),
|
||||
startWith([dump]),
|
||||
),
|
||||
),
|
||||
catchError((_, original$) => {
|
||||
state.retrigger()
|
||||
|
||||
return interval(timeout).pipe(
|
||||
switchMap(() =>
|
||||
from(api.echo({ message: 'ping', timeout })).pipe(
|
||||
catchError(() => EMPTY),
|
||||
),
|
||||
),
|
||||
return state.pipe(
|
||||
filter(current => current === 'running'),
|
||||
take(1),
|
||||
switchMap(() => watch$),
|
||||
switchMap(() => original$),
|
||||
)
|
||||
}),
|
||||
tap(() => connectionService.websocketConnected$.next(true)),
|
||||
)
|
||||
|
||||
return authService.isVerified$.pipe(
|
||||
switchMap(verified => (verified ? websocket$ : EMPTY)),
|
||||
startWith([bootstrapper.init()]),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4,24 +4,19 @@ import { tap } from 'rxjs/operators'
|
||||
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({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class PatchMonitorService extends Observable<any> {
|
||||
// @TODO not happy with Observable<void>
|
||||
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() // @TODO shouldn't this be subscribed in app component with the others? Do we ever need to unsubscribe?
|
||||
|
||||
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