diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index b39e87164..17b58a87d 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -1,6 +1,6 @@ -import { BrowserModule } from '@angular/platform-browser'; +import { BrowserModule, BrowserTransferStateModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; -import { HttpClientModule } from '@angular/common/http'; +import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { InfiniteScrollModule } from 'ngx-infinite-scroll'; @@ -48,6 +48,7 @@ import { faAngleDoubleDown, faAngleDoubleUp, faAngleDown, faAngleUp, faBolt, faC import { ApiDocsComponent } from './components/api-docs/api-docs.component'; import { TermsOfServiceComponent } from './components/terms-of-service/terms-of-service.component'; import { StorageService } from './services/storage.service'; +import { HttpCacheInterceptor } from './services/http-cache.interceptor'; @NgModule({ declarations: [ @@ -85,6 +86,7 @@ import { StorageService } from './services/storage.service'; ], imports: [ BrowserModule.withServerTransition({ appId: 'serverApp' }), + BrowserTransferStateModule, AppRoutingModule, HttpClientModule, BrowserAnimationsModule, @@ -100,6 +102,7 @@ import { StorageService } from './services/storage.service'; AudioService, SeoService, StorageService, + { provide: HTTP_INTERCEPTORS, useClass: HttpCacheInterceptor, multi: true } ], bootstrap: [AppComponent] }) diff --git a/frontend/src/app/app.server.module.ts b/frontend/src/app/app.server.module.ts index e97fbea5b..ff4885db1 100644 --- a/frontend/src/app/app.server.module.ts +++ b/frontend/src/app/app.server.module.ts @@ -1,13 +1,19 @@ +import { HTTP_INTERCEPTORS } from '@angular/common/http'; import { NgModule } from '@angular/core'; -import { ServerModule } from '@angular/platform-server'; +import { ServerModule, ServerTransferStateModule } from '@angular/platform-server'; import { AppModule } from './app.module'; import { AppComponent } from './components/app/app.component'; +import { HttpCacheInterceptor } from './services/http-cache.interceptor'; @NgModule({ imports: [ AppModule, ServerModule, + ServerTransferStateModule, + ], + providers: [ + { provide: HTTP_INTERCEPTORS, useClass: HttpCacheInterceptor, multi: true } ], bootstrap: [AppComponent], }) diff --git a/frontend/src/app/services/http-cache.interceptor.ts b/frontend/src/app/services/http-cache.interceptor.ts new file mode 100644 index 000000000..2143477cb --- /dev/null +++ b/frontend/src/app/services/http-cache.interceptor.ts @@ -0,0 +1,42 @@ +import { Inject, Injectable, PLATFORM_ID } from '@angular/core'; +import { HttpInterceptor, HttpEvent, HttpRequest, HttpHandler, HttpResponse } from '@angular/common/http'; +import { Observable, of } from 'rxjs'; +import { tap } from 'rxjs/operators'; +import { TransferState, makeStateKey } from '@angular/platform-browser'; +import { isPlatformBrowser } from '@angular/common'; + +@Injectable() +export class HttpCacheInterceptor implements HttpInterceptor { + isBrowser: boolean = isPlatformBrowser(this.platformId); + + constructor( + private transferState: TransferState, + @Inject(PLATFORM_ID) private platformId: any, + ) { } + + intercept(request: HttpRequest, next: HttpHandler): Observable> { + if (this.isBrowser && request.method === 'GET') { + + const cachedResponse = this.transferState.get(makeStateKey(request.url), null); + if (cachedResponse) { + const modifiedResponse = new HttpResponse({ + headers: cachedResponse.headers, + body: cachedResponse.body, + status: cachedResponse.status, + statusText: cachedResponse.statusText, + url: cachedResponse.url + }); + this.transferState.remove(makeStateKey(request.url)); + return of(modifiedResponse); + } + } + + return next.handle(request) + .pipe(tap((event: HttpEvent) => { + if (!this.isBrowser && event instanceof HttpResponse) { + const keyId = request.url.split('/').slice(3).join('/'); + this.transferState.set(makeStateKey('/' + keyId), event); + } + })); + } +} diff --git a/frontend/src/app/services/websocket.service.ts b/frontend/src/app/services/websocket.service.ts index a9b38d9c3..71bbe5263 100644 --- a/frontend/src/app/services/websocket.service.ts +++ b/frontend/src/app/services/websocket.service.ts @@ -6,12 +6,14 @@ import { Block, Transaction } from '../interfaces/electrs.interface'; import { Subscription } from 'rxjs'; import { ApiService } from './api.service'; import { take } from 'rxjs/operators'; - +import { TransferState, makeStateKey } from '@angular/platform-browser'; const OFFLINE_RETRY_AFTER_MS = 10000; const OFFLINE_PING_CHECK_AFTER_MS = 30000; const EXPECT_PING_RESPONSE_AFTER_MS = 4000; +const initData = makeStateKey('/api/v1/init-data'); + @Injectable({ providedIn: 'root' }) @@ -32,6 +34,7 @@ export class WebsocketService { constructor( private stateService: StateService, private apiService: ApiService, + private transferState: TransferState, ) { if (!this.stateService.isBrowser) { // @ts-ignore @@ -40,11 +43,17 @@ export class WebsocketService { this.apiService.getInitData$() .pipe(take(1)) .subscribe((response) => this.handleResponse(response)); - } else { this.network = this.stateService.network === 'bisq' && !this.stateService.env.BISQ_SEPARATE_BACKEND ? '' : this.stateService.network; this.websocketSubject = webSocket(this.webSocketUrl.replace('{network}', this.network ? '/' + this.network : '')); - this.startSubscription(); + + const theInitData = this.transferState.get(initData, null); + if (theInitData) { + this.handleResponse(theInitData.body); + this.startSubscription(false, true); + } else { + this.startSubscription(); + } this.stateService.networkChanged$.subscribe((network) => { if (network === 'bisq' && !this.stateService.env.BISQ_SEPARATE_BACKEND) { @@ -70,12 +79,14 @@ export class WebsocketService { } } - startSubscription(retrying = false) { - this.stateService.isLoadingWebSocket$.next(true); + startSubscription(retrying = false, hasInitData = false) { + if (!hasInitData) { + this.stateService.isLoadingWebSocket$.next(true); + this.websocketSubject.next({'action': 'init'}); + } if (retrying) { this.stateService.connectionState$.next(1); } - this.websocketSubject.next({'action': 'init'}); this.subscription = this.websocketSubject .subscribe((response: WebsocketResponse) => { this.stateService.isLoadingWebSocket$.next(false);