diff --git a/diagnostic-ui/package-lock.json b/diagnostic-ui/package-lock.json
index 228821c37..b5c9180f0 100644
--- a/diagnostic-ui/package-lock.json
+++ b/diagnostic-ui/package-lock.json
@@ -5,6 +5,7 @@
"requires": true,
"packages": {
"": {
+ "name": "diagnostic-ui",
"version": "0.3.0",
"dependencies": {
"@angular/animations": "^12.2.5",
diff --git a/setup-wizard/package-lock.json b/setup-wizard/package-lock.json
index 472d338e5..543477034 100644
--- a/setup-wizard/package-lock.json
+++ b/setup-wizard/package-lock.json
@@ -5,6 +5,7 @@
"requires": true,
"packages": {
"": {
+ "name": "setup-wizard",
"version": "0.0.1",
"dependencies": {
"@angular/common": "^12.2.1",
diff --git a/system-images/compat/Cargo.lock b/system-images/compat/Cargo.lock
index d2bdd0b3a..5700196b5 100644
--- a/system-images/compat/Cargo.lock
+++ b/system-images/compat/Cargo.lock
@@ -216,6 +216,15 @@ version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+[[package]]
+name = "bitmaps"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "031043d04099746d8db04daf1fa424b2bc8bd69d92b25962dcde24da39ab64a2"
+dependencies = [
+ "typenum",
+]
+
[[package]]
name = "bitvec"
version = "0.19.5"
@@ -1461,6 +1470,20 @@ dependencies = [
"unicode-normalization",
]
+[[package]]
+name = "imbl"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "543682c9082b25e63d03b5acbd65ad111fd49dd93e70843e5175db4ff81d606b"
+dependencies = [
+ "bitmaps",
+ "rand_core 0.6.3",
+ "rand_xoshiro",
+ "sized-chunks",
+ "typenum",
+ "version_check",
+]
+
[[package]]
name = "indenter"
version = "0.3.3"
@@ -2105,6 +2128,7 @@ dependencies = [
"async-trait",
"fd-lock-rs",
"futures",
+ "imbl",
"json-patch",
"json-ptr",
"lazy_static",
@@ -2525,6 +2549,15 @@ dependencies = [
"rand_core 0.6.3",
]
+[[package]]
+name = "rand_xoshiro"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa"
+dependencies = [
+ "rand_core 0.6.3",
+]
+
[[package]]
name = "redox_syscall"
version = "0.1.57"
@@ -3078,6 +3111,16 @@ version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "533494a8f9b724d33625ab53c6c4800f7cc445895924a8ef649222dcb76e938b"
+[[package]]
+name = "sized-chunks"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16d69225bde7a69b235da73377861095455d298f2b970996eec25ddbb42b3d1e"
+dependencies = [
+ "bitmaps",
+ "typenum",
+]
+
[[package]]
name = "slab"
version = "0.4.4"
diff --git a/ui/src/app/app.component.ts b/ui/src/app/app.component.ts
index 272ceca59..524101f7c 100644
--- a/ui/src/app/app.component.ts
+++ b/ui/src/app/app.component.ts
@@ -88,16 +88,15 @@ export class AppComponent {
async init () {
await this.storage.create()
await this.authService.init()
- await this.patch.init()
this.router.initialNavigation()
// watch auth
this.authService.watch$()
- .subscribe(auth => {
+ .subscribe(async auth => {
// VERIFIED
if (auth === AuthState.VERIFIED) {
- this.patch.start()
+ await this.patch.start()
this.showMenu = true
// if on the login screen, route to dashboard
@@ -196,7 +195,7 @@ export class AppComponent {
.subscribe(async connectionFailure => {
if (connectionFailure === ConnectionFailure.None) {
if (this.offlineToast) {
- this.offlineToast.dismiss()
+ await this.offlineToast.dismiss()
this.offlineToast = undefined
}
} else {
@@ -206,16 +205,13 @@ export class AppComponent {
case ConnectionFailure.Network:
message = 'Phone or computer has no network connection.'
break
- case ConnectionFailure.Diagnosing:
- message = new IonicSafeString('Running network diagnostics ')
- break
case ConnectionFailure.Tor:
message = 'Browser unable to connect over Tor.'
- link = 'https://docs.start9.com/support/FAQ/setup-faq.html#tor-failure'
+ link = 'https://docs.start9.com/support/FAQ/troubleshooting.html#tor-failure'
break
case ConnectionFailure.Lan:
message = 'Embassy not found on Local Area Network.'
- link = 'https://docs.start9.com/support/FAQ/setup-faq.html#lan-failure'
+ link = 'https://docs.start9.com/support/FAQ/troubleshooting.html#lan-failure'
break
}
await this.presentToastOffline(message, link)
diff --git a/ui/src/app/app.module.ts b/ui/src/app/app.module.ts
index 7d25a6e1c..6e10ef844 100644
--- a/ui/src/app/app.module.ts
+++ b/ui/src/app/app.module.ts
@@ -3,7 +3,7 @@ import { BrowserModule } from '@angular/platform-browser'
import { RouteReuseStrategy } from '@angular/router'
import { IonicModule, IonicRouteStrategy, IonNav } from '@ionic/angular'
import { Drivers } from '@ionic/storage'
-import { IonicStorageModule } from '@ionic/storage-angular'
+import { IonicStorageModule, Storage } from '@ionic/storage-angular'
import { HttpClientModule } from '@angular/common/http'
import { AppComponent } from './app.component'
import { AppRoutingModule } from './app-routing.module'
@@ -48,8 +48,10 @@ import { GlobalErrorHandler } from './services/global-error-handler.service'
providers: [
FormBuilder,
IonNav,
- Storage,
- { provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
+ {
+ provide: RouteReuseStrategy,
+ useClass: IonicRouteStrategy,
+ },
{
provide: ApiService,
useFactory: ApiServiceFactory,
@@ -58,9 +60,12 @@ import { GlobalErrorHandler } from './services/global-error-handler.service'
{
provide: PatchDbService,
useFactory: PatchDbServiceFactory,
- deps: [ConfigService, ApiService, LocalStorageBootstrap, AuthService],
+ deps: [ConfigService, ApiService, LocalStorageBootstrap, AuthService, Storage],
+ },
+ {
+ provide: ErrorHandler,
+ useClass: GlobalErrorHandler,
},
- { provide: ErrorHandler, useClass: GlobalErrorHandler},
],
bootstrap: [AppComponent],
schemas: [ CUSTOM_ELEMENTS_SCHEMA ],
diff --git a/ui/src/app/pages/login/login.page.ts b/ui/src/app/pages/login/login.page.ts
index 17b345035..84e9d872b 100644
--- a/ui/src/app/pages/login/login.page.ts
+++ b/ui/src/app/pages/login/login.page.ts
@@ -52,6 +52,7 @@ export class LoginPage {
password: this.password,
metadata: { platforms: getPlatforms() },
})
+
this.authService.setVerified()
this.password = ''
} catch (e) {
diff --git a/ui/src/app/services/api/embassy-api.service.ts b/ui/src/app/services/api/embassy-api.service.ts
index 11640d299..b3cfea8ea 100644
--- a/ui/src/app/services/api/embassy-api.service.ts
+++ b/ui/src/app/services/api/embassy-api.service.ts
@@ -1,20 +1,20 @@
import { Subject, Observable } from 'rxjs'
-import { Http, Update, Operation, Revision, Source, Store } from 'patch-db-client'
+import { Http, Update, Operation, Revision, Source, Store, RPCResponse } from 'patch-db-client'
import { RR } from './api.types'
import { DataModel } from 'src/app/services/patch-db/data-model'
import { RequestError } from '../http.service'
+import { map } from 'rxjs/operators'
export abstract class ApiService implements Source, Http {
protected readonly sync$ = new Subject>()
/** PatchDb Source interface. Post/Patch requests provide a source of patches to the db. */
// sequenceStream '_' is not used by the live api, but is overridden by the mock
- watch$ (_?: Store): Observable> {
- return this.sync$.asObservable()
+ watch$ (_?: Store): Observable>> {
+ return this.sync$.asObservable().pipe(map( result => ({ result,
+ jsonrpc: '2.0'})))
}
- connectionMade$ = new Subject()
-
// for getting static files: ex icons, instructions, licenses
abstract getStatic (url: string): Promise
@@ -209,7 +209,6 @@ export abstract class ApiService implements Source, Http {
throw e
})
.then(({ response, revision }) => {
- this.connectionMade$.next()
if (revision) this.sync$.next(revision)
return response
})
diff --git a/ui/src/app/services/api/embassy-mock-api.service.ts b/ui/src/app/services/api/embassy-mock-api.service.ts
index b73ede22f..67fd4d714 100644
--- a/ui/src/app/services/api/embassy-mock-api.service.ts
+++ b/ui/src/app/services/api/embassy-mock-api.service.ts
@@ -59,7 +59,11 @@ export class MockApiService extends ApiService {
async login (params: RR.LoginReq): Promise {
await pauseFor(2000)
- this.mockPatch$.next({ id: 1, value: mockPatchData, expireId: null })
+
+ setTimeout(() => {
+ this.mockPatch$.next({ id: 1, value: mockPatchData, expireId: null })
+ }, 2000)
+
return null
}
diff --git a/ui/src/app/services/connection.service.ts b/ui/src/app/services/connection.service.ts
index 49368db86..08c088173 100644
--- a/ui/src/app/services/connection.service.ts
+++ b/ui/src/app/services/connection.service.ts
@@ -1,7 +1,6 @@
import { Injectable } from '@angular/core'
import { BehaviorSubject, combineLatest, fromEvent, merge, Subscription } from 'rxjs'
import { PatchConnection, PatchDbService } from './patch-db/patch-db.service'
-import { HttpService, Method } from './http.service'
import { distinctUntilChanged } from 'rxjs/operators'
import { ConfigService } from './config.service'
@@ -13,7 +12,6 @@ export class ConnectionService {
private readonly connectionFailure$ = new BehaviorSubject(ConnectionFailure.None)
constructor (
- private readonly httpService: HttpService,
private readonly configService: ConfigService,
private readonly patch: PatchDbService,
) { }
@@ -46,10 +44,10 @@ export class ConnectionService {
),
])
.subscribe(async ([network, patchConnection, progress]) => {
- if (patchConnection !== PatchConnection.Disconnected) {
- this.connectionFailure$.next(ConnectionFailure.None)
- } else if (!network) {
+ 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()) {
@@ -64,7 +62,6 @@ export class ConnectionService {
export enum ConnectionFailure {
None = 'none',
- Diagnosing = 'diagnosing',
Network = 'network',
Tor = 'tor',
Lan = 'lan',
diff --git a/ui/src/app/services/patch-db/patch-db.factory.ts b/ui/src/app/services/patch-db/patch-db.factory.ts
index e2c652916..80fa43c35 100644
--- a/ui/src/app/services/patch-db/patch-db.factory.ts
+++ b/ui/src/app/services/patch-db/patch-db.factory.ts
@@ -1,4 +1,4 @@
-import { MockSource, PollSource, Source, WebsocketSource } from 'patch-db-client'
+import { MockSource, PollSource, WebsocketSource } from 'patch-db-client'
import { ConfigService } from 'src/app/services/config.service'
import { DataModel } from './data-model'
import { LocalStorageBootstrap } from './local-storage-bootstrap'
@@ -8,29 +8,47 @@ import { AuthService } from '../auth.service'
import { MockApiService } from '../api/embassy-mock-api.service'
import { filter } from 'rxjs/operators'
import { exists } from 'src/app/util/misc.util'
+import { Storage } from '@ionic/storage-angular'
export function PatchDbServiceFactory (
config: ConfigService,
embassyApi: ApiService,
bootstrapper: LocalStorageBootstrap,
auth: AuthService,
+ storage: Storage,
): PatchDbService {
-
- const { mocks, patchDb: { poll }, supportsWebSockets } = config
-
- let source: Source
+ const {
+ mocks,
+ patchDb: { poll },
+ } = config
if (mocks.enabled) {
- source = new MockSource((embassyApi as MockApiService).mockPatch$.pipe(filter(exists)))
+ const source = new MockSource(
+ (embassyApi as MockApiService).mockPatch$.pipe(filter(exists)),
+ )
+ return new PatchDbService(
+ source,
+ source,
+ embassyApi,
+ bootstrapper,
+ auth,
+ storage,
+ )
} else {
- if (!supportsWebSockets) {
- source = new PollSource({ ...poll }, embassyApi)
- } else {
- const protocol = window.location.protocol === 'http:' ? 'ws' : 'wss'
- const host = window.location.host
- source = new WebsocketSource(`${protocol}://${host}/ws/db`)
- }
- }
+ const protocol = window.location.protocol === 'http:' ? 'ws' : 'wss'
+ const host = window.location.host
+ const wsSource = new WebsocketSource(
+ `${protocol}://${host}/ws/db`,
+ )
+ const pollSource = new PollSource({ ...poll }, embassyApi)
- return new PatchDbService(source, embassyApi, bootstrapper, auth)
-}
\ No newline at end of file
+ return new PatchDbService(
+ wsSource,
+ pollSource,
+ embassyApi,
+ bootstrapper,
+ auth,
+ storage,
+ )
+ }
+}
diff --git a/ui/src/app/services/patch-db/patch-db.service.ts b/ui/src/app/services/patch-db/patch-db.service.ts
index 9a7aa5690..00aa21a64 100644
--- a/ui/src/app/services/patch-db/patch-db.service.ts
+++ b/ui/src/app/services/patch-db/patch-db.service.ts
@@ -1,7 +1,15 @@
import { Inject, Injectable, InjectionToken } 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 { catchError, debounceTime, finalize, map, tap } from 'rxjs/operators'
+import {
+ catchError,
+ debounceTime,
+ finalize,
+ mergeMap,
+ tap,
+ withLatestFrom,
+} from 'rxjs/operators'
import { pauseFor } from 'src/app/util/misc.util'
import { ApiService } from '../api/embassy-api.service'
import { AuthService } from '../auth.service'
@@ -11,6 +19,7 @@ export const PATCH_HTTP = new InjectionToken>('')
export const PATCH_SOURCE = new InjectionToken>('')
export const BOOTSTRAPPER = new InjectionToken>('')
export const AUTH = new InjectionToken('')
+export const STORAGE = new InjectionToken('')
export enum PatchConnection {
Initializing = 'initializing',
@@ -22,90 +31,153 @@ export enum PatchConnection {
providedIn: 'root',
})
export class PatchDbService {
+ private readonly WS_SUCCESS = 'wsSuccess'
private patchConnection$ = new BehaviorSubject(PatchConnection.Initializing)
+ private wsSuccess$ = new BehaviorSubject(false)
+ private polling$ = new BehaviorSubject(false)
private patchDb: PatchDB
- private patchSub: Subscription
- data: DataModel
+ private subs: Subscription[] = []
+ private sources$: BehaviorSubject[]> = new BehaviorSubject([
+ this.wsSource,
+ ])
- getData () { return this.patchDb.store.cache.data }
+ data: DataModel
+ errors = 0
+
+ getData () {
+ return this.patchDb.store.cache.data
+ }
constructor (
- @Inject(PATCH_SOURCE) private readonly source: Source,
+ @Inject(PATCH_SOURCE) private readonly wsSource: Source,
+ @Inject(PATCH_SOURCE) private readonly pollSource: Source,
@Inject(PATCH_HTTP) private readonly http: ApiService,
- @Inject(BOOTSTRAPPER) private readonly bootstrapper: Bootstrapper,
+ @Inject(BOOTSTRAPPER)
+ private readonly bootstrapper: Bootstrapper,
@Inject(AUTH) private readonly auth: AuthService,
+ @Inject(STORAGE) private readonly storage: Storage,
) { }
async init (): Promise {
const cache = await this.bootstrapper.init()
- this.patchDb = new PatchDB([this.source, this.http], this.http, cache)
+ this.sources$.next([this.wsSource, this.http])
+
+ this.patchDb = new PatchDB(this.sources$, this.http, cache)
+
+ this.patchConnection$.next(PatchConnection.Initializing)
this.data = this.patchDb.store.cache.data
}
- start (): void {
- console.log(this.patchSub ? 'restarting patch-db' : 'starting patch-db')
+ async start (): Promise {
+ await this.init()
- // make sure everything is stopped before initializing
- if (this.patchSub) {
- this.patchSub.unsubscribe()
- this.patchSub = undefined
- }
+ this.subs.push(
+ // Connection Error
+ this.patchDb.connectionError$
+ .pipe(
+ debounceTime(420),
+ withLatestFrom(this.polling$),
+ mergeMap(async ([e, polling]) => {
+ if (polling) {
+ console.log('patchDB: POLLING FAILED', e)
+ this.patchConnection$.next(PatchConnection.Disconnected)
+ await pauseFor(2000)
+ this.sources$.next([this.pollSource, this.http])
+ return
+ }
- this.patchSub = this.patchDb.sync$()
- .pipe(
- debounceTime(400),
- tap(cache => {
- this.patchConnection$.next(PatchConnection.Connected)
- this.bootstrapper.update(cache)
- }),
+ console.log('patchDB: WEBSOCKET FAILED', e)
+ this.polling$.next(true)
+ this.sources$.next([this.pollSource, this.http])
+ }),
+ )
+ .subscribe({
+ complete: () => {
+ console.warn('patchDB: SYNC COMPLETE')
+ },
+ }),
+
+ // RPC ERROR
+ this.patchDb.rpcError$
+ .pipe(
+ tap(({ error }) => {
+ if (error.code === 34) {
+ console.log('patchDB: Unauthorized. Logging out.')
+ this.auth.setUnverified()
+ }
+ }),
+ )
+ .subscribe({
+ complete: () => {
+ console.warn('patchDB: SYNC COMPLETE')
+ },
+ }),
+
+ // GOOD CONNECTION
+ this.patchDb.cache$
+ .pipe(
+ debounceTime(420),
+ withLatestFrom(this.patchConnection$, this.wsSuccess$, this.polling$),
+ tap(async ([cache, connection, wsSuccess, polling]) => {
+ this.bootstrapper.update(cache)
+
+ if (connection === PatchConnection.Initializing) {
+ console.log(
+ polling
+ ? 'patchDB: POLL CONNECTED'
+ : 'patchDB: WEBSOCKET CONNECTED',
+ )
+ this.patchConnection$.next(PatchConnection.Connected)
+ if (!wsSuccess && !polling) {
+ console.log('patchDB: WEBSOCKET SUCCESS')
+ this.storage.set(this.WS_SUCCESS, 'true')
+ this.wsSuccess$.next(true)
+ }
+ } else if (
+ connection === PatchConnection.Disconnected &&
+ wsSuccess
+ ) {
+ console.log('patchDB: SWITCHING BACK TO WEBSOCKETS')
+ this.patchConnection$.next(PatchConnection.Initializing)
+ this.polling$.next(false)
+ this.sources$.next([this.wsSource, this.http])
+ }
+ }),
+ )
+ .subscribe({
+ complete: () => {
+ console.warn('patchDB: SYNC COMPLETE')
+ },
+ }),
)
- .subscribe({
- error: async e => {
- console.error('patch-db SYNC ERROR', e)
- this.patchConnection$.next(PatchConnection.Disconnected)
- if (e.code === 34) {
- this.auth.setUnverified()
- } else {
- await pauseFor(4000)
- this.start()
- }
- },
- complete: () => {
- console.warn('patch-db SYNC COMPLETE')
- },
- })
}
stop (): void {
- console.log('stopping patch-db')
- this.patchConnection$.next(PatchConnection.Initializing)
- if (this.patchSub) {
- this.patchSub.unsubscribe()
- this.patchSub = undefined
+ if (this.patchDb) {
+ console.log('patchDB: STOPPING')
+ this.patchConnection$.next(PatchConnection.Initializing)
+ this.patchDb.store.reset()
}
- }
-
- connected$ (): Observable {
- return this.patchConnection$
- .pipe(
- map(status => status === PatchConnection.Connected),
- )
+ this.subs.forEach((x) => x.unsubscribe())
+ this.subs = []
}
watchPatchConnection$ (): Observable {
return this.patchConnection$.asObservable()
}
- watch$: Store['watch$'] = (...args: (string | number)[]): Observable => {
- console.log('WATCHING', ...args)
- return this.patchDb.store.watch$(...(args as []))
- .pipe(
- tap(data => console.log('NEW VALUE', data, ...args)),
- catchError(e => {
- console.error('Error watching patch-db', e)
+ watch$: Store['watch$'] = (
+ ...args: (string | number)[],
+ ): Observable => {
+ const argsString = '/' + args.join('/')
+ console.log('patchDB: WATCHING ', argsString)
+ return this.patchDb.store.watch$(...(args as [])).pipe(
+ tap((data) => console.log('patchDB: NEW VALUE', argsString, data)),
+ catchError((e) => {
+ console.error('patchDB: WATCH ERROR', e)
return of(e.message)
}),
- finalize(() => console.log('UNSUBSCRIBING', ...args)),
+ finalize(() => console.log('patchDB: UNSUBSCRIBING', argsString)),
)
}
}