diff --git a/modules/common/engine/private_api.ts b/modules/common/engine/private_api.ts index 83569ebcb..00152c3ad 100644 --- a/modules/common/engine/private_api.ts +++ b/modules/common/engine/private_api.ts @@ -5,5 +5,6 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ + export { FileLoader as ɵFileLoader, CommonEngine as ɵCommonEngine, - RenderOptions as ɵRenderOptions } from './src/index'; + RenderOptions as ɵRenderOptions } from './src/index'; diff --git a/modules/common/private_api.ts b/modules/common/private_api.ts new file mode 100644 index 000000000..0fd06ad59 --- /dev/null +++ b/modules/common/private_api.ts @@ -0,0 +1,8 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +export { TransferHttpCacheInterceptor as ɵTransferHttpCacheInterceptor } from './src/transfer_http'; diff --git a/modules/common/public_api.ts b/modules/common/public_api.ts index 2f35e43f6..09b8c3b81 100644 --- a/modules/common/public_api.ts +++ b/modules/common/public_api.ts @@ -7,4 +7,4 @@ */ export { TransferHttpCacheModule } from './src/transfer_http'; export { StateTransferInitializerModule } from './src/state-transfer-initializer/module'; - +export * from './private_api'; diff --git a/modules/common/spec/transfer_http.spec.ts b/modules/common/spec/transfer_http.spec.ts new file mode 100644 index 000000000..5b72491e9 --- /dev/null +++ b/modules/common/spec/transfer_http.spec.ts @@ -0,0 +1,40 @@ +import { ɵTransferHttpCacheInterceptor as TransferHttpCacheInterceptor } from '@nguniversal/common'; +import { of } from 'rxjs'; +import { HttpParams } from '@angular/common/http'; + +function mockAppRef(): any { + return { + isStable: of(true) + }; +} + +function mockTransferState(): any { + return { + store: {} + }; +} + +describe('TransferHttp', () => { + describe('keys', () => { + it('should encode url params', () => { + const interceptor = new TransferHttpCacheInterceptor(mockAppRef(), mockTransferState()); + const key = interceptor['makeCacheKey']('GET', 'https://google.com/api', + new HttpParams().append('foo', 'bar')); + expect(key).toEqual('G.https://google.com/api?foo=bar'); + }); + it('should sort the keys by unicode points', () => { + const interceptor = new TransferHttpCacheInterceptor(mockAppRef(), mockTransferState()); + const key = interceptor['makeCacheKey']('GET', 'https://google.com/api', + new HttpParams().append('b', 'foo').append('a', 'bar')); + expect(key).toEqual('G.https://google.com/api?a=bar&b=foo'); + }); + it('should make equal keys if order of params changes', () => { + const interceptor = new TransferHttpCacheInterceptor(mockAppRef(), mockTransferState()); + const key1 = interceptor['makeCacheKey']('GET', 'https://google.com/api', + new HttpParams().append('a', 'bar').append('b', 'foo')); + const key2 = interceptor['makeCacheKey']('GET', 'https://google.com/api', + new HttpParams().append('b', 'foo').append('a', 'bar')); + expect(key1).toEqual(key2); + }); + }); +}); diff --git a/modules/common/src/transfer_http.ts b/modules/common/src/transfer_http.ts index 994775b14..59e000963 100644 --- a/modules/common/src/transfer_http.ts +++ b/modules/common/src/transfer_http.ts @@ -12,10 +12,16 @@ import { HttpHeaders, HttpInterceptor, HttpRequest, - HttpResponse + HttpResponse, + HttpParams } from '@angular/common/http'; import {ApplicationRef, Injectable, NgModule} from '@angular/core'; -import {BrowserTransferStateModule, TransferState, makeStateKey} from '@angular/platform-browser'; +import { + BrowserTransferStateModule, + TransferState, + makeStateKey, + StateKey +} from '@angular/platform-browser'; import {Observable, of as observableOf} from 'rxjs'; import {tap, take, filter} from 'rxjs/operators'; @@ -41,8 +47,15 @@ export class TransferHttpCacheInterceptor implements HttpInterceptor { private isCacheActive = true; private invalidateCacheEntry(url: string) { - this.transferState.remove(makeStateKey('G.' + url)); - this.transferState.remove(makeStateKey('H.' + url)); + Object.keys(this.transferState['store']) + .forEach(key => key.includes(url) ? this.transferState.remove(makeStateKey(key)) : null); + } + + private makeCacheKey(method: string, url: string, params: HttpParams): StateKey { + // make the params encoded same as a url so it's easy to identify + const encodedParams = params.keys().sort().map(k => `${k}=${params.get(k)}`).join('&'); + const key = (method === 'GET' ? 'G.' : 'H.') + url + '?' + encodedParams; + return makeStateKey(key); } constructor(appRef: ApplicationRef, private transferState: TransferState) { @@ -68,8 +81,7 @@ export class TransferHttpCacheInterceptor implements HttpInterceptor { return next.handle(req); } - const key = (req.method === 'GET' ? 'G.' : 'H.') + req.url; - const storeKey = makeStateKey(key); + const storeKey = this.makeCacheKey(req.method, req.url, req.params); if (this.transferState.hasKey(storeKey)) { // Request found in cache. Respond using it.