/** * @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 {MockRequest, MockResponse} from './fetch'; export interface DehydratedResponse { body: string|null; status: number; statusText: string; headers: {[name: string]: string}; } export type DehydratedCache = { [url: string]: DehydratedResponse }; export type DehydratedCacheStorage = { [name: string]: DehydratedCache }; export class MockCacheStorage implements CacheStorage { private caches = new Map(); constructor(private origin: string, hydrateFrom?: string) { if (hydrateFrom !== undefined) { const hydrated = JSON.parse(hydrateFrom) as DehydratedCacheStorage; Object.keys(hydrated).forEach(name => { this.caches.set(name, new MockCache(this.origin, hydrated[name])); }); } } async has(name: string): Promise { return this.caches.has(name); } async keys(): Promise { return Array.from(this.caches.keys()); } async open(name: string): Promise { if (!this.caches.has(name)) { this.caches.set(name, new MockCache(this.origin)); } return this.caches.get(name) as any; } async match(req: Request): Promise { return await Array.from(this.caches.values()) .reduce>(async(answer, cache): Promise => { const curr = await answer; if (curr !== undefined) { return curr; } return cache.match(req); }, Promise.resolve(undefined)); } async 'delete'(name: string): Promise { if (this.caches.has(name)) { this.caches.delete(name); return true; } return false; } dehydrate(): string { const dehydrated: DehydratedCacheStorage = {}; Array.from(this.caches.keys()).forEach(name => { const cache = this.caches.get(name)!; dehydrated[name] = cache.dehydrate(); }); return JSON.stringify(dehydrated); } } export class MockCache { private cache = new Map(); constructor(private origin: string, hydrated?: DehydratedCache) { if (hydrated !== undefined) { Object.keys(hydrated).forEach(url => { const resp = hydrated[url]; this.cache.set( url, new MockResponse( resp.body, {status: resp.status, statusText: resp.statusText, headers: resp.headers})); }); } } async add(request: RequestInfo): Promise { throw 'Not implemented'; } async addAll(requests: RequestInfo[]): Promise { throw 'Not implemented'; } async 'delete'(request: RequestInfo, options?: CacheQueryOptions): Promise { let url = (typeof request === 'string' ? request : request.url); if (this.cache.has(url)) { this.cache.delete(url); return true; } else if (options?.ignoreSearch) { url = this.stripQueryAndHash(url); const cachedUrl = [...this.cache.keys()].find(key => url === this.stripQueryAndHash(key)); if (cachedUrl) { this.cache.delete(cachedUrl); return true; } } return false; } async keys(match?: Request|string): Promise { if (match !== undefined) { throw 'Not implemented'; } return Array.from(this.cache.keys()); } async match(request: RequestInfo, options?: CacheQueryOptions): Promise { let url = (typeof request === 'string' ? request : request.url); if (url.startsWith(this.origin)) { url = '/' + url.substr(this.origin.length); } // TODO: cleanup typings. Typescript doesn't know this can resolve to undefined. let res = this.cache.get(url); if (!res && options?.ignoreSearch) { // check if cache has url by ignoring search url = this.stripQueryAndHash(url); const matchingReq = [...this.cache.keys()].find(key => url === this.stripQueryAndHash(key)); if (matchingReq !== undefined) res = this.cache.get(matchingReq); } if (res !== undefined) { res = res.clone(); } return res!; } async matchAll(request?: Request|string, options?: CacheQueryOptions): Promise { if (request === undefined) { return Array.from(this.cache.values()); } const url = (typeof request === 'string' ? request : request.url); const res = await this.match(url, options); if (res) { return [res]; } else { return []; } } async put(request: RequestInfo, response: Response): Promise { const url = (typeof request === 'string' ? request : request.url); this.cache.set(url, response.clone()); // Even though the body above is cloned, consume it here because the // real cache consumes the body. await response.text(); return; } dehydrate(): DehydratedCache { const dehydrated: DehydratedCache = {}; Array.from(this.cache.keys()).forEach(url => { const resp = this.cache.get(url) as MockResponse; const dehydratedResp = { body: resp._body, status: resp.status, statusText: resp.statusText, headers: {}, } as DehydratedResponse; resp.headers.forEach((value: string, name: string) => { dehydratedResp.headers[name] = value; }); dehydrated[url] = dehydratedResp; }); return dehydrated; } /** remove the query/hash part from a url*/ private stripQueryAndHash(url: string): string { return url.replace(/[?#].*/, ''); } } // This can be used to simulate a situation (bug?), where the user clears the caches from DevTools, // while the SW is still running (e.g. serving another tab) and keeps references to the deleted // caches. export async function clearAllCaches(caches: CacheStorage): Promise { const cacheNames = await caches.keys(); const cacheInstances = await Promise.all(cacheNames.map(name => caches.open(name))); // Delete all cache instances from `CacheStorage`. await Promise.all(cacheNames.map(name => caches.delete(name))); // Delete all entries from each cache instance. await Promise.all(cacheInstances.map(async cache => { const keys = await cache.keys(); await Promise.all(keys.map(key => cache.delete(key))); })); }