diff --git a/src/app/core/auth/auth.effects.spec.ts b/src/app/core/auth/auth.effects.spec.ts index 996db991bcc..c4ee9734b3d 100644 --- a/src/app/core/auth/auth.effects.spec.ts +++ b/src/app/core/auth/auth.effects.spec.ts @@ -22,6 +22,7 @@ import { throwError as observableThrow, } from 'rxjs'; +import { environment } from '../../../environments/environment'; import { AppState, storeModuleConfig, @@ -52,6 +53,7 @@ import { RetrieveAuthMethodsErrorAction, RetrieveAuthMethodsSuccessAction, RetrieveTokenAction, + SetUserAsIdleAction, } from './auth.actions'; import { AuthEffects } from './auth.effects'; import { authReducer } from './auth.reducer'; @@ -107,6 +109,28 @@ describe('AuthEffects', () => { store = TestBed.inject(Store as any); }); + describe('trackIdleness$', () => { + let emitted: any[]; + + beforeEach(() => { + emitted = []; + actions = of({ type: StoreActionTypes.REHYDRATE }); + }); + + it('should set the user as idle when the timer runs out in the browser', fakeAsync(() => { + authEffects.trackIdleness$.subscribe((action) => emitted.push(action)); + tick(environment.auth.ui.timeUntilIdle); + expect(emitted).toEqual([new SetUserAsIdleAction()]); + })); + + it('should not start the timer on the server', fakeAsync(() => { + (authEffects as any).platformId = 'server'; + authEffects.trackIdleness$.subscribe((action) => emitted.push(action)); + tick(environment.auth.ui.timeUntilIdle); + expect(emitted).toEqual([]); + })); + }); + describe('authenticate$', () => { describe('when credentials are correct', () => { it('should return a AUTHENTICATE_SUCCESS action in response to a AUTHENTICATE action', () => { diff --git a/src/app/core/auth/auth.effects.ts b/src/app/core/auth/auth.effects.ts index 89b4df80ece..9967fd3f879 100644 --- a/src/app/core/auth/auth.effects.ts +++ b/src/app/core/auth/auth.effects.ts @@ -1,6 +1,9 @@ +import { isPlatformBrowser } from '@angular/common'; import { + Inject, Injectable, NgZone, + PLATFORM_ID, Type, } from '@angular/core'; // import @ngrx @@ -304,6 +307,8 @@ export class AuthEffects { * @method trackIdleness */ public trackIdleness$: Observable = createEffect(() => this.actions$.pipe( + // Only in the browser: on the server this timer keeps an unfinished render in memory until it fires + filter(() => isPlatformBrowser(this.platformId)), filter((action: Action) => !IDLE_TIMER_IGNORE_TYPES.includes(action.type)), // Using switchMap the effect will stop subscribing to the previous timer if a new action comes // in, and start a new timer @@ -323,11 +328,13 @@ export class AuthEffects { * @param {AuthorizationDataService} authorizationsService * @param {AuthService} authService * @param {Store} store + * @param {object} platformId */ constructor(private actions$: Actions, private zone: NgZone, private authorizationsService: AuthorizationDataService, private authService: AuthService, - private store: Store) { + private store: Store, + @Inject(PLATFORM_ID) private platformId: object) { } } diff --git a/src/app/core/services/server-xhr.service.ts b/src/app/core/services/server-xhr.service.ts index af59172c7e7..5816db19ab0 100644 --- a/src/app/core/services/server-xhr.service.ts +++ b/src/app/core/services/server-xhr.service.ts @@ -33,6 +33,9 @@ const agentOptions: HttpAgentOptions = { const httpAgent = new HttpAgent(agentOptions); const httpsAgent = new HttpsAgent(agentOptions); +// The status codes xhr2 follows on its own. +const REDIRECT_STATUSES = [301, 302, 303, 307, 308]; + /** * Constructs the XMLHttpRequest instances used for all HttpClient requests. * Emulated by https://github.com/pwnall/node-xhr2 on the server. @@ -43,6 +46,8 @@ const httpsAgent = new HttpsAgent(agentOptions); * This was added to be able to perform a working XSRF request from the node server, as it needs to set a cookie for the XSRF token. * - Override NodeJS HTTP(S) agents to keep sessions alive between requests. * This improves SSR performance by reducing REST request overhead on the server. + * - Read and discard the body of redirect responses that xhr2 follows. Otherwise their socket stays busy + * until the REST server closes it, and keeps the SSR app that made the request in memory. * * Note that this must be provided in ServerAppModule; * it doesn't work when added as a Universal engine provider. @@ -60,6 +65,14 @@ export class ServerXhrService implements XhrFactory { httpsAgent, }); + const onHttpResponse = (xhr as any)._onHttpResponse; + (xhr as any)._onHttpResponse = function(request, response) { + if (REDIRECT_STATUSES.includes(response.statusCode)) { + response.resume(); + } + return onHttpResponse.call(this, request, response); + }; + return xhr; } }