red-ui/libs/red-cache/src/lib/caches/cache-api.service.ts
2020-11-12 19:14:43 +02:00

280 lines
10 KiB
TypeScript

import { Injectable } from '@angular/core';
import { DYNAMIC_CACHES, APP_LEVEL_CACHE } from './cache-utils';
import { HttpEvent, HttpHeaders, HttpRequest, HttpResponse } from '@angular/common/http';
import { from, Observable, throwError } from 'rxjs';
import { map, mergeMap } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class CacheApiService {
constructor() {
this.checkCachesExpiration();
}
cacheRequest(request: HttpRequest<any>, httpResponse: HttpResponse<any>): Promise<any> {
if (httpResponse.status < 300 && httpResponse.status >= 200) {
const url = this._buildUrl(request);
for (const dynCache of DYNAMIC_CACHES) {
for (const cacheUrl of dynCache.urls) {
if (url.indexOf(cacheUrl) >= 0) {
return caches.open(dynCache.name).then((cache) => {
return this._handleFetchResponse(httpResponse, dynCache, cache, url);
});
}
}
}
}
}
getCachedRequest(request: HttpRequest<any>): Observable<HttpEvent<any>> {
const url = this._buildUrl(request);
return from(caches.match(url)).pipe(
mergeMap((response) => {
if (response) {
const expires = response.headers.get('_expires');
if (expires) {
// if not expired, return, else override
if (parseInt(expires, 10) > new Date().getTime()) {
// console.log('[CACHE-API] Returning from cache: ', url);
return this._toHttpResponse(response);
} else {
// console.log('[CACHE-API] cache expired: ', url);
}
} else {
// console.log('[CACHE-API] Returning from cache: ', url);
return this._toHttpResponse(response);
}
}
return throwError(new Error('Request not Cached'));
})
);
}
cacheValue(name: string, valueReference: any, ttl = 3600): Promise<any> {
if (this.cachesAvailable) {
return caches.open(APP_LEVEL_CACHE).then((cache) => {
const string = JSON.stringify(valueReference);
const expires = new Date().getTime() + ttl * 1000;
const response = new Response(string, {
headers: {
_expires: `${expires}`
}
});
const request = new Request(name);
// console.log('should cache', valueReference, string, response);
return cache.put(request, response);
});
} else {
return Promise.resolve();
}
}
removeCache(cacheId: string): Promise<any> {
if (this.cachesAvailable) {
return caches.open(APP_LEVEL_CACHE).then((cache) => {
return cache.delete(cacheId);
});
} else {
return Promise.resolve(undefined);
}
}
deleteDynamicCache(cacheName: string): Promise<any> {
if (this.cachesAvailable && DYNAMIC_CACHES.some((cache) => cache.name === cacheName)) {
return caches.delete(cacheName);
} else {
return Promise.resolve(undefined);
}
}
deleteDynamicCacheEntry(cacheName: string, cacheEntry: string): Promise<any> {
if (this.cachesAvailable && DYNAMIC_CACHES.some((cache) => cache.name === cacheName)) {
return caches.open(cacheName).then((cache) => {
return cache.delete(cacheEntry);
});
} else {
return Promise.resolve(undefined);
}
}
getCachedValue(name: string): Promise<any> {
if (this.cachesAvailable) {
return caches.open(APP_LEVEL_CACHE).then((cache) => {
return cache.match(name).then((result) => {
if (result) {
const expires = result.headers.get('_expires');
try {
if (parseInt(expires, 10) > new Date().getTime()) {
return result.json();
}
} catch (e) {}
}
return undefined;
});
});
} else {
return Promise.resolve(undefined);
}
}
_buildUrl(request: HttpRequest<any>) {
let url;
if (request.method === 'GET') {
url = request.urlWithParams;
}
if (request.method === 'POST') {
const body = request.body;
let hash;
if (Array.isArray(body)) {
hash = JSON.stringify(body.sort());
} else {
hash = JSON.stringify(body);
}
const separator = request.urlWithParams.indexOf('?') > 0 ? '&' : '?';
url = request.urlWithParams + separator + btoa(hash);
}
return url;
}
isCachable(event: HttpRequest<any>) {
// only do shit for post and get
if (this.cachesAvailable && (event.method === 'GET' || event.method === 'POST')) {
// quick check if it has the potential of caching
const preliminaryUrl = event.url;
let tryCache = false;
for (const cache of DYNAMIC_CACHES) {
if (cache.methods.indexOf(event.method) >= 0) {
for (const url of cache.urls) {
if (preliminaryUrl.indexOf(url) >= 0) {
tryCache = true;
break;
}
}
}
}
return tryCache;
}
return false;
}
private _toHttpResponse(response: Response): Observable<HttpResponse<any>> {
response = response.clone();
const contentType = response.headers.get('content-type');
let obs: Observable<any>;
if (contentType) {
if (contentType.toLowerCase().indexOf('application/json') >= 0) {
obs = from(response.json());
}
if (contentType.toLowerCase().indexOf('text/') >= 0) {
obs = from(response.text());
}
if (contentType.toLowerCase().indexOf('image/') >= 0) {
if (contentType.toLowerCase().indexOf('image/svg') >= 0) {
obs = from(response.text());
} else {
obs = from(response.blob());
}
}
if (contentType.toLowerCase().indexOf('application/pdf') >= 0) {
obs = from(response.blob());
}
}
// console.log('[CACHE-API] content type', contentType, response.url);
return obs.pipe(
map((body) => {
// console.log('[CACHE-API] BODY', body);
return new HttpResponse({
body,
status: response.status,
statusText: response.statusText,
url: response.url,
headers: this._toHttpHeaders(response.headers)
});
})
);
}
private _handleFetchResponse(httpResponse: HttpResponse<any>, dynCache, cache, url) {
const expires = new Date().getTime() + dynCache.maxAge * 1000;
const cachedResponseFields = {
status: httpResponse.status,
statusText: httpResponse.statusText,
headers: {
_expires: undefined
}
};
httpResponse.headers.keys().forEach((key) => {
cachedResponseFields.headers[key] = httpResponse.headers.get(key);
});
cachedResponseFields.headers._expires = expires;
let body;
if (cachedResponseFields.headers['content-type'].indexOf('application/json') >= 0) {
body = JSON.stringify(httpResponse.body);
} else {
body = httpResponse.body;
}
cache.put(url, new Response(body, cachedResponseFields));
}
checkCachesExpiration() {
if (this.cachesAvailable) {
const now = new Date().getTime();
for (const dynCache of DYNAMIC_CACHES) {
this._handleCacheExpiration(dynCache, now);
}
}
}
private _handleCacheExpiration(dynCache, now) {
caches.open(dynCache.name).then((cache) => {
cache.keys().then((keys) => {
// removed expired;
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
// console.log('[CACHE-API] checking cache key: ', key);
cache.match(key).then((response) => {
const expires = response.headers.get('_expires');
try {
if (parseInt(expires, 10) < now) {
console.log('remove cache', key);
cache.delete(key);
}
} catch (e) {}
});
}
});
cache.keys().then((keys) => {
if (keys.length > dynCache.maxSize) {
const keysToRemove = keys.slice(0, keys.length - dynCache.maxSize);
// console.log('[CACHE-API] cache to large - removing keys: ', keysToRemove);
for (let i = 0; i < keysToRemove.length; i++) {
const key = keys[i];
cache.delete(key);
}
}
});
});
}
get cachesAvailable(): boolean {
return !!window.caches;
}
private _toHttpHeaders(headers: Headers): HttpHeaders {
let httpHeaders = new HttpHeaders();
headers.forEach((value, key) => {
httpHeaders = httpHeaders.append(key, value);
});
return httpHeaders;
}
}