mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-04-04 14:29:45 +00:00
refactor: isolate network toast and login redirect to separate services (#1412)
* refactor: isolate network toast and login redirect to separate services * chore: remove accidentally committed sketch of a service * chore: tidying things up * feat: add `GlobalModule` encapsulating all global subscription services * remove angular build cache when building deps * chore: fix more issues found while testing * chore: fix issues reported by testing * chore: fix template error * chore: fix server-info * chore: fix server-info * fix: switch to Observable to fix race conditions * fix embassy name display on load * update patchdb * clean up patch data watch Co-authored-by: Lucy Cifferello <12953208+elvece@users.noreply.github.com>
This commit is contained in:
@@ -266,7 +266,7 @@ export abstract class ApiService implements Source<DataModel>, Http<DataModel> {
|
||||
private syncResponse<
|
||||
T,
|
||||
F extends (...args: any[]) => Promise<{ response: T; revision?: Revision }>,
|
||||
>(f: F, temp?: Operation): (...args: Parameters<F>) => Promise<T> {
|
||||
>(f: F, temp?: Operation<unknown>): (...args: Parameters<F>) => Promise<T> {
|
||||
return (...a) => {
|
||||
// let expireId = undefined
|
||||
// if (temp) {
|
||||
|
||||
@@ -19,9 +19,23 @@ import { BehaviorSubject } from 'rxjs'
|
||||
import { LocalStorageBootstrap } from '../patch-db/local-storage-bootstrap'
|
||||
import { mockPatchData } from './mock-patch'
|
||||
|
||||
const PROGRESS: InstallProgress = {
|
||||
size: 120,
|
||||
downloaded: 0,
|
||||
'download-complete': false,
|
||||
validated: 0,
|
||||
'validation-complete': false,
|
||||
unpacked: 0,
|
||||
'unpack-complete': false,
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class MockApiService extends ApiService {
|
||||
readonly mockPatch$ = new BehaviorSubject<Update<DataModel>>(undefined)
|
||||
readonly mockPatch$ = new BehaviorSubject<Update<DataModel>>({
|
||||
id: 1,
|
||||
value: mockPatchData,
|
||||
expireId: null,
|
||||
})
|
||||
private readonly revertTime = 4000
|
||||
sequence: number
|
||||
|
||||
@@ -391,8 +405,13 @@ export class MockApiService extends ApiService {
|
||||
|
||||
await pauseFor(8000)
|
||||
|
||||
appPatch[0].value = PackageMainStatus.Stopped
|
||||
this.updateMock(appPatch)
|
||||
const newPatch = [
|
||||
{
|
||||
...appPatch[0],
|
||||
value: PackageMainStatus.Stopped,
|
||||
},
|
||||
]
|
||||
this.updateMock(newPatch)
|
||||
}
|
||||
|
||||
await pauseFor(1000)
|
||||
@@ -454,32 +473,21 @@ export class MockApiService extends ApiService {
|
||||
params: RR.InstallPackageReq,
|
||||
): Promise<RR.InstallPackageRes> {
|
||||
await pauseFor(2000)
|
||||
const initialProgress: InstallProgress = {
|
||||
size: 120,
|
||||
downloaded: 0,
|
||||
'download-complete': false,
|
||||
validated: 0,
|
||||
'validation-complete': false,
|
||||
unpacked: 0,
|
||||
'unpack-complete': false,
|
||||
}
|
||||
|
||||
setTimeout(async () => {
|
||||
this.updateProgress(params.id, initialProgress)
|
||||
this.updateProgress(params.id)
|
||||
}, 1000)
|
||||
|
||||
const pkg: PackageDataEntry = {
|
||||
...Mock.LocalPkgs[params.id],
|
||||
state: PackageState.Installing,
|
||||
'install-progress': initialProgress,
|
||||
installed: undefined,
|
||||
}
|
||||
|
||||
const patch = [
|
||||
const patch: Operation<PackageDataEntry>[] = [
|
||||
{
|
||||
op: PatchOp.ADD,
|
||||
path: `/package-data/${params.id}`,
|
||||
value: pkg,
|
||||
value: {
|
||||
...Mock.LocalPkgs[params.id],
|
||||
state: PackageState.Installing,
|
||||
'install-progress': { ...PROGRESS },
|
||||
installed: undefined,
|
||||
},
|
||||
},
|
||||
]
|
||||
return this.withRevision(patch)
|
||||
@@ -527,32 +535,20 @@ export class MockApiService extends ApiService {
|
||||
params: RR.RestorePackagesReq,
|
||||
): Promise<RR.RestorePackagesRes> {
|
||||
await pauseFor(2000)
|
||||
const patch: Operation[] = params.ids.map(id => {
|
||||
const initialProgress: InstallProgress = {
|
||||
size: 120,
|
||||
downloaded: 120,
|
||||
'download-complete': true,
|
||||
validated: 0,
|
||||
'validation-complete': false,
|
||||
unpacked: 0,
|
||||
'unpack-complete': false,
|
||||
}
|
||||
|
||||
const pkg: PackageDataEntry = {
|
||||
...Mock.LocalPkgs[id],
|
||||
state: PackageState.Restoring,
|
||||
'install-progress': initialProgress,
|
||||
installed: undefined,
|
||||
}
|
||||
|
||||
const patch: Operation<PackageDataEntry>[] = params.ids.map(id => {
|
||||
setTimeout(async () => {
|
||||
this.updateProgress(id, initialProgress)
|
||||
this.updateProgress(id)
|
||||
}, 2000)
|
||||
|
||||
return {
|
||||
op: PatchOp.ADD,
|
||||
path: `/package-data/${id}`,
|
||||
value: pkg,
|
||||
value: {
|
||||
...Mock.LocalPkgs[id],
|
||||
state: PackageState.Restoring,
|
||||
'install-progress': { ...PROGRESS },
|
||||
installed: undefined,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
@@ -703,11 +699,11 @@ export class MockApiService extends ApiService {
|
||||
await pauseFor(2000)
|
||||
|
||||
setTimeout(async () => {
|
||||
const patch2 = [
|
||||
const patch2: RemoveOperation[] = [
|
||||
{
|
||||
op: PatchOp.REMOVE,
|
||||
path: `/package-data/${params.id}`,
|
||||
} as RemoveOperation,
|
||||
},
|
||||
]
|
||||
this.updateMock(patch2)
|
||||
}, this.revertTime)
|
||||
@@ -727,11 +723,11 @@ export class MockApiService extends ApiService {
|
||||
params: RR.DeleteRecoveredPackageReq,
|
||||
): Promise<RR.DeleteRecoveredPackageRes> {
|
||||
await pauseFor(2000)
|
||||
const patch = [
|
||||
const patch: RemoveOperation[] = [
|
||||
{
|
||||
op: PatchOp.REMOVE,
|
||||
path: `/recovered-packages/${params.id}`,
|
||||
} as RemoveOperation,
|
||||
},
|
||||
]
|
||||
return this.withRevision(patch)
|
||||
}
|
||||
@@ -747,30 +743,30 @@ export class MockApiService extends ApiService {
|
||||
}
|
||||
}
|
||||
|
||||
private async updateProgress(
|
||||
id: string,
|
||||
initialProgress: InstallProgress,
|
||||
): Promise<void> {
|
||||
private async updateProgress(id: string): Promise<void> {
|
||||
const progress = { ...PROGRESS }
|
||||
const phases = [
|
||||
{ progress: 'downloaded', completion: 'download-complete' },
|
||||
{ progress: 'validated', completion: 'validation-complete' },
|
||||
{ progress: 'unpacked', completion: 'unpack-complete' },
|
||||
]
|
||||
|
||||
for (let phase of phases) {
|
||||
let i = initialProgress[phase.progress]
|
||||
while (i < initialProgress.size) {
|
||||
let i = progress[phase.progress]
|
||||
while (i < progress.size) {
|
||||
await pauseFor(250)
|
||||
i = Math.min(i + 5, initialProgress.size)
|
||||
initialProgress[phase.progress] = i
|
||||
if (i === initialProgress.size) {
|
||||
initialProgress[phase.completion] = true
|
||||
i = Math.min(i + 5, progress.size)
|
||||
progress[phase.progress] = i
|
||||
|
||||
if (i === progress.size) {
|
||||
progress[phase.completion] = true
|
||||
}
|
||||
|
||||
const patch = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: `/package-data/${id}/install-progress`,
|
||||
value: initialProgress,
|
||||
value: { ...progress },
|
||||
},
|
||||
]
|
||||
this.updateMock(patch)
|
||||
@@ -778,7 +774,7 @@ export class MockApiService extends ApiService {
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
const patch2: any = [
|
||||
const patch2: Operation<PackageDataEntry>[] = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: `/package-data/${id}`,
|
||||
@@ -822,7 +818,7 @@ export class MockApiService extends ApiService {
|
||||
this.updateMock(patch2)
|
||||
|
||||
setTimeout(async () => {
|
||||
const patch3: Operation[] = [
|
||||
const patch3: Operation<ServerStatus>[] = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: '/server-info/status',
|
||||
@@ -847,7 +843,7 @@ export class MockApiService extends ApiService {
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
private async updateMock(patch: Operation[]): Promise<void> {
|
||||
private async updateMock<T>(patch: Operation<T>[]): Promise<void> {
|
||||
if (!this.sequence) {
|
||||
const { sequence } = await this.bootstrapper.init()
|
||||
this.sequence = sequence
|
||||
@@ -861,7 +857,7 @@ export class MockApiService extends ApiService {
|
||||
}
|
||||
|
||||
private async withRevision<T>(
|
||||
patch: Operation[],
|
||||
patch: Operation<unknown>[],
|
||||
response: T = null,
|
||||
): Promise<WithRevision<T>> {
|
||||
if (!this.sequence) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { BehaviorSubject, Observable } from 'rxjs'
|
||||
import { distinctUntilChanged } from 'rxjs/operators'
|
||||
import { Observable, ReplaySubject } from 'rxjs'
|
||||
import { distinctUntilChanged, map } from 'rxjs/operators'
|
||||
import { Storage } from '@ionic/storage-angular'
|
||||
|
||||
export enum AuthState {
|
||||
@@ -12,27 +12,29 @@ export enum AuthState {
|
||||
})
|
||||
export class AuthService {
|
||||
private readonly LOGGED_IN_KEY = 'loggedInKey'
|
||||
private readonly authState$: BehaviorSubject<AuthState> = new BehaviorSubject(undefined)
|
||||
private readonly authState$ = new ReplaySubject<AuthState>(1)
|
||||
|
||||
constructor (
|
||||
private readonly storage: Storage,
|
||||
) { }
|
||||
readonly isVerified$ = this.watch$().pipe(
|
||||
map(state => state === AuthState.VERIFIED),
|
||||
)
|
||||
|
||||
async init (): Promise<void> {
|
||||
constructor(private readonly storage: Storage) {}
|
||||
|
||||
async init(): Promise<void> {
|
||||
const loggedIn = await this.storage.get(this.LOGGED_IN_KEY)
|
||||
this.authState$.next( loggedIn ? AuthState.VERIFIED : AuthState.UNVERIFIED)
|
||||
this.authState$.next(loggedIn ? AuthState.VERIFIED : AuthState.UNVERIFIED)
|
||||
}
|
||||
|
||||
watch$ (): Observable<AuthState> {
|
||||
watch$(): Observable<AuthState> {
|
||||
return this.authState$.pipe(distinctUntilChanged())
|
||||
}
|
||||
|
||||
async setVerified (): Promise<void> {
|
||||
async setVerified(): Promise<void> {
|
||||
await this.storage.set(this.LOGGED_IN_KEY, true)
|
||||
this.authState$.next(AuthState.VERIFIED)
|
||||
}
|
||||
|
||||
async setUnverified (): Promise<void> {
|
||||
async setUnverified(): Promise<void> {
|
||||
this.authState$.next(AuthState.UNVERIFIED)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,21 +4,34 @@ import {
|
||||
combineLatest,
|
||||
fromEvent,
|
||||
merge,
|
||||
Observable,
|
||||
Subject,
|
||||
Subscription,
|
||||
} from 'rxjs'
|
||||
import { PatchConnection, PatchDbService } from './patch-db/patch-db.service'
|
||||
import { distinctUntilChanged } from 'rxjs/operators'
|
||||
import {
|
||||
distinctUntilChanged,
|
||||
map,
|
||||
mapTo,
|
||||
startWith,
|
||||
tap,
|
||||
} from 'rxjs/operators'
|
||||
import { ConfigService } from './config.service'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ConnectionService {
|
||||
private readonly networkState$ = new BehaviorSubject<boolean>(true)
|
||||
private readonly connectionFailure$ = new BehaviorSubject<ConnectionFailure>(
|
||||
ConnectionFailure.None,
|
||||
private readonly networkState$ = merge(
|
||||
fromEvent(window, 'online').pipe(mapTo(true)),
|
||||
fromEvent(window, 'offline').pipe(mapTo(false)),
|
||||
).pipe(
|
||||
startWith(null),
|
||||
map(() => navigator.onLine),
|
||||
)
|
||||
|
||||
private readonly connectionFailure$ = new Subject<ConnectionFailure>()
|
||||
|
||||
constructor(
|
||||
private readonly configService: ConfigService,
|
||||
private readonly patch: PatchDbService,
|
||||
@@ -28,15 +41,8 @@ export class ConnectionService {
|
||||
return this.connectionFailure$.asObservable()
|
||||
}
|
||||
|
||||
start(): Subscription[] {
|
||||
const sub1 = merge(
|
||||
fromEvent(window, 'online'),
|
||||
fromEvent(window, 'offline'),
|
||||
).subscribe(event => {
|
||||
this.networkState$.next(event.type === 'online')
|
||||
})
|
||||
|
||||
const sub2 = combineLatest([
|
||||
start(): Observable<unknown> {
|
||||
return combineLatest([
|
||||
// 1
|
||||
this.networkState$.pipe(distinctUntilChanged()),
|
||||
// 2
|
||||
@@ -45,20 +51,21 @@ export class ConnectionService {
|
||||
this.patch
|
||||
.watch$('server-info', 'status-info', 'update-progress')
|
||||
.pipe(distinctUntilChanged()),
|
||||
]).subscribe(async ([network, patchConnection, progress]) => {
|
||||
if (!network) {
|
||||
this.connectionFailure$.next(ConnectionFailure.Network)
|
||||
} else if (patchConnection !== PatchConnection.Disconnected) {
|
||||
this.connectionFailure$.next(ConnectionFailure.None)
|
||||
} else if (!!progress && progress.downloaded === progress.size) {
|
||||
this.connectionFailure$.next(ConnectionFailure.None)
|
||||
} else if (!this.configService.isTor()) {
|
||||
this.connectionFailure$.next(ConnectionFailure.Lan)
|
||||
} else {
|
||||
this.connectionFailure$.next(ConnectionFailure.Tor)
|
||||
}
|
||||
})
|
||||
return [sub1, sub2]
|
||||
]).pipe(
|
||||
tap(([network, patchConnection, progress]) => {
|
||||
if (!network) {
|
||||
this.connectionFailure$.next(ConnectionFailure.Network)
|
||||
} else if (patchConnection !== PatchConnection.Disconnected) {
|
||||
this.connectionFailure$.next(ConnectionFailure.None)
|
||||
} else if (!!progress && progress.downloaded === progress.size) {
|
||||
this.connectionFailure$.next(ConnectionFailure.None)
|
||||
} else if (!this.configService.isTor()) {
|
||||
this.connectionFailure$.next(ConnectionFailure.Lan)
|
||||
} else {
|
||||
this.connectionFailure$.next(ConnectionFailure.Tor)
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import {
|
||||
AbstractControl,
|
||||
FormArray,
|
||||
FormBuilder,
|
||||
FormControl,
|
||||
FormGroup,
|
||||
ValidationErrors,
|
||||
ValidatorFn,
|
||||
Validators,
|
||||
} from '@angular/forms'
|
||||
@@ -69,7 +67,7 @@ export class FormService {
|
||||
}
|
||||
|
||||
getListItem(spec: ValueSpecList, entry: any) {
|
||||
const listItemValidators = this.getListItemValidators(spec)
|
||||
const listItemValidators = getListItemValidators(spec)
|
||||
if (isValueSpecListOf(spec, 'string')) {
|
||||
return this.formBuilder.control(entry, listItemValidators)
|
||||
} else if (isValueSpecListOf(spec, 'number')) {
|
||||
@@ -83,14 +81,6 @@ export class FormService {
|
||||
}
|
||||
}
|
||||
|
||||
private getListItemValidators(spec: ValueSpecList) {
|
||||
if (isValueSpecListOf(spec, 'string')) {
|
||||
return this.stringValidators(spec.spec)
|
||||
} else if (isValueSpecListOf(spec, 'number')) {
|
||||
return this.numberValidators(spec.spec)
|
||||
}
|
||||
}
|
||||
|
||||
private getFormGroup(
|
||||
config: ConfigSpec,
|
||||
validators: ValidatorFn[] = [],
|
||||
@@ -112,7 +102,7 @@ export class FormService {
|
||||
let value: any
|
||||
switch (spec.type) {
|
||||
case 'string':
|
||||
validators = this.stringValidators(spec)
|
||||
validators = stringValidators(spec)
|
||||
if (currentValue !== undefined) {
|
||||
value = currentValue
|
||||
} else {
|
||||
@@ -120,7 +110,7 @@ export class FormService {
|
||||
}
|
||||
return this.formBuilder.control(value, validators)
|
||||
case 'number':
|
||||
validators = this.numberValidators(spec)
|
||||
validators = numberValidators(spec)
|
||||
if (currentValue !== undefined) {
|
||||
value = currentValue
|
||||
} else {
|
||||
@@ -130,7 +120,7 @@ export class FormService {
|
||||
case 'object':
|
||||
return this.getFormGroup(spec.spec, [], currentValue)
|
||||
case 'list':
|
||||
validators = this.listValidators(spec)
|
||||
validators = listValidators(spec)
|
||||
const mapped = (
|
||||
Array.isArray(currentValue) ? currentValue : (spec.default as any[])
|
||||
).map(entry => {
|
||||
@@ -149,56 +139,64 @@ export class FormService {
|
||||
return this.formBuilder.control(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private stringValidators(
|
||||
spec: ValueSpecString | ListValueSpecString,
|
||||
): ValidatorFn[] {
|
||||
const validators: ValidatorFn[] = []
|
||||
function getListItemValidators(spec: ValueSpecList) {
|
||||
if (isValueSpecListOf(spec, 'string')) {
|
||||
return stringValidators(spec.spec)
|
||||
} else if (isValueSpecListOf(spec, 'number')) {
|
||||
return numberValidators(spec.spec)
|
||||
}
|
||||
}
|
||||
|
||||
if (!(spec as ValueSpecString).nullable) {
|
||||
validators.push(Validators.required)
|
||||
}
|
||||
function stringValidators(
|
||||
spec: ValueSpecString | ListValueSpecString,
|
||||
): ValidatorFn[] {
|
||||
const validators: ValidatorFn[] = []
|
||||
|
||||
if (spec.pattern) {
|
||||
validators.push(Validators.pattern(spec.pattern))
|
||||
}
|
||||
|
||||
return validators
|
||||
if (!(spec as ValueSpecString).nullable) {
|
||||
validators.push(Validators.required)
|
||||
}
|
||||
|
||||
private numberValidators(
|
||||
spec: ValueSpecNumber | ListValueSpecNumber,
|
||||
): ValidatorFn[] {
|
||||
const validators: ValidatorFn[] = []
|
||||
|
||||
validators.push(isNumber())
|
||||
|
||||
if (!(spec as ValueSpecNumber).nullable) {
|
||||
validators.push(Validators.required)
|
||||
}
|
||||
|
||||
if (spec.integral) {
|
||||
validators.push(isInteger())
|
||||
}
|
||||
|
||||
validators.push(numberInRange(spec.range))
|
||||
|
||||
return validators
|
||||
if (spec.pattern) {
|
||||
validators.push(Validators.pattern(spec.pattern))
|
||||
}
|
||||
|
||||
private listValidators(spec: ValueSpecList): ValidatorFn[] {
|
||||
const validators: ValidatorFn[] = []
|
||||
return validators
|
||||
}
|
||||
|
||||
validators.push(listInRange(spec.range))
|
||||
function numberValidators(
|
||||
spec: ValueSpecNumber | ListValueSpecNumber,
|
||||
): ValidatorFn[] {
|
||||
const validators: ValidatorFn[] = []
|
||||
|
||||
validators.push(listItemIssue())
|
||||
validators.push(isNumber())
|
||||
|
||||
if (!isValueSpecListOf(spec, 'enum')) {
|
||||
validators.push(listUnique(spec))
|
||||
}
|
||||
|
||||
return validators
|
||||
if (!(spec as ValueSpecNumber).nullable) {
|
||||
validators.push(Validators.required)
|
||||
}
|
||||
|
||||
if (spec.integral) {
|
||||
validators.push(isInteger())
|
||||
}
|
||||
|
||||
validators.push(numberInRange(spec.range))
|
||||
|
||||
return validators
|
||||
}
|
||||
|
||||
function listValidators(spec: ValueSpecList): ValidatorFn[] {
|
||||
const validators: ValidatorFn[] = []
|
||||
|
||||
validators.push(listInRange(spec.range))
|
||||
|
||||
validators.push(listItemIssue())
|
||||
|
||||
if (!isValueSpecListOf(spec, 'enum')) {
|
||||
validators.push(listUnique(spec))
|
||||
}
|
||||
|
||||
return validators
|
||||
}
|
||||
|
||||
function isFullUnion(
|
||||
@@ -208,48 +206,47 @@ function isFullUnion(
|
||||
}
|
||||
|
||||
export function numberInRange(stringRange: string): ValidatorFn {
|
||||
return (control: AbstractControl): ValidationErrors | null => {
|
||||
return control => {
|
||||
const value = control.value
|
||||
if (!value) return null
|
||||
try {
|
||||
Range.from(stringRange).checkIncludes(value)
|
||||
return null
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
return { numberNotInRange: { value: `Number must be ${e.message}` } }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function isNumber(): ValidatorFn {
|
||||
return (control: AbstractControl): ValidationErrors | null => {
|
||||
return !control.value || control.value == Number(control.value)
|
||||
return control =>
|
||||
!control.value || control.value == Number(control.value)
|
||||
? null
|
||||
: { notNumber: { value: control.value } }
|
||||
}
|
||||
}
|
||||
|
||||
export function isInteger(): ValidatorFn {
|
||||
return (control: AbstractControl): ValidationErrors | null => {
|
||||
return !control.value || control.value == Math.trunc(control.value)
|
||||
return control =>
|
||||
!control.value || control.value == Math.trunc(control.value)
|
||||
? null
|
||||
: { numberNotInteger: { value: control.value } }
|
||||
}
|
||||
}
|
||||
|
||||
export function listInRange(stringRange: string): ValidatorFn {
|
||||
return (control: FormArray): ValidationErrors | null => {
|
||||
return control => {
|
||||
try {
|
||||
Range.from(stringRange).checkIncludes(control.value.length)
|
||||
return null
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
return { listNotInRange: { value: `List must be ${e.message}` } }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function listItemIssue(): ValidatorFn {
|
||||
return (parentControl: FormArray): ValidationErrors | null => {
|
||||
const problemChild = parentControl.controls.find(c => c.invalid)
|
||||
return parentControl => {
|
||||
const { controls } = parentControl as FormArray
|
||||
const problemChild = controls.find(c => c.invalid)
|
||||
if (problemChild) {
|
||||
return { listItemIssue: { value: 'Invalid entries' } }
|
||||
} else {
|
||||
@@ -259,7 +256,7 @@ export function listItemIssue(): ValidatorFn {
|
||||
}
|
||||
|
||||
export function listUnique(spec: ValueSpecList): ValidatorFn {
|
||||
return (control: FormArray): ValidationErrors | null => {
|
||||
return control => {
|
||||
const list = control.value
|
||||
for (let idx = 0; idx < list.length; idx++) {
|
||||
for (let idx2 = idx + 1; idx2 < list.length; idx2++) {
|
||||
@@ -516,25 +513,29 @@ export function convertValuesRecursive(
|
||||
convertValuesRecursive(spec, control)
|
||||
} else if (valueSpec.type === 'list') {
|
||||
const formArr = group.get(key) as FormArray
|
||||
const { controls } = formArr
|
||||
|
||||
if (valueSpec.subtype === 'number') {
|
||||
formArr.controls.forEach(control => {
|
||||
controls.forEach(control => {
|
||||
control.setValue(control.value ? Number(control.value) : null)
|
||||
})
|
||||
} else if (valueSpec.subtype === 'string') {
|
||||
formArr.controls.forEach(control => {
|
||||
controls.forEach(control => {
|
||||
if (!control.value) control.setValue(null)
|
||||
})
|
||||
} else if (valueSpec.subtype === 'object') {
|
||||
formArr.controls.forEach((formGroup: FormGroup) => {
|
||||
controls.forEach(formGroup => {
|
||||
const objectSpec = valueSpec.spec as ListValueSpecObject
|
||||
convertValuesRecursive(objectSpec.spec, formGroup)
|
||||
convertValuesRecursive(objectSpec.spec, formGroup as FormGroup)
|
||||
})
|
||||
} else if (valueSpec.subtype === 'union') {
|
||||
formArr.controls.forEach((formGroup: FormGroup) => {
|
||||
controls.forEach(formGroup => {
|
||||
const unionSpec = valueSpec.spec as ListValueSpecUnion
|
||||
const spec =
|
||||
unionSpec.variants[formGroup.controls[unionSpec.tag.id].value]
|
||||
convertValuesRecursive(spec, formGroup)
|
||||
unionSpec.variants[
|
||||
(formGroup as FormGroup).controls[unionSpec.tag.id].value
|
||||
]
|
||||
convertValuesRecursive(spec, formGroup as FormGroup)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { map, take } from 'rxjs/operators'
|
||||
import { ConfigService } from './config.service'
|
||||
import { Revision } from 'patch-db-client'
|
||||
import { AuthService } from './auth.service'
|
||||
import { HttpError, RpcError } from '@start9labs/shared'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
@@ -98,33 +99,6 @@ export class HttpService {
|
||||
}
|
||||
}
|
||||
|
||||
function RpcError(e: RPCError['error']): void {
|
||||
const { code, message, data } = e
|
||||
|
||||
this.code = code
|
||||
|
||||
if (typeof data === 'string') {
|
||||
this.message = `${message}\n\n${data}`
|
||||
this.revision = null
|
||||
} else {
|
||||
if (data.details) {
|
||||
this.message = `${message}\n\n${data.details}`
|
||||
} else {
|
||||
this.message = message
|
||||
}
|
||||
this.revision = data.revision
|
||||
}
|
||||
}
|
||||
|
||||
function HttpError(e: HttpErrorResponse): void {
|
||||
const { status, statusText } = e
|
||||
|
||||
this.code = status
|
||||
this.message = statusText
|
||||
this.details = null
|
||||
this.revision = null
|
||||
}
|
||||
|
||||
function isRpcError<Error, Result>(
|
||||
arg: { error: Error } | { result: Result },
|
||||
): arg is { error: Error } {
|
||||
@@ -188,10 +162,6 @@ export interface RPCError extends RPCBase {
|
||||
|
||||
export type RPCResponse<T> = RPCSuccess<T> | RPCError
|
||||
|
||||
type HttpError = HttpErrorResponse & {
|
||||
error: { code: string; message: string }
|
||||
}
|
||||
|
||||
export interface HttpOptions {
|
||||
method: Method
|
||||
url: string
|
||||
|
||||
@@ -8,8 +8,9 @@ const SHOW_DISK_REPAIR = 'SHOW_DISK_REPAIR'
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class LocalStorageService {
|
||||
showDevTools$: BehaviorSubject<boolean> = new BehaviorSubject(false)
|
||||
showDiskRepair$: BehaviorSubject<boolean> = new BehaviorSubject(false)
|
||||
readonly showDevTools$ = new BehaviorSubject<boolean>(false)
|
||||
readonly showDiskRepair$ = new BehaviorSubject<boolean>(false)
|
||||
|
||||
constructor(private readonly storage: Storage) {}
|
||||
|
||||
async init() {
|
||||
|
||||
@@ -27,16 +27,16 @@ import {
|
||||
export class MarketplaceService extends AbstractMarketplaceService {
|
||||
private readonly notes = new Map<string, Record<string, string>>()
|
||||
|
||||
private readonly init$: Observable<Marketplace> = defer(() =>
|
||||
this.patch.watch$('ui', 'marketplace'),
|
||||
).pipe(
|
||||
map(marketplace =>
|
||||
marketplace?.['selected-id']
|
||||
? marketplace['known-hosts'][marketplace['selected-id']]
|
||||
: this.config.marketplace,
|
||||
),
|
||||
shareReplay(),
|
||||
)
|
||||
private readonly init$: Observable<Marketplace> = this.patch
|
||||
.watch$('ui', 'marketplace')
|
||||
.pipe(
|
||||
map(marketplace =>
|
||||
marketplace?.['selected-id']
|
||||
? marketplace['known-hosts'][marketplace['selected-id']]
|
||||
: this.config.marketplace,
|
||||
),
|
||||
shareReplay(),
|
||||
)
|
||||
|
||||
private readonly data$: Observable<MarketplaceData> = this.init$.pipe(
|
||||
switchMap(({ url }) =>
|
||||
|
||||
@@ -1,54 +1,46 @@
|
||||
import { MockSource, PollSource, WebsocketSource } from 'patch-db-client'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { LocalStorageBootstrap } from './local-storage-bootstrap'
|
||||
import { PatchDbService } from './patch-db.service'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { AuthService } from '../auth.service'
|
||||
import { MockApiService } from '../api/embassy-mock-api.service'
|
||||
import { filter } from 'rxjs/operators'
|
||||
import { inject, InjectionToken } from '@angular/core'
|
||||
import { exists } from '@start9labs/shared'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { Storage } from '@ionic/storage-angular'
|
||||
import { filter } from 'rxjs/operators'
|
||||
import {
|
||||
Bootstrapper,
|
||||
MockSource,
|
||||
PollSource,
|
||||
Source,
|
||||
WebsocketSource,
|
||||
} from 'patch-db-client'
|
||||
|
||||
export function PatchDbServiceFactory(
|
||||
config: ConfigService,
|
||||
embassyApi: ApiService,
|
||||
bootstrapper: LocalStorageBootstrap,
|
||||
auth: AuthService,
|
||||
storage: Storage,
|
||||
): PatchDbService {
|
||||
const {
|
||||
useMocks,
|
||||
patchDb: { poll },
|
||||
} = config
|
||||
import { ConfigService } from '../config.service'
|
||||
import { LocalStorageBootstrap } from './local-storage-bootstrap'
|
||||
import { ApiService } from '../api/embassy-api.service'
|
||||
import { MockApiService } from '../api/embassy-mock-api.service'
|
||||
import { DataModel } from './data-model'
|
||||
|
||||
if (useMocks) {
|
||||
const source = new MockSource<DataModel>(
|
||||
(embassyApi as MockApiService).mockPatch$.pipe(filter(exists)),
|
||||
)
|
||||
return new PatchDbService(
|
||||
source,
|
||||
source,
|
||||
embassyApi,
|
||||
bootstrapper,
|
||||
auth,
|
||||
storage,
|
||||
)
|
||||
} else {
|
||||
const protocol = window.location.protocol === 'http:' ? 'ws' : 'wss'
|
||||
const host = window.location.host
|
||||
const wsSource = new WebsocketSource<DataModel>(
|
||||
`${protocol}://${host}/ws/db`,
|
||||
)
|
||||
const pollSource = new PollSource<DataModel>({ ...poll }, embassyApi)
|
||||
export const PATCH_SOURCE = new InjectionToken<Source<DataModel>[]>(
|
||||
'[wsSources, pollSources]',
|
||||
)
|
||||
export const BOOTSTRAPPER = new InjectionToken<Bootstrapper<DataModel>>('', {
|
||||
factory: () => inject(LocalStorageBootstrap),
|
||||
})
|
||||
|
||||
return new PatchDbService(
|
||||
wsSource,
|
||||
pollSource,
|
||||
embassyApi,
|
||||
bootstrapper,
|
||||
auth,
|
||||
storage,
|
||||
)
|
||||
}
|
||||
export function mockSourceFactory({
|
||||
mockPatch$,
|
||||
}: MockApiService): Source<DataModel>[] {
|
||||
return Array(2).fill(
|
||||
new MockSource<DataModel>(mockPatch$.pipe(filter(exists))),
|
||||
)
|
||||
}
|
||||
|
||||
export function realSourceFactory(
|
||||
embassyApi: ApiService,
|
||||
config: ConfigService,
|
||||
{ defaultView }: Document,
|
||||
): Source<DataModel>[] {
|
||||
const { patchDb } = config
|
||||
const { host } = defaultView.location
|
||||
const protocol = defaultView.location.protocol === 'http:' ? 'ws' : 'wss'
|
||||
|
||||
return [
|
||||
new WebsocketSource<DataModel>(`${protocol}://${host}/ws/db`),
|
||||
new PollSource<DataModel>({ ...patchDb.poll }, embassyApi),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
import { Inject, Injectable, InjectionToken } from '@angular/core'
|
||||
import { Inject, Injectable } from '@angular/core'
|
||||
import { Storage } from '@ionic/storage-angular'
|
||||
import { Bootstrapper, PatchDB, Source, Store } from 'patch-db-client'
|
||||
import { BehaviorSubject, Observable, of, Subscription } from 'rxjs'
|
||||
import {
|
||||
BehaviorSubject,
|
||||
Observable,
|
||||
of,
|
||||
ReplaySubject,
|
||||
Subscription,
|
||||
} from 'rxjs'
|
||||
import {
|
||||
catchError,
|
||||
debounceTime,
|
||||
filter,
|
||||
finalize,
|
||||
mergeMap,
|
||||
skip,
|
||||
switchMap,
|
||||
take,
|
||||
tap,
|
||||
@@ -17,13 +23,7 @@ import { isEmptyObject, pauseFor } from '@start9labs/shared'
|
||||
import { DataModel } from './data-model'
|
||||
import { ApiService } from '../api/embassy-api.service'
|
||||
import { AuthService } from '../auth.service'
|
||||
import { patch } from '@start9labs/emver'
|
||||
|
||||
export const PATCH_HTTP = new InjectionToken<Source<DataModel>>('')
|
||||
export const PATCH_SOURCE = new InjectionToken<Source<DataModel>>('')
|
||||
export const BOOTSTRAPPER = new InjectionToken<Bootstrapper<DataModel>>('')
|
||||
export const AUTH = new InjectionToken<AuthService>('')
|
||||
export const STORAGE = new InjectionToken<Storage>('')
|
||||
import { BOOTSTRAPPER, PATCH_SOURCE } from './patch-db.factory'
|
||||
|
||||
export enum PatchConnection {
|
||||
Initializing = 'initializing',
|
||||
@@ -36,13 +36,13 @@ export enum PatchConnection {
|
||||
})
|
||||
export class PatchDbService {
|
||||
private readonly WS_SUCCESS = 'wsSuccess'
|
||||
private patchConnection$ = new BehaviorSubject(PatchConnection.Initializing)
|
||||
private patchConnection$ = new ReplaySubject<PatchConnection>(1)
|
||||
private wsSuccess$ = new BehaviorSubject(false)
|
||||
private polling$ = new BehaviorSubject(false)
|
||||
private patchDb: PatchDB<DataModel>
|
||||
private subs: Subscription[] = []
|
||||
private sources$: BehaviorSubject<Source<DataModel>[]> = new BehaviorSubject([
|
||||
this.wsSource,
|
||||
this.sources[0],
|
||||
])
|
||||
|
||||
data: DataModel
|
||||
@@ -61,18 +61,18 @@ export class PatchDbService {
|
||||
}
|
||||
|
||||
constructor(
|
||||
@Inject(PATCH_SOURCE) private readonly wsSource: Source<DataModel>,
|
||||
@Inject(PATCH_SOURCE) private readonly pollSource: Source<DataModel>,
|
||||
@Inject(PATCH_HTTP) private readonly http: ApiService,
|
||||
// [wsSources, pollSources]
|
||||
@Inject(PATCH_SOURCE) private readonly sources: Source<DataModel>[],
|
||||
@Inject(BOOTSTRAPPER)
|
||||
private readonly bootstrapper: Bootstrapper<DataModel>,
|
||||
@Inject(AUTH) private readonly auth: AuthService,
|
||||
@Inject(STORAGE) private readonly storage: Storage,
|
||||
private readonly http: ApiService,
|
||||
private readonly auth: AuthService,
|
||||
private readonly storage: Storage,
|
||||
) {}
|
||||
|
||||
async init(): Promise<void> {
|
||||
const cache = await this.bootstrapper.init()
|
||||
this.sources$.next([this.wsSource, this.http])
|
||||
this.sources$.next([this.sources[0], this.http])
|
||||
|
||||
this.patchDb = new PatchDB(this.sources$, this.http, cache)
|
||||
|
||||
@@ -94,13 +94,13 @@ export class PatchDbService {
|
||||
console.log('patchDB: POLLING FAILED', e)
|
||||
this.patchConnection$.next(PatchConnection.Disconnected)
|
||||
await pauseFor(2000)
|
||||
this.sources$.next([this.pollSource, this.http])
|
||||
this.sources$.next([this.sources[1], this.http])
|
||||
return
|
||||
}
|
||||
|
||||
console.log('patchDB: WEBSOCKET FAILED', e)
|
||||
this.polling$.next(true)
|
||||
this.sources$.next([this.pollSource, this.http])
|
||||
this.sources$.next([this.sources[1], this.http])
|
||||
}),
|
||||
)
|
||||
.subscribe({
|
||||
@@ -152,7 +152,7 @@ export class PatchDbService {
|
||||
console.log('patchDB: SWITCHING BACK TO WEBSOCKETS')
|
||||
this.patchConnection$.next(PatchConnection.Initializing)
|
||||
this.polling$.next(false)
|
||||
this.sources$.next([this.wsSource, this.http])
|
||||
this.sources$.next([this.sources[0], this.http])
|
||||
}
|
||||
}),
|
||||
)
|
||||
@@ -180,19 +180,14 @@ export class PatchDbService {
|
||||
|
||||
// prettier-ignore
|
||||
watch$: Store<DataModel>['watch$'] = (...args: (string | number)[]): Observable<DataModel> => {
|
||||
// TODO: refactor with a better solution to race condition
|
||||
const argsString = '/' + args.join('/')
|
||||
const source$ =
|
||||
this.patchDb?.store.watch$(...(args as [])) ||
|
||||
this.patchConnection$.pipe(
|
||||
skip(1),
|
||||
take(1),
|
||||
switchMap(() => this.patchDb.store.watch$(...(args as []))),
|
||||
)
|
||||
|
||||
console.log('patchDB: WATCHING ', argsString)
|
||||
|
||||
return source$.pipe(
|
||||
return this.patchConnection$.pipe(
|
||||
filter(status => status === PatchConnection.Connected),
|
||||
take(1),
|
||||
switchMap(() => this.patchDb.store.watch$(...(args as []))),
|
||||
tap(data => console.log('patchDB: NEW VALUE', argsString, data)),
|
||||
catchError(e => {
|
||||
console.error('patchDB: WATCH ERROR', e)
|
||||
|
||||
@@ -47,18 +47,25 @@ function getDependencyStatus(pkg: PackageDataEntry): DependencyStatus {
|
||||
}
|
||||
|
||||
function getHealthStatus(status: Status): HealthStatus {
|
||||
if (status.main.status === PackageMainStatus.Running) {
|
||||
const values = Object.values(status.main.health)
|
||||
if (values.some(h => h.result === 'failure')) {
|
||||
return HealthStatus.Failure
|
||||
} else if (values.some(h => h.result === 'starting')) {
|
||||
return HealthStatus.Starting
|
||||
} else if (values.some(h => h.result === 'loading')) {
|
||||
return HealthStatus.Loading
|
||||
} else {
|
||||
return HealthStatus.Healthy
|
||||
}
|
||||
if (status.main.status !== PackageMainStatus.Running || !status.main.health) {
|
||||
return
|
||||
}
|
||||
|
||||
const values = Object.values(status.main.health)
|
||||
|
||||
if (values.some(h => h.result === 'failure')) {
|
||||
return HealthStatus.Failure
|
||||
}
|
||||
|
||||
if (values.some(h => h.result === 'starting')) {
|
||||
return HealthStatus.Starting
|
||||
}
|
||||
|
||||
if (values.some(h => h.result === 'loading')) {
|
||||
return HealthStatus.Loading
|
||||
}
|
||||
|
||||
return HealthStatus.Healthy
|
||||
}
|
||||
|
||||
export interface StatusRendering {
|
||||
|
||||
@@ -37,7 +37,7 @@ export class ServerConfigService {
|
||||
|
||||
try {
|
||||
await this.saveFns[key](data)
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
|
||||
@@ -5,5 +5,5 @@ import { Injectable } from '@angular/core'
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class SplitPaneTracker {
|
||||
sidebarOpen$: BehaviorSubject<boolean> = new BehaviorSubject(false)
|
||||
}
|
||||
readonly sidebarOpen$ = new BehaviorSubject<boolean>(false)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user