Skip to content

Instantly share code, notes, and snippets.

@ngclient
Last active September 5, 2019 18:40
Show Gist options
  • Save ngclient/70a65e0ae05eda53f5b04d5427327d98 to your computer and use it in GitHub Desktop.
Save ngclient/70a65e0ae05eda53f5b04d5427327d98 to your computer and use it in GitHub Desktop.
http cache extensions
import { Injectable } from '@angular/core';
import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest, HttpResponse } from '@angular/common/http';
import { Observable, Subscriber } from 'rxjs';
import { HttpCacheService } from './http-cache.service';
/**
* Caches HTTP requests.
* Use ExtendedHttpClient fluent API to configure caching for each request.
*/
@Injectable()
export class CacheInterceptor implements HttpInterceptor {
private forceUpdate = false;
constructor(private httpCacheService: HttpCacheService) {}
/**
* Configures interceptor options
* @param options If update option is enabled, forces request to be made and updates cache entry.
* @return The configured instance.
*/
configure(options?: { update?: boolean } | null): CacheInterceptor {
const instance = new CacheInterceptor(this.httpCacheService);
if (options && options.update) {
instance.forceUpdate = true;
}
return instance;
}
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (request.method !== 'GET') {
return next.handle(request);
}
return new Observable((subscriber: Subscriber<HttpEvent<any>>) => {
const cachedData = this.forceUpdate ? null : this.httpCacheService.getCacheData(request.urlWithParams);
if (cachedData !== null) {
// Create new response to avoid side-effects
subscriber.next(new HttpResponse(cachedData as Object));
subscriber.complete();
} else {
next.handle(request).subscribe(
event => {
if (event instanceof HttpResponse) {
this.httpCacheService.setCacheData(request.urlWithParams, event);
}
subscriber.next(event);
},
error => subscriber.error(error),
() => subscriber.complete()
);
}
});
}
}
import { Injectable } from '@angular/core';
import { HttpResponse } from '@angular/common/http';
import { Logger } from '../logger.service';
const log = new Logger('HttpCacheService');
const cachePersistenceKey = 'httpCache';
export interface HttpCacheEntry {
lastUpdated: Date;
data: HttpResponse<any>;
}
/**
* Provides a cache facility for HTTP requests with configurable persistence policy.
*/
@Injectable()
export class HttpCacheService {
private cachedData: { [key: string]: HttpCacheEntry } = {};
private storage: Storage | null = null;
constructor() {
this.loadCacheData();
}
/**
* Sets the cache data for the specified request.
* @param url The request URL.
* @param data The received data.
* @param lastUpdated The cache last update, current date is used if not specified.
*/
setCacheData(url: string, data: HttpResponse<any>, lastUpdated?: Date) {
this.cachedData[url] = {
lastUpdated: lastUpdated || new Date(),
data: data
};
log.debug(`Cache set for key: "${url}"`);
this.saveCacheData();
}
/**
* Gets the cached data for the specified request.
* @param url The request URL.
* @return The cached data or null if no cached data exists for this request.
*/
getCacheData(url: string): HttpResponse<any> | null {
const cacheEntry = this.cachedData[url];
if (cacheEntry) {
log.debug(`Cache hit for key: "${url}"`);
return cacheEntry.data;
}
return null;
}
/**
* Gets the cached entry for the specified request.
* @param url The request URL.
* @return The cache entry or null if no cache entry exists for this request.
*/
getHttpCacheEntry(url: string): HttpCacheEntry | null {
return this.cachedData[url] || null;
}
/**
* Clears the cached entry (if exists) for the specified request.
* @param url The request URL.
*/
clearCache(url: string): void {
delete this.cachedData[url];
log.debug(`Cache cleared for key: "${url}"`);
this.saveCacheData();
}
/**
* Cleans cache entries older than the specified date.
* @param expirationDate The cache expiration date. If no date is specified, all cache is cleared.
*/
cleanCache(expirationDate?: Date) {
if (expirationDate) {
Object.entries(this.cachedData).forEach(([key, value]) => {
if (expirationDate >= value.lastUpdated) {
delete this.cachedData[key];
}
});
} else {
this.cachedData = {};
}
this.saveCacheData();
}
/**
* Sets the cache persistence policy.
* Note that changing the cache persistence will also clear the cache from its previous storage.
* @param persistence How the cache should be persisted, it can be either local or session storage, or if no value is
* provided it will be only in-memory (default).
*/
setPersistence(persistence?: 'local' | 'session') {
this.cleanCache();
this.storage = persistence === 'local' || persistence === 'session' ? window[persistence + 'Storage'] : null;
this.loadCacheData();
}
private saveCacheData() {
if (this.storage) {
this.storage.setItem(cachePersistenceKey, JSON.stringify(this.cachedData));
}
}
private loadCacheData() {
const data = this.storage ? this.storage.getItem(cachePersistenceKey) : null;
this.cachedData = data ? JSON.parse(data) : {};
}
}
/**
* Simple logger system with the possibility of registering custom outputs.
*
* 4 different log levels are provided, with corresponding methods:
* - debug : for debug information
* - info : for informative status of the application (success, ...)
* - warning : for non-critical errors that do not prevent normal application behavior
* - error : for critical errors that prevent normal application behavior
*
* Example usage:
* ```
* import { Logger } from 'app/core/logger.service';
*
* const log = new Logger('myFile');
* ...
* log.debug('something happened');
* ```
*
* To disable debug and info logs in production, add this snippet to your root component:
* ```
* export class AppComponent implements OnInit {
* ngOnInit() {
* if (environment.production) {
* Logger.enableProductionMode();
* }
* ...
* }
* }
*
* If you want to process logs through other outputs than console, you can add LogOutput functions to Logger.outputs.
*/
/**
* The possible log levels.
* LogLevel.Off is never emitted and only used with Logger.level property to disable logs.
*/
export enum LogLevel {
Off = 0,
Error,
Warning,
Info,
Debug
}
/**
* Log output handler function.
*/
export type LogOutput = (source: string | undefined, level: LogLevel, ...objects: any[]) => void;
export class Logger {
/**
* Current logging level.
* Set it to LogLevel.Off to disable logs completely.
*/
static level = LogLevel.Debug;
/**
* Additional log outputs.
*/
static outputs: LogOutput[] = [];
/**
* Enables production mode.
* Sets logging level to LogLevel.Warning.
*/
static enableProductionMode() {
Logger.level = LogLevel.Warning;
}
constructor(private source?: string) {}
/**
* Logs messages or objects with the debug level.
* Works the same as console.log().
*/
debug(...objects: any[]) {
this.log(console.log, LogLevel.Debug, objects);
}
/**
* Logs messages or objects with the info level.
* Works the same as console.log().
*/
info(...objects: any[]) {
this.log(console.info, LogLevel.Info, objects);
}
/**
* Logs messages or objects with the warning level.
* Works the same as console.log().
*/
warn(...objects: any[]) {
this.log(console.warn, LogLevel.Warning, objects);
}
/**
* Logs messages or objects with the error level.
* Works the same as console.log().
*/
error(...objects: any[]) {
this.log(console.error, LogLevel.Error, objects);
}
private log(func: Function, level: LogLevel, objects: any[]) {
if (level <= Logger.level) {
const log = this.source ? ['[' + this.source + ']'].concat(objects) : objects;
func.apply(console, log);
Logger.outputs.forEach(output => output.apply(output, [this.source, level, ...objects]));
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment