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, httpResponse: HttpResponse): Promise { 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): Observable> { 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 { 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 { if (this.cachesAvailable) { return caches.open(APP_LEVEL_CACHE).then((cache) => { return cache.delete(cacheId); }); } else { return Promise.resolve(undefined); } } deleteDynamicCache(cacheName: string): Promise { 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 { 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 { 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) { 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) { // 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> { response = response.clone(); const contentType = response.headers.get('content-type'); let obs: Observable; 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, 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; } }