280 lines
10 KiB
TypeScript
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;
|
|
}
|
|
}
|