2017-09-28 19:18:12 -04:00
|
|
|
/**
|
|
|
|
* @license
|
2020-05-19 15:08:49 -04:00
|
|
|
* Copyright Google LLC All Rights Reserved.
|
2017-09-28 19:18:12 -04:00
|
|
|
*
|
|
|
|
* 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
|
|
|
|
*/
|
|
|
|
|
2020-07-06 09:55:37 -04:00
|
|
|
import {MockResponse} from './fetch';
|
|
|
|
import {normalizeUrl} from './utils';
|
2017-09-28 19:18:12 -04:00
|
|
|
|
|
|
|
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<string, MockCache>();
|
|
|
|
|
2017-10-02 18:59:57 -04:00
|
|
|
constructor(private origin: string, hydrateFrom?: string) {
|
2017-09-28 19:18:12 -04:00
|
|
|
if (hydrateFrom !== undefined) {
|
|
|
|
const hydrated = JSON.parse(hydrateFrom) as DehydratedCacheStorage;
|
2020-04-13 19:40:21 -04:00
|
|
|
Object.keys(hydrated).forEach(name => {
|
|
|
|
this.caches.set(name, new MockCache(this.origin, hydrated[name]));
|
|
|
|
});
|
2017-09-28 19:18:12 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-13 19:40:21 -04:00
|
|
|
async has(name: string): Promise<boolean> {
|
|
|
|
return this.caches.has(name);
|
|
|
|
}
|
2017-09-28 19:18:12 -04:00
|
|
|
|
2020-04-13 19:40:21 -04:00
|
|
|
async keys(): Promise<string[]> {
|
|
|
|
return Array.from(this.caches.keys());
|
|
|
|
}
|
2017-09-28 19:18:12 -04:00
|
|
|
|
|
|
|
async open(name: string): Promise<Cache> {
|
|
|
|
if (!this.caches.has(name)) {
|
2017-10-02 18:59:57 -04:00
|
|
|
this.caches.set(name, new MockCache(this.origin));
|
2017-09-28 19:18:12 -04:00
|
|
|
}
|
2017-12-22 12:36:47 -05:00
|
|
|
return this.caches.get(name) as any;
|
2017-09-28 19:18:12 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
async match(req: Request): Promise<Response|undefined> {
|
|
|
|
return await Array.from(this.caches.values())
|
|
|
|
.reduce<Promise<Response|undefined>>(async(answer, cache): Promise<Response|undefined> => {
|
|
|
|
const curr = await answer;
|
|
|
|
if (curr !== undefined) {
|
|
|
|
return curr;
|
|
|
|
}
|
|
|
|
|
|
|
|
return cache.match(req);
|
|
|
|
}, Promise.resolve<Response|undefined>(undefined));
|
|
|
|
}
|
|
|
|
|
|
|
|
async 'delete'(name: string): Promise<boolean> {
|
|
|
|
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 => {
|
2020-04-13 19:40:21 -04:00
|
|
|
const cache = this.caches.get(name)!;
|
2017-09-28 19:18:12 -04:00
|
|
|
dehydrated[name] = cache.dehydrate();
|
|
|
|
});
|
|
|
|
return JSON.stringify(dehydrated);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-12-22 12:36:47 -05:00
|
|
|
export class MockCache {
|
2017-09-28 19:18:12 -04:00
|
|
|
private cache = new Map<string, Response>();
|
|
|
|
|
2017-10-02 18:59:57 -04:00
|
|
|
constructor(private origin: string, hydrated?: DehydratedCache) {
|
2017-09-28 19:18:12 -04:00
|
|
|
if (hydrated !== undefined) {
|
|
|
|
Object.keys(hydrated).forEach(url => {
|
|
|
|
const resp = hydrated[url];
|
|
|
|
this.cache.set(
|
2020-04-13 19:40:21 -04:00
|
|
|
url,
|
|
|
|
new MockResponse(
|
|
|
|
resp.body,
|
|
|
|
{status: resp.status, statusText: resp.statusText, headers: resp.headers}));
|
2017-09-28 19:18:12 -04:00
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-13 19:40:21 -04:00
|
|
|
async add(request: RequestInfo): Promise<void> {
|
|
|
|
throw 'Not implemented';
|
|
|
|
}
|
2017-09-28 19:18:12 -04:00
|
|
|
|
2020-04-13 19:40:21 -04:00
|
|
|
async addAll(requests: RequestInfo[]): Promise<void> {
|
|
|
|
throw 'Not implemented';
|
|
|
|
}
|
2017-09-28 19:18:12 -04:00
|
|
|
|
2020-04-29 10:07:34 -04:00
|
|
|
async 'delete'(request: RequestInfo, options?: CacheQueryOptions): Promise<boolean> {
|
2020-07-06 09:55:37 -04:00
|
|
|
let url = this.getRequestUrl(request);
|
2017-09-28 19:18:12 -04:00
|
|
|
if (this.cache.has(url)) {
|
|
|
|
this.cache.delete(url);
|
|
|
|
return true;
|
2020-04-29 10:07:34 -04:00
|
|
|
} 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;
|
|
|
|
}
|
2017-09-28 19:18:12 -04:00
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
async keys(match?: Request|string): Promise<string[]> {
|
|
|
|
if (match !== undefined) {
|
|
|
|
throw 'Not implemented';
|
|
|
|
}
|
|
|
|
return Array.from(this.cache.keys());
|
|
|
|
}
|
|
|
|
|
|
|
|
async match(request: RequestInfo, options?: CacheQueryOptions): Promise<Response> {
|
2020-07-06 09:55:37 -04:00
|
|
|
let url = this.getRequestUrl(request);
|
2017-09-28 19:18:12 -04:00
|
|
|
// TODO: cleanup typings. Typescript doesn't know this can resolve to undefined.
|
|
|
|
let res = this.cache.get(url);
|
2020-04-29 10:07:34 -04:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2017-09-28 19:18:12 -04:00
|
|
|
if (res !== undefined) {
|
|
|
|
res = res.clone();
|
|
|
|
}
|
2020-04-13 19:40:21 -04:00
|
|
|
return res!;
|
2017-09-28 19:18:12 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
async matchAll(request?: Request|string, options?: CacheQueryOptions): Promise<Response[]> {
|
|
|
|
if (request === undefined) {
|
|
|
|
return Array.from(this.cache.values());
|
|
|
|
}
|
2020-07-06 09:55:37 -04:00
|
|
|
const res = await this.match(request, options);
|
2020-04-29 10:07:34 -04:00
|
|
|
if (res) {
|
|
|
|
return [res];
|
2017-09-28 19:18:12 -04:00
|
|
|
} else {
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async put(request: RequestInfo, response: Response): Promise<void> {
|
2020-07-06 09:55:37 -04:00
|
|
|
const url = this.getRequestUrl(request);
|
2017-09-28 19:18:12 -04:00
|
|
|
this.cache.set(url, response.clone());
|
2017-10-10 15:54:41 -04:00
|
|
|
|
|
|
|
// Even though the body above is cloned, consume it here because the
|
|
|
|
// real cache consumes the body.
|
|
|
|
await response.text();
|
|
|
|
|
2017-09-28 19:18:12 -04:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
dehydrate(): DehydratedCache {
|
|
|
|
const dehydrated: DehydratedCache = {};
|
|
|
|
Array.from(this.cache.keys()).forEach(url => {
|
2017-12-22 12:36:47 -05:00
|
|
|
const resp = this.cache.get(url) as MockResponse;
|
2017-09-28 19:18:12 -04:00
|
|
|
const dehydratedResp = {
|
|
|
|
body: resp._body,
|
|
|
|
status: resp.status,
|
|
|
|
statusText: resp.statusText,
|
|
|
|
headers: {},
|
|
|
|
} as DehydratedResponse;
|
|
|
|
|
2020-04-13 19:40:21 -04:00
|
|
|
resp.headers.forEach((value: string, name: string) => {
|
|
|
|
dehydratedResp.headers[name] = value;
|
|
|
|
});
|
2017-09-28 19:18:12 -04:00
|
|
|
|
|
|
|
dehydrated[url] = dehydratedResp;
|
|
|
|
});
|
|
|
|
return dehydrated;
|
|
|
|
}
|
2020-04-29 10:07:34 -04:00
|
|
|
|
2020-07-06 09:55:37 -04:00
|
|
|
/** Get the normalized URL from a `RequestInfo` value. */
|
|
|
|
private getRequestUrl(request: RequestInfo): string {
|
|
|
|
const url = typeof request === 'string' ? request : request.url;
|
|
|
|
return normalizeUrl(url, this.origin);
|
|
|
|
}
|
|
|
|
|
2020-04-29 10:07:34 -04:00
|
|
|
/** remove the query/hash part from a url*/
|
|
|
|
private stripQueryAndHash(url: string): string {
|
|
|
|
return url.replace(/[?#].*/, '');
|
|
|
|
}
|
2017-12-22 12:36:47 -05:00
|
|
|
}
|
2018-09-20 14:04:37 -04:00
|
|
|
|
|
|
|
// 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<void> {
|
|
|
|
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)));
|
|
|
|
}));
|
|
|
|
}
|