transfer_cache.ts
/**
* @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
*/
import {APP_BOOTSTRAP_LISTENER, ApplicationRef, inject, InjectionToken, makeStateKey, Provider, StateKey, TransferState, ɵENABLED_SSR_FEATURES as ENABLED_SSR_FEATURES, ɵwhenStable as whenStable} from '@angular/core';
import {Observable, of} from 'rxjs';
import {tap} from 'rxjs/operators';
import {HttpHeaders} from './headers';
import {HTTP_ROOT_INTERCEPTOR_FNS, HttpHandlerFn} from './interceptor';
import {HttpRequest} from './request';
import {HttpEvent, HttpResponse} from './response';
interface TransferHttpResponse {
body: any;
headers: Record<string, string[]>;
status?: number;
statusText?: string;
url?: string;
responseType?: HttpRequest<unknown>['responseType'];
}
const CACHE_STATE = new InjectionToken<{isCacheActive: boolean}>(
ngDevMode ? 'HTTP_TRANSFER_STATE_CACHE_STATE' : '');
/**
* A list of allowed HTTP methods to cache.
*/
const ALLOWED_METHODS = ['GET', 'HEAD'];
export function transferCacheInterceptorFn(
req: HttpRequest<unknown>, next: HttpHandlerFn): Observable<HttpEvent<unknown>> {
const {isCacheActive} = inject(CACHE_STATE);
// Stop using the cache if the application has stabilized, indicating initial rendering
// is complete.
if (!isCacheActive || !ALLOWED_METHODS.includes(req.method)) {
// Cache is no longer active or method is not HEAD or GET.
// Pass the request through.
return next(req);
}
const transferState = inject(TransferState);
const storeKey = makeCacheKey(req);
const response = transferState.get(storeKey, null);
if (response) {
// Request found in cache. Respond using it.
let body: ArrayBuffer|Blob|string|undefined = response.body;
switch (response.responseType) {
case 'arraybuffer':
body = new TextEncoder().encode(response.body).buffer;
break;
case 'blob':
body = new Blob([response.body]);
break;
}
return of(
new HttpResponse({
body,
headers: new HttpHeaders(response.headers),
status: response.status,
statusText: response.statusText,
url: response.url,
}),
);
}
// Request not found in cache. Make the request and cache it.
return next(req).pipe(
tap((event: HttpEvent<unknown>) => {
if (event instanceof HttpResponse) {
transferState.set<TransferHttpResponse>(storeKey, {
body: event.body,
headers: getHeadersMap(event.headers),
status: event.status,
statusText: event.statusText,
url: event.url || '',
responseType: req.responseType,
});
}
}),
);
}
function getHeadersMap(headers: HttpHeaders): Record<string, string[]> {
const headersMap: Record<string, string[]> = {};
for (const key of headers.keys()) {
const values = headers.getAll(key);
if (values !== null) {
headersMap[key] = values;
}
}
return headersMap;
}
function makeCacheKey(request: HttpRequest<any>): StateKey<TransferHttpResponse> {
// make the params encoded same as a url so it's easy to identify
const {params, method, responseType, url} = request;
const encodedParams = params.keys().sort().map((k) => `${k}=${params.getAll(k)}`).join('&');
const key = method + '.' + responseType + '.' + url + '?' + encodedParams;
const hash = generateHash(key);
return makeStateKey(hash);
}
/**
* A method that returns a hash representation of a string using a variant of DJB2 hash
* algorithm.
*
* This is the same hashing logic that is used to generate component ids.
*/
function generateHash(value: string): string {
let hash = 0;
for (const char of value) {
hash = Math.imul(31, hash) + char.charCodeAt(0) << 0;
}
// Force positive number hash.
// 2147483647 = equivalent of Integer.MAX_VALUE.
hash += 2147483647 + 1;
return hash.toString();
}
/**
* Returns the DI providers needed to enable HTTP transfer cache.
*
* By default, when using server rendering, requests are performed twice: once on the server and
* other one on the browser.
*
* When these providers are added, requests performed on the server are cached and reused during the
* bootstrapping of the application in the browser thus avoiding duplicate requests and reducing
* load time.
*
*/
export function withHttpTransferCache(): Provider[] {
return [
{
provide: CACHE_STATE,
useFactory: () => {
inject(ENABLED_SSR_FEATURES).add('httpcache');
return {isCacheActive: true};
}
},
{
provide: HTTP_ROOT_INTERCEPTOR_FNS,
useValue: transferCacheInterceptorFn,
multi: true,
deps: [TransferState, CACHE_STATE]
},
{
provide: APP_BOOTSTRAP_LISTENER,
multi: true,
useFactory: () => {
const appRef = inject(ApplicationRef);
const cacheState = inject(CACHE_STATE);
return () => {
whenStable(appRef).then(() => {
cacheState.isCacheActive = false;
});
};
}
}
];
}